use crate::Error;
use crate::easy::{HighlightFile, HighlightLines};
use crate::escape::Escape;
use crate::highlighting::{Color, FontStyle, Style, Theme};
use crate::parsing::{
BasicScopeStackOp, ParseState, Scope, ScopeStack, ScopeStackOp, SyntaxReference, SyntaxSet,
SCOPE_REPO,
};
use crate::util::LinesWithEndings;
use std::fmt::Write;
use std::io::{BufRead};
use std::path::Path;
pub struct ClassedHTMLGenerator<'a> {
syntax_set: &'a SyntaxSet,
open_spans: isize,
parse_state: ParseState,
scope_stack: ScopeStack,
html: String,
style: ClassStyle,
}
impl<'a> ClassedHTMLGenerator<'a> {
#[deprecated(since="4.2.0", note="Please use `new_with_class_style` instead")]
pub fn new(syntax_reference: &'a SyntaxReference, syntax_set: &'a SyntaxSet) -> ClassedHTMLGenerator<'a> {
Self::new_with_class_style(syntax_reference, syntax_set, ClassStyle::Spaced)
}
pub fn new_with_class_style(
syntax_reference: &'a SyntaxReference,
syntax_set: &'a SyntaxSet,
style: ClassStyle,
) -> ClassedHTMLGenerator<'a> {
let parse_state = ParseState::new(syntax_reference);
let open_spans = 0;
let html = String::new();
let scope_stack = ScopeStack::new();
ClassedHTMLGenerator {
syntax_set,
open_spans,
parse_state,
scope_stack,
html,
style,
}
}
pub fn parse_html_for_line_which_includes_newline(&mut self, line: &str) -> Result<(), Error>{
let parsed_line = self.parse_state.parse_line(line, self.syntax_set)?;
let (formatted_line, delta) = line_tokens_to_classed_spans(
line,
parsed_line.as_slice(),
self.style,
&mut self.scope_stack,
)?;
self.open_spans += delta;
self.html.push_str(formatted_line.as_str());
Ok(())
}
#[deprecated(since="4.5.0", note="Please use `parse_html_for_line_which_includes_newline` instead")]
pub fn parse_html_for_line(&mut self, line: &str) {
self.parse_html_for_line_which_includes_newline(line).expect("Please use `parse_html_for_line_which_includes_newline` instead");
self.html.push('\n');
}
pub fn finalize(mut self) -> String {
for _ in 0..self.open_spans {
self.html.push_str("</span>");
}
self.html
}
}
#[deprecated(since="4.2.0", note="Please use `css_for_theme_with_class_style` instead.")]
pub fn css_for_theme(theme: &Theme) -> String {
css_for_theme_with_class_style(theme, ClassStyle::Spaced).expect("Please use `css_for_theme_with_class_style` instead.")
}
pub fn css_for_theme_with_class_style(theme: &Theme, style: ClassStyle) -> Result<String, Error> {
let mut css = String::new();
css.push_str("/*\n");
let name = theme.name.clone().unwrap_or_else(|| "unknown theme".to_string());
css.push_str(&format!(" * theme \"{}\" generated by syntect\n", name));
css.push_str(" */\n\n");
match style {
ClassStyle::Spaced => {
css.push_str(".code {\n");
}
ClassStyle::SpacedPrefixed { prefix } => {
css.push_str(&format!(".{}code {{\n", prefix));
}
};
if let Some(fgc) = theme.settings.foreground {
css.push_str(&format!(
" color: #{:02x}{:02x}{:02x};\n",
fgc.r, fgc.g, fgc.b
));
}
if let Some(bgc) = theme.settings.background {
css.push_str(&format!(
" background-color: #{:02x}{:02x}{:02x};\n",
bgc.r, bgc.g, bgc.b
));
}
css.push_str("}\n\n");
for i in &theme.scopes {
for scope_selector in &i.scope.selectors {
let scopes = scope_selector.extract_scopes();
for k in &scopes {
scope_to_selector(&mut css, *k, style);
css.push(' '); }
css.pop(); css.push_str(", "); }
let len = css.len();
css.truncate(len - 2); css.push_str(" {\n");
if let Some(fg) = i.style.foreground {
css.push_str(&format!(" color: #{:02x}{:02x}{:02x};\n", fg.r, fg.g, fg.b));
}
if let Some(bg) = i.style.background {
css.push_str(&format!(
" background-color: #{:02x}{:02x}{:02x};\n",
bg.r, bg.g, bg.b
));
}
if let Some(fs) = i.style.font_style {
if fs.contains(FontStyle::UNDERLINE) {
css.push_str("text-decoration: underline;\n");
}
if fs.contains(FontStyle::BOLD) {
css.push_str("font-weight: bold;\n");
}
if fs.contains(FontStyle::ITALIC) {
css.push_str("font-style: italic;\n");
}
}
css.push_str("}\n");
}
Ok(css)
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum ClassStyle {
Spaced,
SpacedPrefixed { prefix: &'static str },
}
fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) {
let repo = SCOPE_REPO.lock().unwrap();
for i in 0..(scope.len()) {
let atom = scope.atom_at(i as usize);
let atom_s = repo.atom_str(atom);
if i != 0 {
s.push(' ')
}
match style {
ClassStyle::Spaced => {}
ClassStyle::SpacedPrefixed { prefix } => {
s.push_str(prefix);
}
}
s.push_str(atom_s);
}
}
fn scope_to_selector(s: &mut String, scope: Scope, style: ClassStyle) {
let repo = SCOPE_REPO.lock().unwrap();
for i in 0..(scope.len()) {
let atom = scope.atom_at(i as usize);
let atom_s = repo.atom_str(atom);
s.push('.');
match style {
ClassStyle::Spaced => {}
ClassStyle::SpacedPrefixed { prefix } => {
s.push_str(prefix);
}
}
s.push_str(atom_s);
}
}
pub fn highlighted_html_for_string(
s: &str,
ss: &SyntaxSet,
syntax: &SyntaxReference,
theme: &Theme,
) -> Result<String, Error> {
let mut highlighter = HighlightLines::new(syntax, theme);
let (mut output, bg) = start_highlighted_html_snippet(theme);
for line in LinesWithEndings::from(s) {
let regions = highlighter.highlight_line(line, ss)?;
append_highlighted_html_for_styled_line(
®ions[..],
IncludeBackground::IfDifferent(bg),
&mut output,
)?;
}
output.push_str("</pre>\n");
Ok(output)
}
pub fn highlighted_html_for_file<P: AsRef<Path>>(
path: P,
ss: &SyntaxSet,
theme: &Theme,
) -> Result<String, Error> {
let mut highlighter = HighlightFile::new(path, ss, theme)?;
let (mut output, bg) = start_highlighted_html_snippet(theme);
let mut line = String::new();
while highlighter.reader.read_line(&mut line)? > 0 {
{
let regions = highlighter.highlight_lines.highlight_line(&line, ss)?;
append_highlighted_html_for_styled_line(
®ions[..],
IncludeBackground::IfDifferent(bg),
&mut output,
)?;
}
line.clear();
}
output.push_str("</pre>\n");
Ok(output)
}
pub fn line_tokens_to_classed_spans(
line: &str,
ops: &[(usize, ScopeStackOp)],
style: ClassStyle,
stack: &mut ScopeStack,
) -> Result<(String, isize), Error> {
let mut s = String::with_capacity(line.len() + ops.len() * 8); let mut cur_index = 0;
let mut span_delta = 0;
let mut span_empty = false;
let mut span_start = 0;
for &(i, ref op) in ops {
if i > cur_index {
span_empty = false;
write!(s, "{}", Escape(&line[cur_index..i]))?;
cur_index = i
}
stack.apply_with_hook(op, |basic_op, _| match basic_op {
BasicScopeStackOp::Push(scope) => {
span_start = s.len();
span_empty = true;
s.push_str("<span class=\"");
scope_to_classes(&mut s, scope, style);
s.push_str("\">");
span_delta += 1;
}
BasicScopeStackOp::Pop => {
if !span_empty {
s.push_str("</span>");
} else {
s.truncate(span_start);
}
span_delta -= 1;
span_empty = false;
}
})?;
}
write!(s, "{}", Escape(&line[cur_index..line.len()]))?;
Ok((s, span_delta))
}
#[deprecated(since="4.6.0", note="Use `line_tokens_to_classed_spans` instead, this can panic and highlight incorrectly")]
pub fn tokens_to_classed_spans(
line: &str,
ops: &[(usize, ScopeStackOp)],
style: ClassStyle,
) -> (String, isize) {
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")
}
#[deprecated(since="3.1.0", note="Use `line_tokens_to_classed_spans` instead to avoid incorrect highlighting and panics")]
pub fn tokens_to_classed_html(line: &str,
ops: &[(usize, ScopeStackOp)],
style: ClassStyle)
-> String {
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
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum IncludeBackground {
No,
Yes,
IfDifferent(Color),
}
fn write_css_color(s: &mut String, c: Color) {
if c.a != 0xFF {
write!(s, "#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a).unwrap();
} else {
write!(s, "#{:02x}{:02x}{:02x}", c.r, c.g, c.b).unwrap();
}
}
pub fn styled_line_to_highlighted_html(v: &[(Style, &str)], bg: IncludeBackground) -> Result<String, Error> {
let mut s: String = String::new();
append_highlighted_html_for_styled_line(v, bg, &mut s)?;
Ok(s)
}
pub fn append_highlighted_html_for_styled_line(
v: &[(Style, &str)],
bg: IncludeBackground,
s: &mut String,
) -> Result<(), Error> {
let mut prev_style: Option<&Style> = None;
for &(ref style, text) in v.iter() {
let unify_style = if let Some(ps) = prev_style {
style == ps || (style.background == ps.background && text.trim().is_empty())
} else {
false
};
if unify_style {
write!(s, "{}", Escape(text))?;
} else {
if prev_style.is_some() {
write!(s, "</span>")?;
}
prev_style = Some(style);
write!(s, "<span style=\"")?;
let include_bg = match bg {
IncludeBackground::Yes => true,
IncludeBackground::No => false,
IncludeBackground::IfDifferent(c) => style.background != c,
};
if include_bg {
write!(s, "background-color:")?;
write_css_color(s, style.background);
write!(s, ";")?;
}
if style.font_style.contains(FontStyle::UNDERLINE) {
write!(s, "text-decoration:underline;")?;
}
if style.font_style.contains(FontStyle::BOLD) {
write!(s, "font-weight:bold;")?;
}
if style.font_style.contains(FontStyle::ITALIC) {
write!(s, "font-style:italic;")?;
}
write!(s, "color:")?;
write_css_color(s, style.foreground);
write!(s, ";\">{}", Escape(text))?;
}
}
if prev_style.is_some() {
write!(s, "</span>")?;
}
Ok(())
}
pub fn start_highlighted_html_snippet(t: &Theme) -> (String, Color) {
let c = t.settings.background.unwrap_or(Color::WHITE);
(
format!(
"<pre style=\"background-color:#{:02x}{:02x}{:02x};\">\n",
c.r, c.g, c.b
),
c,
)
}
#[cfg(all(
feature = "default-syntaxes",
feature = "default-themes",
))]
#[cfg(test)]
mod tests {
use super::*;
use crate::highlighting::{HighlightIterator, HighlightState, Highlighter, Style, ThemeSet};
use crate::parsing::{ParseState, ScopeStack, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
use crate::util::LinesWithEndings;
#[test]
fn tokens() {
let ss = SyntaxSet::load_defaults_newlines();
let syntax = ss.find_syntax_by_name("Markdown").unwrap();
let mut state = ParseState::new(syntax);
let line = "[w](t.co) *hi* **five**";
let ops = state.parse_line(line, &ss).expect("#[cfg(test)]");
let mut stack = ScopeStack::new();
let (html, _) = line_tokens_to_classed_spans(line, &ops[..], ClassStyle::Spaced, &mut stack).expect("#[cfg(test)]");
println!("{}", html);
assert_eq!(html, include_str!("../testdata/test2.html").trim_end());
let ts = ThemeSet::load_defaults();
let highlighter = Highlighter::new(&ts.themes["InspiredGitHub"]);
let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
let regions: Vec<(Style, &str)> = iter.collect();
let html2 = styled_line_to_highlighted_html(®ions[..], IncludeBackground::Yes).expect("#[cfg(test)]");
println!("{}", html2);
assert_eq!(html2, include_str!("../testdata/test1.html").trim_end());
}
#[test]
fn strings() {
let ss = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let s = include_str!("../testdata/highlight_test.erb");
let syntax = ss.find_syntax_by_extension("erb").unwrap();
let html = highlighted_html_for_string(s, &ss, syntax, &ts.themes["base16-ocean.dark"]).expect("#[cfg(test)]");
assert_eq!(html, include_str!("../testdata/test3.html"));
let html2 = highlighted_html_for_file(
"testdata/highlight_test.erb",
&ss,
&ts.themes["base16-ocean.dark"],
)
.unwrap();
assert_eq!(html2, html);
let html3 = highlighted_html_for_file(
"testdata/Packages/Rust/Cargo.sublime-syntax",
&ss,
&ts.themes["InspiredGitHub"],
)
.unwrap();
println!("{}", html3);
assert_eq!(html3, include_str!("../testdata/test4.html"));
}
#[test]
fn tricky_test_syntax() {
let mut builder = SyntaxSetBuilder::new();
builder.add_from_folder("testdata", true).unwrap();
let ss = builder.build();
let ts = ThemeSet::load_defaults();
let html = highlighted_html_for_file(
"testdata/testing-syntax.testsyntax",
&ss,
&ts.themes["base16-ocean.dark"],
)
.unwrap();
println!("{}", html);
assert_eq!(html, include_str!("../testdata/test5.html"));
}
#[test]
fn test_classed_html_generator_doesnt_panic() {
let current_code = "{\n \"headers\": [\"Number\", \"Title\"],\n \"records\": [\n [\"1\", \"Gutenberg\"],\n [\"2\", \"Printing\"]\n ],\n}\n";
let syntax_def = SyntaxDefinition::load_from_str(
include_str!("../testdata/JSON.sublime-syntax"),
true,
None,
)
.unwrap();
let mut syntax_set_builder = SyntaxSetBuilder::new();
syntax_set_builder.add(syntax_def);
let syntax_set = syntax_set_builder.build();
let syntax = syntax_set.find_syntax_by_name("JSON").unwrap();
let mut html_generator =
ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced);
for line in LinesWithEndings::from(current_code) {
html_generator.parse_html_for_line_which_includes_newline(line).expect("#[cfg(test)]");
}
html_generator.finalize();
}
#[test]
fn test_classed_html_generator() {
let current_code = "x + y\n";
let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_name("R").unwrap();
let mut html_generator =
ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced);
for line in LinesWithEndings::from(current_code) {
html_generator.parse_html_for_line_which_includes_newline(line).expect("#[cfg(test)]");
}
let html = html_generator.finalize();
assert_eq!(html, "<span class=\"source r\">x <span class=\"keyword operator arithmetic r\">+</span> y\n</span>");
}
#[test]
fn test_classed_html_generator_prefixed() {
let current_code = "x + y\n";
let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_name("R").unwrap();
let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
syntax,
&syntax_set,
ClassStyle::SpacedPrefixed { prefix: "foo-" },
);
for line in LinesWithEndings::from(current_code) {
html_generator.parse_html_for_line_which_includes_newline(line).expect("#[cfg(test)]");
}
let html = html_generator.finalize();
assert_eq!(html, "<span class=\"foo-source foo-r\">x <span class=\"foo-keyword foo-operator foo-arithmetic foo-r\">+</span> y\n</span>");
}
#[test]
fn test_classed_html_generator_no_empty_span() {
let code = "// Rust source
fn main() {
println!(\"Hello World!\");
}
";
let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax = syntax_set.find_syntax_by_extension("rs").unwrap();
let mut html_generator =
ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced);
for line in LinesWithEndings::from(code) {
html_generator.parse_html_for_line_which_includes_newline(line).expect("#[cfg(test)]");
}
let html = html_generator.finalize();
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>");
}
}