feat: add flag to output css classes instead of inline styles in HTML highlighter output

Co-authored-by: Amaan Qureshi <amaanq12@gmail.com>
This commit is contained in:
Jonathan Raphaelson 2024-12-14 23:43:22 -07:00 committed by GitHub
parent 495fe2a6c5
commit 8368f9994d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 86 additions and 46 deletions

View file

@ -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 = "
<!doctype HTML>
<head>
<title>Tree-sitter Highlighting</title>
@ -33,7 +33,9 @@ pub const HTML_HEADER: &str = "
.line {
white-space: pre;
}
</style>
</style>";
pub const HTML_BODY_HEADER: &str = "
</head>
<body>
";
@ -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, "<table>")?;
for (i, line) in renderer.lines().enumerate() {
writeln!(
@ -418,7 +440,7 @@ pub fn html(
writeln!(&mut stdout, "</table>")?;
}
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);

View file

@ -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!(" <style>");
let names = theme_config.theme.highlight_names.iter();
let styles = theme_config.theme.styles.iter();
for (name, style) in names.zip(styles) {
if let Some(css) = &style.css {
println!(" .{name} {{ {css}; }}");
}
}
println!(" </style>");
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 {

View file

@ -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()

View file

@ -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,

View file

@ -1103,28 +1103,28 @@ impl HtmlRenderer {
self.line_offsets.push(0);
}
pub fn render<'a, F>(
pub fn render<F>(
&mut self,
highlighter: impl Iterator<Item = Result<HighlightEvent, Error>>,
source: &'a [u8],
source: &[u8],
attribute_callback: &F,
) -> Result<(), Error>
where
F: Fn(Highlight) -> &'a [u8],
F: Fn(Highlight, &mut Vec<u8>),
{
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<F>(&mut self, attribute_callback: &F)
where
F: Fn(Highlight) -> &'a [u8],
F: Fn(Highlight, &mut Vec<u8>),
{
if let Some(highlight) = self.carriage_return_highlight {
let attribute_string = (attribute_callback)(highlight);
if !attribute_string.is_empty() {
self.html.extend(b"<span ");
self.html.extend(attribute_string);
self.html.extend(b"></span>");
}
self.html.extend(b"<span ");
(attribute_callback)(highlight, &mut self.html);
self.html.extend(b"></span>");
}
}
fn start_highlight<'a, F>(&mut self, h: Highlight, attribute_callback: &F)
fn start_highlight<F>(&mut self, h: Highlight, attribute_callback: &F)
where
F: Fn(Highlight) -> &'a [u8],
F: Fn(Highlight, &mut Vec<u8>),
{
let attribute_string = (attribute_callback)(h);
self.html.extend(b"<span");
if !attribute_string.is_empty() {
self.html.extend(b" ");
self.html.extend(attribute_string);
}
self.html.extend(b"<span ");
(attribute_callback)(h, &mut self.html);
self.html.extend(b">");
}
@ -1184,9 +1177,9 @@ impl HtmlRenderer {
self.html.extend(b"</span>");
}
fn add_text<'a, F>(&mut self, src: &[u8], highlights: &[Highlight], attribute_callback: &F)
fn add_text<F>(&mut self, src: &[u8], highlights: &[Highlight], attribute_callback: &F)
where
F: Fn(Highlight) -> &'a [u8],
F: Fn(Highlight, &mut Vec<u8>),
{
pub const fn html_escape(c: u8) -> Option<&'static [u8]> {
match c as char {