1use 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
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(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 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 #[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 self.html.push('\n');
118 }
119
120 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
134pub 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(' '); }
172 css.pop(); css.push_str(", "); }
175 let len = css.len();
176 css.truncate(len - 2); 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 Spaced,
215 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
263pub 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 ®ions[..],
282 IncludeBackground::IfDifferent(bg),
283 &mut output,
284 )?;
285 }
286 output.push_str("</pre>\n");
287 Ok(output)
288}
289
290pub 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 ®ions[..],
310 IncludeBackground::IfDifferent(bg),
311 &mut output,
312 )?;
313 }
314 line.clear();
315 }
316 output.push_str("</pre>\n");
317 Ok(output)
318}
319
320pub 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); let mut cur_index = 0;
343 let mut span_delta = 0;
344
345 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#[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#[derive(Debug, PartialEq, Eq, Clone, Copy)]
401pub enum IncludeBackground {
402 No,
404 Yes,
406 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
418pub 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
448pub 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
501pub 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 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(®ions[..], 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 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 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 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\">"</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>");
677 }
678}