From f280839d27df455630ce8938dc4bec7ddcc26e16 Mon Sep 17 00:00:00 2001 From: cobyfrombrooklyn-bot Date: Thu, 26 Feb 2026 16:56:44 -0500 Subject: [PATCH] fix: handle hidden crate attributes in fn main wrapping (#2640) The partition_rust_source regex did not account for lines with the hidden line prefix (`# `), so `# #![feature(...)]` was not recognized as a crate-level attribute. This caused wrap_rust_main to place the attribute inside fn main() instead of before it, breaking playground execution for code blocks with hidden feature attributes and no explicit fn main. Updated the HEADER_RE regex to optionally match the `# ` hidden line prefix before crate-level inner attributes. Added unit tests for wrap_rust_main and partition_rust_source covering the hidden prefix case, plus an integration test with a playground code block using a hidden feature attribute. --- crates/mdbook-html/src/html/hide_lines.rs | 37 ++++++++++++++++++- tests/testsuite/playground.rs | 19 ++++++++++ .../playground_hidden_feature_attr/book.toml | 2 + .../src/SUMMARY.md | 3 ++ .../src/feature-attr.md | 7 ++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/testsuite/playground/playground_hidden_feature_attr/book.toml create mode 100644 tests/testsuite/playground/playground_hidden_feature_attr/src/SUMMARY.md create mode 100644 tests/testsuite/playground/playground_hidden_feature_attr/src/feature-attr.md diff --git a/crates/mdbook-html/src/html/hide_lines.rs b/crates/mdbook-html/src/html/hide_lines.rs index 8dbe7b12c5..5413de3901 100644 --- a/crates/mdbook-html/src/html/hide_lines.rs +++ b/crates/mdbook-html/src/html/hide_lines.rs @@ -132,7 +132,7 @@ fn partition_rust_source(s: &str) -> (&str, &str) { r"^(?mx) ( (?: - ^[ \t]*\#!\[.* (?:\r?\n)? + ^[ \t]*(?:\#\x20)?[ \t]*\#!\[.* (?:\r?\n)? | ^\s* (?:\r?\n)? )* @@ -155,6 +155,36 @@ fn partition_rust_source(s: &str) -> (&str, &str) { s.split_at(split_idx) } +#[test] +fn wrap_rust_main_basic() { + // Code without fn main gets wrapped + assert_eq!( + wrap_rust_main("let x = 1;"), + Some("# #![allow(unused)]\n# fn main() {\nlet x = 1;\n# }".to_string()) + ); +} + +#[test] +fn wrap_rust_main_with_feature_attr() { + // Crate-level attributes should be placed before fn main (issue #2640) + // Without hidden prefix: + assert_eq!( + wrap_rust_main("#![feature(rustc_attrs)]\n#[rustc_on_unimplemented = \"oh no\"]\npub trait Foo {}"), + Some("# #![allow(unused)]\n#![feature(rustc_attrs)]\n# fn main() {\n#[rustc_on_unimplemented = \"oh no\"]\npub trait Foo {}\n# }".to_string()) + ); + // With hidden line prefix `# ` (the actual input from markdown): + assert_eq!( + wrap_rust_main("# #![feature(rustc_attrs)]\n#[rustc_on_unimplemented = \"oh no\"]\npub trait Foo {}"), + Some("# #![allow(unused)]\n# #![feature(rustc_attrs)]\n# fn main() {\n#[rustc_on_unimplemented = \"oh no\"]\npub trait Foo {}\n# }".to_string()) + ); +} + +#[test] +fn wrap_rust_main_already_has_main() { + // Code with fn main should not be wrapped + assert_eq!(wrap_rust_main("fn main() {}"), None); +} + #[test] fn it_partitions_rust_source() { assert_eq!(partition_rust_source(""), ("", "")); @@ -190,4 +220,9 @@ fn it_partitions_rust_source() { partition_rust_source(" // Example"), ("", " // Example") ); + // Hidden line prefix with crate attribute (issue #2640) + assert_eq!( + partition_rust_source("# #![feature(rustc_attrs)]\n#[foo]\npub trait Foo {}"), + ("# #![feature(rustc_attrs)]\n", "#[foo]\npub trait Foo {}") + ); } diff --git a/tests/testsuite/playground.rs b/tests/testsuite/playground.rs index d0aeb69981..973e41c0fc 100644 --- a/tests/testsuite/playground.rs +++ b/tests/testsuite/playground.rs @@ -17,6 +17,25 @@ fn playground_on_rust_code() { ); } +// Verifies that hidden crate-level attributes (like #![feature(...)]) are +// properly wrapped when fn main is generated. The attributes should appear +// before fn main, not inside it. (issue #2640) +#[test] +fn playground_hidden_feature_attr() { + BookTest::from_dir("playground/playground_hidden_feature_attr").check_main_file( + "book/feature-attr.html", + str![[r##" +

Feature Attr

+
#![allow(unused)]
+#![feature(rustc_attrs)]
+fn main() {
+#[rustc_on_unimplemented = "oh no"]
+pub trait Foo {}
+}
+"##]], + ); +} + // When the playground is disabled, there should be no playground class. #[test] fn disabled_playground() { diff --git a/tests/testsuite/playground/playground_hidden_feature_attr/book.toml b/tests/testsuite/playground/playground_hidden_feature_attr/book.toml new file mode 100644 index 0000000000..7bf7c972c6 --- /dev/null +++ b/tests/testsuite/playground/playground_hidden_feature_attr/book.toml @@ -0,0 +1,2 @@ +[book] +title = "playground_on_rust_code" diff --git a/tests/testsuite/playground/playground_hidden_feature_attr/src/SUMMARY.md b/tests/testsuite/playground/playground_hidden_feature_attr/src/SUMMARY.md new file mode 100644 index 0000000000..0e6f4e3ddb --- /dev/null +++ b/tests/testsuite/playground/playground_hidden_feature_attr/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +[Hidden Feature Attr](feature-attr.md) diff --git a/tests/testsuite/playground/playground_hidden_feature_attr/src/feature-attr.md b/tests/testsuite/playground/playground_hidden_feature_attr/src/feature-attr.md new file mode 100644 index 0000000000..bf995ef55d --- /dev/null +++ b/tests/testsuite/playground/playground_hidden_feature_attr/src/feature-attr.md @@ -0,0 +1,7 @@ +# Feature Attr + +```rust +# #![feature(rustc_attrs)] +#[rustc_on_unimplemented = "oh no"] +pub trait Foo {} +```