Skip to content

Commit 2532198

Browse files
TheOnlyMrCatKeats
authored andcommitted
Prevent spans crossing line boundaries in class-based code block formatter (#2237)
* Prevent spans crossing line boundaries in class formatter * Add snapshot tests for classed highlighting
1 parent 22dc32a commit 2532198

5 files changed

Lines changed: 133 additions & 51 deletions

File tree

components/markdown/src/codeblock/highlight.rs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use libs::syntect::highlighting::{Color, Theme};
66
use libs::syntect::html::{
77
line_tokens_to_classed_spans, styled_line_to_highlighted_html, ClassStyle, IncludeBackground,
88
};
9-
use libs::syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
9+
use libs::syntect::parsing::{
10+
ParseState, Scope, ScopeStack, SyntaxReference, SyntaxSet, SCOPE_REPO,
11+
};
1012
use libs::tera::escape_html;
1113

1214
/// Not public, but from syntect::html
@@ -18,17 +20,36 @@ fn write_css_color(s: &mut String, c: Color) {
1820
}
1921
}
2022

23+
/// Not public, but from syntect::html
24+
fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) {
25+
let repo = SCOPE_REPO.lock().unwrap();
26+
for i in 0..(scope.len()) {
27+
let atom = scope.atom_at(i as usize);
28+
let atom_s = repo.atom_str(atom);
29+
if i != 0 {
30+
s.push(' ')
31+
}
32+
match style {
33+
ClassStyle::Spaced => {}
34+
ClassStyle::SpacedPrefixed { prefix } => {
35+
s.push_str(prefix);
36+
}
37+
_ => {} // Non-exhaustive
38+
}
39+
s.push_str(atom_s);
40+
}
41+
}
42+
2143
pub(crate) struct ClassHighlighter<'config> {
2244
syntax_set: &'config SyntaxSet,
23-
open_spans: isize,
2445
parse_state: ParseState,
2546
scope_stack: ScopeStack,
2647
}
2748

