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