use std::{
collections::{HashMap, HashSet},
fmt::Write,
fs,
io::{self, Write as _},
path::{self, Path, PathBuf},
str,
sync::{atomic::AtomicUsize, Arc},
time::Instant,
};
use anstyle::{Ansi256Color, AnsiColor, Color, Effects, RgbColor};
use anyhow::Result;
use lazy_static::lazy_static;
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Value};
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer};
use tree_sitter_loader::Loader;
pub const HTML_HEAD_HEADER: &str = "
Tree-sitter Highlighting
";
pub const HTML_BODY_HEADER: &str = "
";
pub const HTML_FOOTER: &str = "
";
lazy_static! {
static ref CSS_STYLES_BY_COLOR_ID: Vec =
serde_json::from_str(include_str!("../vendor/xterm-colors.json")).unwrap();
}
#[derive(Debug, Default)]
pub struct Style {
pub ansi: anstyle::Style,
pub css: Option,
}
#[derive(Debug)]
pub struct Theme {
pub styles: Vec")?;
writeln!(&mut stdout, "{HTML_BODY_HEADER}")?;
}
let mut renderer = HtmlRenderer::new();
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 !opts.quiet {
writeln!(&mut stdout, "")?;
for (i, line) in renderer.lines().enumerate() {
writeln!(
&mut stdout,
"| {} | {line} |
",
i + 1,
)?;
}
writeln!(&mut stdout, "
")?;
writeln!(&mut stdout, "{HTML_FOOTER}")?;
}
} else {
let mut style_stack = vec![theme.default_style().ansi];
for event in events {
match event? {
HighlightEvent::HighlightStart(highlight) => {
style_stack.push(theme.styles[highlight.0].ansi);
}
HighlightEvent::HighlightEnd => {
style_stack.pop();
}
HighlightEvent::Source { start, end } => {
let style = style_stack.last().unwrap();
write!(&mut stdout, "{style}").unwrap();
stdout.write_all(&source[start..end])?;
write!(&mut stdout, "{style:#}").unwrap();
}
}
}
}
if opts.print_time {
eprintln!("Time: {}ms", time.elapsed().as_millis());
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::env;
use super::*;
const JUNGLE_GREEN: &str = "#26A69A";
const DARK_CYAN: &str = "#00AF87";
#[test]
fn test_parse_style() {
let original_environment_variable = env::var("COLORTERM");
let mut style = Style::default();
assert_eq!(style.ansi.get_fg_color(), None);
assert_eq!(style.css, None);
// darkcyan is an ANSI color and is preserved
env::set_var("COLORTERM", "");
parse_style(&mut style, Value::String(DARK_CYAN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
Some(Color::Ansi256(Ansi256Color(36)))
);
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");
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
Some(Color::Rgb(RgbColor(38, 166, 154)))
);
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", "");
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
Some(Color::Ansi256(Ansi256Color(36)))
);
assert_eq!(style.css, Some("color: #26a69a".to_string()));
if let Ok(environment_variable) = original_environment_variable {
env::set_var("COLORTERM", environment_variable);
} else {
env::remove_var("COLORTERM");
}
}
}