syntect/
html.rs

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