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