summaryrefslogtreecommitdiffstats
path: root/vendor/mdbook/src/preprocess
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /vendor/mdbook/src/preprocess
parentInitial commit. (diff)
downloadrustc-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.rs207
-rw-r--r--vendor/mdbook/src/preprocess/index.rs105
-rw-r--r--vendor/mdbook/src/preprocess/links.rs937
-rw-r--r--vendor/mdbook/src/preprocess/mod.rs70
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
+ }
+}