Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for hiding lines in other languages #1339

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add support for hiding lines in other languages
  • Loading branch information
thecodewarrior committed Sep 27, 2020
commit 6cb4f4a474d2dc39c25f7c20e2c9e72978e07039
56 changes: 56 additions & 0 deletions guide/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,60 @@ 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`.
- **boring-prefixes** A map of simple to use prefixes for hiding "boring" lines in various languages.
(e.g. `{ python = "~" }` will hide lines in python blocks that start with `~`, but that `~` can be escaped with a backslash.)
- **boring-patterns** A map of complex patterns to use for hiding "boring" lines in various languages.

#### Boring patterns
Boring patterns define what lines in a code block are "boring" and should be hidden. There is already a built-in pattern for rust,
which should behave identically to rustdoc. In rust, a boring line starts with a `#`, however that can be escaped using a `##`, and
lines that start with `#!` (e.g. `#![...]`) or `#[` (e.g. `#[...]`) *won't* be boring.

```toml
[output.html.playground.boring-prefixes]
python = "~"
[output.html.playground.boring-patterns]
somelanguage = "<custom pattern>"
```

The simplest way to add a boring pattern is to add a "boring 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 boring prefix will almost always work, sometimes a more complex pattern is necessary (e.g. rust won't mark `#[...]` lines as boring),
and this is where fully-fledged boring patterns kick in. Boring 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 pattern:

- If the line doesn't match, the line is left unchanged
- If the line matches and the `escape` group matches, the output is the entire line with the escape group cut out of the middle.
- If the line matched and the `escape` group *doesn't* match, the output 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 boring 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 output.
- `(?P<escape>\\)?`
If we find a backslash this group will match and trigger the escape mechanism.
- `{} ?`
Match the prefix and optionally the space after it. Note how this isn't in a group, meaning it
won't be included in the final 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 +355,8 @@ level = 0
editable = false
copy-js = true
line-numbers = false
boring-patterns = {}
boring-prefixes = {}

[output.html.search]
enable = true
Expand Down
17 changes: 13 additions & 4 deletions guide/src/format/theme/syntax-highlighting.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,20 @@ Will render as
# }
```

**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.**
By default, this only works for code examples that are annotated with `rust`. However, you can
define custom patterns for other languages in your `book.toml`. Unless you need something complex
(e.g. rust uses `#` but doesn't hide `#[...]` lines), adding a new language is trivial. Just add
a new `boring-prefix` entry in your `book.toml` with the language name and prefix character
(you can also do multi-character prefixes if you really want to):

```toml
[output.html.playground.boring-prefixes]
python = "~"
```

The auto-generated prefix patterns will hide any lines that begin with the given prefix, but
the prefix can be escaped using a backslash. If you need something more complex than that,
you can use a [fully custom pattern](../config.md#boring-patterns).

## Improve default theme

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 boring line patterns (language name -> pattern)
///
/// Expects groups named `escape` and `prefix`
/// 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 used
pub boring_patterns: HashMap<String, String>,
/// Additional boring line 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 boring_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,
boring_patterns: HashMap::new(),
boring_prefixes: HashMap::new(),
}
}
}
Expand Down Expand Up @@ -759,6 +772,8 @@ mod tests {
copyable: true,
copy_js: true,
line_numbers: false,
boring_patterns: HashMap::new(),
boring_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_BORING_PATTERN.transform_lines(&content)
}
)
} else {
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
format!(
"<code class=\"{}\">{}</code>",
classes,
RUST_BORING_PATTERN.transform_lines(code)
)
}
} else if let Some(pattern) = playground_config.boring_patterns.get(&language) {
format!(
"<code class=\"{}\">{}</code>",
classes,
BoringPattern::new(pattern).transform_lines(code)
)
} else if let Some(prefix) = playground_config.boring_prefixes.get(&language) {
format!(
"<code class=\"{}\">{}</code>",
classes,
BoringPattern::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_BORING_PATTERN: BoringPattern =
BoringPattern::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 BoringPattern {
regex: Regex,
}

impl BoringPattern {
fn new(pattern: &str) -> BoringPattern {
BoringPattern {
regex: Regex::new(pattern).unwrap(),
}
}

fn new_simple(prefix: &str) -> BoringPattern {
BoringPattern {
regex: Regex::new(&format!(
r"^(\s*)(?P<escape>\\)?{} ?(.*)$",
regex::escape(prefix)
))
.unwrap(),
}
}

/// Expects groups named `escape` and `prefix`
/// 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 was boring
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, boring) = self.transform(line);
if boring {
result += "<span class=\"boring\">";
}
result += &out;
result += "\n";
if boring {
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