diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index e369086fb4..a3c6c4ed76 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -1,6 +1,5 @@ use self::take_lines::{ - take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines, - take_rustdoc_include_lines, + take_anchored_lines, take_lines, take_rustdoc_anchored_lines, take_rustdoc_lines, }; use anyhow::{Context, Result}; use mdbook_core::book::{Book, BookItem}; @@ -91,32 +90,41 @@ where let path = path.as_ref(); let source = source.as_ref(); let mut previous_end_index = 0; - let mut replaced = String::new(); + let mut replaced = String::with_capacity(s.len()); for link in find_links(s) { - replaced.push_str(&s[previous_end_index..link.start_index]); + // Text from end of last link to start of current link + let text = &s[previous_end_index..link.start_index]; - match link.render_with_path(path, chapter_title) { + // Text from start of line to start of current link. + let prefix = text.rfind('\n').map_or(text, |i| &text[i + 1..]); + + // Stable equivalent of trim_suffix() + replaced.push_str(text.strip_suffix(prefix).unwrap_or(text)); + + match link.render_with_path(path, chapter_title, prefix) { 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 { + previous_end_index = link.end_index; + + if depth >= MAX_LINK_NESTED_DEPTH { error!( "Stack depth exceeded in {}. Check for cyclic includes", source.display() ); + continue; + } + + 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); } - previous_end_index = link.end_index; } Err(e) => { error!("Error updating \"{}\", {}", link.link_text, e); @@ -286,22 +294,23 @@ impl<'a> Link<'a> { 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(); + let mut props = rest.as_str().split_whitespace(); + let file_arg = props.next(); 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)) => { + ("include", Some(path)) => Some(parse_include_path(path)), + ("playground", Some(path)) => { + Some(LinkType::Playground(path.into(), props.collect())) + } + ("playpen", Some(path)) => { warn!( "the {{{{#playpen}}}} expression has been \ renamed to {{{{#playground}}}}, \ please update your book to use the new name" ); - Some(LinkType::Playground(pth.into(), props)) + Some(LinkType::Playground(path.into(), props.collect())) } - ("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)), + ("rustdoc_include", Some(path)) => Some(parse_rustdoc_include_path(path)), _ => None, } } @@ -325,68 +334,120 @@ impl<'a> Link<'a> { &self, base: P, chapter_title: &mut String, + prefix: &str, ) -> Result { + use std::fmt::Write; let base = base.as_ref(); + let mut out = String::new(); 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::Escaped => { + write!(out, "{prefix}{}", &self.link_text[1..]).expect("String writes don't fail"); + Ok(out) + } + + LinkType::Include(ref path, ref range_or_anchor) => { + use RangeOrAnchor::*; + + let target = base.join(path); + + let contents = fs::read_to_string(&target).with_context(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display(), + ) + })?; + + let lines: Vec<_> = match range_or_anchor { + Range(range) => take_lines(&contents, range.clone()).collect(), + Anchor(anchor) => take_anchored_lines(&contents, anchor).collect(), + }; + + // Count shared leading spaces + let trim = lines + .iter() + .map(|line| line.bytes().take_while(|&b| b == b' ').count()) + .fold(usize::MAX, std::cmp::min); + + for line in lines { + write!(out, "{prefix}{}\n", &line[trim..]).expect("String writes don't fail"); + } + + // Trim trailing new line + out.pop(); + Ok(out) } - 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()) + LinkType::RustdocInclude(ref path, ref range_or_anchor) => { + let target = base.join(path); + + let contents = fs::read_to_string(&target).with_context(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display(), + ) + })?; + + match range_or_anchor { + RangeOrAnchor::Range(range) => { + for (line, show) in take_rustdoc_lines(&contents, range.clone()) { + write!( + out, + "{prefix}{}{line}\n", + show.then_some("").unwrap_or("# ") + ) + .expect("String writes don't fail"); } - RangeOrAnchor::Anchor(anchor) => { - take_rustdoc_include_anchored_lines(&s, anchor) + } + RangeOrAnchor::Anchor(anchor) => { + for (line, show) in take_rustdoc_anchored_lines(&contents, anchor) { + write!( + out, + "{prefix}{}{line}\n", + show.then_some("").unwrap_or("# ") + ) + .expect("String writes don't fail"); } - }) - .with_context(|| { - format!( - "Could not read file for link {} ({})", - self.link_text, - target.display(), - ) - }) + } + } + + // Trim trailing new line + out.pop(); + Ok(out) } - LinkType::Playground(ref pat, ref attrs) => { - let target = base.join(pat); - let mut contents = fs::read_to_string(&target).with_context(|| { + LinkType::Playground(ref path, ref attrs) => { + let target = base.join(path); + + let 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'); + + let mut out = String::with_capacity(contents.len() + 11); + + out.push_str(prefix); + out.push_str("```rust"); + for s in attrs { + out.push_str(","); + out.push_str(s); } - Ok(format!( - "```{}{}\n{}```\n", - ftype, - attrs.join(","), - contents - )) + out.push_str("\n"); + + for line in contents.lines() { + write!(out, "{prefix}{line}\n").expect("String writes don't fail"); + } + + out.push_str("```"); + + Ok(out) } + LinkType::Title(title) => { *chapter_title = title.to_owned(); Ok(String::new()) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs index e884a994e9..78e9604ac4 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -3,7 +3,7 @@ use std::ops::Bound::{Excluded, Included, Unbounded}; use std::ops::RangeBounds; /// Take a range of lines from a string. -pub(super) fn take_lines>(s: &str, range: R) -> String { +pub(super) fn take_lines>(s: &str, range: R) -> impl Iterator { let start = match range.start_bound() { Excluded(&n) => n + 1, Included(&n) => n, @@ -11,15 +11,9 @@ pub(super) fn take_lines>(s: &str, range: R) -> String { }; let lines = s.lines().skip(start); match range.end_bound() { - Excluded(end) => lines - .take(end.saturating_sub(start)) - .collect::>() - .join("\n"), - Included(end) => lines - .take((end + 1).saturating_sub(start)) - .collect::>() - .join("\n"), - Unbounded => lines.collect::>().join("\n"), + Excluded(end) => lines.take(end.saturating_sub(start)), + Included(end) => lines.take((end + 1).saturating_sub(start)), + Unbounded => lines.take(usize::MAX), } } @@ -28,225 +22,345 @@ static_regex!(ANCHOR_END, r"ANCHOR_END:\s*(?P[\w_-]+)"); /// Take anchored lines from a string. /// Lines containing anchor are ignored. -pub(super) fn take_anchored_lines(s: &str, anchor: &str) -> String { - let mut retained = Vec::<&str>::new(); - let mut anchor_found = false; - - for l in s.lines() { - if anchor_found { - match ANCHOR_END.captures(l) { - Some(cap) => { - if &cap["anchor_name"] == anchor { - break; - } - } - None => { - if !ANCHOR_START.is_match(l) { - retained.push(l); - } +pub(super) fn take_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator { + let mut in_anchor = false; + s.lines().filter(move |line| { + if in_anchor { + if let Some(captures) = ANCHOR_END.captures(line) { + if captures[1] == *anchor { + in_anchor = false; } + return false; } - } else if let Some(cap) = ANCHOR_START.captures(l) { - if &cap["anchor_name"] == anchor { - anchor_found = true; + return !ANCHOR_START.is_match(line); + } + + if ANCHOR_END.is_match(line) { + return false; + } + + if let Some(captures) = ANCHOR_START.captures(line) { + if captures[1] == *anchor { + in_anchor = true; } } - } - retained.join("\n") + false + }) } -/// Keep lines contained within the range specified as-is. -/// For any lines not in the range, include them but use `#` at the beginning. This will hide the -/// lines from initial display but include them when expanding the code snippet or testing with -/// rustdoc. -pub(super) fn take_rustdoc_include_lines>(s: &str, range: R) -> String { - let mut output = String::with_capacity(s.len()); - - for (index, line) in s.lines().enumerate() { - if !range.contains(&index) { - output.push_str("# "); - } - output.push_str(line); - output.push('\n'); - } - output.pop(); - output +/// Returns an iterator over (line, true) for lines within the specified range, +/// and (line, false) for those outside of the range. +/// This is to allow hiding the lines from initial display but include them when +/// expanding the code snippet or testing with rustdoc. +pub(super) fn take_rustdoc_lines>( + s: &str, + range: R, +) -> impl Iterator { + s.lines() + .enumerate() + .map(move |(index, line)| (line, range.contains(&index))) } -/// Keep lines between the anchor comments specified as-is. -/// For any lines not between the anchors, include them but use `#` at the beginning. This will -/// hide the lines from initial display but include them when expanding the code snippet or testing -/// with rustdoc. -pub(super) fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String { - let mut output = String::with_capacity(s.len()); - let mut within_anchored_section = false; - - for l in s.lines() { - if within_anchored_section { - match ANCHOR_END.captures(l) { - Some(cap) => { - if &cap["anchor_name"] == anchor { - within_anchored_section = false; - } - } - None => { - if !ANCHOR_START.is_match(l) { - output.push_str(l); - output.push('\n'); - } +/// Returns an iterator over (line, true) for lines between the specified anchor +/// comments, and (line, false) for those outside of the specified anchor. +/// This is to allow hiding the lines from initial display but include them when +/// expanding the code snippet or testing with rustdoc. +pub(super) fn take_rustdoc_anchored_lines<'a>( + s: &'a str, + anchor: &str, +) -> impl Iterator { + let mut in_anchor = false; + s.lines().filter_map(move |line| { + if in_anchor { + if let Some(captures) = ANCHOR_END.captures(line) { + if captures[1] == *anchor { + in_anchor = false; } + return None; } - } else if let Some(cap) = ANCHOR_START.captures(l) { - if &cap["anchor_name"] == anchor { - within_anchored_section = true; + if ANCHOR_START.is_match(line) { + return None; } - } else if !ANCHOR_END.is_match(l) { - output.push_str("# "); - output.push_str(l); - output.push('\n'); + return Some((line, true)); + } + + if ANCHOR_END.is_match(line) { + return None; } - } - output.pop(); - output + if let Some(captures) = ANCHOR_START.captures(line) { + if captures[1] == *anchor { + in_anchor = true; + } + return None; + } + + Some((line, false)) + }) } #[cfg(test)] mod tests { - use super::{ - take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines, - take_rustdoc_include_lines, - }; + use super::{take_anchored_lines, take_lines, take_rustdoc_anchored_lines, take_rustdoc_lines}; #[test] #[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled fn take_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; - assert_eq!(take_lines(s, 1..3), "ipsum\ndolor"); - assert_eq!(take_lines(s, 3..), "sit\namet"); - assert_eq!(take_lines(s, ..3), "Lorem\nipsum\ndolor"); - assert_eq!(take_lines(s, ..), s); + assert_eq!( + take_lines(s, 1..3).collect::>().join("\n"), + "ipsum\ndolor" + ); + assert_eq!( + take_lines(s, 3..).collect::>().join("\n"), + "sit\namet" + ); + assert_eq!( + take_lines(s, ..3).collect::>().join("\n"), + "Lorem\nipsum\ndolor" + ); + assert_eq!(take_lines(s, ..).collect::>().join("\n"), s); // corner cases - assert_eq!(take_lines(s, 4..3), ""); - assert_eq!(take_lines(s, ..100), s); + assert_eq!(take_lines(s, 4..3).collect::>().join("\n"), ""); + assert_eq!(take_lines(s, ..100).collect::>().join("\n"), s); } #[test] fn take_anchored_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; - assert_eq!(take_anchored_lines(s, "test"), ""); + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "" + ); let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet"; - assert_eq!(take_anchored_lines(s, "test"), ""); + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "" + ); let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet"; - assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something"), ""); + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "dolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines(s, "something") + .collect::>() + .join("\n"), + "" + ); let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum"; - assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something"), ""); + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "dolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines(s, "something") + .collect::>() + .join("\n"), + "" + ); let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum"; - assert_eq!(take_anchored_lines(s, "test"), "ipsum\ndolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something"), ""); + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "ipsum\ndolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines(s, "something") + .collect::>() + .join("\n"), + "" + ); let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum"; assert_eq!( - take_anchored_lines(s, "test2"), + take_anchored_lines(s, "test2") + .collect::>() + .join("\n"), "ipsum\ndolor\nsit\namet\nlorem" ); - assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something"), ""); + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "dolor\nsit\namet" + ); + assert_eq!( + take_anchored_lines(s, "something") + .collect::>() + .join("\n"), + "" + ); + + let s = "Lorem\nANCHOR: test\nipsum\nANCHOR_END: test\ndolor\nANCHOR: test\nsit\nANCHOR_END: test\namet"; + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "ipsum\nsit" + ); } #[test] #[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled - fn take_rustdoc_include_lines_test() { + fn take_rustdoc_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; assert_eq!( - take_rustdoc_include_lines(s, 1..3), + take_rustdoc_lines(s, 1..3) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\nipsum\ndolor\n# sit\n# amet" ); assert_eq!( - take_rustdoc_include_lines(s, 3..), + take_rustdoc_lines(s, 3..) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\nsit\namet" ); assert_eq!( - take_rustdoc_include_lines(s, ..3), + take_rustdoc_lines(s, ..3) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "Lorem\nipsum\ndolor\n# sit\n# amet" ); - assert_eq!(take_rustdoc_include_lines(s, ..), s); + assert_eq!( + take_rustdoc_lines(s, ..) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), + s + ); // corner cases assert_eq!( - take_rustdoc_include_lines(s, 4..3), + take_rustdoc_lines(s, 4..3) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" ); - assert_eq!(take_rustdoc_include_lines(s, ..100), s); + assert_eq!( + take_rustdoc_lines(s, ..100) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), + s + ); } #[test] - fn take_rustdoc_include_anchored_lines_test() { + fn take_rustdoc_anchored_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_anchored_lines(s, "test") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" ); let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_anchored_lines(s, "test") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" ); let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_anchored_lines(s, "test") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\ndolor\nsit\namet" ); assert_eq!( - take_rustdoc_include_anchored_lines(s, "something"), + take_rustdoc_anchored_lines(s, "something") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" ); let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_anchored_lines(s, "test") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum" ); assert_eq!( - take_rustdoc_include_anchored_lines(s, "something"), + take_rustdoc_anchored_lines(s, "something") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum" ); let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_anchored_lines(s, "test") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\nipsum\ndolor\nsit\namet\n# lorem\n# ipsum" ); assert_eq!( - take_rustdoc_include_anchored_lines(s, "something"), + take_rustdoc_anchored_lines(s, "something") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum" ); let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test2"), + take_rustdoc_anchored_lines(s, "test2") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\nipsum\ndolor\nsit\namet\nlorem\n# ipsum" ); assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_anchored_lines(s, "test") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum" ); assert_eq!( - take_rustdoc_include_anchored_lines(s, "something"), + take_rustdoc_anchored_lines(s, "something") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum" ); let s = "Lorem\nANCHOR: test\nipsum\nANCHOR_END: test\ndolor\nANCHOR: test\nsit\nANCHOR_END: test\namet"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_anchored_lines(s, "test") + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), "# Lorem\nipsum\n# dolor\nsit\n# amet" ); } diff --git a/tests/testsuite/includes.rs b/tests/testsuite/includes.rs index 42d71ddcbe..a124b46f6a 100644 --- a/tests/testsuite/includes.rs +++ b/tests/testsuite/includes.rs @@ -35,6 +35,13 @@ fn anchored_include() { fn main() { let x = 1; } +

Multiple anchor pairs

+
#![allow(unused)]
+fn main() {
+println!("yes");
+println!("yes");
+println!("yes");
+}
"##]], ); } @@ -109,5 +116,70 @@ fn rustdoc_include() { fn main() { some_other_function(); } +

Multiple anchor pairs

+
// This tests multiple anchor pairs that are meant to be included
+fn main() {
+    println!("yes");
+    println!("yes");
+    println!("no");
+    println!("yes");
+    println!("no");
+    println!("no");
+}
"##]]); } + +// Tests `{{#include}}`s that are indented (e.g. ` {{#include}}`) +#[test] +fn indented_include() { + BookTest::from_dir("includes/all_includes").check_main_file( + "book/indented.html", + str![[r##" +

Indented Includes

+
    +
  • +

    No include:

    +

    Sample

    +

    This is not an include.

    +
  • +
  • +

    Basic include:

    +

    Sample

    +

    This is a sample include.

    +
  • +
  • +

    No include:

    +
    fn main() {
    +    some_function();
    +}
    +
  • +
  • +

    Partial include:

    +
    fn main() {
    +    some_function();
    +}
    +
  • +
  • +

    No include:

    +
    fn some_function() {
    +    println!("some function");
    +}
    +
    +fn main() {
    +    some_function();
    +}
    +
  • +
  • +

    Rustdoc include:

    +
    fn some_function() {
    +    println!("some function");
    +}
    +
    +fn main() {
    +    some_function();
    +}
    +
  • +
+"##]], + ); +} diff --git a/tests/testsuite/includes/all_includes/src/SUMMARY.md b/tests/testsuite/includes/all_includes/src/SUMMARY.md index 25a9a15f71..cf663cd938 100644 --- a/tests/testsuite/includes/all_includes/src/SUMMARY.md +++ b/tests/testsuite/includes/all_includes/src/SUMMARY.md @@ -6,3 +6,4 @@ - [Include Anchors](./anchors.md) - [Rustdoc Includes](./rustdoc.md) - [Playground Includes](./playground.md) +- [Indented Includes](./indented.md) diff --git a/tests/testsuite/includes/all_includes/src/anchors.md b/tests/testsuite/includes/all_includes/src/anchors.md index 0ffbc78837..6f47da0f9d 100644 --- a/tests/testsuite/includes/all_includes/src/anchors.md +++ b/tests/testsuite/includes/all_includes/src/anchors.md @@ -3,3 +3,9 @@ ```rust {{#include nested-test-with-anchors.rs:myanchor}} ``` + +## Multiple anchor pairs + +```rust +{{#include multiple-anchors.rs:include-these}} +``` diff --git a/tests/testsuite/includes/all_includes/src/indented.md b/tests/testsuite/includes/all_includes/src/indented.md new file mode 100644 index 0000000000..a3c012bbc0 --- /dev/null +++ b/tests/testsuite/includes/all_includes/src/indented.md @@ -0,0 +1,37 @@ +# Indented Includes + + - No include: + ## Sample + + This is not an include. + + - Basic include: + {{#include sample.md}} + + - No include: + ```rust + fn main() { + some_function(); + } + ``` + + - Partial include: + ```rust + {{#include partially-included-test.rs:5:7}} + ``` + + - No include: + ```rust + # fn some_function() { + # println!("some function"); + # } + # + fn main() { + some_function(); + } + ``` + + - Rustdoc include: + ```rust + {{#rustdoc_include partially-included-test.rs:5:7}} + ``` diff --git a/tests/testsuite/includes/all_includes/src/multiple-anchors.rs b/tests/testsuite/includes/all_includes/src/multiple-anchors.rs new file mode 100644 index 0000000000..ad14e1cb97 --- /dev/null +++ b/tests/testsuite/includes/all_includes/src/multiple-anchors.rs @@ -0,0 +1,15 @@ +// This tests multiple anchor pairs that are meant to be included +fn main() { + // ANCHOR: include-these + println!("yes"); + // ANCHOR: not-these + println!("yes"); + // ANCHOR_END: include-these + println!("no"); + // ANCHOR: include-these + println!("yes"); + // ANCHOR_END: include-these + println!("no"); + // ANCHOR_END: not-these + println!("no"); +} diff --git a/tests/testsuite/includes/all_includes/src/rustdoc.md b/tests/testsuite/includes/all_includes/src/rustdoc.md index 8a342d90b3..80445b478f 100644 --- a/tests/testsuite/includes/all_includes/src/rustdoc.md +++ b/tests/testsuite/includes/all_includes/src/rustdoc.md @@ -11,3 +11,9 @@ ```rust {{#rustdoc_include partially-included-test-with-anchors.rs:rustdoc-include-anchor}} ``` + +## Multiple anchor pairs + +```rust +{{#rustdoc_include multiple-anchors.rs:include-these}} +```