From cc4bfd99904ff5b4729498f8a695927b0942d32d Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Sun, 19 Apr 2026 21:39:53 +0100 Subject: [PATCH 01/27] add test for indented {{#include}}s --- tests/testsuite/includes.rs | 57 +++++++++++++++++++ .../includes/all_includes/src/SUMMARY.md | 1 + .../includes/all_includes/src/indented.md | 37 ++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 tests/testsuite/includes/all_includes/src/indented.md diff --git a/tests/testsuite/includes.rs b/tests/testsuite/includes.rs index 42d71ddcbe..4d22eb8329 100644 --- a/tests/testsuite/includes.rs +++ b/tests/testsuite/includes.rs @@ -111,3 +111,60 @@ fn rustdoc_include() { } "##]]); } + +// 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

+ +"##]], + ) + ; +} 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/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}} + ``` From 9257a6cfae17db16315d3d8970cc14693acee6a9 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Mon, 20 Apr 2026 22:18:31 +0100 Subject: [PATCH 02/27] un-nest if blocks to improve readability --- .../src/builtin_preprocessors/links.rs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index e369086fb4..d5f1fa8e3e 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -98,25 +98,27 @@ where 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 { + 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); From db01457d97784a0efee26344d51c6021fb4ce208 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 14:13:48 +0100 Subject: [PATCH 03/27] add minimum capacity to string alloc --- crates/mdbook-driver/src/builtin_preprocessors/links.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index d5f1fa8e3e..d130468895 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -91,7 +91,7 @@ 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]); From 40c4bc3c42796429855d3ba4d9ad4ecedb91b720 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 14:17:18 +0100 Subject: [PATCH 04/27] format consistency --- .../src/builtin_preprocessors/links.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index d130468895..e239f2be27 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -371,13 +371,15 @@ impl<'a> Link<'a> { 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 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'); From c92c18aae1d1fcd1c91694eb9f9e0c0eae9ed14b Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 14:17:53 +0100 Subject: [PATCH 05/27] add some newlines for readability --- crates/mdbook-driver/src/builtin_preprocessors/links.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index e239f2be27..92ff060f70 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -368,6 +368,7 @@ impl<'a> Link<'a> { ) }) } + LinkType::Playground(ref pat, ref attrs) => { let target = base.join(pat); @@ -384,6 +385,7 @@ impl<'a> Link<'a> { if !contents.ends_with('\n') { contents.push('\n'); } + Ok(format!( "```{}{}\n{}```\n", ftype, @@ -391,6 +393,7 @@ impl<'a> Link<'a> { contents )) } + LinkType::Title(title) => { *chapter_title = title.to_owned(); Ok(String::new()) From c38143275b52abd550b68742b678f342ff52fee3 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 14:23:24 +0100 Subject: [PATCH 06/27] rework helpers from take_lines.rs to return iterators --- .../builtin_preprocessors/links/take_lines.rs | 299 +++++++++++------- 1 file changed, 188 insertions(+), 111 deletions(-) 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..e5333a8c6d 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,88 +22,72 @@ 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(); +pub(super) fn take_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator { let mut anchor_found = false; + let mut done = false; + s.lines().filter(move |line| { + if done { return 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); - } - } - } - } else if let Some(cap) = ANCHOR_START.captures(l) { - if &cap["anchor_name"] == anchor { - anchor_found = true; + if !anchor_found { + if let Some(captures) = ANCHOR_START.captures(line) { + if captures[1] == *anchor { anchor_found = true; } } + return false; + } + + if ANCHOR_START.is_match(line) { return false; } + + if let Some(captures) = ANCHOR_END.captures(line) { + if captures[1] == *anchor { done = true; } + return false; } - } - retained.join("\n") + true + }) } /// 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 +pub(super) fn take_rustdoc_include_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'); - } - } - } - } else if let Some(cap) = ANCHOR_START.captures(l) { - if &cap["anchor_name"] == anchor { - within_anchored_section = true; +pub(super) fn take_rustdoc_include_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator { + let mut in_anchored = false; + let mut done = false; + s.lines().filter_map(move |line| { + if done { + if ANCHOR_START.is_match(line) { return None; } + if ANCHOR_END.is_match(line) { return None; } + return Some((line, false)); + } + + if in_anchored { + if let Some(captures) = ANCHOR_END.captures(line) { + if captures[1] == *anchor { done = true; } + return None; } - } else if !ANCHOR_END.is_match(l) { - output.push_str("# "); - output.push_str(l); - output.push('\n'); + if ANCHOR_START.is_match(line) { return None; } + return Some((line, true)); + } + + if ANCHOR_END.is_match(line) { return None; } + + if let Some(captures) = ANCHOR_START.captures(line) { + if captures[1] == *anchor { in_anchored = true; } + return None; } - } - output.pop(); - output + Some((line, false)) + }) } #[cfg(test)] @@ -123,42 +101,42 @@ mod tests { #[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"), ""); } #[test] @@ -166,87 +144,186 @@ mod tests { fn take_rustdoc_include_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; assert_eq!( - take_rustdoc_include_lines(s, 1..3), - "# Lorem\nipsum\ndolor\n# sit\n# amet" + take_rustdoc_include_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..), - "# Lorem\n# ipsum\n# dolor\nsit\namet" + take_rustdoc_include_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), - "Lorem\nipsum\ndolor\n# sit\n# amet" + take_rustdoc_include_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, ..) + .map(|(line, show)| { + format!("{}{line}", show.then_some("").unwrap_or("# ")) + }) + .collect::>() + .join("\n"), + s ); - assert_eq!(take_rustdoc_include_lines(s, ..), s); // corner cases assert_eq!( - take_rustdoc_include_lines(s, 4..3), - "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" + take_rustdoc_include_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) + .map(|(line, show)| { + format!("{}{line}", show.then_some("").unwrap_or("# ")) + }) + .collect::>() + .join("\n"), + s ); - assert_eq!(take_rustdoc_include_lines(s, ..100), s); } #[test] fn take_rustdoc_include_anchored_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; assert_eq!( - take_rustdoc_include_anchored_lines(s, "test"), + take_rustdoc_include_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_include_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_include_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_include_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_include_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_include_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_include_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_include_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_include_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_include_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_include_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" ); + // TODO: determine if this test actually reflects intended behaviour; the documentation + // describes an anchor as 'a pair of matching lines', and there is no test for this additional + // behaviour in the testsuite. 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_include_anchored_lines(s, "test") + .map(|(line, show)| { + format!("{}{line}", show.then_some("").unwrap_or("# ")) + }) + .collect::>() + .join("\n"), "# Lorem\nipsum\n# dolor\nsit\n# amet" ); } From 3555f933c56400f1d710663e0b5247c6acd0f40b Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 14:25:21 +0100 Subject: [PATCH 07/27] rework link preprocessor to maintain indent level --- .../src/builtin_preprocessors/links.rs | 80 ++++++++++++++----- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index 92ff060f70..c707f2540a 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -94,9 +94,16 @@ where 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) => { previous_end_index = link.end_index; @@ -327,46 +334,81 @@ 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::Escaped => { + write!(out, "{prefix}{}", &self.link_text[1..]) + .expect("String writes don't fail"); + Ok(out) + }, + 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), - }) + 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 in take_lines(&contents, range.clone()) { + write!(out, "{prefix}{line}\n") + .expect("String writes don't fail"); + } + }, + RangeOrAnchor::Anchor(anchor) => { + for line in take_anchored_lines(&contents, anchor) { + write!(out, "{prefix}{line}\n") + .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()) - } - RangeOrAnchor::Anchor(anchor) => { - take_rustdoc_include_anchored_lines(&s, anchor) - } - }) + 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_include_lines(&contents, range.clone()) { + write!(out, "{prefix}{}{line}\n", show.then_some("").unwrap_or("# ")) + .expect("String writes don't fail"); + } + } + RangeOrAnchor::Anchor(anchor) => { + for (line, show) in take_rustdoc_include_anchored_lines(&contents, anchor) { + write!(out, "{prefix}{}{line}\n", show.then_some("").unwrap_or("# ")) + .expect("String writes don't fail"); + } + } + } + + // Trim trailing new line + out.pop(); + Ok(out) } LinkType::Playground(ref pat, ref attrs) => { From 6688e6592141206d0947689a6866f19a8ad2252b Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 14:32:08 +0100 Subject: [PATCH 08/27] update comments for take_rustdoc_include_lines() & take_rustdoc_include_anchored_lines() --- .../builtin_preprocessors/links/take_lines.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 e5333a8c6d..cc1fb81716 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -46,20 +46,20 @@ pub(super) fn take_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator }) } -/// 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. +/// 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_include_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. +/// 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_include_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator { let mut in_anchored = false; let mut done = false; From 50c05ef2e42b0395635103e50ef5ffb1e620d0a5 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 14:40:44 +0100 Subject: [PATCH 09/27] rename take_rustdoc_include_lines & take_rustdoc_include_anchored_lines take_lines & take_anchored_lines do not require 'include' specified in the name, so for clarity the +'rustdoc_' versions shouldn't either --- .../src/builtin_preprocessors/links.rs | 8 ++-- .../builtin_preprocessors/links/take_lines.rs | 48 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index c707f2540a..dc9291ca04 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -1,6 +1,6 @@ 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}; @@ -393,13 +393,13 @@ impl<'a> Link<'a> { match range_or_anchor { RangeOrAnchor::Range(range) => { - for (line, show) in take_rustdoc_include_lines(&contents, range.clone()) { + 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) => { - for (line, show) in take_rustdoc_include_anchored_lines(&contents, 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"); } 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 cc1fb81716..af5af0c5bc 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -50,7 +50,7 @@ pub(super) fn take_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator /// 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_include_lines>(s: &str, range: R) -> impl Iterator { +pub(super) fn take_rustdoc_lines>(s: &str, range: R) -> impl Iterator { s.lines().enumerate().map(move |(index, line)| { (line, range.contains(&index)) }) @@ -60,7 +60,7 @@ pub(super) fn take_rustdoc_include_lines>(s: &str, range: /// 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_include_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator { +pub(super) fn take_rustdoc_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator { let mut in_anchored = false; let mut done = false; s.lines().filter_map(move |line| { @@ -93,8 +93,8 @@ pub(super) fn take_rustdoc_include_anchored_lines<'a>(s: &'a str, anchor: &str) #[cfg(test)] mod tests { use super::{ - 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, }; #[test] @@ -141,10 +141,10 @@ mod tests { #[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("# ")) }) @@ -153,7 +153,7 @@ mod tests { "# 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("# ")) }) @@ -162,7 +162,7 @@ mod tests { "# 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("# ")) }) @@ -171,7 +171,7 @@ mod tests { "Lorem\nipsum\ndolor\n# sit\n# amet" ); assert_eq!( - take_rustdoc_include_lines(s, ..) + take_rustdoc_lines(s, ..) .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) @@ -181,7 +181,7 @@ mod tests { ); // 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("# ")) }) @@ -190,7 +190,7 @@ mod tests { "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" ); assert_eq!( - take_rustdoc_include_lines(s, ..100) + take_rustdoc_lines(s, ..100) .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) @@ -201,10 +201,10 @@ mod tests { } #[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("# ")) }) @@ -215,7 +215,7 @@ mod tests { 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("# ")) }) @@ -226,7 +226,7 @@ mod tests { 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("# ")) }) @@ -235,7 +235,7 @@ mod tests { "# 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("# ")) }) @@ -246,7 +246,7 @@ mod tests { 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("# ")) }) @@ -255,7 +255,7 @@ mod tests { "# 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("# ")) }) @@ -266,7 +266,7 @@ mod tests { 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("# ")) }) @@ -275,7 +275,7 @@ mod tests { "# 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("# ")) }) @@ -286,7 +286,7 @@ mod tests { 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("# ")) }) @@ -295,7 +295,7 @@ mod tests { "# 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("# ")) }) @@ -304,7 +304,7 @@ mod tests { "# 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("# ")) }) @@ -318,7 +318,7 @@ mod tests { // behaviour in the testsuite. 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("# ")) }) From 4105a1b3b8d642cff737152c2b1c7ba205c9faa4 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 16:11:09 +0100 Subject: [PATCH 10/27] fix indent --- .../src/builtin_preprocessors/links/take_lines.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 af5af0c5bc..b40bcdcec4 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -65,9 +65,9 @@ pub(super) fn take_rustdoc_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl let mut done = false; s.lines().filter_map(move |line| { if done { - if ANCHOR_START.is_match(line) { return None; } - if ANCHOR_END.is_match(line) { return None; } - return Some((line, false)); + if ANCHOR_START.is_match(line) { return None; } + if ANCHOR_END.is_match(line) { return None; } + return Some((line, false)); } if in_anchored { From 679e471f210fd8188094fd5bd406d84025e1cbec Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 16:40:54 +0100 Subject: [PATCH 11/27] format --- crates/mdbook-driver/src/builtin_preprocessors/links.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index dc9291ca04..bf051de5e3 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -345,7 +345,7 @@ impl<'a> Link<'a> { write!(out, "{prefix}{}", &self.link_text[1..]) .expect("String writes don't fail"); Ok(out) - }, + } LinkType::Include(ref pat, ref range_or_anchor) => { let target = base.join(pat); @@ -365,13 +365,13 @@ impl<'a> Link<'a> { write!(out, "{prefix}{line}\n") .expect("String writes don't fail"); } - }, + } RangeOrAnchor::Anchor(anchor) => { for line in take_anchored_lines(&contents, anchor) { write!(out, "{prefix}{line}\n") .expect("String writes don't fail"); } - }, + } } // Trim trailing new line From ff6a62a8692473d75686425209b4b8a50e7e3250 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 18:47:51 +0100 Subject: [PATCH 12/27] fix rustdoc_include with multiple anchors --- .../src/builtin_preprocessors/links/take_lines.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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 b40bcdcec4..b2aef14b79 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -62,17 +62,10 @@ pub(super) fn take_rustdoc_lines>(s: &str, range: R) -> im /// 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_anchored = false; - let mut done = false; s.lines().filter_map(move |line| { - if done { - if ANCHOR_START.is_match(line) { return None; } - if ANCHOR_END.is_match(line) { return None; } - return Some((line, false)); - } - if in_anchored { if let Some(captures) = ANCHOR_END.captures(line) { - if captures[1] == *anchor { done = true; } + if captures[1] == *anchor { in_anchored = false; } return None; } if ANCHOR_START.is_match(line) { return None; } @@ -313,9 +306,6 @@ mod tests { "# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum" ); - // TODO: determine if this test actually reflects intended behaviour; the documentation - // describes an anchor as 'a pair of matching lines', and there is no test for this additional - // behaviour in the testsuite. let s = "Lorem\nANCHOR: test\nipsum\nANCHOR_END: test\ndolor\nANCHOR: test\nsit\nANCHOR_END: test\namet"; assert_eq!( take_rustdoc_anchored_lines(s, "test") From d6b648139d72d88c1c1f701f80d371a6712ba4a0 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 19:27:13 +0100 Subject: [PATCH 13/27] add todo comment --- .../mdbook-driver/src/builtin_preprocessors/links/take_lines.rs | 1 + 1 file changed, 1 insertion(+) 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 b2aef14b79..c16be73de5 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -23,6 +23,7 @@ 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<'a>(s: &'a str, anchor: &str) -> impl Iterator { + // TODO: mirror rustdoc version behaviour re; multiple anchors? let mut anchor_found = false; let mut done = false; s.lines().filter(move |line| { From f5a257662a9038b2990a7e49ea834420190a4149 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 21 Apr 2026 19:28:46 +0100 Subject: [PATCH 14/27] add test to testsuite for multiple rustdoc include anchors --- tests/testsuite/includes.rs | 10 ++++++++++ .../includes/all_includes/src/multiple-anchors.rs | 15 +++++++++++++++ .../includes/all_includes/src/rustdoc.md | 6 ++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/testsuite/includes/all_includes/src/multiple-anchors.rs diff --git a/tests/testsuite/includes.rs b/tests/testsuite/includes.rs index 4d22eb8329..7fd2369ef1 100644 --- a/tests/testsuite/includes.rs +++ b/tests/testsuite/includes.rs @@ -109,6 +109,16 @@ 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");
+}
"##]]); } 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}} +``` From 2256eaf80519d283895c53cf601daefca59e0c5d Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Sat, 25 Apr 2026 20:04:48 +0100 Subject: [PATCH 15/27] update take_anchored_lines() to mirror the rustdoc version --- .../builtin_preprocessors/links/take_lines.rs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) 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 c16be73de5..d3e4d6cbea 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -23,27 +23,23 @@ 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<'a>(s: &'a str, anchor: &str) -> impl Iterator { - // TODO: mirror rustdoc version behaviour re; multiple anchors? - let mut anchor_found = false; - let mut done = false; + let mut in_anchor = false; s.lines().filter(move |line| { - if done { return false; } - - if !anchor_found { - if let Some(captures) = ANCHOR_START.captures(line) { - if captures[1] == *anchor { anchor_found = true; } + if in_anchor { + if let Some(captures) = ANCHOR_END.captures(line) { + if captures[1] == *anchor { in_anchor = false; } + return false; } - return false; + return !ANCHOR_START.is_match(line); } - if ANCHOR_START.is_match(line) { return false; } + if ANCHOR_END.is_match(line) { return false; } - if let Some(captures) = ANCHOR_END.captures(line) { - if captures[1] == *anchor { done = true; } - return false; + if let Some(captures) = ANCHOR_START.captures(line) { + if captures[1] == *anchor { in_anchor = true; } } - true + false }) } From c0b8e530581c580f7d84368a8e5516bbd229daad Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Sat, 25 Apr 2026 20:08:21 +0100 Subject: [PATCH 16/27] match variable name across take_anchored_lines & rustdoc version --- .../src/builtin_preprocessors/links/take_lines.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 d3e4d6cbea..3b006207ff 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -58,11 +58,11 @@ pub(super) fn take_rustdoc_lines>(s: &str, range: R) -> im /// 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_anchored = false; + let mut in_anchor = false; s.lines().filter_map(move |line| { - if in_anchored { + if in_anchor { if let Some(captures) = ANCHOR_END.captures(line) { - if captures[1] == *anchor { in_anchored = false; } + if captures[1] == *anchor { in_anchor = false; } return None; } if ANCHOR_START.is_match(line) { return None; } @@ -72,7 +72,7 @@ pub(super) fn take_rustdoc_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl if ANCHOR_END.is_match(line) { return None; } if let Some(captures) = ANCHOR_START.captures(line) { - if captures[1] == *anchor { in_anchored = true; } + if captures[1] == *anchor { in_anchor = true; } return None; } From 348f12f906f88c7c57949965332421a212ba23c4 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Sat, 25 Apr 2026 20:37:01 +0100 Subject: [PATCH 17/27] add multiple anchor include test to testsuite --- tests/testsuite/includes.rs | 7 +++++++ tests/testsuite/includes/all_includes/src/anchors.md | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/tests/testsuite/includes.rs b/tests/testsuite/includes.rs index 7fd2369ef1..1be4ffd5c0 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");
+}
"##]], ); } 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}} +``` From adbfb48559e8e803cbe1d0ecd4a9363231e693be Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Sat, 25 Apr 2026 20:51:33 +0100 Subject: [PATCH 18/27] expand take_anchored_lines unit test to cover multiple anchors --- .../src/builtin_preprocessors/links/take_lines.rs | 3 +++ 1 file changed, 3 insertions(+) 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 3b006207ff..ff0d42b5a9 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -127,6 +127,9 @@ mod tests { ); 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] From b7fbf2638127aa4e5e6d8948ae0b8f0d554fb7da Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Mon, 27 Apr 2026 15:40:40 +0100 Subject: [PATCH 19/27] update expected output from test --- tests/testsuite/includes.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testsuite/includes.rs b/tests/testsuite/includes.rs index 1be4ffd5c0..1a5cc4116a 100644 --- a/tests/testsuite/includes.rs +++ b/tests/testsuite/includes.rs @@ -38,9 +38,9 @@ fn anchored_include() {

