diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
commit | 698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch) | |
tree | 173a775858bd501c378080a10dca74132f05bc50 /vendor/mdbook/src/preprocess | |
parent | Initial commit. (diff) | |
download | rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip |
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/mdbook/src/preprocess')
-rw-r--r-- | vendor/mdbook/src/preprocess/cmd.rs | 207 | ||||
-rw-r--r-- | vendor/mdbook/src/preprocess/index.rs | 105 | ||||
-rw-r--r-- | vendor/mdbook/src/preprocess/links.rs | 937 | ||||
-rw-r--r-- | vendor/mdbook/src/preprocess/mod.rs | 70 |
4 files changed, 1319 insertions, 0 deletions
diff --git a/vendor/mdbook/src/preprocess/cmd.rs b/vendor/mdbook/src/preprocess/cmd.rs new file mode 100644 index 000000000..c47fd5d22 --- /dev/null +++ b/vendor/mdbook/src/preprocess/cmd.rs @@ -0,0 +1,207 @@ +use super::{Preprocessor, PreprocessorContext}; +use crate::book::Book; +use crate::errors::*; +use shlex::Shlex; +use std::io::{self, Read, Write}; +use std::process::{Child, Command, Stdio}; + +/// A custom preprocessor which will shell out to a 3rd-party program. +/// +/// # Preprocessing Protocol +/// +/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will +/// execute the shell command `$cmd supports $renderer`. If the renderer is +/// supported, custom preprocessors should exit with a exit code of `0`, +/// any other exit code be considered as unsupported. +/// +/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)` +/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors +/// should then "return" a processed book by printing it to `stdout` as JSON. +/// For convenience, the `CmdPreprocessor::parse_input()` function can be used +/// to parse the input provided by `mdbook`. +/// +/// Exiting with a non-zero exit code while preprocessing is considered an +/// error. `stderr` is passed directly through to the user, so it can be used +/// for logging or emitting warnings if desired. +/// +/// # Examples +/// +/// An example preprocessor is available in this project's `examples/` +/// directory. +#[derive(Debug, Clone, PartialEq)] +pub struct CmdPreprocessor { + name: String, + cmd: String, +} + +impl CmdPreprocessor { + /// Create a new `CmdPreprocessor`. + pub fn new(name: String, cmd: String) -> CmdPreprocessor { + CmdPreprocessor { name, cmd } + } + + /// A convenience function custom preprocessors can use to parse the input + /// written to `stdin` by a `CmdRenderer`. + pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> { + serde_json::from_reader(reader).with_context(|| "Unable to parse the input") + } + + fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) { + let stdin = child.stdin.take().expect("Child has stdin"); + + if let Err(e) = self.write_input(stdin, book, ctx) { + // Looks like the backend hung up before we could finish + // sending it the render context. Log the error and keep going + warn!("Error writing the RenderContext to the backend, {}", e); + } + } + + fn write_input<W: Write>( + &self, + writer: W, + book: &Book, + ctx: &PreprocessorContext, + ) -> Result<()> { + serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into) + } + + /// The command this `Preprocessor` will invoke. + pub fn cmd(&self) -> &str { + &self.cmd + } + + fn command(&self) -> Result<Command> { + let mut words = Shlex::new(&self.cmd); + let executable = match words.next() { + Some(e) => e, + None => bail!("Command string was empty"), + }; + + let mut cmd = Command::new(executable); + + for arg in words { + cmd.arg(arg); + } + + Ok(cmd) + } +} + +impl Preprocessor for CmdPreprocessor { + fn name(&self) -> &str { + &self.name + } + + fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> { + let mut cmd = self.command()?; + + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .with_context(|| { + format!( + "Unable to start the \"{}\" preprocessor. Is it installed?", + self.name() + ) + })?; + + self.write_input_to_child(&mut child, &book, ctx); + + let output = child.wait_with_output().with_context(|| { + format!( + "Error waiting for the \"{}\" preprocessor to complete", + self.name + ) + })?; + + trace!("{} exited with output: {:?}", self.cmd, output); + ensure!( + output.status.success(), + format!( + "The \"{}\" preprocessor exited unsuccessfully with {} status", + self.name, output.status + ) + ); + + serde_json::from_slice(&output.stdout).with_context(|| { + format!( + "Unable to parse the preprocessed book from \"{}\" processor", + self.name + ) + }) + } + + fn supports_renderer(&self, renderer: &str) -> bool { + debug!( + "Checking if the \"{}\" preprocessor supports \"{}\"", + self.name(), + renderer + ); + + let mut cmd = match self.command() { + Ok(c) => c, + Err(e) => { + warn!( + "Unable to create the command for the \"{}\" preprocessor, {}", + self.name(), + e + ); + return false; + } + }; + + let outcome = cmd + .arg("supports") + .arg(renderer) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .map(|status| status.code() == Some(0)); + + if let Err(ref e) = outcome { + if e.kind() == io::ErrorKind::NotFound { + warn!( + "The command wasn't found, is the \"{}\" preprocessor installed?", + self.name + ); + warn!("\tCommand: {}", self.cmd); + } + } + + outcome.unwrap_or(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MDBook; + use std::path::Path; + + fn guide() -> MDBook { + let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide"); + MDBook::load(example).unwrap() + } + + #[test] + fn round_trip_write_and_parse_input() { + let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string()); + let md = guide(); + let ctx = PreprocessorContext::new( + md.root.clone(), + md.config.clone(), + "some-renderer".to_string(), + ); + + let mut buffer = Vec::new(); + cmd.write_input(&mut buffer, &md.book, &ctx).unwrap(); + + let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap(); + + assert_eq!(got_book, md.book); + assert_eq!(got_ctx, ctx); + } +} diff --git a/vendor/mdbook/src/preprocess/index.rs b/vendor/mdbook/src/preprocess/index.rs new file mode 100644 index 000000000..fd60ad4da --- /dev/null +++ b/vendor/mdbook/src/preprocess/index.rs @@ -0,0 +1,105 @@ +use regex::Regex; +use std::path::Path; + +use crate::errors::*; + +use super::{Preprocessor, PreprocessorContext}; +use crate::book::{Book, BookItem}; + +/// A preprocessor for converting file name `README.md` to `index.md` since +/// `README.md` is the de facto index file in markdown-based documentation. +#[derive(Default)] +pub struct IndexPreprocessor; + +impl IndexPreprocessor { + pub(crate) const NAME: &'static str = "index"; + + /// Create a new `IndexPreprocessor`. + pub fn new() -> Self { + IndexPreprocessor + } +} + +impl Preprocessor for IndexPreprocessor { + fn name(&self) -> &str { + Self::NAME + } + + fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { + let source_dir = ctx.root.join(&ctx.config.book.src); + book.for_each_mut(|section: &mut BookItem| { + if let BookItem::Chapter(ref mut ch) = *section { + if let Some(ref mut path) = ch.path { + if is_readme_file(&path) { + let mut index_md = source_dir.join(path.with_file_name("index.md")); + if index_md.exists() { + warn_readme_name_conflict(&path, &&mut index_md); + } + + path.set_file_name("index.md"); + } + } + } + }); + + Ok(book) + } +} + +fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) { + let file_name = readme_path.as_ref().file_name().unwrap_or_default(); + let parent_dir = index_path + .as_ref() + .parent() + .unwrap_or_else(|| index_path.as_ref()); + warn!( + "It seems that there are both {:?} and index.md under \"{}\".", + file_name, + parent_dir.display() + ); + warn!( + "mdbook converts {:?} into index.html by default. It may cause", + file_name + ); + warn!("unexpected behavior if putting both files under the same directory."); + warn!("To solve the warning, try to rearrange the book structure or disable"); + warn!("\"index\" preprocessor to stop the conversion."); +} + +fn is_readme_file<P: AsRef<Path>>(path: P) -> bool { + lazy_static! { + static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap(); + } + RE.is_match( + path.as_ref() + .file_stem() + .and_then(std::ffi::OsStr::to_str) + .unwrap_or_default(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_stem_exactly_matches_readme_case_insensitively() { + let path = "path/to/Readme.md"; + assert!(is_readme_file(path)); + + let path = "path/to/README.md"; + assert!(is_readme_file(path)); + + let path = "path/to/rEaDmE.md"; + assert!(is_readme_file(path)); + + let path = "path/to/README.markdown"; + assert!(is_readme_file(path)); + + let path = "path/to/README"; + assert!(is_readme_file(path)); + + let path = "path/to/README-README.md"; + assert!(!is_readme_file(path)); + } +} diff --git a/vendor/mdbook/src/preprocess/links.rs b/vendor/mdbook/src/preprocess/links.rs new file mode 100644 index 000000000..7ca6fd345 --- /dev/null +++ b/vendor/mdbook/src/preprocess/links.rs @@ -0,0 +1,937 @@ +use crate::errors::*; +use crate::utils::{ + take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines, + take_rustdoc_include_lines, +}; +use regex::{CaptureMatches, Captures, Regex}; +use std::fs; +use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo}; +use std::path::{Path, PathBuf}; + +use super::{Preprocessor, PreprocessorContext}; +use crate::book::{Book, BookItem}; + +const ESCAPE_CHAR: char = '\\'; +const MAX_LINK_NESTED_DEPTH: usize = 10; + +/// A preprocessor for expanding helpers in a chapter. Supported helpers are: +/// +/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular +///. lines, or only between the specified anchors. +/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines +///. specified or the lines between specified anchors, and include the rest of the file behind `#`. +/// This hides the lines from initial display but shows them when the reader expands the code +/// block and provides them to Rustdoc for testing. +/// - `{{# playground}}` - Insert runnable Rust files +/// - `{{# title}}` - Override \<title\> of a webpage. +#[derive(Default)] +pub struct LinkPreprocessor; + +impl LinkPreprocessor { + pub(crate) const NAME: &'static str = "links"; + + /// Create a new `LinkPreprocessor`. + pub fn new() -> Self { + LinkPreprocessor + } +} + +impl Preprocessor for LinkPreprocessor { + fn name(&self) -> &str { + Self::NAME + } + + fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { + let src_dir = ctx.root.join(&ctx.config.book.src); + + book.for_each_mut(|section: &mut BookItem| { + if let BookItem::Chapter(ref mut ch) = *section { + if let Some(ref chapter_path) = ch.path { + let base = chapter_path + .parent() + .map(|dir| src_dir.join(dir)) + .expect("All book items have a parent"); + + let mut chapter_title = ch.name.clone(); + let content = + replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title); + ch.content = content; + if chapter_title != ch.name { + ctx.chapter_titles + .borrow_mut() + .insert(chapter_path.clone(), chapter_title); + } + } + } + }); + + Ok(book) + } +} + +fn replace_all<P1, P2>( + s: &str, + path: P1, + source: P2, + depth: usize, + chapter_title: &mut String, +) -> String +where + P1: AsRef<Path>, + P2: AsRef<Path>, +{ + // When replacing one thing in a string by something with a different length, + // the indices after that will not correspond, + // we therefore have to store the difference to correct this + let path = path.as_ref(); + let source = source.as_ref(); + let mut previous_end_index = 0; + let mut replaced = String::new(); + + for link in find_links(s) { + replaced.push_str(&s[previous_end_index..link.start_index]); + + match link.render_with_path(&path, chapter_title) { + Ok(new_content) => { + if depth < MAX_LINK_NESTED_DEPTH { + if let Some(rel_path) = link.link_type.relative_path(path) { + replaced.push_str(&replace_all( + &new_content, + rel_path, + source, + depth + 1, + chapter_title, + )); + } else { + replaced.push_str(&new_content); + } + } else { + error!( + "Stack depth exceeded in {}. Check for cyclic includes", + source.display() + ); + } + previous_end_index = link.end_index; + } + Err(e) => { + error!("Error updating \"{}\", {}", link.link_text, e); + for cause in e.chain().skip(1) { + warn!("Caused By: {}", cause); + } + + // This should make sure we include the raw `{{# ... }}` snippet + // in the page content if there are any errors. + previous_end_index = link.start_index; + } + } + } + + replaced.push_str(&s[previous_end_index..]); + replaced +} + +#[derive(PartialEq, Debug, Clone)] +enum LinkType<'a> { + Escaped, + Include(PathBuf, RangeOrAnchor), + Playground(PathBuf, Vec<&'a str>), + RustdocInclude(PathBuf, RangeOrAnchor), + Title(&'a str), +} + +#[derive(PartialEq, Debug, Clone)] +enum RangeOrAnchor { + Range(LineRange), + Anchor(String), +} + +// A range of lines specified with some include directive. +#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type +#[derive(PartialEq, Debug, Clone)] +enum LineRange { + Range(Range<usize>), + RangeFrom(RangeFrom<usize>), + RangeTo(RangeTo<usize>), + RangeFull(RangeFull), +} + +impl RangeBounds<usize> for LineRange { + fn start_bound(&self) -> Bound<&usize> { + match self { + LineRange::Range(r) => r.start_bound(), + LineRange::RangeFrom(r) => r.start_bound(), + LineRange::RangeTo(r) => r.start_bound(), + LineRange::RangeFull(r) => r.start_bound(), + } + } + + fn end_bound(&self) -> Bound<&usize> { + match self { + LineRange::Range(r) => r.end_bound(), + LineRange::RangeFrom(r) => r.end_bound(), + LineRange::RangeTo(r) => r.end_bound(), + LineRange::RangeFull(r) => r.end_bound(), + } + } +} + +impl From<Range<usize>> for LineRange { + fn from(r: Range<usize>) -> LineRange { + LineRange::Range(r) + } +} + +impl From<RangeFrom<usize>> for LineRange { + fn from(r: RangeFrom<usize>) -> LineRange { + LineRange::RangeFrom(r) + } +} + +impl From<RangeTo<usize>> for LineRange { + fn from(r: RangeTo<usize>) -> LineRange { + LineRange::RangeTo(r) + } +} + +impl From<RangeFull> for LineRange { + fn from(r: RangeFull) -> LineRange { + LineRange::RangeFull(r) + } +} + +impl<'a> LinkType<'a> { + fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> { + let base = base.as_ref(); + match self { + LinkType::Escaped => None, + LinkType::Include(p, _) => Some(return_relative_path(base, &p)), + LinkType::Playground(p, _) => Some(return_relative_path(base, &p)), + LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)), + LinkType::Title(_) => None, + } + } +} +fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf { + base.as_ref() + .join(relative) + .parent() + .expect("Included file should not be /") + .to_path_buf() +} + +fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor { + let mut parts = parts.unwrap_or("").splitn(3, ':').fuse(); + + let next_element = parts.next(); + let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) { + // subtract 1 since line numbers usually begin with 1 + Some(value.saturating_sub(1)) + } else if let Some("") = next_element { + None + } else if let Some(anchor) = next_element { + return RangeOrAnchor::Anchor(String::from(anchor)); + } else { + None + }; + + let end = parts.next(); + // If `end` is empty string or any other value that can't be parsed as a usize, treat this + // include as a range with only a start bound. However, if end isn't specified, include only + // the single line specified by `start`. + let end = end.map(|s| s.parse::<usize>()); + + match (start, end) { + (Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)), + (Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)), + (Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)), + (None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)), + (None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)), + } +} + +fn parse_include_path(path: &str) -> LinkType<'static> { + let mut parts = path.splitn(2, ':'); + + let path = parts.next().unwrap().into(); + let range_or_anchor = parse_range_or_anchor(parts.next()); + + LinkType::Include(path, range_or_anchor) +} + +fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> { + let mut parts = path.splitn(2, ':'); + + let path = parts.next().unwrap().into(); + let range_or_anchor = parse_range_or_anchor(parts.next()); + + LinkType::RustdocInclude(path, range_or_anchor) +} + +#[derive(PartialEq, Debug, Clone)] +struct Link<'a> { + start_index: usize, + end_index: usize, + link_type: LinkType<'a>, + link_text: &'a str, +} + +impl<'a> Link<'a> { + fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> { + let link_type = match (cap.get(0), cap.get(1), cap.get(2)) { + (_, Some(typ), Some(title)) if typ.as_str() == "title" => { + Some(LinkType::Title(title.as_str())) + } + (_, Some(typ), Some(rest)) => { + let mut path_props = rest.as_str().split_whitespace(); + let file_arg = path_props.next(); + let props: Vec<&str> = path_props.collect(); + + match (typ.as_str(), file_arg) { + ("include", Some(pth)) => Some(parse_include_path(pth)), + ("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)), + ("playpen", Some(pth)) => { + warn!( + "the {{{{#playpen}}}} expression has been \ + renamed to {{{{#playground}}}}, \ + please update your book to use the new name" + ); + Some(LinkType::Playground(pth.into(), props)) + } + ("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)), + _ => None, + } + } + (Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => { + Some(LinkType::Escaped) + } + _ => None, + }; + + link_type.and_then(|lnk_type| { + cap.get(0).map(|mat| Link { + start_index: mat.start(), + end_index: mat.end(), + link_type: lnk_type, + link_text: mat.as_str(), + }) + }) + } + + fn render_with_path<P: AsRef<Path>>( + &self, + base: P, + chapter_title: &mut String, + ) -> Result<String> { + let base = base.as_ref(); + match self.link_type { + // omit the escape char + LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), + LinkType::Include(ref pat, ref range_or_anchor) => { + let target = base.join(pat); + + fs::read_to_string(&target) + .map(|s| match range_or_anchor { + RangeOrAnchor::Range(range) => take_lines(&s, range.clone()), + RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor), + }) + .with_context(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display(), + ) + }) + } + LinkType::RustdocInclude(ref pat, ref range_or_anchor) => { + let target = base.join(pat); + + fs::read_to_string(&target) + .map(|s| match range_or_anchor { + RangeOrAnchor::Range(range) => { + take_rustdoc_include_lines(&s, range.clone()) + } + RangeOrAnchor::Anchor(anchor) => { + take_rustdoc_include_anchored_lines(&s, anchor) + } + }) + .with_context(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display(), + ) + }) + } + LinkType::Playground(ref pat, ref attrs) => { + let target = base.join(pat); + + let mut contents = fs::read_to_string(&target).with_context(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display() + ) + })?; + let ftype = if !attrs.is_empty() { "rust," } else { "rust" }; + if !contents.ends_with('\n') { + contents.push('\n'); + } + Ok(format!( + "```{}{}\n{}```\n", + ftype, + attrs.join(","), + contents + )) + } + LinkType::Title(title) => { + *chapter_title = title.to_owned(); + Ok(String::new()) + } + } + } +} + +struct LinkIter<'a>(CaptureMatches<'a, 'a>); + +impl<'a> Iterator for LinkIter<'a> { + type Item = Link<'a>; + fn next(&mut self) -> Option<Link<'a>> { + for cap in &mut self.0 { + if let Some(inc) = Link::from_capture(cap) { + return Some(inc); + } + } + None + } +} + +fn find_links(contents: &str) -> LinkIter<'_> { + // lazily compute following regex + // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?; + lazy_static! { + static ref RE: Regex = Regex::new( + r"(?x) # insignificant whitespace mode + \\\{\{\#.*\}\} # match escaped link + | # or + \{\{\s* # link opening parens and whitespace + \#([a-zA-Z0-9_]+) # link type + \s+ # separating whitespace + ([^}]+) # link target path and space separated properties + \}\} # link closing parens" + ) + .unwrap(); + } + LinkIter(RE.captures_iter(contents)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_replace_all_escaped() { + let start = r" + Some text over here. + ```hbs + \{{#include file.rs}} << an escaped link! + ```"; + let end = r" + Some text over here. + ```hbs + {{#include file.rs}} << an escaped link! + ```"; + let mut chapter_title = "test_replace_all_escaped".to_owned(); + assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end); + } + + #[test] + fn test_set_chapter_title() { + let start = r"{{#title My Title}} + # My Chapter + "; + let end = r" + # My Chapter + "; + let mut chapter_title = "test_set_chapter_title".to_owned(); + assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end); + assert_eq!(chapter_title, "My Title"); + } + + #[test] + fn test_find_links_no_link() { + let s = "Some random text without link..."; + assert!(find_links(s).collect::<Vec<_>>() == vec![]); + } + + #[test] + fn test_find_links_partial_link() { + let s = "Some random text with {{#playground..."; + assert!(find_links(s).collect::<Vec<_>>() == vec![]); + let s = "Some random text with {{#include..."; + assert!(find_links(s).collect::<Vec<_>>() == vec![]); + let s = "Some random text with \\{{#include..."; + assert!(find_links(s).collect::<Vec<_>>() == vec![]); + } + + #[test] + fn test_find_links_empty_link() { + let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}..."; + assert!(find_links(s).collect::<Vec<_>>() == vec![]); + } + + #[test] + fn test_find_links_unknown_link_type() { + let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}..."; + assert!(find_links(s).collect::<Vec<_>>() == vec![]); + } + + #[test] + fn test_find_links_simple_link() { + let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}..."; + + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + + assert_eq!( + res, + vec![ + Link { + start_index: 22, + end_index: 45, + link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]), + link_text: "{{#playground file.rs}}", + }, + Link { + start_index: 50, + end_index: 74, + link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]), + link_text: "{{#playground test.rs }}", + }, + ] + ); + } + + #[test] + fn test_find_links_with_special_characters() { + let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}..."; + + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 57, + link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]), + link_text: "{{#playground foo-bar\\baz/_c++.rs}}", + },] + ); + } + + #[test] + fn test_find_links_with_range() { + let s = "Some random text with {{#include file.rs:10:20}}..."; + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 48, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Range(LineRange::from(9..20)) + ), + link_text: "{{#include file.rs:10:20}}", + }] + ); + } + + #[test] + fn test_find_links_with_line_number() { + let s = "Some random text with {{#include file.rs:10}}..."; + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 45, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Range(LineRange::from(9..10)) + ), + link_text: "{{#include file.rs:10}}", + }] + ); + } + + #[test] + fn test_find_links_with_from_range() { + let s = "Some random text with {{#include file.rs:10:}}..."; + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 46, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Range(LineRange::from(9..)) + ), + link_text: "{{#include file.rs:10:}}", + }] + ); + } + + #[test] + fn test_find_links_with_to_range() { + let s = "Some random text with {{#include file.rs::20}}..."; + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 46, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Range(LineRange::from(..20)) + ), + link_text: "{{#include file.rs::20}}", + }] + ); + } + + #[test] + fn test_find_links_with_full_range() { + let s = "Some random text with {{#include file.rs::}}..."; + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 44, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Range(LineRange::from(..)) + ), + link_text: "{{#include file.rs::}}", + }] + ); + } + + #[test] + fn test_find_links_with_no_range_specified() { + let s = "Some random text with {{#include file.rs}}..."; + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 42, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Range(LineRange::from(..)) + ), + link_text: "{{#include file.rs}}", + }] + ); + } + + #[test] + fn test_find_links_with_anchor() { + let s = "Some random text with {{#include file.rs:anchor}}..."; + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![Link { + start_index: 22, + end_index: 49, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Anchor(String::from("anchor")) + ), + link_text: "{{#include file.rs:anchor}}", + }] + ); + } + + #[test] + fn test_find_links_escaped_link() { + let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ..."; + + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + + assert_eq!( + res, + vec![Link { + start_index: 41, + end_index: 74, + link_type: LinkType::Escaped, + link_text: "\\{{#playground file.rs editable}}", + }] + ); + } + + #[test] + fn test_find_playgrounds_with_properties() { + let s = + "Some random text with escaped playground {{#playground file.rs editable }} and some \ + more\n text {{#playground my.rs editable no_run should_panic}} ..."; + + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!( + res, + vec![ + Link { + start_index: 41, + end_index: 74, + link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]), + link_text: "{{#playground file.rs editable }}", + }, + Link { + start_index: 95, + end_index: 145, + link_type: LinkType::Playground( + PathBuf::from("my.rs"), + vec!["editable", "no_run", "should_panic"], + ), + link_text: "{{#playground my.rs editable no_run should_panic}}", + }, + ] + ); + } + + #[test] + fn test_find_all_link_types() { + let s = + "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \ + insignifficant in escaped link}} some more\n text {{#playground my.rs editable \ + no_run should_panic}} ..."; + + let res = find_links(s).collect::<Vec<_>>(); + println!("\nOUTPUT: {:?}\n", res); + assert_eq!(res.len(), 3); + assert_eq!( + res[0], + Link { + start_index: 41, + end_index: 61, + link_type: LinkType::Include( + PathBuf::from("file.rs"), + RangeOrAnchor::Range(LineRange::from(..)) + ), + link_text: "{{#include file.rs}}", + } + ); + assert_eq!( + res[1], + Link { + start_index: 66, + end_index: 115, + link_type: LinkType::Escaped, + link_text: "\\{{#contents are insignifficant in escaped link}}", + } + ); + assert_eq!( + res[2], + Link { + start_index: 133, + end_index: 183, + link_type: LinkType::Playground( + PathBuf::from("my.rs"), + vec!["editable", "no_run", "should_panic"] + ), + link_text: "{{#playground my.rs editable no_run should_panic}}", + } + ); + } + + #[test] + fn parse_without_colon_includes_all() { + let link_type = parse_include_path("arbitrary"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(RangeFull)) + ) + ); + } + + #[test] + fn parse_with_nothing_after_colon_includes_all() { + let link_type = parse_include_path("arbitrary:"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(RangeFull)) + ) + ); + } + + #[test] + fn parse_with_two_colons_includes_all() { + let link_type = parse_include_path("arbitrary::"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(RangeFull)) + ) + ); + } + + #[test] + fn parse_with_garbage_after_two_colons_includes_all() { + let link_type = parse_include_path("arbitrary::NaN"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(RangeFull)) + ) + ); + } + + #[test] + fn parse_with_one_number_after_colon_only_that_line() { + let link_type = parse_include_path("arbitrary:5"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(4..5)) + ) + ); + } + + #[test] + fn parse_with_one_based_start_becomes_zero_based() { + let link_type = parse_include_path("arbitrary:1"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(0..1)) + ) + ); + } + + #[test] + fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() { + let link_type = parse_include_path("arbitrary:0"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(0..1)) + ) + ); + } + + #[test] + fn parse_start_only_range() { + let link_type = parse_include_path("arbitrary:5:"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(4..)) + ) + ); + } + + #[test] + fn parse_start_with_garbage_interpreted_as_start_only_range() { + let link_type = parse_include_path("arbitrary:5:NaN"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(4..)) + ) + ); + } + + #[test] + fn parse_end_only_range() { + let link_type = parse_include_path("arbitrary::5"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(..5)) + ) + ); + } + + #[test] + fn parse_start_and_end_range() { + let link_type = parse_include_path("arbitrary:5:10"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(4..10)) + ) + ); + } + + #[test] + fn parse_with_negative_interpreted_as_anchor() { + let link_type = parse_include_path("arbitrary:-5"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Anchor("-5".to_string()) + ) + ); + } + + #[test] + fn parse_with_floating_point_interpreted_as_anchor() { + let link_type = parse_include_path("arbitrary:-5.7"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Anchor("-5.7".to_string()) + ) + ); + } + + #[test] + fn parse_with_anchor_followed_by_colon() { + let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Anchor("some-anchor".to_string()) + ) + ); + } + + #[test] + fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() { + let link_type = parse_include_path("arbitrary:5:10:17:anything:"); + assert_eq!( + link_type, + LinkType::Include( + PathBuf::from("arbitrary"), + RangeOrAnchor::Range(LineRange::from(4..10)) + ) + ); + } +} diff --git a/vendor/mdbook/src/preprocess/mod.rs b/vendor/mdbook/src/preprocess/mod.rs new file mode 100644 index 000000000..894e20035 --- /dev/null +++ b/vendor/mdbook/src/preprocess/mod.rs @@ -0,0 +1,70 @@ +//! Book preprocessing. + +pub use self::cmd::CmdPreprocessor; +pub use self::index::IndexPreprocessor; +pub use self::links::LinkPreprocessor; + +mod cmd; +mod index; +mod links; + +use crate::book::Book; +use crate::config::Config; +use crate::errors::*; + +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +/// Extra information for a `Preprocessor` to give them more context when +/// processing a book. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PreprocessorContext { + /// The location of the book directory on disk. + pub root: PathBuf, + /// The book configuration (`book.toml`). + pub config: Config, + /// The `Renderer` this preprocessor is being used with. + pub renderer: String, + /// The calling `mdbook` version. + pub mdbook_version: String, + #[serde(skip)] + pub(crate) chapter_titles: RefCell<HashMap<PathBuf, String>>, + #[serde(skip)] + __non_exhaustive: (), +} + +impl PreprocessorContext { + /// Create a new `PreprocessorContext`. + pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self { + PreprocessorContext { + root, + config, + renderer, + mdbook_version: crate::MDBOOK_VERSION.to_string(), + chapter_titles: RefCell::new(HashMap::new()), + __non_exhaustive: (), + } + } +} + +/// An operation which is run immediately after loading a book into memory and +/// before it gets rendered. +pub trait Preprocessor { + /// Get the `Preprocessor`'s name. + fn name(&self) -> &str; + + /// Run this `Preprocessor`, allowing it to update the book before it is + /// given to a renderer. + fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>; + + /// A hint to `MDBook` whether this preprocessor is compatible with a + /// particular renderer. + /// + /// By default, always returns `true`. + fn supports_renderer(&self, _renderer: &str) -> bool { + true + } +} |