1 //! Rendering highlighted code as HTML+CSS
2 use crate::easy::{HighlightFile, HighlightLines};
3 use crate::escape::Escape;
4 use crate::highlighting::{Color, FontStyle, Style, Theme};
5 use crate::parsing::{
6 BasicScopeStackOp, ParseState, Scope, ScopeStack, ScopeStackOp, SyntaxReference, SyntaxSet,
7 SCOPE_REPO,
8 };
9 use crate::util::LinesWithEndings;
10 use std::fmt::Write;
11
12 use std::io::{self, BufRead};
13 use std::path::Path;
14
15 /// Output HTML for a line of code with `<span>` elements using class names
16 ///
17 /// Because this has to keep track of open and closed `<span>` tags, it is a `struct` with
18 /// additional state.
19 ///
20 /// There is a [`finalize()`] method that must be called in the end in order
21 /// to close all open `<span>` tags.
22 ///
23 /// Note that because CSS classes have slightly different matching semantics
24 /// than Textmate themes, this may produce somewhat less accurate
25 /// highlighting than the other highlighting functions which directly use
26 /// inline colors as opposed to classes and a stylesheet.
27 ///
28 /// [`finalize()`]: #method.finalize
29 ///
30 /// # Example
31 ///
32 /// ```
33 /// use syntect::html::{ClassedHTMLGenerator, ClassStyle};
34 /// use syntect::parsing::SyntaxSet;
35 /// use syntect::util::LinesWithEndings;
36 ///
37 /// let current_code = r#"
38 /// x <- 5
39 /// y <- 6
40 /// x + y
41 /// "#;
42 ///
43 /// let syntax_set = SyntaxSet::load_defaults_newlines();
44 /// let syntax = syntax_set.find_syntax_by_name("R").unwrap();
45 /// let mut html_generator = ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
46 /// for line in LinesWithEndings::from(current_code) {
47 /// html_generator.parse_html_for_line_which_includes_newline(&line);
48 /// }
49 /// let output_html = html_generator.finalize();
50 /// ```
51 pub struct ClassedHTMLGenerator<'a> {
52 syntax_set: &'a SyntaxSet,
53 open_spans: isize,
54 parse_state: ParseState,
55 scope_stack: ScopeStack,
56 html: String,
57 style: ClassStyle,
58 }
59
60 impl<'a> ClassedHTMLGenerator<'a> {
61 #[deprecated(since="4.2.0", note="Please use `new_with_class_style` instead")]
new(syntax_reference: &'a SyntaxReference, syntax_set: &'a SyntaxSet) -> ClassedHTMLGenerator<'a>62 pub fn new(syntax_reference: &'a SyntaxReference, syntax_set: &'a SyntaxSet) -> ClassedHTMLGenerator<'a> {
63 Self::new_with_class_style(syntax_reference, syntax_set, ClassStyle::Spaced)
64 }
65
new_with_class_style( syntax_reference: &'a SyntaxReference, syntax_set: &'a SyntaxSet, style: ClassStyle, ) -> ClassedHTMLGenerator<'a>66 pub fn new_with_class_style(
67 syntax_reference: &'a SyntaxReference,
68 syntax_set: &'a SyntaxSet,
69 style: ClassStyle,
70 ) -> ClassedHTMLGenerator<'a> {
71 let parse_state = ParseState::new(syntax_reference);
72 let open_spans = 0;
73 let html = String::new();
74 let scope_stack = ScopeStack::new();
75 ClassedHTMLGenerator {
76 syntax_set,
77 open_spans,
78 parse_state,
79 scope_stack,
80 html,
81 style,
82 }
83 }
84
85 /// Parse the line of code and update the internal HTML buffer with tagged HTML
86 ///
87 /// *Note:* This function requires `line` to include a newline at the end and
88 /// also use of the `load_defaults_newlines` version of the syntaxes.
parse_html_for_line_which_includes_newline(&mut self, line: &str)89 pub fn parse_html_for_line_which_includes_newline(&mut self, line: &str) {
90 let parsed_line = self.parse_state.parse_line(line, &self.syntax_set);
91 let (formatted_line, delta) = line_tokens_to_classed_spans(
92 line,
93 parsed_line.as_slice(),
94 self.style,
95 &mut self.scope_stack,
96 );
97 self.open_spans += delta;
98 self.html.push_str(formatted_line.as_str());
99 }
100
101 /// Parse the line of code and update the internal HTML buffer with tagged HTML
102 ///
103 /// ## Warning
104 /// Due to an unfortunate oversight this function adds a newline after the HTML line,
105 /// and thus requires lines to be passed without newlines in them, and thus requires
106 /// usage of the `load_defaults_nonewlines` version of the default syntaxes.
107 ///
108 /// These versions of the syntaxes can have occasionally incorrect highlighting
109 /// but this function can't be changed without breaking compatibility so is deprecated.
110 #[deprecated(since="4.5.0", note="Please use `parse_html_for_line_which_includes_newline` instead")]
parse_html_for_line(&mut self, line: &str)111 pub fn parse_html_for_line(&mut self, line: &str) {
112 self.parse_html_for_line_which_includes_newline(line);
113 // retain newline
114 self.html.push_str("\n");
115 }
116
117 /// Close all open `<span>` tags and return the finished HTML string
finalize(mut self) -> String118 pub fn finalize(mut self) -> String {
119 for _ in 0..self.open_spans {
120 self.html.push_str("</span>");
121 }
122 self.html
123 }
124 }
125
126 #[deprecated(since="4.2.0", note="Please use `css_for_theme_with_class_style` instead.")]
css_for_theme(theme: &Theme) -> String127 pub fn css_for_theme(theme: &Theme) -> String {
128 css_for_theme_with_class_style(theme, ClassStyle::Spaced)
129 }
130
131 /// Create a complete CSS for a given theme. Can be used inline, or written to a CSS file.
css_for_theme_with_class_style(theme: &Theme, style: ClassStyle) -> String132 pub fn css_for_theme_with_class_style(theme: &Theme, style: ClassStyle) -> String {
133 let mut css = String::new();
134
135 css.push_str("/*\n");
136 let name = theme.name.clone().unwrap_or("unknown theme".to_string());
137 css.push_str(&format!(" * theme \"{}\" generated by syntect\n", name));
138 css.push_str(" */\n\n");
139
140 match style {
141 ClassStyle::Spaced => {
142 css.push_str(".code {\n");
143 }
144 ClassStyle::SpacedPrefixed { prefix } => {
145 css.push_str(&format!(".{}code {{\n", prefix));
146 }
147 };
148 if let Some(fgc) = theme.settings.foreground {
149 css.push_str(&format!(
150 " color: #{:02x}{:02x}{:02x};\n",
151 fgc.r, fgc.g, fgc.b
152 ));
153 }
154 if let Some(bgc) = theme.settings.background {
155 css.push_str(&format!(
156 " background-color: #{:02x}{:02x}{:02x};\n",
157 bgc.r, bgc.g, bgc.b
158 ));
159 }
160 css.push_str("}\n\n");
161
162 for i in &theme.scopes {
163 for scope_selector in &i.scope.selectors {
164 let scopes = scope_selector.extract_scopes();
165 for k in &scopes {
166 scope_to_selector(&mut css, *k, style);
167 css.push_str(" "); // join multiple scopes
168 }
169 css.pop(); // remove trailing space
170 css.push_str(", "); // join multiple selectors
171 }
172 let len = css.len();
173 css.truncate(len - 2); // remove trailing ", "
174 css.push_str(" {\n");
175
176 if let Some(fg) = i.style.foreground {
177 css.push_str(&format!(" color: #{:02x}{:02x}{:02x};\n", fg.r, fg.g, fg.b));
178 }
179
180 if let Some(bg) = i.style.background {
181 css.push_str(&format!(
182 " background-color: #{:02x}{:02x}{:02x};\n",
183 bg.r, bg.g, bg.b
184 ));
185 }
186
187 if let Some(fs) = i.style.font_style {
188 if fs.contains(FontStyle::UNDERLINE) {
189 css.push_str(&format!("font-style: underline;\n"));
190 }
191 if fs.contains(FontStyle::BOLD) {
192 css.push_str(&format!("font-weight: bold;\n"));
193 }
194 if fs.contains(FontStyle::ITALIC) {
195 css.push_str(&format!("font-style: italic;\n"));
196 }
197 }
198 css.push_str("}\n");
199 }
200
201 css
202 }
203
204 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
205 #[non_exhaustive]
206 pub enum ClassStyle {
207 /// The classes are the atoms of the scope separated by spaces
208 /// (e.g `source.php` becomes `source php`).
209 /// This isn't that fast since it has to use the scope repository
210 /// to look up scope names.
211 Spaced,
212 /// Like `Spaced`, but the given prefix will be prepended to all
213 /// classes. This is useful to prevent class name collisions, and
214 /// can ensure that the theme's CSS applies precisely to syntect's
215 /// output.
216 ///
217 /// The prefix must be a valid CSS class name. To help ennforce
218 /// this invariant and prevent accidental foot-shooting, it must
219 /// be statically known. (If this requirement is onerous, please
220 /// file an issue; the HTML generator can also be forked
221 /// separately from the rest of syntect, as it only uses the
222 /// public API.)
223 SpacedPrefixed { prefix: &'static str },
224 }
225
scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle)226 fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) {
227 let repo = SCOPE_REPO.lock().unwrap();
228 for i in 0..(scope.len()) {
229 let atom = scope.atom_at(i as usize);
230 let atom_s = repo.atom_str(atom);
231 if i != 0 {
232 s.push_str(" ")
233 }
234 match style {
235 ClassStyle::Spaced => {}
236 ClassStyle::SpacedPrefixed { prefix } => {
237 s.push_str(&prefix);
238 }
239 }
240 s.push_str(atom_s);
241 }
242 }
243
scope_to_selector(s: &mut String, scope: Scope, style: ClassStyle)244 fn scope_to_selector(s: &mut String, scope: Scope, style: ClassStyle) {
245 let repo = SCOPE_REPO.lock().unwrap();
246 for i in 0..(scope.len()) {
247 let atom = scope.atom_at(i as usize);
248 let atom_s = repo.atom_str(atom);
249 s.push_str(".");
250 match style {
251 ClassStyle::Spaced => {}
252 ClassStyle::SpacedPrefixed { prefix } => {
253 s.push_str(&prefix);
254 }
255 }
256 s.push_str(atom_s);
257 }
258 }
259
260 /// Convenience method that combines `start_highlighted_html_snippet`, `styled_line_to_highlighted_html`
261 /// and `HighlightLines` from `syntect::easy` to create a full highlighted HTML snippet for
262 /// a string (which can contain many lines).
263 ///
264 /// Note that the `syntax` passed in must be from a `SyntaxSet` compiled for newline characters.
265 /// This is easy to get with `SyntaxSet::load_defaults_newlines()`. (Note: this was different before v3.0)
highlighted_html_for_string( s: &str, ss: &SyntaxSet, syntax: &SyntaxReference, theme: &Theme, ) -> String266 pub fn highlighted_html_for_string(
267 s: &str,
268 ss: &SyntaxSet,
269 syntax: &SyntaxReference,
270 theme: &Theme,
271 ) -> String {
272 let mut highlighter = HighlightLines::new(syntax, theme);
273 let (mut output, bg) = start_highlighted_html_snippet(theme);
274
275 for line in LinesWithEndings::from(s) {
276 let regions = highlighter.highlight(line, ss);
277 append_highlighted_html_for_styled_line(
278 ®ions[..],
279 IncludeBackground::IfDifferent(bg),
280 &mut output,
281 );
282 }
283 output.push_str("</pre>\n");
284 output
285 }
286
287 /// Convenience method that combines `start_highlighted_html_snippet`, `styled_line_to_highlighted_html`
288 /// and `HighlightFile` from `syntect::easy` to create a full highlighted HTML snippet for
289 /// a file.
290 ///
291 /// Note that the `syntax` passed in must be from a `SyntaxSet` compiled for newline characters.
292 /// This is easy to get with `SyntaxSet::load_defaults_newlines()`. (Note: this was different before v3.0)
highlighted_html_for_file<P: AsRef<Path>>( path: P, ss: &SyntaxSet, theme: &Theme, ) -> io::Result<String>293 pub fn highlighted_html_for_file<P: AsRef<Path>>(
294 path: P,
295 ss: &SyntaxSet,
296 theme: &Theme,
297 ) -> io::Result<String> {
298 let mut highlighter = HighlightFile::new(path, ss, theme)?;
299 let (mut output, bg) = start_highlighted_html_snippet(theme);
300
301 let mut line = String::new();
302 while highlighter.reader.read_line(&mut line)? > 0 {
303 {
304 let regions = highlighter.highlight_lines.highlight(&line, ss);
305 append_highlighted_html_for_styled_line(
306 ®ions[..],
307 IncludeBackground::IfDifferent(bg),
308 &mut output,
309 );
310 }
311 line.clear();
312 }
313 output.push_str("</pre>\n");
314 Ok(output)
315 }
316
317 /// Output HTML for a line of code with `<span>` elements
318 /// specifying classes for each token. The span elements are nested
319 /// like the scope stack and the scopes are mapped to classes based
320 /// on the `ClassStyle` (see it's docs).
321 ///
322 /// See `ClassedHTMLGenerator` for a more convenient wrapper, this is the advanced
323 /// version of the function that gives more control over the parsing flow.
324 ///
325 /// For this to work correctly you must concatenate all the lines in a `<pre>`
326 /// tag since some span tags opened on a line may not be closed on that line
327 /// and later lines may close tags from previous lines.
328 ///
329 /// Returns the HTML string and the number of `<span>` tags opened
330 /// (negative for closed). So that you can emit the correct number of closing
331 /// tags at the end.
line_tokens_to_classed_spans( line: &str, ops: &[(usize, ScopeStackOp)], style: ClassStyle, stack: &mut ScopeStack, ) -> (String, isize)332 pub fn line_tokens_to_classed_spans(
333 line: &str,
334 ops: &[(usize, ScopeStackOp)],
335 style: ClassStyle,
336 stack: &mut ScopeStack,
337 ) -> (String, isize) {
338 let mut s = String::with_capacity(line.len() + ops.len() * 8); // a guess
339 let mut cur_index = 0;
340 let mut span_delta = 0;
341
342 // check and skip emty inner <span> tags
343 let mut span_empty = false;
344 let mut span_start = 0;
345
346 for &(i, ref op) in ops {
347 if i > cur_index {
348 span_empty = false;
349 write!(s, "{}", Escape(&line[cur_index..i])).unwrap();
350 cur_index = i
351 }
352 stack.apply_with_hook(op, |basic_op, _| match basic_op {
353 BasicScopeStackOp::Push(scope) => {
354 span_start = s.len();
355 span_empty = true;
356 s.push_str("<span class=\"");
357 scope_to_classes(&mut s, scope, style);
358 s.push_str("\">");
359 span_delta += 1;
360 }
361 BasicScopeStackOp::Pop => {
362 if span_empty == false {
363 s.push_str("</span>");
364 } else {
365 s.truncate(span_start);
366 }
367 span_delta -= 1;
368 span_empty = false;
369 }
370 });
371 }
372 write!(s, "{}", Escape(&line[cur_index..line.len()])).unwrap();
373 (s, span_delta)
374 }
375
376 /// Preserved for compatibility, always use `line_tokens_to_classed_spans`
377 /// and keep a `ScopeStack` between lines for correct highlighting that won't
378 /// sometimes crash.
379 #[deprecated(since="4.6.0", note="Use `line_tokens_to_classed_spans` instead, this can panic and highlight incorrectly")]
tokens_to_classed_spans( line: &str, ops: &[(usize, ScopeStackOp)], style: ClassStyle, ) -> (String, isize)380 pub fn tokens_to_classed_spans(
381 line: &str,
382 ops: &[(usize, ScopeStackOp)],
383 style: ClassStyle,
384 ) -> (String, isize) {
385 line_tokens_to_classed_spans(line, ops, style, &mut ScopeStack::new())
386 }
387
388 #[deprecated(since="3.1.0", note="Use `line_tokens_to_classed_spans` instead to avoid incorrect highlighting and panics")]
tokens_to_classed_html(line: &str, ops: &[(usize, ScopeStackOp)], style: ClassStyle) -> String389 pub fn tokens_to_classed_html(line: &str,
390 ops: &[(usize, ScopeStackOp)],
391 style: ClassStyle)
392 -> String {
393 line_tokens_to_classed_spans(line, ops, style, &mut ScopeStack::new()).0
394 }
395
396 /// Determines how background color attributes are generated
397 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
398 pub enum IncludeBackground {
399 /// Don't include `background-color`, for performance or so that you can use your own background.
400 No,
401 /// Set background color attributes on every node
402 Yes,
403 /// Only set the `background-color` if it is different than the default (presumably set on a parent element)
404 IfDifferent(Color),
405 }
406
write_css_color(s: &mut String, c: Color)407 fn write_css_color(s: &mut String, c: Color) {
408 if c.a != 0xFF {
409 write!(s, "#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a).unwrap();
410 } else {
411 write!(s, "#{:02x}{:02x}{:02x}", c.r, c.g, c.b).unwrap();
412 }
413 }
414
415 /// Output HTML for a line of code with `<span>` elements using inline
416 /// `style` attributes to set the correct font attributes.
417 /// The `bg` attribute determines if the spans will have the `background-color`
418 /// attribute set. See the `IncludeBackground` enum's docs.
419 ///
420 /// The lines returned don't include a newline at the end.
421 /// # Examples
422 ///
423 /// ```
424 /// use syntect::easy::HighlightLines;
425 /// use syntect::parsing::SyntaxSet;
426 /// use syntect::highlighting::{ThemeSet, Style};
427 /// use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
428 ///
429 /// // Load these once at the start of your program
430 /// let ps = SyntaxSet::load_defaults_newlines();
431 /// let ts = ThemeSet::load_defaults();
432 ///
433 /// let syntax = ps.find_syntax_by_name("Ruby").unwrap();
434 /// let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
435 /// let regions = h.highlight("5", &ps);
436 /// let html = styled_line_to_highlighted_html(®ions[..], IncludeBackground::No);
437 /// assert_eq!(html, "<span style=\"color:#d08770;\">5</span>");
438 /// ```
styled_line_to_highlighted_html(v: &[(Style, &str)], bg: IncludeBackground) -> String439 pub fn styled_line_to_highlighted_html(v: &[(Style, &str)], bg: IncludeBackground) -> String {
440 let mut s: String = String::new();
441 append_highlighted_html_for_styled_line(v, bg, &mut s);
442 s
443 }
444
445 /// Like `styled_line_to_highlighted_html` but appends to a `String` for increased efficiency.
446 /// In fact `styled_line_to_highlighted_html` is just a wrapper around this function.
append_highlighted_html_for_styled_line( v: &[(Style, &str)], bg: IncludeBackground, mut s: &mut String, )447 pub fn append_highlighted_html_for_styled_line(
448 v: &[(Style, &str)],
449 bg: IncludeBackground,
450 mut s: &mut String,
451 ) {
452 let mut prev_style: Option<&Style> = None;
453 for &(ref style, text) in v.iter() {
454 let unify_style = if let Some(ps) = prev_style {
455 style == ps || (style.background == ps.background && text.trim().is_empty())
456 } else {
457 false
458 };
459 if unify_style {
460 write!(s, "{}", Escape(text)).unwrap();
461 } else {
462 if prev_style.is_some() {
463 write!(s, "</span>").unwrap();
464 }
465 prev_style = Some(style);
466 write!(s, "<span style=\"").unwrap();
467 let include_bg = match bg {
468 IncludeBackground::Yes => true,
469 IncludeBackground::No => false,
470 IncludeBackground::IfDifferent(c) => (style.background != c),
471 };
472 if include_bg {
473 write!(s, "background-color:").unwrap();
474 write_css_color(&mut s, style.background);
475 write!(s, ";").unwrap();
476 }
477 if style.font_style.contains(FontStyle::UNDERLINE) {
478 write!(s, "text-decoration:underline;").unwrap();
479 }
480 if style.font_style.contains(FontStyle::BOLD) {
481 write!(s, "font-weight:bold;").unwrap();
482 }
483 if style.font_style.contains(FontStyle::ITALIC) {
484 write!(s, "font-style:italic;").unwrap();
485 }
486 write!(s, "color:").unwrap();
487 write_css_color(&mut s, style.foreground);
488 write!(s, ";\">{}", Escape(text)).unwrap();
489 }
490 }
491 if prev_style.is_some() {
492 write!(s, "</span>").unwrap();
493 }
494 }
495
496 /// Returns a `<pre style="...">\n` tag with the correct background color for the given theme.
497 /// This is for if you want to roll your own HTML output, you probably just want to use
498 /// `highlighted_html_for_string`.
499 ///
500 /// If you don't care about the background color you can just prefix the lines from
501 /// `styled_line_to_highlighted_html` with a `<pre>`. This is meant to be used with
502 /// `IncludeBackground::IfDifferent`.
503 ///
504 /// As of `v3.0` this method also returns the background color to be passed to `IfDifferent`.
505 ///
506 /// You're responsible for creating the string `</pre>` to close this, I'm not gonna provide a
507 /// helper for that :-)
start_highlighted_html_snippet(t: &Theme) -> (String, Color)508 pub fn start_highlighted_html_snippet(t: &Theme) -> (String, Color) {
509 let c = t.settings.background.unwrap_or(Color::WHITE);
510 (
511 format!(
512 "<pre style=\"background-color:#{:02x}{:02x}{:02x};\">\n",
513 c.r, c.g, c.b
514 ),
515 c,
516 )
517 }
518
519 #[cfg(all(
520 feature = "assets",
521 any(feature = "dump-load", feature = "dump-load-rs")
522 ))]
523 #[cfg(test)]
524 mod tests {
525 use super::*;
526 use crate::highlighting::{HighlightIterator, HighlightState, Highlighter, Style, ThemeSet};
527 use crate::parsing::{ParseState, ScopeStack, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
528 use crate::util::LinesWithEndings;
529 #[test]
tokens()530 fn tokens() {
531 let ss = SyntaxSet::load_defaults_newlines();
532 let syntax = ss.find_syntax_by_name("Markdown").unwrap();
533 let mut state = ParseState::new(syntax);
534 let line = "[w](t.co) *hi* **five**";
535 let ops = state.parse_line(line, &ss);
536 let mut stack = ScopeStack::new();
537
538 // use util::debug_print_ops;
539 // debug_print_ops(line, &ops);
540
541 let (html, _) = line_tokens_to_classed_spans(line, &ops[..], ClassStyle::Spaced, &mut stack);
542 println!("{}", html);
543 assert_eq!(html, include_str!("../testdata/test2.html").trim_end());
544
545 let ts = ThemeSet::load_defaults();
546 let highlighter = Highlighter::new(&ts.themes["InspiredGitHub"]);
547 let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
548 let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
549 let regions: Vec<(Style, &str)> = iter.collect();
550
551 let html2 = styled_line_to_highlighted_html(®ions[..], IncludeBackground::Yes);
552 println!("{}", html2);
553 assert_eq!(html2, include_str!("../testdata/test1.html").trim_end());
554 }
555
556 #[test]
strings()557 fn strings() {
558 let ss = SyntaxSet::load_defaults_newlines();
559 let ts = ThemeSet::load_defaults();
560 let s = include_str!("../testdata/highlight_test.erb");
561 let syntax = ss.find_syntax_by_extension("erb").unwrap();
562 let html = highlighted_html_for_string(s, &ss, syntax, &ts.themes["base16-ocean.dark"]);
563 // println!("{}", html);
564 assert_eq!(html, include_str!("../testdata/test3.html"));
565 let html2 = highlighted_html_for_file(
566 "testdata/highlight_test.erb",
567 &ss,
568 &ts.themes["base16-ocean.dark"],
569 )
570 .unwrap();
571 assert_eq!(html2, html);
572
573 // YAML is a tricky syntax and InspiredGitHub is a fancy theme, this is basically an integration test
574 let html3 = highlighted_html_for_file(
575 "testdata/Packages/Rust/Cargo.sublime-syntax",
576 &ss,
577 &ts.themes["InspiredGitHub"],
578 )
579 .unwrap();
580 println!("{}", html3);
581 assert_eq!(html3, include_str!("../testdata/test4.html"));
582 }
583
584 #[test]
tricky_test_syntax()585 fn tricky_test_syntax() {
586 // This syntax I wrote tests edge cases of prototypes
587 // I verified the output HTML against what ST3 does with the same syntax and file
588 let mut builder = SyntaxSetBuilder::new();
589 builder.add_from_folder("testdata", true).unwrap();
590 let ss = builder.build();
591 let ts = ThemeSet::load_defaults();
592 let html = highlighted_html_for_file(
593 "testdata/testing-syntax.testsyntax",
594 &ss,
595 &ts.themes["base16-ocean.dark"],
596 )
597 .unwrap();
598 println!("{}", html);
599 assert_eq!(html, include_str!("../testdata/test5.html"));
600 }
601
602 #[test]
test_classed_html_generator_doesnt_panic()603 fn test_classed_html_generator_doesnt_panic() {
604 let current_code = "{\n \"headers\": [\"Number\", \"Title\"],\n \"records\": [\n [\"1\", \"Gutenberg\"],\n [\"2\", \"Printing\"]\n ],\n}\n";
605 let syntax_def = SyntaxDefinition::load_from_str(
606 include_str!("../testdata/JSON.sublime-syntax"),
607 true,
608 None,
609 )
610 .unwrap();
611 let mut syntax_set_builder = SyntaxSetBuilder::new();
612 syntax_set_builder.add(syntax_def);
613 let syntax_set = syntax_set_builder.build();
614 let syntax = syntax_set.find_syntax_by_name("JSON").unwrap();
615
616 let mut html_generator =
617 ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
618 for line in LinesWithEndings::from(current_code) {
619 html_generator.parse_html_for_line_which_includes_newline(&line);
620 }
621 html_generator.finalize();
622 }
623
624 #[test]
test_classed_html_generator()625 fn test_classed_html_generator() {
626 let current_code = "x + y\n";
627 let syntax_set = SyntaxSet::load_defaults_newlines();
628 let syntax = syntax_set.find_syntax_by_name("R").unwrap();
629
630 let mut html_generator =
631 ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
632 for line in LinesWithEndings::from(current_code) {
633 html_generator.parse_html_for_line_which_includes_newline(&line);
634 }
635 let html = html_generator.finalize();
636 assert_eq!(html, "<span class=\"source r\">x <span class=\"keyword operator arithmetic r\">+</span> y\n</span>");
637 }
638
639 #[test]
test_classed_html_generator_prefixed()640 fn test_classed_html_generator_prefixed() {
641 let current_code = "x + y\n";
642 let syntax_set = SyntaxSet::load_defaults_newlines();
643 let syntax = syntax_set.find_syntax_by_name("R").unwrap();
644 let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
645 &syntax,
646 &syntax_set,
647 ClassStyle::SpacedPrefixed { prefix: "foo-" },
648 );
649 for line in LinesWithEndings::from(current_code) {
650 html_generator.parse_html_for_line_which_includes_newline(&line);
651 }
652 let html = html_generator.finalize();
653 assert_eq!(html, "<span class=\"foo-source foo-r\">x <span class=\"foo-keyword foo-operator foo-arithmetic foo-r\">+</span> y\n</span>");
654 }
655
656 #[test]
test_classed_html_generator_no_empty_span()657 fn test_classed_html_generator_no_empty_span() {
658 let code = "// Rust source
659 fn main() {
660 println!(\"Hello World!\");
661 }
662 ";
663 let syntax_set = SyntaxSet::load_defaults_newlines();
664 let syntax = syntax_set.find_syntax_by_extension("rs").unwrap();
665 let mut html_generator =
666 ClassedHTMLGenerator::new_with_class_style(&syntax, &syntax_set, ClassStyle::Spaced);
667 for line in LinesWithEndings::from(code) {
668 html_generator.parse_html_for_line_which_includes_newline(&line);
669 }
670 let html = html_generator.finalize();
671 assert_eq!(html, "<span class=\"source rust\"><span class=\"comment line double-slash rust\"><span class=\"punctuation definition comment rust\">//</span> Rust source\n</span><span class=\"meta function rust\"><span class=\"meta function rust\"><span class=\"storage type function rust\">fn</span> </span><span class=\"entity name function rust\">main</span></span><span class=\"meta function rust\"><span class=\"meta function parameters rust\"><span class=\"punctuation section parameters begin rust\">(</span></span><span class=\"meta function rust\"><span class=\"meta function parameters rust\"><span class=\"punctuation section parameters end rust\">)</span></span></span></span><span class=\"meta function rust\"> </span><span class=\"meta function rust\"><span class=\"meta block rust\"><span class=\"punctuation section block begin rust\">{</span>\n <span class=\"support macro rust\">println!</span><span class=\"meta group rust\"><span class=\"punctuation section group begin rust\">(</span></span><span class=\"meta group rust\"><span class=\"string quoted double rust\"><span class=\"punctuation definition string begin rust\">"</span>Hello World!<span class=\"punctuation definition string end rust\">"</span></span></span><span class=\"meta group rust\"><span class=\"punctuation section group end rust\">)</span></span><span class=\"punctuation terminator rust\">;</span>\n</span><span class=\"meta block rust\"><span class=\"punctuation section block end rust\">}</span></span></span>\n</span>");
672 }
673 }
674