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:
parent
495fe2a6c5
commit
8368f9994d
5 changed files with 86 additions and 46 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue