syntect/
html.rs

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