1use crate::easy::{HighlightFile, HighlightLines, HighlightOptions};
3use crate::escape::Escape;
4use crate::highlighting::{Color, FontStyle, Style, Theme};
5use crate::parsing::{
6 BasicScopeStackOp, ParseState, Scope, ScopeStack, ScopeStackOp, SyntaxReference, SyntaxSet,
7 SCOPE_REPO,
8};
9use crate::util::LinesWithEndings;
10use crate::Error;
11use std::fmt::Write;
12
13use std::io::BufRead;
14use std::path::Path;
15
16pub 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, true );
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 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 #[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 self.html.push('\n');
125 }
126
127 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
145pub 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 css.push_str(&format!(".{}code {{\n", prefix));
163 }
164 };
165 if let Some(fgc) = theme.settings.foreground {
166 css.push_str(&format!(
167 " color: #{:02x}{:02x}{:02x};\n",
168 fgc.r, fgc.g, fgc.b
169 ));
170 }
171 if let Some(bgc) = theme.settings.background {
172 css.push_str(&format!(
173 " background-color: #{:02x}{:02x}{:02x};\n",
174 bgc.r, bgc.g, bgc.b
175 ));
176 }
177 css.push_str("}\n\n");
178
179 for i in &theme.scopes {
180 for scope_selector in &i.scope.selectors {
181 let scopes = scope_selector.extract_scopes();
182 for k in &scopes {
183 scope_to_selector(&mut css, *k, style);
184 css.push(' '); }
186 css.pop(); css.push_str(", "); }
189 let len = css.len();
190 css.truncate(len - 2); css.push_str(" {\n");
192
193 if let Some(fg) = i.style.foreground {
194 css.push_str(&format!(" color: #{:02x}{:02x}{:02x};\n", fg.r, fg.g, fg.b));
195 }
196
197 if let Some(bg) = i.style.background {
198 css.push_str(&format!(
199 " background-color: #{:02x}{:02x}{:02x};\n",
200 bg.r, bg.g, bg.b
201 ));
202 }
203
204 if let Some(fs) = i.style.font_style {
205 if fs.contains(FontStyle::UNDERLINE) {
206 css.push_str("text-decoration: underline;\n");
207 }
208 if fs.contains(FontStyle::BOLD) {
209 css.push_str("font-weight: bold;\n");
210 }
211 if fs.contains(FontStyle::ITALIC) {
212 css.push_str("font-style: italic;\n");
213 }
214 }
215 css.push_str("}\n");
216 }
217
218 Ok(css)
219}
220
221#[derive(Debug, PartialEq, Eq, Clone, Copy)]
222#[non_exhaustive]
223pub enum ClassStyle {
224 Spaced,
229 SpacedPrefixed { prefix: &'static str },
241}
242
243fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) {
244 let repo = SCOPE_REPO.lock().unwrap();
245 for i in 0..(scope.len()) {
246 let atom = scope.atom_at(i as usize);
247 let atom_s = repo.atom_str(atom);
248 if i != 0 {
249 s.push(' ')
250 }
251 match style {
252 ClassStyle::Spaced => {}
253 ClassStyle::SpacedPrefixed { prefix } => {
254 s.push_str(prefix);
255 }
256 }
257 s.push_str(atom_s);
258 }
259}
260
261fn scope_to_selector(s: &mut String, scope: Scope, style: ClassStyle) {
262 let repo = SCOPE_REPO.lock().unwrap();
263 for i in 0..(scope.len()) {
264 let atom = scope.atom_at(i as usize);
265 let atom_s = repo.atom_str(atom);
266 s.push('.');
267 match style {
268 ClassStyle::Spaced => {}
269 ClassStyle::SpacedPrefixed { prefix } => {
270 s.push_str(prefix);
271 }
272 }
273 s.push_str(atom_s);
274 }
275}
276
277pub fn highlighted_html_for_string(
284 s: &str,
285 ss: &SyntaxSet,
286 syntax: &SyntaxReference,
287 theme: &Theme,
288) -> Result<String, Error> {
289 let mut highlighter = HighlightLines::new(syntax, theme, HighlightOptions::default());
290 let (mut output, bg) = start_highlighted_html_snippet(theme);
291
292 for line in LinesWithEndings::from(s) {
293 let regions = highlighter.highlight_line(line, ss)?;
294 append_highlighted_html_for_styled_line(
295 ®ions[..],
296 IncludeBackground::IfDifferent(bg),
297 &mut output,
298 )?;
299 }
300 output.push_str("</pre>\n");
301 Ok(output)
302}
303
304pub fn highlighted_html_for_file<P: AsRef<Path>>(
311 path: P,
312 ss: &SyntaxSet,
313 theme: &Theme,
314) -> Result<String, Error> {
315 let mut highlighter = HighlightFile::new(path, ss, theme, HighlightOptions::default())?;
316 let (mut output, bg) = start_highlighted_html_snippet(theme);
317
318 let mut line = String::new();
319 while highlighter.reader.read_line(&mut line)? > 0 {
320 {
321 let regions = highlighter.highlight_lines.highlight_line(&line, ss)?;
322 append_highlighted_html_for_styled_line(
323 ®ions[..],
324 IncludeBackground::IfDifferent(bg),
325 &mut output,
326 )?;
327 }
328 line.clear();
329 }
330 output.push_str("</pre>\n");
331 Ok(output)
332}
333
334pub fn line_tokens_to_classed_spans(
350 line: &str,
351 ops: &[(usize, ScopeStackOp)],
352 style: ClassStyle,
353 stack: &mut ScopeStack,
354) -> Result<(String, isize), Error> {
355 let mut s = String::with_capacity(line.len() + ops.len() * 8); let mut cur_index = 0;
357 let mut span_delta = 0;
358
359 let mut span_empty = false;
361 let mut span_start = 0;
362
363 for &(i, ref op) in ops {
364 if i > cur_index {
365 span_empty = false;
366 write!(s, "{}", Escape(&line[cur_index..i]))?;
367 cur_index = i
368 }
369 stack.apply_with_hook(op, |basic_op, _| match basic_op {
370 BasicScopeStackOp::Push(scope) => {
371 span_start = s.len();
372 span_empty = true;
373 s.push_str("<span class=\"");
374 scope_to_classes(&mut s, scope, style);
375 s.push_str("\">");
376 span_delta += 1;
377 }
378 BasicScopeStackOp::Pop => {
379 if !span_empty {
380 s.push_str("</span>");
381 } else {
382 s.truncate(span_start);
383 }
384 span_delta -= 1;
385 span_empty = false;
386 }
387 })?;
388 }
389 write!(s, "{}", Escape(&line[cur_index..line.len()]))?;
390 Ok((s, span_delta))
391}
392
393#[deprecated(
397 since = "4.6.0",
398 note = "Use `line_tokens_to_classed_spans` instead, this can panic and highlight incorrectly"
399)]
400pub fn tokens_to_classed_spans(
401 line: &str,
402 ops: &[(usize, ScopeStackOp)],
403 style: ClassStyle,
404) -> (String, isize) {
405 line_tokens_to_classed_spans(line, ops, style, &mut ScopeStack::new()).expect(
406 "Use `line_tokens_to_classed_spans` instead, this can panic and highlight incorrectly",
407 )
408}
409
410#[deprecated(
411 since = "3.1.0",
412 note = "Use `line_tokens_to_classed_spans` instead to avoid incorrect highlighting and panics"
413)]
414pub fn tokens_to_classed_html(
415 line: &str,
416 ops: &[(usize, ScopeStackOp)],
417 style: ClassStyle,
418) -> String {
419 line_tokens_to_classed_spans(line, ops, style, &mut ScopeStack::new())
420 .expect(
421 "Use `line_tokens_to_classed_spans` instead to avoid incorrect highlighting and panics",
422 )
423 .0
424}
425
426#[derive(Debug, PartialEq, Eq, Clone, Copy)]
428pub enum IncludeBackground {
429 No,
431 Yes,
433 IfDifferent(Color),
435}
436
437fn write_css_color(s: &mut String, c: Color) {
438 if c.a != 0xFF {
439 write!(s, "#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a).unwrap();
440 } else {
441 write!(s, "#{:02x}{:02x}{:02x}", c.r, c.g, c.b).unwrap();
442 }
443}
444
445pub fn styled_line_to_highlighted_html(
470 v: &[(Style, &str)],
471 bg: IncludeBackground,
472) -> Result<String, Error> {
473 let mut s: String = String::new();
474 append_highlighted_html_for_styled_line(v, bg, &mut s)?;
475 Ok(s)
476}
477
478pub fn append_highlighted_html_for_styled_line(
481 v: &[(Style, &str)],
482 bg: IncludeBackground,
483 s: &mut String,
484) -> Result<(), Error> {
485 let mut prev_style: Option<&Style> = None;
486 for &(ref style, text) in v.iter() {
487 let unify_style = if let Some(ps) = prev_style {
488 style == ps || (style.background == ps.background && text.trim().is_empty())
489 } else {
490 false
491 };
492 if unify_style {
493 write!(s, "{}", Escape(text))?;
494 } else {
495 if prev_style.is_some() {
496 write!(s, "</span>")?;
497 }
498 prev_style = Some(style);
499 write!(s, "<span style=\"")?;
500 let include_bg = match bg {
501 IncludeBackground::Yes => true,
502 IncludeBackground::No => false,
503 IncludeBackground::IfDifferent(c) => style.background != c,
504 };
505 if include_bg {
506 write!(s, "background-color:")?;
507 write_css_color(s, style.background);
508 write!(s, ";")?;
509 }
510 if style.font_style.contains(FontStyle::UNDERLINE) {
511 write!(s, "text-decoration:underline;")?;
512 }
513 if style.font_style.contains(FontStyle::BOLD) {
514 write!(s, "font-weight:bold;")?;
515 }
516 if style.font_style.contains(FontStyle::ITALIC) {
517 write!(s, "font-style:italic;")?;
518 }
519 write!(s, "color:")?;
520 write_css_color(s, style.foreground);
521 write!(s, ";\">{}", Escape(text))?;
522 }
523 }
524 if prev_style.is_some() {
525 write!(s, "</span>")?;
526 }
527
528 Ok(())
529}
530
531pub fn start_highlighted_html_snippet(t: &Theme) -> (String, Color) {
544 let c = t.settings.background.unwrap_or(Color::WHITE);
545 (
546 format!(
547 "<pre style=\"background-color:#{:02x}{:02x}{:02x};\">\n",
548 c.r, c.g, c.b
549 ),
550 c,
551 )
552}
553
554#[cfg(all(feature = "default-syntaxes", feature = "default-themes",))]
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use crate::highlighting::{HighlightIterator, HighlightState, Highlighter, Style, ThemeSet};
559 use crate::parsing::{ParseState, ScopeStack, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
560 use crate::util::LinesWithEndings;
561 #[test]
562 fn tokens() {
563 let ss = SyntaxSet::load_defaults_newlines();
564 let syntax = ss.find_syntax_by_name("Markdown").unwrap();
565 let mut state = ParseState::new(syntax, false);
566 let line = "[w](t.co) *hi* **five**";
567 let ops = state.parse_line(line, &ss).expect("#[cfg(test)]");
568 let mut stack = ScopeStack::new();
569
570 let (html, _) =
574 line_tokens_to_classed_spans(line, &ops[..], ClassStyle::Spaced, &mut stack)
575 .expect("#[cfg(test)]");
576 println!("{}", html);
577 assert_eq!(html, include_str!("../testdata/test2.html").trim_end());
578
579 let ts = ThemeSet::load_defaults();
580 let highlighter = Highlighter::new(&ts.themes["InspiredGitHub"]);
581 let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
582 let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
583 let regions: Vec<(Style, &str)> = iter.collect();
584
585 let html2 = styled_line_to_highlighted_html(®ions[..], IncludeBackground::Yes)
586 .expect("#[cfg(test)]");
587 println!("{}", html2);
588 assert_eq!(html2, include_str!("../testdata/test1.html").trim_end());
589 }
590
591 #[test]
592 fn strings() {
593 let ss = SyntaxSet::load_defaults_newlines();
594 let ts = ThemeSet::load_defaults();
595 let s = include_str!("../testdata/highlight_test.erb");
596 let syntax = ss.find_syntax_by_extension("erb").unwrap();
597 let html = highlighted_html_for_string(s, &ss, syntax, &ts.themes["base16-ocean.dark"])
598 .expect("#[cfg(test)]");
599 assert_eq!(html, include_str!("../testdata/test3.html"));
601 let html2 = highlighted_html_for_file(
602 "testdata/highlight_test.erb",
603 &ss,
604 &ts.themes["base16-ocean.dark"],
605 )
606 .unwrap();
607 assert_eq!(html2, html);
608
609 let html3 = highlighted_html_for_file(
611 "testdata/Packages/Rust/Cargo.sublime-syntax",
612 &ss,
613 &ts.themes["InspiredGitHub"],
614 )
615 .unwrap();
616 println!("{}", html3);
617 assert_eq!(html3, include_str!("../testdata/test4.html"));
618 }
619
620 #[test]
621 fn tricky_test_syntax() {
622 let mut builder = SyntaxSetBuilder::new();
625 builder.add_from_folder("testdata", true).unwrap();
626 let ss = builder.build();
627 let ts = ThemeSet::load_defaults();
628 let html = highlighted_html_for_file(
629 "testdata/testing-syntax.testsyntax",
630 &ss,
631 &ts.themes["base16-ocean.dark"],
632 )
633 .unwrap();
634 println!("{}", html);
635 assert_eq!(html, include_str!("../testdata/test5.html"));
636 }
637
638 #[test]
639 fn test_classed_html_generator_doesnt_panic() {
640 let current_code = "{\n \"headers\": [\"Number\", \"Title\"],\n \"records\": [\n [\"1\", \"Gutenberg\"],\n [\"2\", \"Printing\"]\n ],\n}\n";
641 let syntax_def = SyntaxDefinition::load_from_str(
642 include_str!("../testdata/JSON.sublime-syntax"),
643 true,
644 None,
645 )
646 .unwrap();
647 let mut syntax_set_builder = SyntaxSetBuilder::new();
648 syntax_set_builder.add(syntax_def);
649 let syntax_set = syntax_set_builder.build();
650 let syntax = syntax_set.find_syntax_by_name("JSON").unwrap();
651
652 let mut html_generator =
653 ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced);
654 for line in LinesWithEndings::from(current_code) {
655 html_generator
656 .parse_html_for_line_which_includes_newline(line)
657 .expect("#[cfg(test)]");
658 }
659 html_generator.finalize();
660 }
661
662 #[test]
663 fn test_classed_html_generator() {
664 let current_code = "x + y\n";
665 let syntax_set = SyntaxSet::load_defaults_newlines();
666 let syntax = syntax_set.find_syntax_by_name("R").unwrap();
667
668 let mut html_generator =
669 ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced);
670 for line in LinesWithEndings::from(current_code) {
671 html_generator
672 .parse_html_for_line_which_includes_newline(line)
673 .expect("#[cfg(test)]");
674 }
675 let html = html_generator.finalize();
676 assert_eq!(html, "<span class=\"source r\">x <span class=\"keyword operator arithmetic r\">+</span> y\n</span>");
677 }
678
679 #[test]
680 fn test_classed_html_generator_prefixed() {
681 let current_code = "x + y\n";
682 let syntax_set = SyntaxSet::load_defaults_newlines();
683 let syntax = syntax_set.find_syntax_by_name("R").unwrap();
684 let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
685 syntax,
686 &syntax_set,
687 ClassStyle::SpacedPrefixed { prefix: "foo-" },
688 );
689 for line in LinesWithEndings::from(current_code) {
690 html_generator
691 .parse_html_for_line_which_includes_newline(line)
692 .expect("#[cfg(test)]");
693 }
694 let html = html_generator.finalize();
695 assert_eq!(html, "<span class=\"foo-source foo-r\">x <span class=\"foo-keyword foo-operator foo-arithmetic foo-r\">+</span> y\n</span>");
696 }
697
698 #[test]
699 fn test_classed_html_generator_no_empty_span() {
700 let code = "// Rust source
701fn main() {
702 println!(\"Hello World!\");
703}
704";
705 let syntax_set = SyntaxSet::load_defaults_newlines();
706 let syntax = syntax_set.find_syntax_by_extension("rs").unwrap();
707 let mut html_generator =
708 ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced);
709 for line in LinesWithEndings::from(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=\"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>");
716 }
717}