diff --git a/src/utils/highlight.rs b/src/utils/highlight.rs
index ec67b546..954ab59d 100644
--- a/src/utils/highlight.rs
+++ b/src/utils/highlight.rs
@@ -7,7 +7,7 @@ use std::borrow::Cow;
use regex::Regex;
use syntect::{
html::{self, ClassStyle},
- parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
+ parsing::{ParseState, Scope, ScopeStack, ScopeStackOp, SyntaxReference, SyntaxSet},
};
pub struct HtmlGenerator<'a> {
@@ -42,19 +42,57 @@ impl<'a> HtmlGenerator<'a> {
} else {
(Cow::from(line), false)
};
- let parsed_line = self.parse_state.parse_line(&line, self.syntaxes);
- let (formatted_line, delta) = html::line_tokens_to_classed_spans(
+ let parsed_line = if did_boringify {
+ // The empty scope is a valid prefix of every other scope.
+ // If we tried to just use a scope called "boring", we'd need to modify
+ // the Rust syntax definition.
+ let boring = Scope::new("").expect("boring is a valid scope");
+ // Close all open spans, insert `boring`, then re-open all of them.
+ // `boring` must be at the very top, so that the parser doesn't touch it.
+ let mut final_parsed_line = Vec::new();
+ if self.scope_stack.len() != 0 {
+ final_parsed_line.push((0, ScopeStackOp::Pop(self.scope_stack.len())));
+ }
+ final_parsed_line.push((0, ScopeStackOp::Push(boring.clone())));
+ for item in &self.scope_stack.scopes {
+ final_parsed_line.push((0, ScopeStackOp::Push(item.clone())));
+ }
+ // Now run the parser.
+ // It should see basically the stack it expects, except the `boring` at the very top,
+ // which it shouldn't touch because it doesn't know it's there.
+ let inner_parsed_line = self.parse_state.parse_line(&line, self.syntaxes);
+ final_parsed_line.extend_from_slice(&inner_parsed_line);
+ // Figure out what the final stack is.
+ let mut stack_at_end = self.scope_stack.clone();
+ for (_, item) in inner_parsed_line {
+ stack_at_end.apply(&item);
+ }
+ // Pop everything, including `boring`.
+ final_parsed_line.push((line.len(), ScopeStackOp::Pop(stack_at_end.len() + 1)));
+ // Push all the state back on at the end.
+ for item in stack_at_end.scopes.into_iter() {
+ final_parsed_line.push((line.len(), ScopeStackOp::Push(item)));
+ }
+ final_parsed_line
+ } else {
+ self.parse_state.parse_line(&line, self.syntaxes)
+ };
+ let (mut formatted_line, delta) = html::line_tokens_to_classed_spans(
&line,
parsed_line.as_slice(),
self.style,
&mut self.scope_stack,
);
+ if did_boringify {
+ // Since the boring scope is preceded only by a Pop operation,
+ // it must be the first match on the line for
+ formatted_line = formatted_line.replace(
+ r#""#,
+ r#""#,
+ );
+ }
self.open_spans += delta;
- self.html.push_str(&if did_boringify {
- format!("{}", formatted_line)
- } else {
- formatted_line
- });
+ self.html.push_str(&formatted_line);
}
pub fn finalize(mut self) -> String {
diff --git a/tests/dummy_book/src/example.rs b/tests/dummy_book/src/example.rs
index 6b49705c..534a1de8 100644
--- a/tests/dummy_book/src/example.rs
+++ b/tests/dummy_book/src/example.rs
@@ -3,4 +3,9 @@ fn main() {
#
# // You can even hide lines! :D
# println!("I am hidden! Expand the code snippet to see me");
+
+ // You can hide lines within string literals.
+ let _t = "interesting string
+# boring string
+ ";
}
diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs
index b01713eb..57389926 100644
--- a/tests/rendered_output.rs
+++ b/tests/rendered_output.rs
@@ -200,6 +200,21 @@ fn rustdoc_include_hides_the_unspecified_part_of_the_file() {
assert_contains_strings(nested, &text);
}
+#[test]
+fn boringify_properly_splits_string() {
+ let temp = DummyBook::new().build().unwrap();
+ let md = MDBook::load(temp.path()).unwrap();
+ md.build().unwrap();
+
+ let nested = temp.path().join("book/second.html");
+ let text = vec![
+ r#""interesting string"#,
+ r#"boring string"#,
+ ];
+
+ assert_contains_strings(nested, &text);
+}
+
#[test]
fn chapter_content_appears_in_rendered_document() {
let content = vec![