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..4e1c49da 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -223,6 +223,20 @@ 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 = "~" } +``` + +- **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]. diff --git a/guide/src/format/mdbook.md b/guide/src/format/mdbook.md index 62e89843..2872b250 100644 --- a/guide/src/format/mdbook.md +++ b/guide/src/format/mdbook.md @@ -2,11 +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]. -This currently only works with Rust language code blocks. +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() { @@ -28,7 +28,47 @@ 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): + +```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: + +~~~markdown +```python,hidelines=!!! +!!!hidden() +nothidden(): +!!! hidden() + !!!hidden() + nothidden() +``` +~~~ ## Rust Playground 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..709aa066 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 ✓"); @@ -873,67 +892,64 @@ 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]; 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,50 @@ fn add_playground_pre( .into_owned() } -fn hide_lines(content: &str) -> String { +/// Modifies all `` blocks to convert "hidden" lines and to wrap them in +/// a ``. +fn hide_lines(html: &str, code_config: &Code) -> String { + let language_regex = Regex::new(r"\blanguage-(\w+)\b").unwrap(); + let hidelines_regex = Regex::new(r"\bhidelines=(\S+)").unwrap(); + CODE_BLOCK_RE + .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 + language_regex.captures(classes).and_then(|capture| { + code_config.hidelines.get(&capture[1]).map(|p| p.as_str()) + }) + } + }; + + 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 +1034,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 +1089,7 @@ struct RenderItemContext<'a> { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn original_build_header_links() { @@ -1065,17 +1145,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 +1175,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 +1199,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 +1223,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 +1243,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