Skip to content

Commit

Permalink
Merge pull request #67 from mfontanini/more-themes
Browse files Browse the repository at this point in the history
Pull in all of bat's syntect themes
  • Loading branch information
mfontanini authored Nov 26, 2023
2 parents 45b2043 + 9d17aba commit 5ba06ed
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 20 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Binary file added bat/themes.bin
Binary file not shown.
1 change: 1 addition & 0 deletions bat/update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 2 additions & 4 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -1147,7 +1145,7 @@ mod test {
}

fn try_build_presentation(elements: Vec<MarkdownElement>) -> Result<Presentation, BuildError> {
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();
Expand Down
2 changes: 1 addition & 1 deletion src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
};
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(());
Expand Down
128 changes: 115 additions & 13 deletions src/render/highlighting.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,84 @@
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<SyntaxSet> = Lazy::new(|| {
let contents = include_bytes!("../../bat/syntaxes.bin");
bincode::deserialize(contents).expect("syntaxes are broken")
});
static THEMES: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);

static THEMES: Lazy<LazyThemeSet> = 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<String, Vec<u8>>,
#[serde(skip)]
themes: Mutex<BTreeMap<String, Arc<Theme>>>,
}

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<Arc<Theme>> {
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<Theme>,
}

impl CodeHighlighter {
/// Construct a new highlighted using the given [syntect] theme name.
pub fn new(theme: &str) -> Result<Self, ThemeNotFound> {
let theme = THEMES.themes.get(theme).ok_or(ThemeNotFound)?;
let theme = THEMES.get(theme).ok_or(ThemeNotFound)?;
Ok(Self { theme })
}

/// Create a highlighter for a specific language.
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 }
}

Expand Down Expand Up @@ -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")
}
}

pub(crate) struct LanguageHighlighter<'a> {
highlighter: HighlightLines<'a>,
}

impl LanguageHighlighter {
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<StyledTokens<'a>> {
pub(crate) fn style_line<'b>(&mut self, line: &'b str) -> impl Iterator<Item = StyledTokens<'b>> {
self.highlighter
.highlight_line(line, &SYNTAX_SET)
.unwrap()
.into_iter()
.map(|(style, tokens)| StyledTokens { style, tokens })
.collect()
}
}

Expand All @@ -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()
}
}

Expand All @@ -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<crossterm::style::Color> {
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::*;
Expand All @@ -139,4 +236,9 @@ mod test {
assert!(syntax.is_some(), "extension {extension} for {language:?} not found");
}
}

#[test]
fn default_highlighter() {
CodeHighlighter::default();
}
}
2 changes: 1 addition & 1 deletion themes/light.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ code:
minimum_size: 50
minimum_margin:
percent: 8
theme_name: InspiredGitHub
theme_name: GitHub
padding:
horizontal: 2
vertical: 1
Expand Down

0 comments on commit 5ba06ed

Please sign in to comment.