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 | use log::{error, warn}; |
14 | use once_cell::sync::Lazy; |
15 | |
16 | const ESCAPE_CHAR: char = ' \\' ; |
17 | const 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)] |
30 | pub struct LinkPreprocessor; |
31 | |
32 | impl 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 | |
41 | impl 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 | |
74 | fn replace_all<P1, P2>( |
75 | s: &str, |
76 | path: P1, |
77 | source: P2, |
78 | depth: usize, |
79 | chapter_title: &mut String, |
80 | ) -> String |
81 | where |
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)] |
136 | enum 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)] |
145 | enum 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)] |
153 | enum LineRange { |
154 | Range(Range<usize>), |
155 | RangeFrom(RangeFrom<usize>), |
156 | RangeTo(RangeTo<usize>), |
157 | RangeFull(RangeFull), |
158 | } |
159 | |
160 | impl 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 | |
180 | impl From<Range<usize>> for LineRange { |
181 | fn from(r: Range<usize>) -> LineRange { |
182 | LineRange::Range(r) |
183 | } |
184 | } |
185 | |
186 | impl From<RangeFrom<usize>> for LineRange { |
187 | fn from(r: RangeFrom<usize>) -> LineRange { |
188 | LineRange::RangeFrom(r) |
189 | } |
190 | } |
191 | |
192 | impl From<RangeTo<usize>> for LineRange { |
193 | fn from(r: RangeTo<usize>) -> LineRange { |
194 | LineRange::RangeTo(r) |
195 | } |
196 | } |
197 | |
198 | impl From<RangeFull> for LineRange { |
199 | fn from(r: RangeFull) -> LineRange { |
200 | LineRange::RangeFull(r) |
201 | } |
202 | } |
203 | |
204 | impl<'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 | } |
216 | fn 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 | |
224 | fn 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 | |
254 | fn 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 | |
263 | fn 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)] |
273 | struct Link<'a> { |
274 | start_index: usize, |
275 | end_index: usize, |
276 | link_type: LinkType<'a>, |
277 | link_text: &'a str, |
278 | } |
279 | |
280 | impl<'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 | |
396 | struct LinkIter<'a>(CaptureMatches<'a, 'a>); |
397 | |
398 | impl<'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 | |
410 | fn 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 |
416 | re: \\\{\{\#.*\}\} # match escaped link |
417 | re: | # or |
418 | re: \{\{\s* # link opening parens and whitespace |
419 | re: \#([a-zA-Z0-9_]+) # link type |
420 | re: \s+ # separating whitespace |
421 | re: ([^}]+) # link target path and space separated properties |
422 | re: \}\} # link closing parens" , |
423 | ) |
424 | .unwrap() |
425 | }); |
426 | |
427 | LinkIter(RE.captures_iter(text:contents)) |
428 | } |
429 | |
430 | #[cfg (test)] |
431 | mod 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 | |