diff --git a/cli/src/highlight.rs b/cli/src/highlight.rs index 60f73d49..8668b204 100644 --- a/cli/src/highlight.rs +++ b/cli/src/highlight.rs @@ -16,7 +16,7 @@ use serde_json::{json, Value}; use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer}; use tree_sitter_loader::Loader; -pub const HTML_HEADER: &str = " +pub const HTML_HEAD_HEADER: &str = " Tree-sitter Highlighting @@ -33,7 +33,9 @@ pub const HTML_HEADER: &str = " .line { white-space: pre; } - + "; + +pub const HTML_BODY_HEADER: &str = " "; @@ -268,7 +270,7 @@ fn hex_string_to_rgb(s: &str) -> Option<(u8, u8, u8)> { } fn style_to_css(style: anstyle::Style) -> String { - let mut result = "style='".to_string(); + let mut result = String::new(); let effects = style.get_effects(); if effects.contains(Effects::UNDERLINE) { write!(&mut result, "text-decoration: underline;").unwrap(); @@ -282,7 +284,6 @@ fn style_to_css(style: anstyle::Style) -> String { if let Some(color) = style.get_fg_color() { write_color(&mut result, color); } - result.push('\''); result } @@ -377,13 +378,18 @@ pub fn ansi( Ok(()) } +pub struct HtmlOptions { + pub inline_styles: bool, + pub quiet: bool, + pub print_time: bool, +} + pub fn html( loader: &Loader, theme: &Theme, source: &[u8], config: &HighlightConfiguration, - quiet: bool, - print_time: bool, + opts: &HtmlOptions, cancellation_flag: Option<&AtomicUsize>, ) -> Result<()> { use std::io::Write; @@ -398,14 +404,30 @@ pub fn html( })?; let mut renderer = HtmlRenderer::new(); - renderer.render(events, source, &move |highlight| { - theme.styles[highlight.0] - .css - .as_ref() - .map_or_else(|| "".as_bytes(), |css_style| css_style.as_bytes()) + renderer.render(events, source, &move |highlight, output| { + if opts.inline_styles { + output.extend(b"style='"); + output.extend( + theme.styles[highlight.0] + .css + .as_ref() + .map_or_else(|| "".as_bytes(), |css_style| css_style.as_bytes()), + ); + output.extend(b"'"); + } else { + output.extend(b"class='"); + let mut parts = theme.highlight_names[highlight.0].split('.').peekable(); + while let Some(part) = parts.next() { + output.extend(part.as_bytes()); + if parts.peek().is_some() { + output.extend(b" "); + } + } + output.extend(b"'"); + } })?; - if !quiet { + if !opts.quiet { writeln!(&mut stdout, "")?; for (i, line) in renderer.lines().enumerate() { writeln!( @@ -418,7 +440,7 @@ pub fn html( writeln!(&mut stdout, "
")?; } - if print_time { + if opts.print_time { eprintln!("Time: {}ms", time.elapsed().as_millis()); } @@ -449,7 +471,7 @@ mod tests { style.ansi.get_fg_color(), Some(Color::Ansi256(Ansi256Color(36))) ); - assert_eq!(style.css, Some("style=\'color: #00af87\'".to_string())); + assert_eq!(style.css, Some("color: #00af87".to_string())); // junglegreen is not an ANSI color and is preserved when the terminal supports it env::set_var("COLORTERM", "truecolor"); @@ -458,7 +480,7 @@ mod tests { style.ansi.get_fg_color(), Some(Color::Rgb(RgbColor(38, 166, 154))) ); - assert_eq!(style.css, Some("style=\'color: #26a69a\'".to_string())); + assert_eq!(style.css, Some("color: #26a69a".to_string())); // junglegreen gets approximated as darkcyan when the terminal does not support it env::set_var("COLORTERM", ""); @@ -467,7 +489,7 @@ mod tests { style.ansi.get_fg_color(), Some(Color::Ansi256(Ansi256Color(36))) ); - assert_eq!(style.css, Some("style=\'color: #26a69a\'".to_string())); + assert_eq!(style.css, Some("color: #26a69a".to_string())); if let Ok(environment_variable) = original_environment_variable { env::set_var("COLORTERM", environment_variable); diff --git a/cli/src/main.rs b/cli/src/main.rs index 7a2fdc58..ec71d855 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -361,6 +361,11 @@ struct Query { struct Highlight { #[arg(long, short = 'H', help = "Generate highlighting as an HTML document")] pub html: bool, + #[arg( + long, + help = "When generating HTML, use css classes rather than inline styles" + )] + pub css_classes: bool, #[arg( long, help = "Check that highlighting captures conform strictly to standards" @@ -1136,10 +1141,11 @@ impl Highlight { let quiet = self.quiet; let html_mode = quiet || self.html; + let inline_styles = !self.css_classes; let paths = collect_paths(self.paths_file.as_deref(), self.paths)?; if html_mode && !quiet { - println!("{}", highlight::HTML_HEADER); + println!("{}", highlight::HTML_HEAD_HEADER); } let cancellation_flag = util::cancel_on_signal(); @@ -1203,15 +1209,32 @@ impl Highlight { } } + if html_mode && !quiet { + println!(" "); + println!("{}", highlight::HTML_BODY_HEADER); + } + let source = fs::read(path)?; if html_mode { + let html_opts = highlight::HtmlOptions { + inline_styles, + quiet, + print_time: self.time, + }; highlight::html( &loader, &theme_config.theme, &source, highlight_config, - quiet, - self.time, + &html_opts, Some(&cancellation_flag), )?; } else { diff --git a/cli/src/tests/highlight_test.rs b/cli/src/tests/highlight_test.rs index ab9dfcbe..331c42db 100644 --- a/cli/src/tests/highlight_test.rs +++ b/cli/src/tests/highlight_test.rs @@ -718,7 +718,9 @@ fn to_html<'a>( .map(Highlight), ); renderer - .render(events, src, &|highlight| HTML_ATTRS[highlight.0].as_bytes()) + .render(events, src, &|highlight, output| { + output.extend(HTML_ATTRS[highlight.0].as_bytes()); + }) .unwrap(); Ok(renderer .lines() diff --git a/highlight/src/c_lib.rs b/highlight/src/c_lib.rs index 0cf3e848..9cc5c073 100644 --- a/highlight/src/c_lib.rs +++ b/highlight/src/c_lib.rs @@ -304,9 +304,9 @@ impl TSHighlighter { output .renderer .set_carriage_return_highlight(self.carriage_return_index.map(Highlight)); - let result = output - .renderer - .render(highlights, source_code, &|s| self.attribute_strings[s.0]); + let result = output.renderer.render(highlights, source_code, &|s, out| { + out.extend(self.attribute_strings[s.0]); + }); match result { Err(Error::Cancelled | Error::Unknown) => ErrorCode::Timeout, Err(Error::InvalidLanguage) => ErrorCode::InvalidLanguage, diff --git a/highlight/src/lib.rs b/highlight/src/lib.rs index abd9fb5e..3dc20098 100644 --- a/highlight/src/lib.rs +++ b/highlight/src/lib.rs @@ -1103,28 +1103,28 @@ impl HtmlRenderer { self.line_offsets.push(0); } - pub fn render<'a, F>( + pub fn render( &mut self, highlighter: impl Iterator>, - source: &'a [u8], + source: &[u8], attribute_callback: &F, ) -> Result<(), Error> where - F: Fn(Highlight) -> &'a [u8], + F: Fn(Highlight, &mut Vec), { let mut highlights = Vec::new(); for event in highlighter { match event { Ok(HighlightEvent::HighlightStart(s)) => { highlights.push(s); - self.start_highlight(s, attribute_callback); + self.start_highlight(s, &attribute_callback); } Ok(HighlightEvent::HighlightEnd) => { highlights.pop(); self.end_highlight(); } Ok(HighlightEvent::Source { start, end }) => { - self.add_text(&source[start..end], &highlights, attribute_callback); + self.add_text(&source[start..end], &highlights, &attribute_callback); } Err(a) => return Err(a), } @@ -1153,30 +1153,23 @@ impl HtmlRenderer { }) } - fn add_carriage_return<'a, F>(&mut self, attribute_callback: &F) + fn add_carriage_return(&mut self, attribute_callback: &F) where - F: Fn(Highlight) -> &'a [u8], + F: Fn(Highlight, &mut Vec), { if let Some(highlight) = self.carriage_return_highlight { - let attribute_string = (attribute_callback)(highlight); - if !attribute_string.is_empty() { - self.html.extend(b""); - } + self.html.extend(b""); } } - fn start_highlight<'a, F>(&mut self, h: Highlight, attribute_callback: &F) + fn start_highlight(&mut self, h: Highlight, attribute_callback: &F) where - F: Fn(Highlight) -> &'a [u8], + F: Fn(Highlight, &mut Vec), { - let attribute_string = (attribute_callback)(h); - self.html.extend(b""); } @@ -1184,9 +1177,9 @@ impl HtmlRenderer { self.html.extend(b""); } - fn add_text<'a, F>(&mut self, src: &[u8], highlights: &[Highlight], attribute_callback: &F) + fn add_text(&mut self, src: &[u8], highlights: &[Highlight], attribute_callback: &F) where - F: Fn(Highlight) -> &'a [u8], + F: Fn(Highlight, &mut Vec), { pub const fn html_escape(c: u8) -> Option<&'static [u8]> { match c as char {