use crate::error::Result; use crate::loader::Loader; use ansi_term::{Color, Style}; use lazy_static::lazy_static; use serde_json::Value; use std::collections::HashMap; use std::{fmt, fs, io, mem, path}; use tree_sitter::{Language, PropertySheet}; use tree_sitter_highlight::{highlight, highlight_html, HighlightEvent, Properties, Scope}; lazy_static! { static ref CSS_STYLES_BY_COLOR_ID: Vec = serde_json::from_str(include_str!("../vendor/xterm-colors.json")).unwrap(); } pub struct Theme { ansi_styles: Vec>, css_styles: Vec>, } impl Theme { pub fn load(path: &path::Path) -> io::Result { let json = fs::read_to_string(path)?; Ok(Self::new(&json)) } pub fn new(json: &str) -> Self { let mut ansi_styles = vec![None; 30]; let mut css_styles = vec![None; 30]; if let Ok(colors) = serde_json::from_str::>(json) { for (scope, style_value) in colors { let mut style = Style::default(); parse_style(&mut style, style_value); ansi_styles[scope as usize] = Some(style); css_styles[scope as usize] = Some(style_to_css(style)); } } Self { ansi_styles, css_styles, } } fn ansi_style(&self, scope: Scope) -> Option<&Style> { self.ansi_styles[scope as usize].as_ref() } fn css_style(&self, scope: Scope) -> Option<&str> { self.css_styles[scope as usize].as_ref().map(|s| s.as_str()) } } impl Default for Theme { fn default() -> Self { Theme::new( r#" { "attribute": {"color": 124, "italic": true}, "comment": {"color": 245, "italic": true}, "constant.builtin": {"color": 94, "bold": true}, "constant": 94, "constructor": 136, "embedded": null, "function.builtin": {"color": 26, "bold": true}, "function": 26, "keyword": 56, "number": {"color": 94, "bold": true}, "property": 124, "operator": {"color": 239, "bold": true}, "punctuation.bracket": 239, "punctuation.delimiter": 239, "string.special": 30, "string": 28, "tag": {"color": 18}, "variable.builtin": {"bold": true} } "#, ) } } impl fmt::Debug for Theme { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{{")?; let mut first = true; for (i, style) in self.ansi_styles.iter().enumerate() { if let Some(style) = style { let scope = Scope::from_usize(i).unwrap(); if !first { write!(f, ", ")?; } write!(f, "{:?}: {:?}", scope, style)?; first = false; } } write!(f, "}}")?; Ok(()) } } fn parse_style(style: &mut Style, json: Value) { if let Value::Object(entries) = json { for (property_name, value) in entries { match property_name.as_str() { "italic" => *style = style.italic(), "bold" => *style = style.bold(), "dimmed" => *style = style.dimmed(), "underline" => *style = style.underline(), "color" => { if let Some(color) = parse_color(value) { *style = style.fg(color); } } _ => {} } } } else if let Some(color) = parse_color(json) { *style = style.fg(color); } } fn parse_color(json: Value) -> Option { match json { Value::Number(n) => match n.as_u64() { Some(n) => Some(Color::Fixed(n as u8)), _ => None, }, Value::String(s) => match s.to_lowercase().as_str() { "blue" => Some(Color::Blue), "cyan" => Some(Color::Cyan), "green" => Some(Color::Green), "purple" => Some(Color::Purple), "red" => Some(Color::Red), "white" => Some(Color::White), "yellow" => Some(Color::Yellow), s => { if s.starts_with("#") && s.len() >= 7 { if let (Ok(red), Ok(green), Ok(blue)) = ( u8::from_str_radix(&s[1..3], 16), u8::from_str_radix(&s[3..5], 16), u8::from_str_radix(&s[5..7], 16), ) { Some(Color::RGB(red, green, blue)) } else { None } } else { None } } }, _ => None, } } fn style_to_css(style: Style) -> String { use std::fmt::Write; let mut result = "style='".to_string(); if style.is_bold { write!(&mut result, "font-weight: bold;").unwrap(); } if style.is_italic { write!(&mut result, "font-style: italic;").unwrap(); } if let Some(color) = style.foreground { write!(&mut result, "color: {};", color_to_css(color)).unwrap(); } result.push('\''); result } fn color_to_css(color: Color) -> &'static str { match color { Color::Black => "black", Color::Blue => "blue", Color::Red => "red", Color::Green => "green", Color::Yellow => "yellow", Color::Cyan => "cyan", Color::Purple => "purple", Color::White => "white", Color::Fixed(n) => CSS_STYLES_BY_COLOR_ID[n as usize].as_str(), _ => panic!("Unsupported color type"), } } pub fn ansi( loader: &Loader, theme: &Theme, source: &[u8], language: Language, property_sheet: &PropertySheet, ) -> Result<()> { use std::io::Write; let stdout = io::stdout(); let mut stdout = stdout.lock(); let mut scope_stack = Vec::new(); for event in highlight(loader, source, language, property_sheet)? { match event { HighlightEvent::Source(s) => { if let Some(style) = scope_stack.last().and_then(|s| theme.ansi_style(*s)) { write!(&mut stdout, "{}", style.paint(s))?; } else { write!(&mut stdout, "{}", s)?; } } HighlightEvent::ScopeStart(s) => { scope_stack.push(s); } HighlightEvent::ScopeEnd(_) => { scope_stack.pop(); } } } Ok(()) } pub const HTML_HEADER: &'static str = " Tree-sitter Highlighting "; pub const HTML_FOOTER: &'static str = " "; pub fn html( loader: &Loader, theme: &Theme, source: &[u8], language: Language, property_sheet: &PropertySheet, ) -> Result<()> { use std::io::Write; let stdout = io::stdout(); let mut stdout = stdout.lock(); write!(&mut stdout, "\n")?; let lines = highlight_html(loader, source, language, property_sheet, |scope| { if let Some(css_style) = theme.css_style(scope) { css_style } else { "" } })?; for (i, line) in lines.into_iter().enumerate() { write!( &mut stdout, "\n", i + 1, line )?; } write!(&mut stdout, "
{}{}
\n")?; Ok(()) }