1use self::take_lines::{
2 take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
3 take_rustdoc_include_lines,
4};
5use anyhow::{Context, Result};
6use mdbook_core::book::{Book, BookItem};
7use mdbook_core::static_regex;
8use mdbook_core::utils::fs;
9use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
10use regex::{CaptureMatches, Captures};
11use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
12use std::path::{Path, PathBuf};
13use tracing::{error, warn};
14
15mod take_lines;
16
17const ESCAPE_CHAR: char = '\\';
18const MAX_LINK_NESTED_DEPTH: usize = 10;
19
20#[derive(Default)]
31#[non_exhaustive]
32pub struct LinkPreprocessor;
33
34impl LinkPreprocessor {
35 pub const NAME: &'static str = "links";
37
38 pub fn new() -> Self {
40 LinkPreprocessor
41 }
42}
43
44impl Preprocessor for LinkPreprocessor {
45 fn name(&self) -> &str {
46 Self::NAME
47 }
48
49 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
50 let src_dir = ctx.root.join(&ctx.config.book.src);
51
52 book.for_each_mut(|section: &mut BookItem| {
53 if let BookItem::Chapter(ref mut ch) = *section {
54 if let Some(ref chapter_path) = ch.path {
55 let base = chapter_path
56 .parent()
57 .map(|dir| src_dir.join(dir))
58 .expect("All book items have a parent");
59
60 let mut chapter_title = ch.name.clone();
61 let content =
62 replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
63 ch.content = content;
64 if chapter_title != ch.name {
65 ctx.chapter_titles
66 .borrow_mut()
67 .insert(chapter_path.clone(), chapter_title);
68 }
69 }
70 }
71 });
72
73 Ok(book)
74 }
75}
76
77fn replace_all<P1, P2>(
78 s: &str,
79 path: P1,
80 source: P2,
81 depth: usize,
82 chapter_title: &mut String,
83) -> String
84where
85 P1: AsRef<Path>,
86 P2: AsRef<Path>,
87{
88 let path = path.as_ref();
92 let source = source.as_ref();
93 let mut previous_end_index = 0;
94 let mut replaced = String::new();
95
96 for link in find_links(s) {
97 replaced.push_str(&s[previous_end_index..link.start_index]);
98
99 match link.render_with_path(path, chapter_title) {
100 Ok(new_content) => {
101 if depth < MAX_LINK_NESTED_DEPTH {
102 if let Some(rel_path) = link.link_type.relative_path(path) {
103 replaced.push_str(&replace_all(
104 &new_content,
105 rel_path,
106 source,
107 depth + 1,
108 chapter_title,
109 ));
110 } else {
111 replaced.push_str(&new_content);
112 }
113 } else {
114 error!(
115 "Stack depth exceeded in {}. Check for cyclic includes",
116 source.display()
117 );
118 }
119 previous_end_index = link.end_index;
120 }
121 Err(e) => {
122 error!("Error updating \"{}\", {}", link.link_text, e);
123 for cause in e.chain().skip(1) {
124 warn!("Caused By: {}", cause);
125 }
126
127 previous_end_index = link.start_index;
130 }
131 }
132 }
133
134 replaced.push_str(&s[previous_end_index..]);
135 replaced
136}
137
138#[derive(PartialEq, Debug, Clone)]
139enum LinkType<'a> {
140 Escaped,
141 Include(PathBuf, RangeOrAnchor),
142 Playground(PathBuf, Vec<&'a str>),
143 RustdocInclude(PathBuf, RangeOrAnchor),
144 Title(&'a str),
145}
146
147#[derive(PartialEq, Debug, Clone)]
148enum RangeOrAnchor {
149 Range(LineRange),
150 Anchor(String),
151}
152
153#[derive(PartialEq, Debug, Clone)]
155enum LineRange {
156 Range(Range<usize>),
157 RangeFrom(RangeFrom<usize>),
158 RangeTo(RangeTo<usize>),
159 RangeFull(RangeFull),
160}
161
162impl RangeBounds<usize> for LineRange {
163 fn start_bound(&self) -> Bound<&usize> {
164 match self {
165 LineRange::Range(r) => r.start_bound(),
166 LineRange::RangeFrom(r) => r.start_bound(),
167 LineRange::RangeTo(r) => r.start_bound(),
168 LineRange::RangeFull(r) => r.start_bound(),
169 }
170 }
171
172 fn end_bound(&self) -> Bound<&usize> {
173 match self {
174 LineRange::Range(r) => r.end_bound(),
175 LineRange::RangeFrom(r) => r.end_bound(),
176 LineRange::RangeTo(r) => r.end_bound(),
177 LineRange::RangeFull(r) => r.end_bound(),
178 }
179 }
180}
181
182impl From<Range<usize>> for LineRange {
183 fn from(r: Range<usize>) -> LineRange {
184 LineRange::Range(r)
185 }
186}
187
188impl From<RangeFrom<usize>> for LineRange {
189 fn from(r: RangeFrom<usize>) -> LineRange {
190 LineRange::RangeFrom(r)
191 }
192}
193
194impl From<RangeTo<usize>> for LineRange {
195 fn from(r: RangeTo<usize>) -> LineRange {
196 LineRange::RangeTo(r)
197 }
198}
199
200impl From<RangeFull> for LineRange {
201 fn from(r: RangeFull) -> LineRange {
202 LineRange::RangeFull(r)
203 }
204}
205
206impl<'a> LinkType<'a> {
207 fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
208 let base = base.as_ref();
209 match self {
210 LinkType::Escaped => None,
211 LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
212 LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
213 LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
214 LinkType::Title(_) => None,
215 }
216 }
217}
218fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
219 base.as_ref()
220 .join(relative)
221 .parent()
222 .expect("Included file should not be /")
223 .to_path_buf()
224}
225
226fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
227 let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
228
229 let next_element = parts.next();
230 let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
231 Some(value.saturating_sub(1))
233 } else if let Some("") = next_element {
234 None
235 } else if let Some(anchor) = next_element {
236 return RangeOrAnchor::Anchor(String::from(anchor));
237 } else {
238 None
239 };
240
241 let end = parts.next();
242 let end = end.map(|s| s.parse::<usize>());
246
247 match (start, end) {
248 (Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
249 (Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
250 (Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
251 (None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
252 (None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
253 }
254}
255
256fn parse_include_path(path: &str) -> LinkType<'static> {
257 let mut parts = path.splitn(2, ':');
258
259 let path = parts.next().unwrap().into();
260 let range_or_anchor = parse_range_or_anchor(parts.next());
261
262 LinkType::Include(path, range_or_anchor)
263}
264
265fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
266 let mut parts = path.splitn(2, ':');
267
268 let path = parts.next().unwrap().into();
269 let range_or_anchor = parse_range_or_anchor(parts.next());
270
271 LinkType::RustdocInclude(path, range_or_anchor)
272}
273
274#[derive(PartialEq, Debug, Clone)]
275struct Link<'a> {
276 start_index: usize,
277 end_index: usize,
278 link_type: LinkType<'a>,
279 link_text: &'a str,
280}
281
282impl<'a> Link<'a> {
283 fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
284 let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
285 (_, Some(typ), Some(title)) if typ.as_str() == "title" => {
286 Some(LinkType::Title(title.as_str()))
287 }
288 (_, Some(typ), Some(rest)) => {
289 let mut path_props = rest.as_str().split_whitespace();
290 let file_arg = path_props.next();
291 let props: Vec<&str> = path_props.collect();
292
293 match (typ.as_str(), file_arg) {
294 ("include", Some(pth)) => Some(parse_include_path(pth)),
295 ("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
296 ("playpen", Some(pth)) => {
297 warn!(
298 "the {{{{#playpen}}}} expression has been \
299 renamed to {{{{#playground}}}}, \
300 please update your book to use the new name"
301 );
302 Some(LinkType::Playground(pth.into(), props))
303 }
304 ("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
305 _ => None,
306 }
307 }
308 (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
309 Some(LinkType::Escaped)
310 }
311 _ => None,
312 };
313
314 link_type.and_then(|lnk_type| {
315 cap.get(0).map(|mat| Link {
316 start_index: mat.start(),
317 end_index: mat.end(),
318 link_type: lnk_type,
319 link_text: mat.as_str(),
320 })
321 })
322 }
323
324 fn render_with_path<P: AsRef<Path>>(
325 &self,
326 base: P,
327 chapter_title: &mut String,
328 ) -> Result<String> {
329 let base = base.as_ref();
330 match self.link_type {
331 LinkType::Escaped => Ok(self.link_text[1..].to_owned()),
333 LinkType::Include(ref pat, ref range_or_anchor) => {
334 let target = base.join(pat);
335
336 fs::read_to_string(&target)
337 .map(|s| match range_or_anchor {
338 RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
339 RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
340 })
341 .with_context(|| {
342 format!(
343 "Could not read file for link {} ({})",
344 self.link_text,
345 target.display(),
346 )
347 })
348 }
349 LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
350 let target = base.join(pat);
351
352 fs::read_to_string(&target)
353 .map(|s| match range_or_anchor {
354 RangeOrAnchor::Range(range) => {
355 take_rustdoc_include_lines(&s, range.clone())
356 }
357 RangeOrAnchor::Anchor(anchor) => {
358 take_rustdoc_include_anchored_lines(&s, anchor)
359 }
360 })
361 .with_context(|| {
362 format!(
363 "Could not read file for link {} ({})",
364 self.link_text,
365 target.display(),
366 )
367 })
368 }
369 LinkType::Playground(ref pat, ref attrs) => {
370 let target = base.join(pat);
371
372 let mut contents = fs::read_to_string(&target).with_context(|| {
373 format!(
374 "Could not read file for link {} ({})",
375 self.link_text,
376 target.display()
377 )
378 })?;
379 let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
380 if !contents.ends_with('\n') {
381 contents.push('\n');
382 }
383 Ok(format!(
384 "```{}{}\n{}```\n",
385 ftype,
386 attrs.join(","),
387 contents
388 ))
389 }
390 LinkType::Title(title) => {
391 *chapter_title = title.to_owned();
392 Ok(String::new())
393 }
394 }
395 }
396}
397
398struct LinkIter<'a>(CaptureMatches<'a, 'a>);
399
400impl<'a> Iterator for LinkIter<'a> {
401 type Item = Link<'a>;
402 fn next(&mut self) -> Option<Link<'a>> {
403 for cap in &mut self.0 {
404 if let Some(inc) = Link::from_capture(cap) {
405 return Some(inc);
406 }
407 }
408 None
409 }
410}
411
412fn find_links(contents: &str) -> LinkIter<'_> {
413 static_regex!(
414 LINK,
415 r"(?x) # insignificant whitespace mode
416 \\\{\{\#.*\}\} # match escaped link
417 | # or
418 \{\{\s* # link opening parens and whitespace
419 \#([a-zA-Z0-9_]+) # link type
420 \s+ # separating whitespace
421 ([^}]+) # link target path and space separated properties
422 \}\} # link closing parens"
423 );
424
425 LinkIter(LINK.captures_iter(contents))
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_replace_all_escaped() {
434 let start = r"
435 Some text over here.
436 ```hbs
437 \{{#include file.rs}} << an escaped link!
438 ```";
439 let end = r"
440 Some text over here.
441 ```hbs
442 {{#include file.rs}} << an escaped link!
443 ```";
444 let mut chapter_title = "test_replace_all_escaped".to_owned();
445 assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
446 }
447
448 #[test]
449 fn test_set_chapter_title() {
450 let start = r"{{#title My Title}}
451 # My Chapter
452 ";
453 let end = r"
454 # My Chapter
455 ";
456 let mut chapter_title = "test_set_chapter_title".to_owned();
457 assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
458 assert_eq!(chapter_title, "My Title");
459 }
460
461 #[test]
462 fn test_find_links_no_link() {
463 let s = "Some random text without link...";
464 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
465 }
466
467 #[test]
468 fn test_find_links_partial_link() {
469 let s = "Some random text with {{#playground...";
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 let s = "Some random text with \\{{#include...";
474 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
475 }
476
477 #[test]
478 fn test_find_links_empty_link() {
479 let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
480 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
481 }
482
483 #[test]
484 fn test_find_links_unknown_link_type() {
485 let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
486 assert!(find_links(s).collect::<Vec<_>>() == vec![]);
487 }
488
489 #[test]
490 fn test_find_links_simple_link() {
491 let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
492
493 let res = find_links(s).collect::<Vec<_>>();
494 println!("\nOUTPUT: {res:?}\n");
495
496 assert_eq!(
497 res,
498 vec![
499 Link {
500 start_index: 22,
501 end_index: 45,
502 link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
503 link_text: "{{#playground file.rs}}",
504 },
505 Link {
506 start_index: 50,
507 end_index: 74,
508 link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
509 link_text: "{{#playground test.rs }}",
510 },
511 ]
512 );
513 }
514
515 #[test]
516 fn test_find_links_with_special_characters() {
517 let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
518
519 let res = find_links(s).collect::<Vec<_>>();
520 println!("\nOUTPUT: {res:?}\n");
521
522 assert_eq!(
523 res,
524 vec![Link {
525 start_index: 22,
526 end_index: 57,
527 link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
528 link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
529 },]
530 );
531 }
532
533 #[test]
534 fn test_find_links_with_range() {
535 let s = "Some random text with {{#include file.rs:10:20}}...";
536 let res = find_links(s).collect::<Vec<_>>();
537 println!("\nOUTPUT: {res:?}\n");
538 assert_eq!(
539 res,
540 vec![Link {
541 start_index: 22,
542 end_index: 48,
543 link_type: LinkType::Include(
544 PathBuf::from("file.rs"),
545 RangeOrAnchor::Range(LineRange::from(9..20))
546 ),
547 link_text: "{{#include file.rs:10:20}}",
548 }]
549 );
550 }
551
552 #[test]
553 fn test_find_links_with_line_number() {
554 let s = "Some random text with {{#include file.rs:10}}...";
555 let res = find_links(s).collect::<Vec<_>>();
556 println!("\nOUTPUT: {res:?}\n");
557 assert_eq!(
558 res,
559 vec![Link {
560 start_index: 22,
561 end_index: 45,
562 link_type: LinkType::Include(
563 PathBuf::from("file.rs"),
564 RangeOrAnchor::Range(LineRange::from(9..10))
565 ),
566 link_text: "{{#include file.rs:10}}",
567 }]
568 );
569 }
570
571 #[test]
572 fn test_find_links_with_from_range() {
573 let s = "Some random text with {{#include file.rs:10:}}...";
574 let res = find_links(s).collect::<Vec<_>>();
575 println!("\nOUTPUT: {res:?}\n");
576 assert_eq!(
577 res,
578 vec![Link {
579 start_index: 22,
580 end_index: 46,
581 link_type: LinkType::Include(
582 PathBuf::from("file.rs"),
583 RangeOrAnchor::Range(LineRange::from(9..))
584 ),
585 link_text: "{{#include file.rs:10:}}",
586 }]
587 );
588 }
589
590 #[test]
591 fn test_find_links_with_to_range() {
592 let s = "Some random text with {{#include file.rs::20}}...";
593 let res = find_links(s).collect::<Vec<_>>();
594 println!("\nOUTPUT: {res:?}\n");
595 assert_eq!(
596 res,
597 vec![Link {
598 start_index: 22,
599 end_index: 46,
600 link_type: LinkType::Include(
601 PathBuf::from("file.rs"),
602 RangeOrAnchor::Range(LineRange::from(..20))
603 ),
604 link_text: "{{#include file.rs::20}}",
605 }]
606 );
607 }
608
609 #[test]
610 fn test_find_links_with_full_range() {
611 let s = "Some random text with {{#include file.rs::}}...";
612 let res = find_links(s).collect::<Vec<_>>();
613 println!("\nOUTPUT: {res:?}\n");
614 assert_eq!(
615 res,
616 vec![Link {
617 start_index: 22,
618 end_index: 44,
619 link_type: LinkType::Include(
620 PathBuf::from("file.rs"),
621 RangeOrAnchor::Range(LineRange::from(..))
622 ),
623 link_text: "{{#include file.rs::}}",
624 }]
625 );
626 }
627
628 #[test]
629 fn test_find_links_with_no_range_specified() {
630 let s = "Some random text with {{#include file.rs}}...";
631 let res = find_links(s).collect::<Vec<_>>();
632 println!("\nOUTPUT: {res:?}\n");
633 assert_eq!(
634 res,
635 vec![Link {
636 start_index: 22,
637 end_index: 42,
638 link_type: LinkType::Include(
639 PathBuf::from("file.rs"),
640 RangeOrAnchor::Range(LineRange::from(..))
641 ),
642 link_text: "{{#include file.rs}}",
643 }]
644 );
645 }
646
647 #[test]
648 fn test_find_links_with_anchor() {
649 let s = "Some random text with {{#include file.rs:anchor}}...";
650 let res = find_links(s).collect::<Vec<_>>();
651 println!("\nOUTPUT: {res:?}\n");
652 assert_eq!(
653 res,
654 vec![Link {
655 start_index: 22,
656 end_index: 49,
657 link_type: LinkType::Include(
658 PathBuf::from("file.rs"),
659 RangeOrAnchor::Anchor(String::from("anchor"))
660 ),
661 link_text: "{{#include file.rs:anchor}}",
662 }]
663 );
664 }
665
666 #[test]
667 fn test_find_links_escaped_link() {
668 let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
669
670 let res = find_links(s).collect::<Vec<_>>();
671 println!("\nOUTPUT: {res:?}\n");
672
673 assert_eq!(
674 res,
675 vec![Link {
676 start_index: 41,
677 end_index: 74,
678 link_type: LinkType::Escaped,
679 link_text: "\\{{#playground file.rs editable}}",
680 }]
681 );
682 }
683
684 #[test]
685 fn test_find_playgrounds_with_properties() {
686 let s = "Some random text with escaped playground {{#playground file.rs editable }} and some \
687 more\n text {{#playground my.rs editable no_run should_panic}} ...";
688
689 let res = find_links(s).collect::<Vec<_>>();
690 println!("\nOUTPUT: {res:?}\n");
691 assert_eq!(
692 res,
693 vec![
694 Link {
695 start_index: 41,
696 end_index: 74,
697 link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
698 link_text: "{{#playground file.rs editable }}",
699 },
700 Link {
701 start_index: 95,
702 end_index: 145,
703 link_type: LinkType::Playground(
704 PathBuf::from("my.rs"),
705 vec!["editable", "no_run", "should_panic"],
706 ),
707 link_text: "{{#playground my.rs editable no_run should_panic}}",
708 },
709 ]
710 );
711 }
712
713 #[test]
714 fn test_find_all_link_types() {
715 let s = "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: {res:?}\n");
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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]
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}