2849
impl<'config> ClassHighlighter<'config> {
2950
pub fn new(syntax: &SyntaxReference, syntax_set: &'config SyntaxSet) -> Self {
3051
let parse_state = ParseState::new(syntax);
31-
Self { syntax_set, open_spans: 0, parse_state, scope_stack: ScopeStack::new() }
52+
Self { syntax_set, parse_state, scope_stack: ScopeStack::new() }
3253
}
3354

3455
/// Parse the line of code and update the internal HTML buffer with tagged HTML
@@ -39,24 +60,28 @@ impl<'config> ClassHighlighter<'config> {
3960
debug_assert!(line.ends_with('\n'));
4061
let parsed_line =
4162
self.parse_state.parse_line(line, self.syntax_set).expect("failed to parse line");
42-
let (formatted_line, delta) = line_tokens_to_classed_spans(
63+
64+
let mut formatted_line = String::with_capacity(line.len() + self.scope_stack.len() * 8); // A guess
65+
for scope in self.scope_stack.as_slice() {
66+
formatted_line.push_str("<span class=\"");
67+
scope_to_classes(&mut formatted_line, *scope, CLASS_STYLE);
68+
formatted_line.push_str("\">");
69+
}
70+
71+
let (formatted_contents, _) = line_tokens_to_classed_spans(
4372
line,
4473
parsed_line.as_slice(),
4574
CLASS_STYLE,
4675
&mut self.scope_stack,
4776
)
4877
.expect("line_tokens_to_classed_spans should not fail");
49-
self.open_spans += delta;
50-
formatted_line
51-
}
78+
formatted_line.push_str(&formatted_contents);
5279

53-
/// Close all open `<span>` tags and return the finished HTML string
54-
pub fn finalize(&mut self) -> String {
55-
let mut html = String::with_capacity((self.open_spans * 7) as usize);
56-
for _ in 0..self.open_spans {
57-
html.push_str("</span>");
80+
for _ in 0..self.scope_stack.len() {
81+
formatted_line.push_str("</span>");
5882
}
59-
html
83+
84+
formatted_line
6085
}
6186
}
6287

@@ -130,15 +155,6 @@ impl<'config> SyntaxHighlighter<'config> {
130155
}
131156
}
132157

133-
pub fn finalize(&mut self) -> Option<String> {
134-
use SyntaxHighlighter::*;
135-
136-
match self {
137-
Inlined(_) | NoHighlight => None,
138-
Classed(h) => Some(h.finalize()),
139-
}
140-
}
141-
142158
/// Inlined needs to set the background/foreground colour on <pre>
143159
pub fn pre_style(&self) -> Option<String> {
144160
use SyntaxHighlighter::*;
@@ -210,7 +226,6 @@ mod tests {
210226
for line in LinesWithEndings::from(code) {
211227
out.push_str(&highlighter.highlight_line(line));
212228
}
213-
out.push_str(&highlighter.finalize());
214229

215230
assert!(out.starts_with("<span class"));
216231
assert!(out.ends_with("</span>"));

components/markdown/src/codeblock/mod.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,6 @@ impl<'config> CodeBlock<'config> {
168168
}
169169
}
170170

171-
if let Some(rest) = self.highlighter.finalize() {
172-
buffer.push_str(&rest);
173-
}
174-
175171
if self.line_numbers {
176172
buffer.push_str("</tbody></table>");
177173
}

components/markdown/tests/codeblocks.rs

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@ use config::Config;
22

33
mod common;
44

5-
fn render_codeblock(content: &str, highlight_code: bool) -> String {
5+
enum HighlightMode {
6+
None,
7+
Inlined,
8+
Classed,
9+
}
10+
11+
fn render_codeblock(content: &str, highlight_mode: HighlightMode) -> String {
612
let mut config = Config::default_for_test();
7-
config.markdown.highlight_code = highlight_code;
13+
match highlight_mode {
14+
HighlightMode::None => {}
15+
HighlightMode::Inlined => {
16+
config.markdown.highlight_code = true;
17+
}
18+
HighlightMode::Classed => {
19+
config.markdown.highlight_code = true;
20+
config.markdown.highlight_theme = "css".to_owned();
21+
}
22+
}
823
common::render_with_config(content, config).unwrap().body
924
}
1025

@@ -17,7 +32,7 @@ foo
1732
bar
1833
```
1934
"#,
20-
false,
35+
HighlightMode::None,
2136
);
2237
insta::assert_snapshot!(body);
2338
}
@@ -33,7 +48,7 @@ baz
3348
bat
3449
```
3550
"#,
36-
true,
51+
HighlightMode::Inlined,
3752
);
3853
insta::assert_snapshot!(body);
3954
}
@@ -49,7 +64,7 @@ bar
4964
baz
5065
```
5166
"#,
52-
true,
67+
HighlightMode::Inlined,
5368
);
5469
insta::assert_snapshot!(body);
5570
}
@@ -65,7 +80,7 @@ bar
6580
baz
6681
```
6782
"#,
68-
true,
83+
HighlightMode::Inlined,
6984
);
7085
insta::assert_snapshot!(body);
7186
}
@@ -81,7 +96,7 @@ bar
8196
baz
8297
```
8398
"#,
84-
true,
99+
HighlightMode::Inlined,
85100
);
86101
insta::assert_snapshot!(body);
87102
}
@@ -97,7 +112,7 @@ bar
97112
baz
98113
```
99114
"#,
100-
true,
115+
HighlightMode::Inlined,
101116
);
102117
let body2 = render_codeblock(
103118
r#"
@@ -108,7 +123,7 @@ bar
108123
baz
109124
```
110125
"#,
111-
true,
126+
HighlightMode::Inlined,
112127
);
113128
assert_eq!(body, body2);
114129
}
@@ -124,7 +139,7 @@ bar
124139
baz
125140
```
126141
"#,
127-
true,
142+
HighlightMode::Inlined,
128143
);
129144
insta::assert_snapshot!(body);
130145
}
@@ -140,7 +155,7 @@ bar
140155
baz
141156
```
142157
"#,
143-
true,
158+
HighlightMode::Inlined,
144159
);
145160
insta::assert_snapshot!(body);
146161
}
@@ -156,7 +171,7 @@ bar
156171
baz
157172
```
158173
"#,
159-
true,
174+
HighlightMode::Inlined,
160175
);
161176
insta::assert_snapshot!(body);
162177
}
@@ -172,7 +187,7 @@ bar
172187
baz
173188
```
174189
"#,
175-
true,
190+
HighlightMode::Inlined,
176191
);
177192
insta::assert_snapshot!(body);
178193
}
@@ -188,7 +203,7 @@ bar
188203
baz
189204
```
190205
"#,
191-
true,
206+
HighlightMode::Inlined,
192207
);
193208
insta::assert_snapshot!(body);
194209
}
@@ -204,7 +219,7 @@ bar
204219
baz
205220
```
206221
"#,
207-
true,
222+
HighlightMode::Inlined,
208223
);
209224
insta::assert_snapshot!(body);
210225
}
@@ -220,7 +235,24 @@ bar
220235
baz
221236
```
222237
"#,
223-
true,
238+
HighlightMode::Inlined,
239+
);
240+
insta::assert_snapshot!(body);
241+
}
242+
243+
#[test]
244+
fn can_highlight_with_classes() {
245+
let body = render_codeblock(
246+
r#"
247+
```html,hl_lines=3-4
248+
<link
249+
rel="stylesheet"
250+
type="text/css"
251+
href="main.css"
252+
/>
253+
```
254+
"#,
255+
HighlightMode::Classed,
224256
);
225257
insta::assert_snapshot!(body);
226258
}
@@ -234,14 +266,14 @@ foo
234266
bar
235267
```
236268
"#,
237-
true,
269+
HighlightMode::Inlined,
238270
);
239271
insta::assert_snapshot!(body);
240272
}
241273

242274
#[test]
243275
fn can_add_line_numbers_windows_eol() {
244-
let body = render_codeblock("```linenos\r\nfoo\r\nbar\r\n```\r\n", true);
276+
let body = render_codeblock("```linenos\r\nfoo\r\nbar\r\n```\r\n", HighlightMode::Inlined);
245277
insta::assert_snapshot!(body);
246278
}
247279

@@ -254,7 +286,7 @@ foo
254286
bar
255287
```
256288
"#,
257-
true,
289+
HighlightMode::Inlined,
258290
);
259291
insta::assert_snapshot!(body);
260292
}
@@ -268,7 +300,24 @@ foo
268300
bar
269301
```
270302
"#,
271-
true,
303+
HighlightMode::Inlined,
304+
);
305+
insta::assert_snapshot!(body);
306+
}
307+
308+
#[test]
309+
fn can_add_line_numbers_with_classes() {
310+
let body = render_codeblock(
311+
r#"
312+
```html,linenos
313+
<link
314+
rel="stylesheet"
315+
type="text/css"
316+
href="main.css"
317+
/>
318+
```
319+
"#,
320+
HighlightMode::Classed,
272321
);
273322
insta::assert_snapshot!(body);
274323
}
@@ -283,7 +332,7 @@ fn can_render_shortcode_in_codeblock() {
283332
</div>
284333
```
285334
"#,
286-
true,
335+
HighlightMode::Inlined,
287336
);
288337
insta::assert_snapshot!(body);
289338
}
@@ -300,7 +349,7 @@ text2
300349
text3
301350
```
302351
"#,
303-
true,
352+
HighlightMode::Inlined,
304353
);
305354
insta::assert_snapshot!(body);
306355
}
@@ -323,7 +372,7 @@ A quote
323372
<!-- end text goes here -->
324373
```
325374
"#,
326-
true,
375+
HighlightMode::Inlined,
327376
);
328377
insta::assert_snapshot!(body);
329378
}
@@ -337,7 +386,7 @@ foo
337386
bar
338387
```
339388
"#,
340-
true,
389+
HighlightMode::Inlined,
341390
);
342391
insta::assert_snapshot!(body);
343392
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: components/markdown/tests/codeblocks.rs
3+
expression: body
4+
---
5+
<pre data-linenos data-lang="html" class="language-html z-code"><code class="language-html" data-lang="html"><table><tbody><tr><td>1</td><td><span class="z-text z-html z-basic"><span class="z-meta z-tag z-inline z-any z-html"><span class="z-punctuation z-definition z-tag z-begin z-html">&lt;</span><span class="z-entity z-name z-tag z-inline z-any z-html">link</span>
6+
</span></span></td></tr><tr><td>2</td><td><span class="z-text z-html z-basic"><span class="z-meta z-tag z-inline z-any z-html"> <span class="z-meta z-attribute-with-value z-html"><span class="z-entity z-other z-attribute-name z-html">rel</span><span class="z-punctuation z-separator z-key-value z-html">=</span><span class="z-string z-quoted z-double z-html"><span class="z-punctuation z-definition z-string z-begin z-html">&quot;</span>stylesheet<span class="z-punctuation z-definition z-string z-end z-html">&quot;</span></span></span>
7+
</span></span></td></tr><tr><td>3</td><td><span class="z-text z-html z-basic"><span class="z-meta z-tag z-inline z-any z-html"> <span class="z-meta z-attribute-with-value z-html"><span class="z-entity z-other z-attribute-name z-html">type</span><span class="z-punctuation z-separator z-key-value z-html">=</span><span class="z-string z-quoted z-double z-html"><span class="z-punctuation z-definition z-string z-begin z-html">&quot;</span>text/css<span class="z-punctuation z-definition z-string z-end z-html">&quot;</span></span></span>
8+
</span></span></td></tr><tr><td>4</td><td><span class="z-text z-html z-basic"><span class="z-meta z-tag z-inline z-any z-html"> <span class="z-meta z-attribute-with-value z-html"><span class="z-entity z-other z-attribute-name z-html">href</span><span class="z-punctuation z-separator z-key-value z-html">=</span><span class="z-string z-quoted z-double z-html"><span class="z-punctuation z-definition z-string z-begin z-html">&quot;</span>main.css<span class="z-punctuation z-definition z-string z-end z-html">&quot;</span></span></span>
9+
</span></span></td></tr><tr><td>5</td><td><span class="z-text z-html z-basic"><span class="z-meta z-tag z-inline z-any z-html"><span class="z-punctuation z-definition z-tag z-end z-html">/&gt;</span></span>
10+
</span></td></tr></tbody></table></code></pre>
11+

0 commit comments

Comments
 (0)