diff --git a/guide/book.toml b/guide/book.toml index 96dcf3120f..da61aa42e1 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -15,6 +15,9 @@ site-url = "/mdBook/" editable = true line-numbers = true +[output.html.playground.line-hiding-prefixes] +python = "~" + [output.html.search] limit-results = 20 use-boolean-and = true diff --git a/guide/src/format/config.md b/guide/src/format/config.md index d9c77e665c..89c2f60d10 100644 --- a/guide/src/format/config.md +++ b/guide/src/format/config.md @@ -239,6 +239,59 @@ Available configuration options for the `[output.html.playground]` table: - **copy-js:** Copy JavaScript files for the editor to the output directory. Defaults to `true`. - **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`. +- **line-hiding-prefixes** A map of simple to use prefixes for hiding lines in various languages. + (e.g. `{ python = "~" }` will hide lines in python blocks that start with `~`, but that `~` can be escaped with a backslash.) +- **line-hiding-patterns** A map of complex patterns to use for hiding lines in various languages. + +#### Line-hiding patterns +Line-hiding patterns define what lines in a code block should be hidden with togglable visibility. There is already a built-in pattern for rust, +which should behave identically to rustdoc. In rust, a hidden line starts with a `#`, however that can be escaped using a `##`, and +lines that start with `#!` (e.g. `#![...]`) or `#[` (e.g. `#[...]`) *won't* be hidden. + +```toml +[output.html.playground.line-hiding-prefixes] +python = "~" +[output.html.playground.line-hiding-patterns] +somelanguage = "" +``` + +The simplest way to add a line-hiding pattern is to add a line-hiding prefix, which is really an auto-generated pattern. +The generated pattern will hide any lines that begin with the specified prefix, unless it's preceded by a backslash. + +While a simple prefix will almost always work, sometimes a more complex pattern is necessary (e.g. rust won't hide `#[...]` lines), +and this is where fully-fledged line-hiding patterns kick in. These patterns are regular expressions and have to follow a few rules. + +- Each pattern should match an entire line. +- The pattern should have a group named `escape`, and this group should be optional. Note the difference between + the *contents* of the group being optional `(?P#?)` and the *group itself* being optional `(?P#)?`. +- Everything else that you *care about* should be in unnamed groups. + +mdBook will then test the regex on each line and follow this process: + +- If the line doesn't match at all, the line is left unchanged and isn't hidden +- If the line matches and the `escape` group matches, the escape group is cut out of the middle and the line isn't hidden +- If the line matches and the `escape` group *doesn't* match, the result is the combined contents of *every match group*. + (This is why you add unnamed groups around everything you care about.) + +Here is the pattern generated for a line-hiding prefix (the `{}` in replaced with the prefix string): +```re +^(\s*)(?P\\)?{}(.*)$ +``` +Breaking it down, we have +- `^(\s*)` + Match the indentation, and put it in a group to preserve it in the hidden output. +- `(?P\\)?` + If we find a backslash this group will match and trigger the escape mechanism. +- `{}` + Match the prefix. Note how this isn't in a group, meaning it won't be included in the hidden line. +- `(.*)$` + Match the rest of the line, and put it in a group to preserve it in the output. + +A more complex example would be the Rust pattern, which is beyond the scope of this guide. Most of the +changes are in the block after the `#` prefix, and are dedicated to ignoring `#[...]` and `#![...]` lines. +```re +^(\s*)(?P#)?#(?: (.*)|([^#!\[ ].*))?$ +``` [Ace]: https://ace.c9.io/ @@ -301,6 +354,8 @@ level = 0 editable = false copy-js = true line-numbers = false +line-hiding-patterns = {} +line-hiding-prefixes = {} [output.html.search] enable = true diff --git a/guide/src/format/mdbook.md b/guide/src/format/mdbook.md index 223ba2159d..4b811b78f0 100644 --- a/guide/src/format/mdbook.md +++ b/guide/src/format/mdbook.md @@ -7,14 +7,14 @@ with a `#` [in the same way that Rustdoc does][rustdoc-hide]. [rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example -```bash +
```rust
 # fn main() {
     let x = 5;
     let y = 6;
 
     println!("{}", x + y);
 # }
-```
+```
Will render as @@ -27,6 +27,47 @@ Will render as # } ``` +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 (you can even do multi-character prefixes if you really want to): + +```toml +[output.html.playground.line-hiding-prefixes] +python = "~" +``` + +The prefix will hide any lines that begin with the given prefix, but the prefix can be escaped using +a backslash. With the python prefix shown above, this: + +
```python
+~def fib():
+    a, b = 0, 1
+    while a < n:
+        print(a, end=' ')
+        a, b = b, a+b
+    print()
+    ~# hide me!
+    \~# leave me be!
+~fib(1000)
+```
+ +will render as + +```python +~def fib(): + a, b = 0, 1 + while a < n: + print(a, end=' ') + a, b = b, a+b + print() + ~# hide me! + \~# leave me be! +~fib(1000) +``` + +If you need something more advanced than a simple prefix (e.g. rust uses `#` but doesn't hide `#[...]` lines), +you can define a custom regular expression using [line-hiding patterns](./config.md#line-hiding-patterns). + ## Including files With the following syntax, you can include files into your book: diff --git a/guide/src/format/theme/syntax-highlighting.md b/guide/src/format/theme/syntax-highlighting.md index 42039cdd41..2ffe0718c2 100644 --- a/guide/src/format/theme/syntax-highlighting.md +++ b/guide/src/format/theme/syntax-highlighting.md @@ -26,38 +26,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 91d4030004..e6f2bea2f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -608,6 +608,17 @@ pub struct Playground { pub copy_js: bool, /// Display line numbers on playground snippets. Default: `false`. pub line_numbers: bool, + /// Additional line-hiding line patterns (language name -> pattern) + /// + /// Expects a group named `escape` + /// If the line doesn't match, it's left unchanged + /// When `escape` matches, the entire string except the `escape` group is used + /// When the line does match, all the groups are concatenated and the line is hidden + pub line_hiding_patterns: HashMap, + /// Additional line-hiding prefixes (language name -> pattern) + /// This is shorthand for a basic pattern that matches lines starting with the + /// passed prefix, using a backslash as the escape character + pub line_hiding_prefixes: HashMap, } impl Default for Playground { @@ -617,6 +628,8 @@ impl Default for Playground { copyable: true, copy_js: true, line_numbers: false, + line_hiding_patterns: HashMap::new(), + line_hiding_prefixes: HashMap::new(), } } } @@ -759,6 +772,8 @@ mod tests { copyable: true, copy_js: true, line_numbers: false, + line_hiding_patterns: HashMap::new(), + line_hiding_prefixes: HashMap::new(), }; let html_should_be = HtmlConfig { curly_quotes: true, diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 918ec29e5e..096cb4cb9b 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -795,13 +795,19 @@ fn add_playground_pre( edition: Option, ) -> String { let regex = Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap(); + let language_regex = Regex::new(r"\blanguage-(\w+)\b").unwrap(); regex .replace_all(html, |caps: &Captures<'_>| { let text = &caps[1]; let classes = &caps[2]; let code = &caps[3]; + let language = if let Some(captures) = language_regex.captures(classes) { + captures[1].to_owned() + } else { + String::from("") + }; - if classes.contains("language-rust") { + if language == "rust" { if (!classes.contains("ignore") && !classes.contains("noplayground") && !classes.contains("noplaypen")) @@ -842,12 +848,28 @@ fn add_playground_pre( ) .into() }; - hide_lines(&content) + RUST_LINE_HIDING_PATTERN.transform_lines(&content) } ) } else { - format!("{}", classes, hide_lines(code)) + format!( + "{}", + classes, + RUST_LINE_HIDING_PATTERN.transform_lines(code) + ) } + } else if let Some(pattern) = playground_config.line_hiding_patterns.get(&language) { + format!( + "{}", + classes, + LineHidingPattern::new(pattern).transform_lines(code) + ) + } else if let Some(prefix) = playground_config.line_hiding_prefixes.get(&language) { + format!( + "{}", + classes, + LineHidingPattern::new_simple(prefix).transform_lines(code) + ) } else { // not language-rust, so no-op text.to_owned() @@ -857,35 +879,75 @@ fn add_playground_pre( } lazy_static! { - static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap(); + static ref RUST_LINE_HIDING_PATTERN: LineHidingPattern = + LineHidingPattern::new(r"^(\s*)(?P#)?#(?: (.*)|([^#!\[ ].*))?$"); } -fn hide_lines(content: &str) -> String { - let mut result = String::with_capacity(content.len()); - for line in content.lines() { - if let Some(caps) = BORING_LINES_REGEX.captures(line) { - if &caps[2] == "#" { - result += &caps[1]; - result += &caps[2]; - result += &caps[3]; - result += "\n"; - continue; - } else if &caps[2] != "!" && &caps[2] != "[" { - result += ""; - result += &caps[1]; - if &caps[2] != " " { - result += &caps[2]; +struct LineHidingPattern { + regex: Regex, +} + +impl LineHidingPattern { + fn new(pattern: &str) -> LineHidingPattern { + LineHidingPattern { + regex: Regex::new(pattern).unwrap(), + } + } + + fn new_simple(prefix: &str) -> LineHidingPattern { + LineHidingPattern { + regex: Regex::new(&format!( + r"^(\s*)(?P\\)?{}(.*)$", + regex::escape(prefix) + )) + .unwrap(), + } + } + + /// Expects a group named `escape` + /// if the string doesn't match, it's returned directly + /// when `escape` matches, the entire string except the `escape` group is returned + /// otherwise, all the groups are concatenated and returned + /// + /// returns the resulting string and a bool specifying if the line should be hidden + fn transform(&self, line: &str) -> (String, bool) { + if let Some(captures) = self.regex.captures(line) { + if let Some(m) = captures.name("escape") { + // pick everything before and everything after the escape group + let mut out = String::with_capacity(line.len()); + out += &line[0..m.start()]; + out += &line[m.end()..line.len()]; + (out, false) + } else { + // combine the contents of all the capture groups. + let mut out = String::with_capacity(line.len()); + for opt in captures.iter().skip(1) { + if let Some(m) = opt { + out += m.as_str() + } } - result += &caps[3]; - result += "\n"; - result += ""; - continue; + (out, true) + } + } else { + (line.to_owned(), false) + } + } + + fn transform_lines(&self, content: &str) -> String { + let mut result = String::with_capacity(content.len()); + for line in content.lines() { + let (out, should_hide) = self.transform(line); + if should_hide { + result += ""; + } + result += &out; + result += "\n"; + if should_hide { + result += "" } } - result += line; - result += "\n"; + result } - result } fn partition_source(s: &str) -> (String, String) { diff --git a/src/theme/book.js b/src/theme/book.js index 5e386369f7..859fb4ff0b 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -166,7 +166,7 @@ function playground_text(playground) { // 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