Repository: rust-lang/mdBook (language: rust)

Issue:
[Feature Request] "Show hidden lines" functionality for non-rust files
As far as I can tell, there is no way to have the "show hidden lines" thing when showing a subset of lines without using `#rustdoc_include`. 

However, `#rustdoc_include` wraps invalid rust (i.e. any file that's actually not supposed to be rust code) automatically with a main function.

For example, something like,

```
```rust
{{#rustdoc_include hello.txt:1:2}}
\```
```

will render as:

```
#![allow(unused)]
fn main() {
hello
}
```

when "Show hidden lines" is clicked, which interferes with copy-pasting and general cleanliness. It would be great if the "Show hidden lines" / "Hide lines" functionality could be toggled on somehow for regular non-rust files for which only a certain range of lines are shown by default. 

#1294 and #1289 seem related, though they appear to still be within the context of rust code files.


Discussion / hints:

Fix patch:
diff --git a/guide/book.toml b/guide/book.toml
index 025efc0b2b..7ef29f13bb 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 e71a9d7bdd..4e1c49da98 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 62e8984390..2872b2501f 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 (<i class="fa fa-eye"></i>) 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 (<i class="fa fa-eye"></i>) 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 f57540f001..6b33faa333 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 a58a48bc3c..4641d1a262 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<String, String>,
+}
+
+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 e753dc2e14..709aa06675 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<RustEdition>,
     ) -> 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<Regex> =
+    Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
+
 fn add_playground_pre(
     html: &str,
     playground_config: &Playground,
     edition: Option<RustEdition>,
 ) -> String {
-    static ADD_PLAYGROUND_PRE: Lazy<Regex> =
-        Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).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!(
-                        "<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
-                        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!("<code class=\"{}\">{}</code>", 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!(
+                    "<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
+                    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 `<code>` blocks to convert "hidden" lines and to wrap them in
+/// a `<span class="boring">`.
+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!(
+                    "<code class=\"{}\">{}</code>",
+                    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!(
+                        "<code class=\"{}\">{}</code>",
+                        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<Regex> = 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 += "<span class=\"boring\">";
+            result += ws;
+            result += rest;
+            result += "\n";
+            result += "</span>";
+            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 = [
           ("<code class=\"language-rust\">x()</code>",
-           "<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
+           "<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
           ("<code class=\"language-rust\">fn main() {}</code>",
            "<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>"),
           ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
-           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>"),
-          ("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
            "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>"),
+          ("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>"),
           ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
-           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</code></pre>"),
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>"),
           ("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
-           "<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code>"),
+           "<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>"),
           ("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
            "<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>"),
         ];
@@ -1095,7 +1175,7 @@ mod tests {
     fn add_playground_edition2015() {
         let inputs = [
           ("<code class=\"language-rust\">x()</code>",
-           "<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
+           "<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
           ("<code class=\"language-rust\">fn main() {}</code>",
            "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
           ("<code class=\"language-rust edition2015\">fn main() {}</code>",
@@ -1119,7 +1199,7 @@ mod tests {
     fn add_playground_edition2018() {
         let inputs = [
           ("<code class=\"language-rust\">x()</code>",
-           "<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
+           "<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
           ("<code class=\"language-rust\">fn main() {}</code>",
            "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
           ("<code class=\"language-rust edition2015\">fn main() {}</code>",
@@ -1143,7 +1223,7 @@ mod tests {
     fn add_playground_edition2021() {
         let inputs = [
             ("<code class=\"language-rust\">x()</code>",
-             "<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
+             "<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
             ("<code class=\"language-rust\">fn main() {}</code>",
              "<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
             ("<code class=\"language-rust edition2015\">fn main() {}</code>",
@@ -1163,4 +1243,60 @@ mod tests {
             assert_eq!(&*got, *should_be);
         }
     }
+
+    #[test]
+    fn hide_lines_language_rust() {
+        let inputs = [
+          (
+           "<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>",
+           "<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>",),
+          (
+           "<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
+           "<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",),
+          (
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>",),
+          (
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>",
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",),
+          (
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>",
+           "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</code></pre>",),
+          (
+           "<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
+           "<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code>",),
+          (
+           "<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",
+           "<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",),
+        ];
+        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 = [
+          (
+           "<code class=\"language-python\">~hidden()\nnothidden():\n~    hidden()\n    ~hidden()\n    nothidden()</code>",
+           "<code class=\"language-python\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\">    hidden()\n</span><span class=\"boring\">    hidden()\n</span>    nothidden()\n</code>",),
+           (
+            "<code class=\"language-python hidelines=!!!\">!!!hidden()\nnothidden():\n!!!    hidden()\n    !!!hidden()\n    nothidden()</code>",
+            "<code class=\"language-python hidelines=!!!\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\">    hidden()\n</span><span class=\"boring\">    hidden()\n</span>    nothidden()\n</code>",),
+        ];
+        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 f2516be7a0..ff3650eb47 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


Test patch: