]>
Commit | Line | Data |
---|---|---|
dc9dc135 | 1 | use crate::errors::*; |
e74abb32 XL |
2 | use crate::utils::{ |
3 | take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines, | |
4 | take_rustdoc_include_lines, | |
5 | }; | |
9fa01778 | 6 | use regex::{CaptureMatches, Captures, Regex}; |
416331ca | 7 | use std::fs; |
e74abb32 | 8 | use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo}; |
ea8adc8c | 9 | use std::path::{Path, PathBuf}; |
ea8adc8c | 10 | |
2c00a5a8 | 11 | use super::{Preprocessor, PreprocessorContext}; |
dc9dc135 | 12 | use crate::book::{Book, BookItem}; |
f25598a0 FG |
13 | use log::{error, warn}; |
14 | use once_cell::sync::Lazy; | |
2c00a5a8 | 15 | |
ea8adc8c | 16 | const ESCAPE_CHAR: char = '\\'; |
9fa01778 | 17 | const MAX_LINK_NESTED_DEPTH: usize = 10; |
ea8adc8c | 18 | |
e74abb32 XL |
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. | |
f035d41b | 27 | /// - `{{# playground}}` - Insert runnable Rust files |
94222f64 | 28 | /// - `{{# title}}` - Override \<title\> of a webpage. |
dc9dc135 | 29 | #[derive(Default)] |
2c00a5a8 XL |
30 | pub struct LinkPreprocessor; |
31 | ||
32 | impl LinkPreprocessor { | |
9fa01778 XL |
33 | pub(crate) const NAME: &'static str = "links"; |
34 | ||
2c00a5a8 XL |
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 { | |
9fa01778 | 43 | Self::NAME |
2c00a5a8 XL |
44 | } |
45 | ||
9fa01778 | 46 | fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { |
2c00a5a8 XL |
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 { | |
f035d41b XL |
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 | ||
94222f64 XL |
57 | let mut chapter_title = ch.name.clone(); |
58 | let content = | |
59 | replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title); | |
f035d41b | 60 | ch.content = content; |
94222f64 XL |
61 | if chapter_title != ch.name { |
62 | ctx.chapter_titles | |
63 | .borrow_mut() | |
64 | .insert(chapter_path.clone(), chapter_title); | |
65 | } | |
f035d41b | 66 | } |
2c00a5a8 XL |
67 | } |
68 | }); | |
69 | ||
9fa01778 | 70 | Ok(book) |
2c00a5a8 XL |
71 | } |
72 | } | |
73 | ||
94222f64 XL |
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 | |
9fa01778 XL |
81 | where |
82 | P1: AsRef<Path>, | |
83 | P2: AsRef<Path>, | |
84 | { | |
ea8adc8c XL |
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 | |
2c00a5a8 | 88 | let path = path.as_ref(); |
9fa01778 | 89 | let source = source.as_ref(); |
ea8adc8c XL |
90 | let mut previous_end_index = 0; |
91 | let mut replaced = String::new(); | |
92 | ||
416331ca XL |
93 | for link in find_links(s) { |
94 | replaced.push_str(&s[previous_end_index..link.start_index]); | |
2c00a5a8 | 95 | |
94222f64 | 96 | match link.render_with_path(&path, chapter_title) { |
2c00a5a8 | 97 | Ok(new_content) => { |
9fa01778 | 98 | if depth < MAX_LINK_NESTED_DEPTH { |
416331ca | 99 | if let Some(rel_path) = link.link_type.relative_path(path) { |
94222f64 XL |
100 | replaced.push_str(&replace_all( |
101 | &new_content, | |
102 | rel_path, | |
103 | source, | |
104 | depth + 1, | |
105 | chapter_title, | |
106 | )); | |
9fa01778 XL |
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 | } | |
416331ca | 116 | previous_end_index = link.end_index; |
2c00a5a8 XL |
117 | } |
118 | Err(e) => { | |
416331ca | 119 | error!("Error updating \"{}\", {}", link.link_text, e); |
f035d41b | 120 | for cause in e.chain().skip(1) { |
9fa01778 XL |
121 | warn!("Caused By: {}", cause); |
122 | } | |
123 | ||
2c00a5a8 XL |
124 | // This should make sure we include the raw `{{# ... }}` snippet |
125 | // in the page content if there are any errors. | |
416331ca | 126 | previous_end_index = link.start_index; |
2c00a5a8 XL |
127 | } |
128 | } | |
ea8adc8c XL |
129 | } |
130 | ||
131 | replaced.push_str(&s[previous_end_index..]); | |
2c00a5a8 | 132 | replaced |
ea8adc8c XL |
133 | } |
134 | ||
2c00a5a8 | 135 | #[derive(PartialEq, Debug, Clone)] |
ea8adc8c XL |
136 | enum LinkType<'a> { |
137 | Escaped, | |
e74abb32 | 138 | Include(PathBuf, RangeOrAnchor), |
f035d41b | 139 | Playground(PathBuf, Vec<&'a str>), |
e74abb32 | 140 | RustdocInclude(PathBuf, RangeOrAnchor), |
94222f64 | 141 | Title(&'a str), |
e74abb32 XL |
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. | |
064997fb | 151 | #[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type |
e74abb32 XL |
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) => r.start_bound(), | |
164 | LineRange::RangeFrom(r) => r.start_bound(), | |
165 | LineRange::RangeTo(r) => r.start_bound(), | |
166 | LineRange::RangeFull(r) => r.start_bound(), | |
167 | } | |
168 | } | |
169 | ||
170 | fn end_bound(&self) -> Bound<&usize> { | |
171 | match self { | |
172 | LineRange::Range(r) => r.end_bound(), | |
173 | LineRange::RangeFrom(r) => r.end_bound(), | |
174 | LineRange::RangeTo(r) => r.end_bound(), | |
175 | LineRange::RangeFull(r) => 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 | } | |
ea8adc8c XL |
202 | } |
203 | ||
9fa01778 XL |
204 | impl<'a> LinkType<'a> { |
205 | fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> { | |
206 | let base = base.as_ref(); | |
207 | match self { | |
208 | LinkType::Escaped => None, | |
e74abb32 | 209 | LinkType::Include(p, _) => Some(return_relative_path(base, &p)), |
f035d41b | 210 | LinkType::Playground(p, _) => Some(return_relative_path(base, &p)), |
e74abb32 | 211 | LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)), |
94222f64 | 212 | LinkType::Title(_) => None, |
9fa01778 XL |
213 | } |
214 | } | |
215 | } | |
216 | fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf { | |
217 | base.as_ref() | |
218 | .join(relative) | |
219 | .parent() | |
220 | .expect("Included file should not be /") | |
221 | .to_path_buf() | |
222 | } | |
223 | ||
e74abb32 XL |
224 | fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor { |
225 | let mut parts = parts.unwrap_or("").splitn(3, ':').fuse(); | |
416331ca XL |
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)) | |
e74abb32 XL |
231 | } else if let Some("") = next_element { |
232 | None | |
416331ca | 233 | } else if let Some(anchor) = next_element { |
e74abb32 | 234 | return RangeOrAnchor::Anchor(String::from(anchor)); |
416331ca XL |
235 | } else { |
236 | None | |
237 | }; | |
238 | ||
2c00a5a8 | 239 | let end = parts.next(); |
e74abb32 XL |
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)), | |
2c00a5a8 XL |
251 | } |
252 | } | |
253 | ||
e74abb32 XL |
254 | fn parse_include_path(path: &str) -> LinkType<'static> { |
255 | let mut parts = path.splitn(2, ':'); | |
256 | ||
257 | let path = parts.next().unwrap().into(); | |
258 | let range_or_anchor = parse_range_or_anchor(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 = path.splitn(2, ':'); | |
265 | ||
266 | let path = parts.next().unwrap().into(); | |
267 | let range_or_anchor = parse_range_or_anchor(parts.next()); | |
268 | ||
269 | LinkType::RustdocInclude(path, range_or_anchor) | |
270 | } | |
271 | ||
2c00a5a8 | 272 | #[derive(PartialEq, Debug, Clone)] |
ea8adc8c XL |
273 | struct Link<'a> { |
274 | start_index: usize, | |
275 | end_index: usize, | |
416331ca | 276 | link_type: LinkType<'a>, |
ea8adc8c XL |
277 | link_text: &'a str, |
278 | } | |
279 | ||
280 | impl<'a> Link<'a> { | |
281 | fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> { | |
ea8adc8c | 282 | let link_type = match (cap.get(0), cap.get(1), cap.get(2)) { |
94222f64 XL |
283 | (_, Some(typ), Some(title)) if typ.as_str() == "title" => { |
284 | Some(LinkType::Title(title.as_str())) | |
285 | } | |
ea8adc8c XL |
286 | (_, Some(typ), Some(rest)) => { |
287 | let mut path_props = rest.as_str().split_whitespace(); | |
2c00a5a8 | 288 | let file_arg = path_props.next(); |
ea8adc8c XL |
289 | let props: Vec<&str> = path_props.collect(); |
290 | ||
2c00a5a8 XL |
291 | match (typ.as_str(), file_arg) { |
292 | ("include", Some(pth)) => Some(parse_include_path(pth)), | |
f035d41b XL |
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 | } | |
e74abb32 | 302 | ("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)), |
ea8adc8c XL |
303 | _ => None, |
304 | } | |
2c00a5a8 XL |
305 | } |
306 | (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => { | |
307 | Some(LinkType::Escaped) | |
308 | } | |
ea8adc8c XL |
309 | _ => None, |
310 | }; | |
311 | ||
416331ca | 312 | link_type.and_then(|lnk_type| { |
2c00a5a8 XL |
313 | cap.get(0).map(|mat| Link { |
314 | start_index: mat.start(), | |
315 | end_index: mat.end(), | |
416331ca | 316 | link_type: lnk_type, |
2c00a5a8 XL |
317 | link_text: mat.as_str(), |
318 | }) | |
ea8adc8c XL |
319 | }) |
320 | } | |
321 | ||
94222f64 XL |
322 | fn render_with_path<P: AsRef<Path>>( |
323 | &self, | |
324 | base: P, | |
325 | chapter_title: &mut String, | |
326 | ) -> Result<String> { | |
ea8adc8c | 327 | let base = base.as_ref(); |
416331ca | 328 | match self.link_type { |
ea8adc8c XL |
329 | // omit the escape char |
330 | LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), | |
e74abb32 | 331 | LinkType::Include(ref pat, ref range_or_anchor) => { |
9fa01778 XL |
332 | let target = base.join(pat); |
333 | ||
416331ca | 334 | fs::read_to_string(&target) |
e74abb32 XL |
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), | |
9fa01778 | 338 | }) |
f035d41b | 339 | .with_context(|| { |
9fa01778 XL |
340 | format!( |
341 | "Could not read file for link {} ({})", | |
342 | self.link_text, | |
343 | target.display(), | |
344 | ) | |
345 | }) | |
346 | } | |
e74abb32 | 347 | LinkType::RustdocInclude(ref pat, ref range_or_anchor) => { |
9fa01778 XL |
348 | let target = base.join(pat); |
349 | ||
416331ca | 350 | fs::read_to_string(&target) |
e74abb32 XL |
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 | } | |
9fa01778 | 358 | }) |
f035d41b | 359 | .with_context(|| { |
416331ca XL |
360 | format!( |
361 | "Could not read file for link {} ({})", | |
362 | self.link_text, | |
363 | target.display(), | |
364 | ) | |
365 | }) | |
366 | } | |
f035d41b | 367 | LinkType::Playground(ref pat, ref attrs) => { |
9fa01778 XL |
368 | let target = base.join(pat); |
369 | ||
5869c6ff | 370 | let mut contents = fs::read_to_string(&target).with_context(|| { |
9fa01778 XL |
371 | format!( |
372 | "Could not read file for link {} ({})", | |
373 | self.link_text, | |
374 | target.display() | |
375 | ) | |
376 | })?; | |
ea8adc8c | 377 | let ftype = if !attrs.is_empty() { "rust," } else { "rust" }; |
5869c6ff XL |
378 | if !contents.ends_with('\n') { |
379 | contents.push('\n'); | |
380 | } | |
2c00a5a8 | 381 | Ok(format!( |
5869c6ff | 382 | "```{}{}\n{}```\n", |
2c00a5a8 XL |
383 | ftype, |
384 | attrs.join(","), | |
385 | contents | |
386 | )) | |
387 | } | |
94222f64 XL |
388 | LinkType::Title(title) => { |
389 | *chapter_title = title.to_owned(); | |
390 | Ok(String::new()) | |
391 | } | |
ea8adc8c XL |
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 in &mut self.0 { | |
402 | if let Some(inc) = Link::from_capture(cap) { | |
403 | return Some(inc); | |
404 | } | |
405 | } | |
406 | None | |
407 | } | |
408 | } | |
409 | ||
dc9dc135 | 410 | fn find_links(contents: &str) -> LinkIter<'_> { |
ea8adc8c | 411 | // lazily compute following regex |
94222f64 | 412 | // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?; |
f25598a0 FG |
413 | static RE: Lazy<Regex> = Lazy::new(|| { |
414 | Regex::new( | |
94222f64 | 415 | r"(?x) # insignificant whitespace mode |
f25598a0 FG |
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", | |
dc9dc135 | 423 | ) |
f25598a0 FG |
424 | .unwrap() |
425 | }); | |
426 | ||
ea8adc8c XL |
427 | LinkIter(RE.captures_iter(contents)) |
428 | } | |
429 | ||
2c00a5a8 XL |
430 | #[cfg(test)] |
431 | mod tests { | |
432 | use super::*; | |
ea8adc8c | 433 | |
9fa01778 XL |
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 | ```"; | |
94222f64 XL |
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"); | |
9fa01778 XL |
461 | } |
462 | ||
2c00a5a8 XL |
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 | } | |
ea8adc8c | 468 | |
2c00a5a8 XL |
469 | #[test] |
470 | fn test_find_links_partial_link() { | |
f035d41b | 471 | let s = "Some random text with {{#playground..."; |
2c00a5a8 XL |
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 | } | |
ea8adc8c | 478 | |
2c00a5a8 XL |
479 | #[test] |
480 | fn test_find_links_empty_link() { | |
f035d41b | 481 | let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}..."; |
2c00a5a8 XL |
482 | assert!(find_links(s).collect::<Vec<_>>() == vec![]); |
483 | } | |
ea8adc8c | 484 | |
2c00a5a8 XL |
485 | #[test] |
486 | fn test_find_links_unknown_link_type() { | |
f035d41b | 487 | let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}..."; |
2c00a5a8 XL |
488 | assert!(find_links(s).collect::<Vec<_>>() == vec![]); |
489 | } | |
ea8adc8c | 490 | |
2c00a5a8 XL |
491 | #[test] |
492 | fn test_find_links_simple_link() { | |
f035d41b | 493 | let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}..."; |
ea8adc8c | 494 | |
2c00a5a8 XL |
495 | let res = find_links(s).collect::<Vec<_>>(); |
496 | println!("\nOUTPUT: {:?}\n", res); | |
ea8adc8c | 497 | |
2c00a5a8 XL |
498 | assert_eq!( |
499 | res, | |
500 | vec![ | |
501 | Link { | |
502 | start_index: 22, | |
f035d41b XL |
503 | end_index: 45, |
504 | link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]), | |
505 | link_text: "{{#playground file.rs}}", | |
2c00a5a8 XL |
506 | }, |
507 | Link { | |
f035d41b XL |
508 | start_index: 50, |
509 | end_index: 74, | |
510 | link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]), | |
511 | link_text: "{{#playground test.rs }}", | |
2c00a5a8 XL |
512 | }, |
513 | ] | |
514 | ); | |
515 | } | |
ea8adc8c | 516 | |
f035d41b XL |
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 | ||
2c00a5a8 XL |
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, | |
9fa01778 XL |
542 | vec![Link { |
543 | start_index: 22, | |
544 | end_index: 48, | |
e74abb32 XL |
545 | link_type: LinkType::Include( |
546 | PathBuf::from("file.rs"), | |
547 | RangeOrAnchor::Range(LineRange::from(9..20)) | |
548 | ), | |
9fa01778 XL |
549 | link_text: "{{#include file.rs:10:20}}", |
550 | }] | |
2c00a5a8 XL |
551 | ); |
552 | } | |
ea8adc8c | 553 | |
2c00a5a8 XL |
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, | |
9fa01778 XL |
561 | vec![Link { |
562 | start_index: 22, | |
563 | end_index: 45, | |
e74abb32 XL |
564 | link_type: LinkType::Include( |
565 | PathBuf::from("file.rs"), | |
566 | RangeOrAnchor::Range(LineRange::from(9..10)) | |
567 | ), | |
9fa01778 XL |
568 | link_text: "{{#include file.rs:10}}", |
569 | }] | |
2c00a5a8 XL |
570 | ); |
571 | } | |
ea8adc8c | 572 | |
2c00a5a8 XL |
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, | |
9fa01778 XL |
580 | vec![Link { |
581 | start_index: 22, | |
582 | end_index: 46, | |
e74abb32 XL |
583 | link_type: LinkType::Include( |
584 | PathBuf::from("file.rs"), | |
585 | RangeOrAnchor::Range(LineRange::from(9..)) | |
586 | ), | |
9fa01778 XL |
587 | link_text: "{{#include file.rs:10:}}", |
588 | }] | |
2c00a5a8 XL |
589 | ); |
590 | } | |
ea8adc8c | 591 | |
2c00a5a8 XL |
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, | |
9fa01778 XL |
599 | vec![Link { |
600 | start_index: 22, | |
601 | end_index: 46, | |
e74abb32 XL |
602 | link_type: LinkType::Include( |
603 | PathBuf::from("file.rs"), | |
604 | RangeOrAnchor::Range(LineRange::from(..20)) | |
605 | ), | |
9fa01778 XL |
606 | link_text: "{{#include file.rs::20}}", |
607 | }] | |
2c00a5a8 XL |
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, | |
9fa01778 XL |
618 | vec![Link { |
619 | start_index: 22, | |
620 | end_index: 44, | |
e74abb32 XL |
621 | link_type: LinkType::Include( |
622 | PathBuf::from("file.rs"), | |
623 | RangeOrAnchor::Range(LineRange::from(..)) | |
624 | ), | |
9fa01778 XL |
625 | link_text: "{{#include file.rs::}}", |
626 | }] | |
2c00a5a8 XL |
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, | |
9fa01778 XL |
637 | vec![Link { |
638 | start_index: 22, | |
639 | end_index: 42, | |
e74abb32 XL |
640 | link_type: LinkType::Include( |
641 | PathBuf::from("file.rs"), | |
642 | RangeOrAnchor::Range(LineRange::from(..)) | |
643 | ), | |
9fa01778 XL |
644 | link_text: "{{#include file.rs}}", |
645 | }] | |
2c00a5a8 XL |
646 | ); |
647 | } | |
648 | ||
416331ca XL |
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, | |
e74abb32 | 659 | link_type: LinkType::Include( |
416331ca | 660 | PathBuf::from("file.rs"), |
e74abb32 | 661 | RangeOrAnchor::Anchor(String::from("anchor")) |
416331ca XL |
662 | ), |
663 | link_text: "{{#include file.rs:anchor}}", | |
664 | }] | |
665 | ); | |
666 | } | |
667 | ||
2c00a5a8 XL |
668 | #[test] |
669 | fn test_find_links_escaped_link() { | |
f035d41b | 670 | let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ..."; |
2c00a5a8 XL |
671 | |
672 | let res = find_links(s).collect::<Vec<_>>(); | |
673 | println!("\nOUTPUT: {:?}\n", res); | |
674 | ||
675 | assert_eq!( | |
676 | res, | |
9fa01778 | 677 | vec![Link { |
f035d41b XL |
678 | start_index: 41, |
679 | end_index: 74, | |
416331ca | 680 | link_type: LinkType::Escaped, |
f035d41b | 681 | link_text: "\\{{#playground file.rs editable}}", |
9fa01778 | 682 | }] |
2c00a5a8 XL |
683 | ); |
684 | } | |
685 | ||
686 | #[test] | |
f035d41b XL |
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}} ..."; | |
2c00a5a8 XL |
691 | |
692 | let res = find_links(s).collect::<Vec<_>>(); | |
693 | println!("\nOUTPUT: {:?}\n", res); | |
694 | assert_eq!( | |
695 | res, | |
696 | vec![ | |
697 | Link { | |
f035d41b XL |
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 }}", | |
2c00a5a8 XL |
702 | }, |
703 | Link { | |
f035d41b XL |
704 | start_index: 95, |
705 | end_index: 145, | |
706 | link_type: LinkType::Playground( | |
2c00a5a8 XL |
707 | PathBuf::from("my.rs"), |
708 | vec!["editable", "no_run", "should_panic"], | |
709 | ), | |
f035d41b | 710 | link_text: "{{#playground my.rs editable no_run should_panic}}", |
2c00a5a8 XL |
711 | }, |
712 | ] | |
713 | ); | |
714 | } | |
715 | ||
716 | #[test] | |
717 | fn test_find_all_link_types() { | |
f035d41b XL |
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 \ | |
2c00a5a8 XL |
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 { | |
f035d41b XL |
729 | start_index: 41, |
730 | end_index: 61, | |
e74abb32 XL |
731 | link_type: LinkType::Include( |
732 | PathBuf::from("file.rs"), | |
733 | RangeOrAnchor::Range(LineRange::from(..)) | |
734 | ), | |
2c00a5a8 XL |
735 | link_text: "{{#include file.rs}}", |
736 | } | |
737 | ); | |
738 | assert_eq!( | |
739 | res[1], | |
740 | Link { | |
f035d41b XL |
741 | start_index: 66, |
742 | end_index: 115, | |
416331ca | 743 | link_type: LinkType::Escaped, |
2c00a5a8 XL |
744 | link_text: "\\{{#contents are insignifficant in escaped link}}", |
745 | } | |
746 | ); | |
747 | assert_eq!( | |
748 | res[2], | |
749 | Link { | |
f035d41b XL |
750 | start_index: 133, |
751 | end_index: 183, | |
752 | link_type: LinkType::Playground( | |
2c00a5a8 XL |
753 | PathBuf::from("my.rs"), |
754 | vec!["editable", "no_run", "should_panic"] | |
755 | ), | |
f035d41b | 756 | link_text: "{{#playground my.rs editable no_run should_panic}}", |
2c00a5a8 XL |
757 | } |
758 | ); | |
759 | } | |
ea8adc8c | 760 | |
e74abb32 XL |
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 | } | |
ea8adc8c | 940 | } |