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