diff --git a/Cargo.lock b/Cargo.lock index 74559fa..c96b79f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -903,6 +903,7 @@ dependencies = [ "clap", "comrak", "crossterm", + "flate2", "hex", "image", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 0664d48..556b6df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ clap = { version = "4.4", features = ["derive", "string"] } comrak = { version = "0.19", default-features = false } crossterm = { version = "0.27", features = ["serde"] } hex = "0.4" +flate2 = "1.0" image = "0.24" merge-struct = "0.1.0" itertools = "0.11" diff --git a/bat/themes.bin b/bat/themes.bin new file mode 100644 index 0000000..c9dfcde Binary files /dev/null and b/bat/themes.bin differ diff --git a/bat/update.sh b/bat/update.sh index f59df7e..1219643 100755 --- a/bat/update.sh +++ b/bat/update.sh @@ -19,6 +19,7 @@ cd $clone_path git reset --hard $git_hash cp assets/syntaxes.bin "$script_dir" +cp assets/themes.bin "$script_dir" acknowledgements_file="$script_dir/acknowledgements.txt" cp LICENSE-MIT "$acknowledgements_file" diff --git a/src/builder.rs b/src/builder.rs index 5c9021d..854e267 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -490,7 +490,7 @@ impl<'a> PresentationBuilder<'a> { let mut code_highlighter = self.highlighter.language_highlighter(&code.language); let padding_style = { let mut highlighter = self.highlighter.language_highlighter(&CodeLanguage::Rust); - highlighter.style_line("//").first().expect("no styles").style + highlighter.style_line("//").next().expect("no styles").style }; let groups = match self.options.allow_mutations { true => code.attributes.highlight_groups.clone(), @@ -669,8 +669,6 @@ impl CodeLine { fn highlight(&self, padding_style: &Style, code_highlighter: &mut LanguageHighlighter) -> String { let mut output = StyledTokens { style: *padding_style, tokens: &self.prefix }.apply_style(); output.push_str(&code_highlighter.highlight_line(&self.code)); - // Remove newline - output.pop(); output.push_str(&StyledTokens { style: *padding_style, tokens: &self.suffix }.apply_style()); output } @@ -1147,7 +1145,7 @@ mod test { } fn try_build_presentation(elements: Vec) -> Result { - let highlighter = CodeHighlighter::new("base16-ocean.dark").unwrap(); + let highlighter = CodeHighlighter::default(); let theme = PresentationTheme::default(); let mut resources = Resources::new("/tmp"); let options = PresentationBuilderOptions::default(); diff --git a/src/export.rs b/src/export.rs index 3fe668a..7260726 100644 --- a/src/export.rs +++ b/src/export.rs @@ -172,7 +172,7 @@ mod test { let arena = Arena::new(); let parser = MarkdownParser::new(&arena); let theme = Default::default(); - let highlighter = CodeHighlighter::new("base16-ocean.dark").unwrap(); + let highlighter = CodeHighlighter::default(); let resources = Resources::new("examples"); let mut exporter = Exporter::new(parser, &theme, highlighter, resources); exporter.extract_metadata(content, Path::new(path)).expect("metadata extraction failed") diff --git a/src/main.rs b/src/main.rs index 999ff9d..8bd5283 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,7 +74,7 @@ fn run(cli: Cli) -> Result<(), Box> { }; let arena = Arena::new(); let parser = MarkdownParser::new(&arena); - let default_highlighter = CodeHighlighter::new("base16-ocean.dark")?; + let default_highlighter = CodeHighlighter::default(); if cli.acknowledgements { display_acknowledgements(); return Ok(()); diff --git a/src/render/highlighting.rs b/src/render/highlighting.rs index 1379967..78b2070 100644 --- a/src/render/highlighting.rs +++ b/src/render/highlighting.rs @@ -1,28 +1,76 @@ use crate::markdown::elements::CodeLanguage; +use crossterm::{ + style::{SetBackgroundColor, SetForegroundColor}, + QueueableCommand, +}; +use flate2::read::ZlibDecoder; use once_cell::sync::Lazy; +use serde::Deserialize; +use std::{ + collections::BTreeMap, + io::{self, Write}, + sync::{Arc, Mutex}, +}; use syntect::{ easy::HighlightLines, highlighting::{Style, Theme, ThemeSet}, parsing::SyntaxSet, - util::as_24_bit_terminal_escaped, }; static SYNTAX_SET: Lazy = Lazy::new(|| { let contents = include_bytes!("../../bat/syntaxes.bin"); bincode::deserialize(contents).expect("syntaxes are broken") }); -static THEMES: Lazy = Lazy::new(ThemeSet::load_defaults); + +static THEMES: Lazy = Lazy::new(|| { + let contents = include_bytes!("../../bat/themes.bin"); + let theme_set: LazyThemeSet = bincode::deserialize(contents).expect("syntaxes are broken"); + let default_themes = ThemeSet::load_defaults(); + theme_set.merge(default_themes); + theme_set +}); + +// This structure mimic's `bat`'s serialized theme set's. +#[derive(Debug, Deserialize)] +struct LazyThemeSet { + serialized_themes: BTreeMap>, + #[serde(skip)] + themes: Mutex>>, +} + +impl LazyThemeSet { + fn merge(&self, themes: ThemeSet) { + let mut all_themes = self.themes.lock().unwrap(); + for (name, theme) in themes.themes { + if !self.serialized_themes.contains_key(&name) { + all_themes.insert(name, theme.into()); + } + } + } + + fn get(&self, theme_name: &str) -> Option> { + let mut themes = self.themes.lock().unwrap(); + if let Some(theme) = themes.get(theme_name) { + return Some(theme.clone()); + } + let serialized = self.serialized_themes.get(theme_name)?; + let decoded: Theme = bincode::deserialize_from(ZlibDecoder::new(serialized.as_slice())).ok()?; + let decoded = Arc::new(decoded); + themes.insert(theme_name.to_string(), decoded); + themes.get(theme_name).cloned() + } +} /// A code highlighter. #[derive(Clone)] pub struct CodeHighlighter { - theme: &'static Theme, + theme: Arc, } impl CodeHighlighter { /// Construct a new highlighted using the given [syntect] theme name. pub fn new(theme: &str) -> Result { - let theme = THEMES.themes.get(theme).ok_or(ThemeNotFound)?; + let theme = THEMES.get(theme).ok_or(ThemeNotFound)?; Ok(Self { theme }) } @@ -30,7 +78,7 @@ impl CodeHighlighter { pub(crate) fn language_highlighter(&self, language: &CodeLanguage) -> LanguageHighlighter { let extension = Self::language_extension(language); let syntax = SYNTAX_SET.find_syntax_by_extension(extension).unwrap(); - let highlighter = HighlightLines::new(syntax, self.theme); + let highlighter = HighlightLines::new(syntax, &self.theme); LanguageHighlighter { highlighter } } @@ -90,23 +138,28 @@ impl CodeHighlighter { } } } -pub(crate) struct LanguageHighlighter { - highlighter: HighlightLines<'static>, + +impl Default for CodeHighlighter { + fn default() -> Self { + Self::new("base16-eighties.dark").expect("default theme not found") + } } -impl LanguageHighlighter { +pub(crate) struct LanguageHighlighter<'a> { + highlighter: HighlightLines<'a>, +} + +impl<'a> LanguageHighlighter<'a> { pub(crate) fn highlight_line(&mut self, line: &str) -> String { - let ranges = self.highlighter.highlight_line(line, &SYNTAX_SET).unwrap(); - as_24_bit_terminal_escaped(&ranges, true) + self.style_line(line).map(|s| s.apply_style()).collect() } - pub(crate) fn style_line<'a>(&mut self, line: &'a str) -> Vec> { + pub(crate) fn style_line<'b>(&mut self, line: &'b str) -> impl Iterator> { self.highlighter .highlight_line(line, &SYNTAX_SET) .unwrap() .into_iter() .map(|(style, tokens)| StyledTokens { style, tokens }) - .collect() } } @@ -117,7 +170,29 @@ pub(crate) struct StyledTokens<'a> { impl<'a> StyledTokens<'a> { pub(crate) fn apply_style(&self) -> String { - as_24_bit_terminal_escaped(&[(self.style, self.tokens)], true) + let background = to_ansi_color(self.style.background); + let foreground = to_ansi_color(self.style.foreground); + + // We do this conversion manually as crossterm will reset the color after styling, and we + // want to "keep it open" so that padding also uses this background color. + // + // Note: these unwraps shouldn't happen as this is an in-memory writer so there's no + // fallible IO here. + let mut cursor = io::BufWriter::new(Vec::new()); + if let Some(color) = background { + cursor.queue(SetBackgroundColor(color)).unwrap(); + } + if let Some(color) = foreground { + cursor.queue(SetForegroundColor(color)).unwrap(); + } + // syntect likes its input to contain \n but we don't want them as we pad text with extra + // " " at the end so we get rid of them here. + for chunk in self.tokens.split('\n') { + cursor.write_all(chunk.as_bytes()).unwrap(); + } + + cursor.flush().unwrap(); + String::from_utf8(cursor.into_inner().unwrap()).unwrap() } } @@ -126,6 +201,28 @@ impl<'a> StyledTokens<'a> { #[error("theme not found")] pub struct ThemeNotFound; +// This code has been adapted from bat's: https://github.com/sharkdp/bat +fn to_ansi_color(color: syntect::highlighting::Color) -> Option { + use crossterm::style::Color; + if color.a == 0 { + Some(match color.r { + 0x00 => Color::Black, + 0x01 => Color::DarkRed, + 0x02 => Color::DarkGreen, + 0x03 => Color::DarkYellow, + 0x04 => Color::DarkBlue, + 0x05 => Color::DarkMagenta, + 0x06 => Color::DarkCyan, + 0x07 => Color::Grey, + n => Color::AnsiValue(n), + }) + } else if color.a == 1 { + None + } else { + Some(Color::Rgb { r: color.r, g: color.g, b: color.b }) + } +} + #[cfg(test)] mod test { use super::*; @@ -139,4 +236,9 @@ mod test { assert!(syntax.is_some(), "extension {extension} for {language:?} not found"); } } + + #[test] + fn default_highlighter() { + CodeHighlighter::default(); + } } diff --git a/themes/light.yaml b/themes/light.yaml index c3b5d16..d09c2c9 100644 --- a/themes/light.yaml +++ b/themes/light.yaml @@ -17,7 +17,7 @@ code: minimum_size: 50 minimum_margin: percent: 8 - theme_name: InspiredGitHub + theme_name: GitHub padding: horizontal: 2 vertical: 1