1use 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
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);
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 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(' '); }
187 css.pop(); css.push_str(", "); }
190 let len = css.len();
191 css.truncate(len - 2); 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 Spaced,
230 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
280fn 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
297pub 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 ®ions[..],
316 IncludeBackground::IfDifferent(bg),
317 &mut output,
318 )?;
319 }
320 output.push_str("</pre>\n");
321 Ok(output)
322}
323
324pub 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 ®ions[..],
344 IncludeBackground::IfDifferent(bg),
345 &mut output,
346 )?;
347 }
348 line.clear();
349 }
350 output.push_str("</pre>\n");
351 Ok(output)
352}
353
354pub 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); let mut cur_index = 0;
377 let mut span_delta = 0;
378
379 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#[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#[derive(Debug, PartialEq, Eq, Clone, Copy)]
448pub enum IncludeBackground {
449 No,
451 Yes,
453 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
465pub 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
498pub 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
551pub 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 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(®ions[..], 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 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 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 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\">"</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>");
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 #[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}