From 7df1d8c838c0e21968666b671885ac185a426fd7 Mon Sep 17 00:00:00 2001 From: Jannik Obermann Date: Wed, 23 Feb 2022 01:02:54 +0100 Subject: [PATCH 1/4] Support hidden lines in languages other than Rust Co-Authored-By: thecodewarrior <5467669+thecodewarrior@users.noreply.github.com> --- guide/book.toml | 3 + guide/src/format/configuration/renderers.md | 11 + guide/src/format/mdbook.md | 41 ++- guide/src/format/theme/syntax-highlighting.md | 32 --- src/config.rs | 19 ++ src/renderer/html_handlebars/hbs_renderer.rs | 259 ++++++++++++++---- src/theme/book.js | 2 +- 7 files changed, 273 insertions(+), 94 deletions(-) diff --git a/guide/book.toml b/guide/book.toml index 025efc0b..7ef29f13 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -17,6 +17,9 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path editable = true line-numbers = true +[output.html.code.hidelines] +python = "~" + [output.html.search] limit-results = 20 use-boolean-and = true diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index e71a9d7b..fd9ad81d 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -223,6 +223,17 @@ runnable = true # displays a run button for rust code [Ace]: https://ace.c9.io/ +### `[output.html.code]` + +The `[output.html.code]` table provides options for controlling code blocks. + +```toml +[output.html.code] +# A prefix string per language (one or more chars). +# Any line starting with whitespace+prefix is hidden. +hidelines = { python = "~" } +``` + ### `[output.html.search]` The `[output.html.search]` table provides options for controlling the built-in text [search]. diff --git a/guide/src/format/mdbook.md b/guide/src/format/mdbook.md index 62e89843..485e73a3 100644 --- a/guide/src/format/mdbook.md +++ b/guide/src/format/mdbook.md @@ -4,7 +4,6 @@ There is a feature in mdBook that lets you hide code lines by prepending them with a `#` [like you would with Rustdoc][rustdoc-hide]. -This currently only works with Rust language code blocks. [rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example @@ -30,6 +29,46 @@ Will render as The code block has an eyeball icon () which will toggle the visibility of the hidden lines. +By default, this only works for code examples that are annotated with `rust`. +However, you can define custom prefixes for other languages by adding a new line-hiding prefix in your `book.toml` with the language name and prefix character(s): + +```toml +[output.html.code.hidelines] +python = "~" +``` + +The prefix will hide any lines that begin with the given prefix. With the python prefix shown above, this: + +```bash +~hidden() +nothidden(): +~ hidden() + ~hidden() + nothidden() +``` + +will render as + +```python +~hidden() +nothidden(): +~ hidden() + ~hidden() + nothidden() +``` + +This behavior can be overridden locally with a different prefix. This has the same effect as above: + +~~~bash +```python,hidelines=!!! +!!!hidden() +nothidden(): +!!! hidden() + !!!hidden() + nothidden() +``` +~~~ + ## Rust Playground Rust language code blocks will automatically get a play button () which will execute the code and display the output just below the code block. diff --git a/guide/src/format/theme/syntax-highlighting.md b/guide/src/format/theme/syntax-highlighting.md index f57540f0..6b33faa3 100644 --- a/guide/src/format/theme/syntax-highlighting.md +++ b/guide/src/format/theme/syntax-highlighting.md @@ -77,38 +77,6 @@ the `theme` folder of your book. Now your theme will be used instead of the default theme. -## Hiding code lines - -There is a feature in mdBook that lets you hide code lines by prepending them -with a `#`. - - -```bash -# fn main() { - let x = 5; - let y = 6; - - println!("{}", x + y); -# } -``` - -Will render as - -```rust -# fn main() { - let x = 5; - let y = 7; - - println!("{}", x + y); -# } -``` - -**At the moment, this only works for code examples that are annotated with -`rust`. Because it would collide with semantics of some programming languages. -In the future, we want to make this configurable through the `book.toml` so that -everyone can benefit from it.** - - ## Improve default theme If you think the default theme doesn't look quite right for a specific language, diff --git a/src/config.rs b/src/config.rs index a58a48bc..4641d1a2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -504,6 +504,8 @@ pub struct HtmlConfig { /// Playground settings. #[serde(alias = "playpen")] pub playground: Playground, + /// Code settings. + pub code: Code, /// Print settings. pub print: Print, /// Don't render section labels. @@ -556,6 +558,7 @@ impl Default for HtmlConfig { additional_js: Vec::new(), fold: Fold::default(), playground: Playground::default(), + code: Code::default(), print: Print::default(), no_section_label: false, search: None, @@ -642,6 +645,22 @@ impl Default for Playground { } } +/// Configuration for tweaking how the the HTML renderer handles code blocks. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct Code { + /// A prefix string to hide lines per language (one or more chars). + pub hidelines: HashMap, +} + +impl Default for Code { + fn default() -> Code { + Code { + hidelines: HashMap::new(), + } + } +} + /// Configuration of the search functionality of the HTML renderer. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index e753dc2e..0681806d 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -1,5 +1,5 @@ use crate::book::{Book, BookItem}; -use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition}; +use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition}; use crate::errors::*; use crate::renderer::html_handlebars::helpers; use crate::renderer::{RenderContext, Renderer}; @@ -110,7 +110,12 @@ impl HtmlHandlebars { debug!("Render template"); let rendered = ctx.handlebars.render("index", &ctx.data)?; - let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition); + let rendered = self.post_process( + rendered, + &ctx.html_config.playground, + &ctx.html_config.code, + ctx.edition, + ); // Write to file debug!("Creating {}", filepath.display()); @@ -121,8 +126,12 @@ impl HtmlHandlebars { ctx.data.insert("path_to_root".to_owned(), json!("")); ctx.data.insert("is_index".to_owned(), json!(true)); let rendered_index = ctx.handlebars.render("index", &ctx.data)?; - let rendered_index = - self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition); + let rendered_index = self.post_process( + rendered_index, + &ctx.html_config.playground, + &ctx.html_config.code, + ctx.edition, + ); debug!("Creating index.html from {}", ctx_path); utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; } @@ -182,8 +191,12 @@ impl HtmlHandlebars { data_404.insert("title".to_owned(), json!(title)); let rendered = handlebars.render("index", &data_404)?; - let rendered = - self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); + let rendered = self.post_process( + rendered, + &html_config.playground, + &html_config.code, + ctx.config.rust.edition, + ); let output_file = get_404_output_file(&html_config.input_404); utils::fs::write_file(destination, output_file, rendered.as_bytes())?; debug!("Creating 404.html ✓"); @@ -195,11 +208,13 @@ impl HtmlHandlebars { &self, rendered: String, playground_config: &Playground, + code_config: &Code, edition: Option, ) -> String { let rendered = build_header_links(&rendered); let rendered = fix_code_blocks(&rendered); let rendered = add_playground_pre(&rendered, playground_config, edition); + let rendered = hide_lines(&rendered, code_config); rendered } @@ -583,8 +598,12 @@ impl Renderer for HtmlHandlebars { debug!("Render template"); let rendered = handlebars.render("index", &data)?; - let rendered = - self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); + let rendered = self.post_process( + rendered, + &html_config.playground, + &html_config.code, + ctx.config.rust.edition, + ); utils::fs::write_file(destination, "print.html", rendered.as_bytes())?; debug!("Creating print.html ✓"); @@ -887,53 +906,50 @@ fn add_playground_pre( let classes = &caps[2]; let code = &caps[3]; - if classes.contains("language-rust") { - if (!classes.contains("ignore") + if classes.contains("language-rust") + && ((!classes.contains("ignore") && !classes.contains("noplayground") && !classes.contains("noplaypen") && playground_config.runnable) - || classes.contains("mdbook-runnable") - { - let contains_e2015 = classes.contains("edition2015"); - let contains_e2018 = classes.contains("edition2018"); - let contains_e2021 = classes.contains("edition2021"); - let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 { - // the user forced edition, we should not overwrite it - "" - } else { - match edition { - Some(RustEdition::E2015) => " edition2015", - Some(RustEdition::E2018) => " edition2018", - Some(RustEdition::E2021) => " edition2021", - None => "", - } - }; - - // wrap the contents in an external pre block - format!( - "
{}
", - classes, - edition_class, - { - let content: Cow<'_, str> = if playground_config.editable - && classes.contains("editable") - || text.contains("fn main") - || text.contains("quick_main!") - { - code.into() - } else { - // we need to inject our own main - let (attrs, code) = partition_source(code); - - format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code) - .into() - }; - hide_lines(&content) - } - ) + || classes.contains("mdbook-runnable")) + { + let contains_e2015 = classes.contains("edition2015"); + let contains_e2018 = classes.contains("edition2018"); + let contains_e2021 = classes.contains("edition2021"); + let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 { + // the user forced edition, we should not overwrite it + "" } else { - format!("{}", classes, hide_lines(code)) - } + match edition { + Some(RustEdition::E2015) => " edition2015", + Some(RustEdition::E2018) => " edition2018", + Some(RustEdition::E2021) => " edition2021", + None => "", + } + }; + + // wrap the contents in an external pre block + format!( + "
{}
", + classes, + edition_class, + { + let content: Cow<'_, str> = if playground_config.editable + && classes.contains("editable") + || text.contains("fn main") + || text.contains("quick_main!") + { + code.into() + } else { + // we need to inject our own main + let (attrs, code) = partition_source(code); + + format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code) + .into() + }; + content + } + ) } else { // not language-rust, so no-op text.to_owned() @@ -942,7 +958,53 @@ fn add_playground_pre( .into_owned() } -fn hide_lines(content: &str) -> String { +fn hide_lines(html: &str, code_config: &Code) -> String { + let regex = Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap(); + let language_regex = Regex::new(r"\blanguage-(\w+)\b").unwrap(); + let hidelines_regex = Regex::new(r"\bhidelines=(\S+)").unwrap(); + regex + .replace_all(html, |caps: &Captures<'_>| { + let text = &caps[1]; + let classes = &caps[2]; + let code = &caps[3]; + + if classes.contains("language-rust") { + format!( + "{}", + classes, + hide_lines_rust(code) + ) + } else { + // First try to get the prefix from the code block + let hidelines_capture = hidelines_regex.captures(classes); + let hidelines_prefix = match &hidelines_capture { + Some(capture) => Some(&capture[1]), + None => { + // Then look up the prefix by language + let language_capture = language_regex.captures(classes); + match &language_capture { + Some(capture) => { + code_config.hidelines.get(&capture[1]).map(|p| p.as_str()) + } + None => None, + } + } + }; + + match hidelines_prefix { + Some(prefix) => format!( + "{}", + classes, + hide_lines_with_prefix(code, prefix) + ), + None => text.to_owned(), + } + } + }) + .into_owned() +} + +fn hide_lines_rust(content: &str) -> String { static BORING_LINES_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap()); let mut result = String::with_capacity(content.len()); @@ -975,6 +1037,26 @@ fn hide_lines(content: &str) -> String { result } +fn hide_lines_with_prefix(content: &str, prefix: &str) -> String { + let mut result = String::with_capacity(content.len()); + for line in content.lines() { + if line.trim_start().starts_with(prefix) { + let pos = line.find(prefix).unwrap(); + let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]); + + result += ""; + result += ws; + result += rest; + result += "\n"; + result += ""; + continue; + } + result += line; + result += "\n"; + } + result +} + fn partition_source(s: &str) -> (String, String) { let mut after_header = false; let mut before = String::new(); @@ -1010,6 +1092,7 @@ struct RenderItemContext<'a> { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn original_build_header_links() { @@ -1065,17 +1148,17 @@ mod tests { fn add_playground() { let inputs = [ ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), + "
# #![allow(unused)]\n#fn main() {\nx()\n#}
"), ("fn main() {}", "
fn main() {}
"), ("let s = \"foo\n # bar\n\";", - "
let s = \"foo\n bar\n\";
"), - ("let s = \"foo\n ## bar\n\";", "
let s = \"foo\n # bar\n\";
"), + ("let s = \"foo\n ## bar\n\";", + "
let s = \"foo\n ## bar\n\";
"), ("let s = \"foo\n # bar\n#\n\";", - "
let s = \"foo\n bar\n\n\";
"), + "
let s = \"foo\n # bar\n#\n\";
"), ("let s = \"foo\n # bar\n\";", - "let s = \"foo\n bar\n\";"), + "let s = \"foo\n # bar\n\";"), ("#![no_std]\nlet s = \"foo\";\n #[some_attr]", "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
"), ]; @@ -1095,7 +1178,7 @@ mod tests { fn add_playground_edition2015() { let inputs = [ ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), + "
# #![allow(unused)]\n#fn main() {\nx()\n#}
"), ("fn main() {}", "
fn main() {}
"), ("fn main() {}", @@ -1119,7 +1202,7 @@ mod tests { fn add_playground_edition2018() { let inputs = [ ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), + "
# #![allow(unused)]\n#fn main() {\nx()\n#}
"), ("fn main() {}", "
fn main() {}
"), ("fn main() {}", @@ -1143,7 +1226,7 @@ mod tests { fn add_playground_edition2021() { let inputs = [ ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), + "
# #![allow(unused)]\n#fn main() {\nx()\n#}
"), ("fn main() {}", "
fn main() {}
"), ("fn main() {}", @@ -1163,4 +1246,60 @@ mod tests { assert_eq!(&*got, *should_be); } } + + #[test] + fn hide_lines_language_rust() { + let inputs = [ + ( + "
\n# #![allow(unused)]\n#fn main() {\nx()\n#}
", + "
\n#![allow(unused)]\nfn main() {\nx()\n}
",), + ( + "
fn main() {}
", + "
fn main() {}
",), + ( + "
let s = \"foo\n # bar\n\";
", + "
let s = \"foo\n bar\n\";
",), + ( + "
let s = \"foo\n ## bar\n\";
", + "
let s = \"foo\n # bar\n\";
",), + ( + "
let s = \"foo\n # bar\n#\n\";
", + "
let s = \"foo\n bar\n\n\";
",), + ( + "let s = \"foo\n # bar\n\";", + "let s = \"foo\n bar\n\";",), + ( + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
", + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
",), + ]; + for (src, should_be) in &inputs { + let got = hide_lines(src, &Code::default()); + assert_eq!(&*got, *should_be); + } + } + + #[test] + fn hide_lines_language_other() { + let inputs = [ + ( + "~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()", + "hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n",), + ( + "!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()", + "hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n",), + ]; + for (src, should_be) in &inputs { + let got = hide_lines( + src, + &Code { + hidelines: { + let mut map = HashMap::new(); + map.insert("python".to_string(), "~".to_string()); + map + }, + }, + ); + assert_eq!(&*got, *should_be); + } + } } diff --git a/src/theme/book.js b/src/theme/book.js index f2516be7..ff3650eb 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -179,7 +179,7 @@ function playground_text(playground, hidden = true) { // even if highlighting doesn't apply code_nodes.forEach(function (block) { block.classList.add('hljs'); }); - Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) { + Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) { var lines = Array.from(block.querySelectorAll('.boring')); // If no lines were hidden, return From 1441fe0b9152daeb2b86f47a153206fa2aa565cc Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 28 May 2023 13:50:21 -0700 Subject: [PATCH 2/4] Explicitly document the `hidelines` key. --- guide/src/format/configuration/renderers.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index fd9ad81d..4e1c49da 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -234,6 +234,9 @@ The `[output.html.code]` table provides options for controlling code blocks. hidelines = { python = "~" } ``` +- **hidelines:** A table that defines how [hidden code lines](../mdbook.md#hiding-code-lines) work for each language. + The key is the language and the value is a string that will cause code lines starting with that prefix to be hidden. + ### `[output.html.search]` The `[output.html.search]` table provides options for controlling the built-in text [search]. From 5572d3d4de1870968bff55c2ee8ca81839f1255e Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 28 May 2023 13:50:34 -0700 Subject: [PATCH 3/4] Expand on hidelines documentation. --- guide/src/format/mdbook.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/guide/src/format/mdbook.md b/guide/src/format/mdbook.md index 485e73a3..2872b250 100644 --- a/guide/src/format/mdbook.md +++ b/guide/src/format/mdbook.md @@ -2,10 +2,11 @@ ## Hiding code lines -There is a feature in mdBook that lets you hide code lines by prepending them -with a `#` [like you would with Rustdoc][rustdoc-hide]. +There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix. -[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example +For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide]. + +[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example ```bash # fn main() { @@ -27,7 +28,7 @@ Will render as # } ``` -The code block has an eyeball icon () which will toggle the visibility of the hidden lines. +When you tap or hover the mouse over the code block, there will be an eyeball icon () which will toggle the visibility of the hidden lines. By default, this only works for code examples that are annotated with `rust`. However, you can define custom prefixes for other languages by adding a new line-hiding prefix in your `book.toml` with the language name and prefix character(s): @@ -59,7 +60,7 @@ nothidden(): This behavior can be overridden locally with a different prefix. This has the same effect as above: -~~~bash +~~~markdown ```python,hidelines=!!! !!!hidden() nothidden(): From c9cfe22fd60462726db58bde8266be052aaa5de7 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 28 May 2023 13:51:30 -0700 Subject: [PATCH 4/4] Apply some code style changes. --- src/renderer/html_handlebars/hbs_renderer.rs | 23 +++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 0681806d..709aa066 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -892,15 +892,15 @@ fn fix_code_blocks(html: &str) -> String { .into_owned() } +static CODE_BLOCK_RE: Lazy = + Lazy::new(|| Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap()); + fn add_playground_pre( html: &str, playground_config: &Playground, edition: Option, ) -> String { - static ADD_PLAYGROUND_PRE: Lazy = - Lazy::new(|| Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap()); - - ADD_PLAYGROUND_PRE + CODE_BLOCK_RE .replace_all(html, |caps: &Captures<'_>| { let text = &caps[1]; let classes = &caps[2]; @@ -958,11 +958,12 @@ fn add_playground_pre( .into_owned() } +/// Modifies all `` blocks to convert "hidden" lines and to wrap them in +/// a ``. fn hide_lines(html: &str, code_config: &Code) -> String { - let regex = Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap(); let language_regex = Regex::new(r"\blanguage-(\w+)\b").unwrap(); let hidelines_regex = Regex::new(r"\bhidelines=(\S+)").unwrap(); - regex + CODE_BLOCK_RE .replace_all(html, |caps: &Captures<'_>| { let text = &caps[1]; let classes = &caps[2]; @@ -981,13 +982,9 @@ fn hide_lines(html: &str, code_config: &Code) -> String { Some(capture) => Some(&capture[1]), None => { // Then look up the prefix by language - let language_capture = language_regex.captures(classes); - match &language_capture { - Some(capture) => { - code_config.hidelines.get(&capture[1]).map(|p| p.as_str()) - } - None => None, - } + language_regex.captures(classes).and_then(|capture| { + code_config.hidelines.get(&capture[1]).map(|p| p.as_str()) + }) } };