1 use crate::errors::*;
2 use crate::utils::{
3 take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
4 take_rustdoc_include_lines,
5 };
6 use regex::{CaptureMatches, Captures, Regex};
7 use std::fs;
8 use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
9 use std::path::{Path, PathBuf};
10
11 use super::{Preprocessor, PreprocessorContext};
12 use crate::book::{Book, BookItem};
13
14 const ESCAPE_CHAR: char = '\\';
15 const MAX_LINK_NESTED_DEPTH: usize = 10;
16
17 /// A preprocessor for expanding helpers in a chapter. Supported helpers are:
18 ///
19 /// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
20 ///. lines, or only between the specified anchors.
21 /// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
22 ///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
23 /// This hides the lines from initial display but shows them when the reader expands the code
24 /// block and provides them to Rustdoc for testing.
25 /// - `{{# playground}}` - Insert runnable Rust files
26 /// - `{{# title}}` - Override \<title\> of a webpage.
27 #[derive(Default)]
28 pub struct LinkPreprocessor;
29
30 impl LinkPreprocessor {
31 pub(crate) const NAME: &'static str = "links";
32
33 /// Create a new `LinkPreprocessor`.
new() -> Self34 pub fn new() -> Self {
35 LinkPreprocessor
36 }
37 }
38
39 impl Preprocessor for LinkPreprocessor {
name(&self) -> &str40 fn name(&self) -> &str {
41 Self::NAME
42 }
43
run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book>44 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
45 let src_dir = ctx.root.join(&ctx.config.book.src);
46
47 book.for_each_mut(|section: &mut BookItem| {
48 if let BookItem::Chapter(ref mut ch) = *section {
49 if let Some(ref chapter_path) = ch.path {
50 let base = chapter_path
51 .parent()
52 .map(|dir| src_dir.join(dir))
53 .expect("All book items have a parent");
54
55 let mut chapter_title = ch.name.clone();
56 let content =
57 replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
58 ch.content = content;
59 if chapter_title != ch.name {
60 ctx.chapter_titles
61 .borrow_mut()
62 .insert(chapter_path.clone(), chapter_title);
63 }
64 }
65 }
66 });
67
68 Ok(book)
69 }
70 }
71
replace_all<P1, P2>( s: &str, path: P1, source: P2, depth: usize, chapter_title: &mut String, ) -> String where P1: AsRef<Path>, P2: AsRef<Path>,72 fn replace_all<P1, P2>(
73 s: &str,
74 path: P1,
75 source: P2,
76 depth: usize,
77 chapter_title: &mut String,
78 ) -> String
79 where
80 P1: AsRef<Path>,
81 P2: AsRef<Path>,
82 {
83 // When replacing one thing in a string by something with a different length,
84 // the indices after that will not correspond,
85 // we therefore have to store the difference to correct this
86 let path = path.as_ref();
87 let source = source.as_ref();
88 let mut previous_end_index = 0;
89 let mut replaced = String::new();
90
91 for link in find_links(s) {
92 replaced.push_str(&s[previous_end_index..link.start_index]);
93
94 match link.render_with_path(&path, chapter_title) {
95 Ok(new_content) => {
96 if depth < MAX_LINK_NESTED_DEPTH {
97 if let Some(rel_path) = link.link_type.relative_path(path) {
98 replaced.push_str(&replace_all(
99 &new_content,
100 rel_path,
101 source,
102 depth + 1,
103 chapter_title,
104 ));
105 } else {
106 replaced.push_str(&new_content);
107 }
108 } else {
109 error!(
110 "Stack depth exceeded in {}. Check for cyclic includes",
111 source.display()
112 );
113 }
114 previous_end_index = link.end_index;
115 }
116 Err(e) => {
117 error!("Error updating \"{}\", {}", link.link_text, e);
118 for cause in e.chain().skip(1) {
119 warn!("Caused By: {}", cause);
120 }
121
122 // This should make sure we include the raw `{{# ... }}` snippet
123 // in the page content if there are any errors.
124 previous_end_index = link.start_index;
125 }
126 }
127 }
128
129 replaced.push_str(&s[previous_end_index..]);
130 replaced
131 }
132
133 #[derive(PartialEq, Debug, Clone)]
134 enum LinkType<'a> {
135 Escaped,
136 Include(PathBuf, RangeOrAnchor),
137 Playground(PathBuf, Vec<&'a str>),
138 RustdocInclude(PathBuf, RangeOrAnchor),
139 Title(&'a str),
140 }
141
142 #[derive(PartialEq, Debug, Clone)]
143 enum RangeOrAnchor {
144 Range(LineRange),
145 Anchor(String),
146 }
147
148 // A range of lines specified with some include directive.
149 #[derive(PartialEq, Debug, Clone)]
150 enum LineRange {
151 Range(Range<usize>),
152 RangeFrom(RangeFrom<usize>),
153 RangeTo(RangeTo<usize>),
154 RangeFull(RangeFull),
155 }
156
157 impl RangeBounds<usize> for LineRange {
start_bound(&self) -> Bound<&usize>158 fn start_bound(&self) -> Bound<&usize> {
159 match self {
160 LineRange::Range(r) => r.start_bound(),
161 LineRange::RangeFrom(r) => r.start_bound(),
162 LineRange::RangeTo(r) => r.start_bound(),
163 LineRange::RangeFull(r) => r.start_bound(),
164 }
165 }
166
end_bound(&self) -> Bound<&usize>167 fn end_bound(&self) -> Bound<&usize> {
168 match self {
169 LineRange::Range(r) => r.end_bound(),
170 LineRange::RangeFrom(r) => r.end_bound(),
171 LineRange::RangeTo(r) => r.end_bound(),
172 LineRange::RangeFull(r) => r.end_bound(),
173 }
174 }
175 }
176
177 impl From<Range<usize>> for LineRange {
from(r: Range<usize>) -> LineRange178 fn from(r: Range<usize>) -> LineRange {
179 LineRange::Range(r)
180 }
181 }
182
183 impl From<RangeFrom<usize>> for LineRange {
from(r: RangeFrom<usize>) -> LineRange184 fn from(r: RangeFrom<usize>) -> LineRange {
185 LineRange::RangeFrom(r)
186 }
187 }
188
189 impl From<RangeTo<usize>> for LineRange {
from(r: RangeTo<usize>) -> LineRange190 fn from(r: RangeTo<usize>) -> LineRange {
191 LineRange::RangeTo(r)
192 }
193 }
194
195 impl From<RangeFull> for LineRange {
from(r: RangeFull) -> LineRange196 fn from(r: RangeFull) -> LineRange {
197 LineRange::RangeFull(r)
198 }
199 }
200
201 impl<'a> LinkType<'a> {
relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf>202 fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
203 let base = base.as_ref();
204 match self {
205 LinkType::Escaped => None,
206 LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
207 LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
208 LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
209 LinkType::Title(_) => None,
210 }
211 }
212 }
return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf213 fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
214 base.as_ref()
215 .join(relative)
216 .parent()
217 .expect("Included file should not be /")
218 .to_path_buf()
219 }
220
parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor221 fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
222 let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
223
224 let next_element = parts.next();
225 let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
226 // subtract 1 since line numbers usually begin with 1
227 Some(value.saturating_sub(1))
228 } else if let Some("") = next_element {
229 None
230 } else if let Some(anchor) = next_element {
231 return RangeOrAnchor::Anchor(String::from(anchor));
232 } else {
233 None
234 };
235
236 let end = parts.next();
237 // If `end` is empty string or any other value that can't be parsed as a usize, treat this
238 // include as a range with only a start bound. However, if end isn't specified, include only
239 // the single line specified by `start`.
240 let end = end.map(|s| s.parse::<usize>());
241
242 match (start, end) {
243 (Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
244 (Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
245 (Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
246 (None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
247 (None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
248 }
249 }
250
parse_include_path(path: &str) -> LinkType<'static>251 fn parse_include_path(path: &str) -> LinkType<'static> {
252 let mut parts = path.splitn(2, ':');
253
254 let path = parts.next().unwrap().into();
255 let range_or_anchor = parse_range_or_anchor(parts.next());
256
257 LinkType::Include(path, range_or_anchor)
258 }
259
parse_rustdoc_include_path(path: &str) -> LinkType<'static>260 fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
261 let mut parts = path.splitn(2, ':');
262
263 let path = parts.next().unwrap().into();
264 let range_or_anchor = parse_range_or_anchor(parts.next());
265
266 LinkType::RustdocInclude(path, range_or_anchor)
267 }
268
269 #[derive(PartialEq, Debug, Clone)]
270 struct Link<'a> {
271 start_index: usize,
272 end_index: usize,
273 link_type: LinkType<'a>,
274 link_text: &'a str,
275 }
276
277 impl<'a> Link<'a> {
from_capture(cap: Captures<'a>) -> Option<Link<'a>>278 fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
279 let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
280 (_, Some(typ), Some(title)) if typ.as_str() == "title" => {
281 Some(LinkType::Title(title.as_str()))
282 }
283 (_, Some(typ), Some(rest)) => {
284 let mut path_props = rest.as_str().split_whitespace();
285 let file_arg = path_props.next();
286 let props: Vec<&str> = path_props.collect();
287
288 match (typ.as_str(), file_arg) {
289 ("include", Some(pth)) => Some(parse_include_path(pth)),
290 ("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
291 ("playpen", Some(pth)) => {
292 warn!(
293 "the {{{{#playpen}}}} expression has been \
294 renamed to {{{{#playground}}}}, \
295 please update your book to use the new name"
296 );
297 Some(LinkType::Playground(pth.into(), props))
298 }
299 ("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
300 _ => None,
301 }
302 }
303 (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
304 Some(LinkType::Escaped)
305 }
306 _ => None,
307 };
308
309 link_type.and_then(|lnk_type| {
310 cap.get(0).map(|mat| Link {
311 start_index: mat.start(),
312 end_index: mat.end(),
313 link_type: lnk_type,
314 link_text: mat.as_str(),
315 })
316 })
317 }
318
render_with_path<P: AsRef<Path>>( &self, base: P, chapter_title: &mut String, ) -> Result<String>319 fn render_with_path<P: AsRef<Path>>(
320 &self,
321 base: P,
322 chapter_title: &mut String,
323 ) -> Result<String> {
324 let base = base.as_ref();
325 match self.link_type {
326 // omit the escape char
327 LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
328 LinkType::Include(ref pat, ref range_or_anchor) => {
329 let target = base.join(pat);
330
331 fs::read_to_string(&target)
332 .map(|s| match range_or_anchor {
333 RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
334 RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
335 })
336 .with_context(|| {
337 format!(
338 "Could not read file for link {} ({})",
339 self.link_text,
340 target.display(),
341 )
342 })
343 }
344 LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
345 let target = base.join(pat);
346
347 fs::read_to_string(&target)
348 .map(|s| match range_or_anchor {
349 RangeOrAnchor::Range(range) => {
350 take_rustdoc_include_lines(&s, range.clone())
351 }
352 RangeOrAnchor::Anchor(anchor) => {
353 take_rustdoc_include_anchored_lines(&s, anchor)
354 }
355 })
356 .with_context(|| {
357 format!(
358 "Could not read file for link {} ({})",
359 self.link_text,
360 target.display(),
361 )
362 })
363 }
364 LinkType::Playground(ref pat, ref attrs) => {
365 let target = base.join(pat);
366
367 let mut contents = fs::read_to_string(&target).with_context(|| {
368 format!(
369 "Could not read file for link {} ({})",
370 self.link_text,
371 target.display()
372 )
373 })?;
374 let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
375 if !contents.ends_with('\n') {
376 contents.push('\n');
377 }
378 Ok(format!(
379 "```{}{}\n{}```\n",
380 ftype,
381 attrs.join(","),
382 contents
383 ))
384 }
385 LinkType::Title(title) => {
386 *chapter_title = title.to_owned();
387 Ok(String::new())
388 }
389 }
390 }
391 }
392
393 struct LinkIter<'a>(CaptureMatches<'a, 'a>);
394
395 impl<'a> Iterator for LinkIter<'a> {
396 type Item = Link<'a>;
next(&mut self) -> Option<Link<'a>>397 fn next(&mut self) -> Option<Link<'a>> {
398 for cap in &mut self.0 {
399 if let Some(inc) = Link::from_capture(cap) {
400 return Some(inc);
401 }
402 }
403 None
404 }
405 }
406
find_links(contents: &str) -> LinkIter<'_>407 fn find_links(contents: &str) -> LinkIter<'_> {
408 // lazily compute following regex
409 // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
410 lazy_static! {
411 static ref RE: Regex = Regex::new(
412 r"(?x) # insignificant whitespace mode
413 \\\{\{\#.*\}\} # match escaped link
414 | # or
415 \{\{\s* # link opening parens and whitespace
416 \#([a-zA-Z0-9_]+) # link type
417 \s+ # separating whitespace
418 ([^}]+) # link target path and space separated properties
419 \}\} # link closing parens"
420 )
421 .unwrap();
422 }
423 LinkIter(RE.captures_iter(contents))
424 }
425
426 #[cfg(test)]
427 mod tests {
428 use super::*;
429
430 #[test]
test_replace_all_escaped()431 fn test_replace_all_escaped() {
432 let start = r"
433 Some text over here.
434 ```hbs
435 \{{#include file.rs}} << an escaped link!
436 ```";
437 let end = r"
438 Some text over here.
439 ```hbs
440 {{#include file.rs}} << an escaped link!
441 ```";
442 let mut chapter_title = "test_replace_all_escaped".to_owned();
443 assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
444 }
445
446 #[test]
test_set_chapter_title()447 fn test_set_chapter_title() {
448 let start = r"{{#title My Title}}
449 # My Chapter
450 ";
451 let end = r"
452 # My Chapter
453 ";
454 let mut chapter_title = "test_set_chapter_title".to_owned();
455 assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
456 assert_eq!(chapter_title, "My Title");
457 }
458
459 #[test]
test_find_links_no_link()460 fn test_find_links_no_link() {
461 let s = "Some random text without link...";
462 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
463 }
464
465 #[test]
test_find_links_partial_link()466 fn test_find_links_partial_link() {
467 let s = "Some random text with {{#playground...";
468 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
469 let s = "Some random text with {{#include...";
470 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
471 let s = "Some random text with \\{{#include...";
472 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
473 }
474
475 #[test]
test_find_links_empty_link()476 fn test_find_links_empty_link() {
477 let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
478 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
479 }
480
481 #[test]
test_find_links_unknown_link_type()482 fn test_find_links_unknown_link_type() {
483 let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
484 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
485 }
486
487 #[test]
test_find_links_simple_link()488 fn test_find_links_simple_link() {
489 let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
490
491 let res = find_links(s).collect::<Vec<_>>();
492 println!("\nOUTPUT: {:?}\n", res);
493
494 assert_eq!(
495 res,
496 vec![
497 Link {
498 start_index: 22,
499 end_index: 45,
500 link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
501 link_text: "{{#playground file.rs}}",
502 },
503 Link {
504 start_index: 50,
505 end_index: 74,
506 link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
507 link_text: "{{#playground test.rs }}",
508 },
509 ]
510 );
511 }
512
513 #[test]
test_find_links_with_special_characters()514 fn test_find_links_with_special_characters() {
515 let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
516
517 let res = find_links(s).collect::<Vec<_>>();
518 println!("\nOUTPUT: {:?}\n", res);
519
520 assert_eq!(
521 res,
522 vec![Link {
523 start_index: 22,
524 end_index: 57,
525 link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
526 link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
527 },]
528 );
529 }
530
531 #[test]
test_find_links_with_range()532 fn test_find_links_with_range() {
533 let s = "Some random text with {{#include file.rs:10:20}}...";
534 let res = find_links(s).collect::<Vec<_>>();
535 println!("\nOUTPUT: {:?}\n", res);
536 assert_eq!(
537 res,
538 vec![Link {
539 start_index: 22,
540 end_index: 48,
541 link_type: LinkType::Include(
542 PathBuf::from("file.rs"),
543 RangeOrAnchor::Range(LineRange::from(9..20))
544 ),
545 link_text: "{{#include file.rs:10:20}}",
546 }]
547 );
548 }
549
550 #[test]
test_find_links_with_line_number()551 fn test_find_links_with_line_number() {
552 let s = "Some random text with {{#include file.rs:10}}...";
553 let res = find_links(s).collect::<Vec<_>>();
554 println!("\nOUTPUT: {:?}\n", res);
555 assert_eq!(
556 res,
557 vec![Link {
558 start_index: 22,
559 end_index: 45,
560 link_type: LinkType::Include(
561 PathBuf::from("file.rs"),
562 RangeOrAnchor::Range(LineRange::from(9..10))
563 ),
564 link_text: "{{#include file.rs:10}}",
565 }]
566 );
567 }
568
569 #[test]
test_find_links_with_from_range()570 fn test_find_links_with_from_range() {
571 let s = "Some random text with {{#include file.rs:10:}}...";
572 let res = find_links(s).collect::<Vec<_>>();
573 println!("\nOUTPUT: {:?}\n", res);
574 assert_eq!(
575 res,
576 vec![Link {
577 start_index: 22,
578 end_index: 46,
579 link_type: LinkType::Include(
580 PathBuf::from("file.rs"),
581 RangeOrAnchor::Range(LineRange::from(9..))
582 ),
583 link_text: "{{#include file.rs:10:}}",
584 }]
585 );
586 }
587
588 #[test]
test_find_links_with_to_range()589 fn test_find_links_with_to_range() {
590 let s = "Some random text with {{#include file.rs::20}}...";
591 let res = find_links(s).collect::<Vec<_>>();
592 println!("\nOUTPUT: {:?}\n", res);
593 assert_eq!(
594 res,
595 vec![Link {
596 start_index: 22,
597 end_index: 46,
598 link_type: LinkType::Include(
599 PathBuf::from("file.rs"),
600 RangeOrAnchor::Range(LineRange::from(..20))
601 ),
602 link_text: "{{#include file.rs::20}}",
603 }]
604 );
605 }
606
607 #[test]
test_find_links_with_full_range()608 fn test_find_links_with_full_range() {
609 let s = "Some random text with {{#include file.rs::}}...";
610 let res = find_links(s).collect::<Vec<_>>();
611 println!("\nOUTPUT: {:?}\n", res);
612 assert_eq!(
613 res,
614 vec![Link {
615 start_index: 22,
616 end_index: 44,
617 link_type: LinkType::Include(
618 PathBuf::from("file.rs"),
619 RangeOrAnchor::Range(LineRange::from(..))
620 ),
621 link_text: "{{#include file.rs::}}",
622 }]
623 );
624 }
625
626 #[test]
test_find_links_with_no_range_specified()627 fn test_find_links_with_no_range_specified() {
628 let s = "Some random text with {{#include file.rs}}...";
629 let res = find_links(s).collect::<Vec<_>>();
630 println!("\nOUTPUT: {:?}\n", res);
631 assert_eq!(
632 res,
633 vec![Link {
634 start_index: 22,
635 end_index: 42,
636 link_type: LinkType::Include(
637 PathBuf::from("file.rs"),
638 RangeOrAnchor::Range(LineRange::from(..))
639 ),
640 link_text: "{{#include file.rs}}",
641 }]
642 );
643 }
644
645 #[test]
test_find_links_with_anchor()646 fn test_find_links_with_anchor() {
647 let s = "Some random text with {{#include file.rs:anchor}}...";
648 let res = find_links(s).collect::<Vec<_>>();
649 println!("\nOUTPUT: {:?}\n", res);
650 assert_eq!(
651 res,
652 vec![Link {
653 start_index: 22,
654 end_index: 49,
655 link_type: LinkType::Include(
656 PathBuf::from("file.rs"),
657 RangeOrAnchor::Anchor(String::from("anchor"))
658 ),
659 link_text: "{{#include file.rs:anchor}}",
660 }]
661 );
662 }
663
664 #[test]
test_find_links_escaped_link()665 fn test_find_links_escaped_link() {
666 let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
667
668 let res = find_links(s).collect::<Vec<_>>();
669 println!("\nOUTPUT: {:?}\n", res);
670
671 assert_eq!(
672 res,
673 vec![Link {
674 start_index: 41,
675 end_index: 74,
676 link_type: LinkType::Escaped,
677 link_text: "\\{{#playground file.rs editable}}",
678 }]
679 );
680 }
681
682 #[test]
test_find_playgrounds_with_properties()683 fn test_find_playgrounds_with_properties() {
684 let s =
685 "Some random text with escaped playground {{#playground file.rs editable }} and some \
686 more\n text {{#playground my.rs editable no_run should_panic}} ...";
687
688 let res = find_links(s).collect::<Vec<_>>();
689 println!("\nOUTPUT: {:?}\n", res);
690 assert_eq!(
691 res,
692 vec![
693 Link {
694 start_index: 41,
695 end_index: 74,
696 link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
697 link_text: "{{#playground file.rs editable }}",
698 },
699 Link {
700 start_index: 95,
701 end_index: 145,
702 link_type: LinkType::Playground(
703 PathBuf::from("my.rs"),
704 vec!["editable", "no_run", "should_panic"],
705 ),
706 link_text: "{{#playground my.rs editable no_run should_panic}}",
707 },
708 ]
709 );
710 }
711
712 #[test]
test_find_all_link_types()713 fn test_find_all_link_types() {
714 let s =
715 "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
716 insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
717 no_run should_panic}} ...";
718
719 let res = find_links(s).collect::<Vec<_>>();
720 println!("\nOUTPUT: {:?}\n", res);
721 assert_eq!(res.len(), 3);
722 assert_eq!(
723 res[0],
724 Link {
725 start_index: 41,
726 end_index: 61,
727 link_type: LinkType::Include(
728 PathBuf::from("file.rs"),
729 RangeOrAnchor::Range(LineRange::from(..))
730 ),
731 link_text: "{{#include file.rs}}",
732 }
733 );
734 assert_eq!(
735 res[1],
736 Link {
737 start_index: 66,
738 end_index: 115,
739 link_type: LinkType::Escaped,
740 link_text: "\\{{#contents are insignifficant in escaped link}}",
741 }
742 );
743 assert_eq!(
744 res[2],
745 Link {
746 start_index: 133,
747 end_index: 183,
748 link_type: LinkType::Playground(
749 PathBuf::from("my.rs"),
750 vec!["editable", "no_run", "should_panic"]
751 ),
752 link_text: "{{#playground my.rs editable no_run should_panic}}",
753 }
754 );
755 }
756
757 #[test]
parse_without_colon_includes_all()758 fn parse_without_colon_includes_all() {
759 let link_type = parse_include_path("arbitrary");
760 assert_eq!(
761 link_type,
762 LinkType::Include(
763 PathBuf::from("arbitrary"),
764 RangeOrAnchor::Range(LineRange::from(RangeFull))
765 )
766 );
767 }
768
769 #[test]
parse_with_nothing_after_colon_includes_all()770 fn parse_with_nothing_after_colon_includes_all() {
771 let link_type = parse_include_path("arbitrary:");
772 assert_eq!(
773 link_type,
774 LinkType::Include(
775 PathBuf::from("arbitrary"),
776 RangeOrAnchor::Range(LineRange::from(RangeFull))
777 )
778 );
779 }
780
781 #[test]
parse_with_two_colons_includes_all()782 fn parse_with_two_colons_includes_all() {
783 let link_type = parse_include_path("arbitrary::");
784 assert_eq!(
785 link_type,
786 LinkType::Include(
787 PathBuf::from("arbitrary"),
788 RangeOrAnchor::Range(LineRange::from(RangeFull))
789 )
790 );
791 }
792
793 #[test]
parse_with_garbage_after_two_colons_includes_all()794 fn parse_with_garbage_after_two_colons_includes_all() {
795 let link_type = parse_include_path("arbitrary::NaN");
796 assert_eq!(
797 link_type,
798 LinkType::Include(
799 PathBuf::from("arbitrary"),
800 RangeOrAnchor::Range(LineRange::from(RangeFull))
801 )
802 );
803 }
804
805 #[test]
parse_with_one_number_after_colon_only_that_line()806 fn parse_with_one_number_after_colon_only_that_line() {
807 let link_type = parse_include_path("arbitrary:5");
808 assert_eq!(
809 link_type,
810 LinkType::Include(
811 PathBuf::from("arbitrary"),
812 RangeOrAnchor::Range(LineRange::from(4..5))
813 )
814 );
815 }
816
817 #[test]
parse_with_one_based_start_becomes_zero_based()818 fn parse_with_one_based_start_becomes_zero_based() {
819 let link_type = parse_include_path("arbitrary:1");
820 assert_eq!(
821 link_type,
822 LinkType::Include(
823 PathBuf::from("arbitrary"),
824 RangeOrAnchor::Range(LineRange::from(0..1))
825 )
826 );
827 }
828
829 #[test]
parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error()830 fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() {
831 let link_type = parse_include_path("arbitrary:0");
832 assert_eq!(
833 link_type,
834 LinkType::Include(
835 PathBuf::from("arbitrary"),
836 RangeOrAnchor::Range(LineRange::from(0..1))
837 )
838 );
839 }
840
841 #[test]
parse_start_only_range()842 fn parse_start_only_range() {
843 let link_type = parse_include_path("arbitrary:5:");
844 assert_eq!(
845 link_type,
846 LinkType::Include(
847 PathBuf::from("arbitrary"),
848 RangeOrAnchor::Range(LineRange::from(4..))
849 )
850 );
851 }
852
853 #[test]
parse_start_with_garbage_interpreted_as_start_only_range()854 fn parse_start_with_garbage_interpreted_as_start_only_range() {
855 let link_type = parse_include_path("arbitrary:5:NaN");
856 assert_eq!(
857 link_type,
858 LinkType::Include(
859 PathBuf::from("arbitrary"),
860 RangeOrAnchor::Range(LineRange::from(4..))
861 )
862 );
863 }
864
865 #[test]
parse_end_only_range()866 fn parse_end_only_range() {
867 let link_type = parse_include_path("arbitrary::5");
868 assert_eq!(
869 link_type,
870 LinkType::Include(
871 PathBuf::from("arbitrary"),
872 RangeOrAnchor::Range(LineRange::from(..5))
873 )
874 );
875 }
876
877 #[test]
parse_start_and_end_range()878 fn parse_start_and_end_range() {
879 let link_type = parse_include_path("arbitrary:5:10");
880 assert_eq!(
881 link_type,
882 LinkType::Include(
883 PathBuf::from("arbitrary"),
884 RangeOrAnchor::Range(LineRange::from(4..10))
885 )
886 );
887 }
888
889 #[test]
parse_with_negative_interpreted_as_anchor()890 fn parse_with_negative_interpreted_as_anchor() {
891 let link_type = parse_include_path("arbitrary:-5");
892 assert_eq!(
893 link_type,
894 LinkType::Include(
895 PathBuf::from("arbitrary"),
896 RangeOrAnchor::Anchor("-5".to_string())
897 )
898 );
899 }
900
901 #[test]
parse_with_floating_point_interpreted_as_anchor()902 fn parse_with_floating_point_interpreted_as_anchor() {
903 let link_type = parse_include_path("arbitrary:-5.7");
904 assert_eq!(
905 link_type,
906 LinkType::Include(
907 PathBuf::from("arbitrary"),
908 RangeOrAnchor::Anchor("-5.7".to_string())
909 )
910 );
911 }
912
913 #[test]
parse_with_anchor_followed_by_colon()914 fn parse_with_anchor_followed_by_colon() {
915 let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored");
916 assert_eq!(
917 link_type,
918 LinkType::Include(
919 PathBuf::from("arbitrary"),
920 RangeOrAnchor::Anchor("some-anchor".to_string())
921 )
922 );
923 }
924
925 #[test]
parse_with_more_than_three_colons_ignores_everything_after_third_colon()926 fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
927 let link_type = parse_include_path("arbitrary:5:10:17:anything:");
928 assert_eq!(
929 link_type,
930 LinkType::Include(
931 PathBuf::from("arbitrary"),
932 RangeOrAnchor::Range(LineRange::from(4..10))
933 )
934 );
935 }
936 }
937