Multiple anchor pairs

#![allow(unused)]
 fn main() {
-    println!("yes");
-    println!("yes");
-    println!("yes");
+println!("yes");
+println!("yes");
+println!("yes");
 }
"##]], ); From b20010c098565c5ba3008b73a9485577847f98f6 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Mon, 27 Apr 2026 15:48:26 +0100 Subject: [PATCH 20/27] rework includes to trim shared leading spaces --- .../src/builtin_preprocessors/links.rs | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index bf051de5e3..65109bf750 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -348,6 +348,8 @@ impl<'a> Link<'a> { } LinkType::Include(ref pat, ref range_or_anchor) => { + use RangeOrAnchor::*; + let target = base.join(pat); let contents = fs::read_to_string(&target) @@ -359,19 +361,26 @@ impl<'a> Link<'a> { ) })?; - match range_or_anchor { - RangeOrAnchor::Range(range) => { - for line in take_lines(&contents, range.clone()) { - write!(out, "{prefix}{line}\n") - .expect("String writes don't fail"); - } - } - RangeOrAnchor::Anchor(anchor) => { - for line in take_anchored_lines(&contents, anchor) { - write!(out, "{prefix}{line}\n") - .expect("String writes don't fail"); - } - } + 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 From ec5ecd234f0e450ccfe103c5f258b8a2f866078b Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 28 Apr 2026 14:48:09 +0100 Subject: [PATCH 21/27] typo --- crates/mdbook-driver/src/builtin_preprocessors/links.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index 65109bf750..7b6d629667 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -423,8 +423,8 @@ impl<'a> Link<'a> { LinkType::Playground(ref pat, ref attrs) => { let target = base.join(pat); - let mut contents = fs::read_to_string(&target). - with_context(|| { + let mut contents = fs::read_to_string(&target) + .with_context(|| { format!( "Could not read file for link {} ({})", self.link_text, From be6fee7688ed2144685e382b6f2c537262d2b892 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 28 Apr 2026 15:48:46 +0100 Subject: [PATCH 22/27] rename 'pat' to 'path' --- .../mdbook-driver/src/builtin_preprocessors/links.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index 7b6d629667..9bd4654cbb 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -347,10 +347,10 @@ impl<'a> Link<'a> { Ok(out) } - LinkType::Include(ref pat, ref range_or_anchor) => { + LinkType::Include(ref path, ref range_or_anchor) => { use RangeOrAnchor::*; - let target = base.join(pat); + let target = base.join(path); let contents = fs::read_to_string(&target) .with_context(|| { @@ -388,8 +388,8 @@ impl<'a> Link<'a> { Ok(out) } - LinkType::RustdocInclude(ref pat, ref range_or_anchor) => { - let target = base.join(pat); + LinkType::RustdocInclude(ref path, ref range_or_anchor) => { + let target = base.join(path); let contents = fs::read_to_string(&target) .with_context(|| { @@ -420,8 +420,8 @@ impl<'a> Link<'a> { Ok(out) } - LinkType::Playground(ref pat, ref attrs) => { - let target = base.join(pat); + LinkType::Playground(ref path, ref attrs) => { + let target = base.join(path); let mut contents = fs::read_to_string(&target) .with_context(|| { From 875b85f9cc194d0a8ac7fc76e087e8162a8c445f Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 28 Apr 2026 16:27:57 +0100 Subject: [PATCH 23/27] newline --- crates/mdbook-driver/src/builtin_preprocessors/links.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index 9bd4654cbb..64ab7d5ead 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -433,6 +433,7 @@ impl<'a> Link<'a> { })?; let ftype = if !attrs.is_empty() { "rust," } else { "rust" }; + if !contents.ends_with('\n') { contents.push('\n'); } From 61ca8d0eb098257819c0ff2e3f747edefcfe00ce Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 28 Apr 2026 16:50:11 +0100 Subject: [PATCH 24/27] remove early collect() --- crates/mdbook-driver/src/builtin_preprocessors/links.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index 64ab7d5ead..c481ce921c 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -295,20 +295,19 @@ 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)), + ("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props.collect())), ("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)) + Some(LinkType::Playground(pth.into(), props.collect())) } ("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)), _ => None, From 5087714e669bcda66cb2d249219505d2e8f37bf9 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 28 Apr 2026 16:53:20 +0100 Subject: [PATCH 25/27] rename 'pth' to 'path' --- .../mdbook-driver/src/builtin_preprocessors/links.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index c481ce921c..cabe458190 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -299,17 +299,17 @@ impl<'a> Link<'a> { 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.collect())), - ("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.collect())) + 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, } } From 56c68e4250cc68485d92266d278d1259cc46b848 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 28 Apr 2026 18:21:48 +0100 Subject: [PATCH 26/27] rework playground links to include prefix indent --- .../src/builtin_preprocessors/links.rs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index cabe458190..dfc7f2eb5d 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -422,7 +422,7 @@ impl<'a> Link<'a> { LinkType::Playground(ref path, ref attrs) => { let target = base.join(path); - let mut contents = fs::read_to_string(&target) + let contents = fs::read_to_string(&target) .with_context(|| { format!( "Could not read file for link {} ({})", @@ -431,18 +431,24 @@ impl<'a> Link<'a> { ) })?; - let ftype = if !attrs.is_empty() { "rust," } else { "rust" }; + 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); + } + out.push_str("\n"); - if !contents.ends_with('\n') { - contents.push('\n'); + for line in contents.lines() { + write!(out, "{prefix}{line}\n") + .expect("String writes don't fail"); } - Ok(format!( - "```{}{}\n{}```\n", - ftype, - attrs.join(","), - contents - )) + out.push_str("```"); + + Ok(out) } LinkType::Title(title) => { From 9070773af862ef20367ef33a17cce6f3b85020f2 Mon Sep 17 00:00:00 2001 From: Joey Sabey Date: Tue, 28 Apr 2026 18:36:37 +0100 Subject: [PATCH 27/27] cargo fmt --- .../src/builtin_preprocessors/links.rs | 97 +++--- .../builtin_preprocessors/links/take_lines.rs | 279 ++++++++++-------- tests/testsuite/includes.rs | 10 +- 3 files changed, 214 insertions(+), 172 deletions(-) diff --git a/crates/mdbook-driver/src/builtin_preprocessors/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs index dfc7f2eb5d..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_anchored_lines, take_rustdoc_lines, + take_anchored_lines, take_lines, take_rustdoc_anchored_lines, take_rustdoc_lines, }; use anyhow::{Context, Result}; use mdbook_core::book::{Book, BookItem}; @@ -98,7 +97,7 @@ where let text = &s[previous_end_index..link.start_index]; // Text from start of line to start of current link. - let prefix = text.rfind('\n').map_or(text, |i| &text[i+1..]); + 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)); @@ -300,7 +299,9 @@ impl<'a> Link<'a> { match (typ.as_str(), file_arg) { ("include", Some(path)) => Some(parse_include_path(path)), - ("playground", Some(path)) => Some(LinkType::Playground(path.into(), props.collect())), + ("playground", Some(path)) => { + Some(LinkType::Playground(path.into(), props.collect())) + } ("playpen", Some(path)) => { warn!( "the {{{{#playpen}}}} expression has been \ @@ -341,8 +342,7 @@ impl<'a> Link<'a> { match self.link_type { // omit the escape char LinkType::Escaped => { - write!(out, "{prefix}{}", &self.link_text[1..]) - .expect("String writes don't fail"); + write!(out, "{prefix}{}", &self.link_text[1..]).expect("String writes don't fail"); Ok(out) } @@ -351,35 +351,27 @@ impl<'a> Link<'a> { 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 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() - } + 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) - ; + 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"); + write!(out, "{prefix}{}\n", &line[trim..]).expect("String writes don't fail"); } // Trim trailing new line @@ -390,26 +382,33 @@ impl<'a> Link<'a> { 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(), - ) - })?; + 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"); + write!( + out, + "{prefix}{}{line}\n", + show.then_some("").unwrap_or("# ") + ) + .expect("String writes don't fail"); } } 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"); + write!( + out, + "{prefix}{}{line}\n", + show.then_some("").unwrap_or("# ") + ) + .expect("String writes don't fail"); } } } @@ -422,14 +421,13 @@ impl<'a> Link<'a> { 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 contents = fs::read_to_string(&target).with_context(|| { + format!( + "Could not read file for link {} ({})", + self.link_text, + target.display() + ) + })?; let mut out = String::with_capacity(contents.len() + 11); @@ -442,8 +440,7 @@ impl<'a> Link<'a> { out.push_str("\n"); for line in contents.lines() { - write!(out, "{prefix}{line}\n") - .expect("String writes don't fail"); + write!(out, "{prefix}{line}\n").expect("String writes don't fail"); } out.push_str("```"); 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 ff0d42b5a9..78e9604ac4 100644 --- a/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links/take_lines.rs @@ -27,16 +27,22 @@ pub(super) fn take_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator 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; + if captures[1] == *anchor { + in_anchor = false; + } + return false; } return !ANCHOR_START.is_match(line); } - if ANCHOR_END.is_match(line) { return false; } + if ANCHOR_END.is_match(line) { + return false; + } if let Some(captures) = ANCHOR_START.captures(line) { - if captures[1] == *anchor { in_anchor = true; } + if captures[1] == *anchor { + in_anchor = true; + } } false @@ -47,32 +53,46 @@ pub(super) fn take_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl Iterator /// 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)) - }) +pub(super) fn take_rustdoc_lines>( + s: &str, + range: R, +) -> impl Iterator { + s.lines() + .enumerate() + .map(move |(index, line)| (line, range.contains(&index))) } /// 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 { +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; } + if captures[1] == *anchor { + in_anchor = false; + } + return None; + } + if ANCHOR_START.is_match(line) { return None; } - if ANCHOR_START.is_match(line) { return None; } return Some((line, true)); } - if ANCHOR_END.is_match(line) { return None; } + if ANCHOR_END.is_match(line) { + return None; + } if let Some(captures) = ANCHOR_START.captures(line) { - if captures[1] == *anchor { in_anchor = true; } + if captures[1] == *anchor { + in_anchor = true; + } return None; } @@ -82,18 +102,24 @@ pub(super) fn take_rustdoc_anchored_lines<'a>(s: &'a str, anchor: &str) -> impl #[cfg(test)] mod tests { - use super::{ - take_anchored_lines, take_lines, take_rustdoc_anchored_lines, - take_rustdoc_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).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, 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).collect::>().join("\n"), ""); @@ -103,33 +129,90 @@ mod tests { #[test] fn take_anchored_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; - assert_eq!(take_anchored_lines(s, "test").collect::>().join("\n"), ""); + 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").collect::>().join("\n"), ""); + 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").collect::>().join("\n"), "dolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something").collect::>().join("\n"), ""); + 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").collect::>().join("\n"), "dolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something").collect::>().join("\n"), ""); + 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").collect::>().join("\n"), "ipsum\ndolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something").collect::>().join("\n"), ""); + 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").collect::>().join("\n"), + take_anchored_lines(s, "test2") + .collect::>() + .join("\n"), "ipsum\ndolor\nsit\namet\nlorem" ); - assert_eq!(take_anchored_lines(s, "test").collect::>().join("\n"), "dolor\nsit\namet"); - assert_eq!(take_anchored_lines(s, "something").collect::>().join("\n"), ""); + 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"); + assert_eq!( + take_anchored_lines(s, "test") + .collect::>() + .join("\n"), + "ipsum\nsit" + ); } #[test] @@ -137,59 +220,47 @@ mod tests { fn take_rustdoc_lines_test() { let s = "Lorem\nipsum\ndolor\nsit\namet"; assert_eq!( - 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_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_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_lines(s, ..) - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) - .collect::>() - .join("\n"), - s + 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_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_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_lines(s, ..) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), + s ); // corner cases assert_eq!( - 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" + 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_lines(s, ..100) - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) - .collect::>() - .join("\n"), - s + take_rustdoc_lines(s, ..100) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) + .collect::>() + .join("\n"), + s ); } @@ -198,9 +269,7 @@ mod tests { let s = "Lorem\nipsum\ndolor\nsit\namet"; assert_eq!( take_rustdoc_anchored_lines(s, "test") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) .collect::>() .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" @@ -209,9 +278,7 @@ mod tests { let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet"; assert_eq!( take_rustdoc_anchored_lines(s, "test") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) .collect::>() .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" @@ -220,18 +287,14 @@ mod tests { let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet"; assert_eq!( take_rustdoc_anchored_lines(s, "test") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) .collect::>() .join("\n"), "# Lorem\n# ipsum\ndolor\nsit\namet" ); assert_eq!( take_rustdoc_anchored_lines(s, "something") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .map(|(line, show)| { format!("{}{line}", show.then_some("").unwrap_or("# ")) }) .collect::>() .join("\n"), "# Lorem\n# ipsum\n# dolor\n# sit\n# amet" @@ -240,18 +303,14 @@ mod tests { let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum"; assert_eq!( take_rustdoc_anchored_lines(s, "test") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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_anchored_lines(s, "something") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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" @@ -260,18 +319,14 @@ mod tests { let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum"; assert_eq!( take_rustdoc_anchored_lines(s, "test") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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_anchored_lines(s, "something") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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" @@ -280,27 +335,21 @@ mod tests { let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum"; assert_eq!( take_rustdoc_anchored_lines(s, "test2") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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_anchored_lines(s, "test") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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_anchored_lines(s, "something") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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" @@ -309,9 +358,7 @@ mod tests { let s = "Lorem\nANCHOR: test\nipsum\nANCHOR_END: test\ndolor\nANCHOR: test\nsit\nANCHOR_END: test\namet"; assert_eq!( take_rustdoc_anchored_lines(s, "test") - .map(|(line, show)| { - format!("{}{line}", show.then_some("").unwrap_or("# ")) - }) + .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 1a5cc4116a..a124b46f6a 100644 --- a/tests/testsuite/includes.rs +++ b/tests/testsuite/includes.rs @@ -132,10 +132,9 @@ fn rustdoc_include() { // 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##" + BookTest::from_dir("includes/all_includes").check_main_file( + "book/indented.html", + str![[r##"

Indented Includes

  • @@ -182,6 +181,5 @@ fn indented_include() {
"##]], - ) - ; + ); }