Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions guide/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions guide/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<custom pattern>"
```

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<escape>#?)` and the *group itself* being optional `(?P<escape>#)?`.
- 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<escape>\\)?{}(.*)$
```
Breaking it down, we have
- `^(\s*)`
Match the indentation, and put it in a group to preserve it in the hidden output.
- `(?P<escape>\\)?`
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<escape>#)?#(?: (.*)|([^#!\[ ].*))?$
```

[Ace]: https://ace.c9.io/

Expand Down Expand Up @@ -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
Expand Down
45 changes: 43 additions & 2 deletions guide/src/format/mdbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<pre><code class="language-markdown">```rust
# fn main() {
let x = 5;
let y = 6;

println!("{}", x + y);
# }
```
```</code></pre>

Will render as

Expand All @@ -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:

<pre><code class="language-markdown">```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)
```</code></pre>

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:
Expand Down
32 changes: 0 additions & 32 deletions guide/src/format/theme/syntax-highlighting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>,
/// 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<String, String>,
}

impl Default for Playground {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
114 changes: 88 additions & 26 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -795,13 +795,19 @@ fn add_playground_pre(
edition: Option<RustEdition>,
) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).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"))
Expand Down Expand Up @@ -842,12 +848,28 @@ fn add_playground_pre(
)
.into()
};
hide_lines(&content)
RUST_LINE_HIDING_PATTERN.transform_lines(&content)
}
)
} else {
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
format!(
"<code class=\"{}\">{}</code>",
classes,
RUST_LINE_HIDING_PATTERN.transform_lines(code)
)
}
} else if let Some(pattern) = playground_config.line_hiding_patterns.get(&language) {
format!(
"<code class=\"{}\">{}</code>",
classes,
LineHidingPattern::new(pattern).transform_lines(code)
)
} else if let Some(prefix) = playground_config.line_hiding_prefixes.get(&language) {
format!(
"<code class=\"{}\">{}</code>",
classes,
LineHidingPattern::new_simple(prefix).transform_lines(code)
)
} else {
// not language-rust, so no-op
text.to_owned()
Expand All @@ -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<escape>#)?#(?: (.*)|([^#!\[ ].*))?$");
}

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 += "<span class=\"boring\">";
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<escape>\\)?{}(.*)$",
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 += "</span>";
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 += "<span class=\"boring\">";
}
result += &out;
result += "\n";
if should_hide {
result += "</span>"
}
}
result += line;
result += "\n";
result
}
result
}

fn partition_source(s: &str) -> (String, String) {
Expand Down
2 changes: 1 addition & 1 deletion src/theme/book.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down