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             &regions[..],
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                 &regions[..],
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(&regions[..], 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(&regions[..], 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\">&quot;</span>Hello World!<span class=\"punctuation definition string end rust\">&quot;</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