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, "", i + 1, )?; } writeln!(&mut stdout, "
{}{line}
")?; 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"); } } }