Skip to content

Commit

Permalink
Support custom syntax highlighting themes
Browse files Browse the repository at this point in the history
Related to getzola#419

Introduces once_cell dependency to store extra SyntaxSet, ThemeSet as statics.
Option<SyntaxSet> is no longer stored in the Config Markdown struct.

Gruvbox tmTheme added to test_site, it is taken from
https://github.com/Colorsublime/Colorsublime-Themes (MIT licensed)
  • Loading branch information
drmason13 committed May 28, 2021
1 parent 14b1a35 commit 5bc9cd8
Show file tree
Hide file tree
Showing 10 changed files with 513 additions and 52 deletions.
6 changes: 3 additions & 3 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 components/config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ chrono = "0.4"
globset = "0.4"
lazy_static = "1"
syntect = "4.1"
once_cell = "1.7.2"

errors = { path = "../errors" }
utils = { path = "../utils" }
8 changes: 3 additions & 5 deletions components/config/src/config/markup.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use serde_derive::{Deserialize, Serialize};
use syntect::parsing::SyntaxSet;

pub const DEFAULT_HIGHLIGHT_THEME: &str = "base16-ocean-dark";

Expand All @@ -25,9 +24,8 @@ pub struct Markdown {

/// A list of directories to search for additional `.sublime-syntax` files in.
pub extra_syntaxes: Vec<String>,
/// The compiled extra syntaxes into a syntax set
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
pub extra_syntax_set: Option<SyntaxSet>,
/// A list of directories to search for additional `.tmTheme` files in.
pub extra_highlight_themes: Vec<String>,
}

impl Markdown {
Expand Down Expand Up @@ -74,7 +72,7 @@ impl Default for Markdown {
external_links_no_referrer: false,
smart_punctuation: false,
extra_syntaxes: vec![],
extra_syntax_set: None,
extra_highlight_themes: vec![],
}
}
}
89 changes: 77 additions & 12 deletions components/config/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ use std::path::{Path, PathBuf};

use globset::{Glob, GlobSet, GlobSetBuilder};
use serde_derive::{Deserialize, Serialize};
use syntect::parsing::SyntaxSetBuilder;
use syntect::parsing::SyntaxSet;
use syntect::{highlighting::ThemeSet, parsing::SyntaxSetBuilder};
use toml::Value as Toml;

use crate::highlighting::THEME_SET;
use crate::highlighting::{
BUILTIN_HIGHLIGHT_THEME_SET, EXTRA_HIGHLIGHT_THEME_SET, EXTRA_SYNTAX_SET,
};
use crate::theme::Theme;
use errors::{bail, Error, Result};
use utils::fs::read_file_with_error;

use self::markup::DEFAULT_HIGHLIGHT_THEME;

// We want a default base url for tests
static DEFAULT_BASE_URL: &str = "http://a-website.com";

Expand Down Expand Up @@ -124,10 +129,6 @@ impl Config {
bail!("A base URL is required in config.toml with key `base_url`");
}

if !THEME_SET.themes.contains_key(&config.highlight_theme) {
bail!("Highlight theme {} not available", config.highlight_theme)
}

if config.languages.iter().any(|l| l.code == config.default_language) {
bail!("Default language `{}` should not appear both in `config.default_language` and `config.languages`", config.default_language)
}
Expand Down Expand Up @@ -174,7 +175,44 @@ impl Config {
path,
&format!("No `{:?}` file found. Are you in the right directory?", file_name),
)?;
Config::parse(&content)

let config = Config::parse(&content)?;
let config_dir = path.parent().unwrap();
config.init_extra_syntaxes_and_highlight_themes(config_dir)?;

Ok(config)
}

// Initialise static once cells: EXTRA_SYNTAX_SET and EXTRA_HIGHLIGHT_THEME_SET
// They can only be initialised once, when building a new site the existing values are reused
fn init_extra_syntaxes_and_highlight_themes(&self, path: &Path) -> Result<()> {
if let Some(extra_syntax_set) = self.load_extra_syntaxes(path)? {
if EXTRA_SYNTAX_SET.get().is_none() {
EXTRA_SYNTAX_SET.set(extra_syntax_set).unwrap();
}
}
if let Some(extra_highlight_theme_set) = self.load_extra_highlight_themes(path)? {
if EXTRA_HIGHLIGHT_THEME_SET.get().is_none() {
EXTRA_HIGHLIGHT_THEME_SET.set(extra_highlight_theme_set).unwrap();
}
}

// validate that the chosen highlight_theme exists in the loaded highlight theme sets
if !BUILTIN_HIGHLIGHT_THEME_SET.themes.contains_key(self.highlight_theme()) {
if let Some(ts) = EXTRA_HIGHLIGHT_THEME_SET.get() {
if !ts.themes.contains_key(self.highlight_theme()) {
bail!(
"Highlight theme {} not found in the extra theme set",
self.highlight_theme()
)
}
} else {
bail!("Highlight theme {} not available.\n\
You can load custom themes by configuring `extra_highlight_themes` with a list of folders containing .tmTheme files", self.highlight_theme())
}
}

Ok(())
}

/// Temporary, while we have the settings in 2 places
Expand All @@ -194,7 +232,7 @@ impl Config {
/// Temporary, while we have the settings in 2 places
/// TODO: remove me in 0.14
pub fn highlight_theme(&self) -> &str {
if self.highlight_theme != markup::DEFAULT_HIGHLIGHT_THEME {
if self.highlight_theme != DEFAULT_HIGHLIGHT_THEME {
&self.highlight_theme
} else {
&self.markdown.highlight_theme
Expand All @@ -216,19 +254,46 @@ impl Config {

/// Attempt to load any extra syntax found in the extra syntaxes of the config
/// TODO: move to markup.rs in 0.14
pub fn load_extra_syntaxes(&mut self, base_path: &Path) -> Result<()> {
pub fn load_extra_syntaxes(&self, base_path: &Path) -> Result<Option<SyntaxSet>> {
let extra_syntaxes = self.extra_syntaxes();
if extra_syntaxes.is_empty() {
return Ok(());
return Ok(None);
}

let mut ss = SyntaxSetBuilder::new();
for dir in &extra_syntaxes {
ss.add_from_folder(base_path.join(dir), true)?;
}
self.markdown.extra_syntax_set = Some(ss.build());

Ok(())
Ok(Some(ss.build()))
}

pub fn get_highlight_theme(&self) -> &'static syntect::highlighting::Theme {
if let Some(theme_set) = EXTRA_HIGHLIGHT_THEME_SET.get() {
theme_set
.themes
.get(self.highlight_theme())
.expect("`highlight_theme` is missing from extra highlight theme set")
} else {
&BUILTIN_HIGHLIGHT_THEME_SET.themes[self.highlight_theme()]
}
}

/// Attempt to load any theme sets found in the extra highlighting themes of the config
/// TODO: move to markup.rs in 0.14
pub fn load_extra_highlight_themes(&self, base_path: &Path) -> Result<Option<ThemeSet>> {
let extra_highlight_themes = self.markdown.extra_highlight_themes.clone();
if extra_highlight_themes.is_empty() {
return Ok(None);
}

let mut ts = ThemeSet::new();
for dir in &extra_highlight_themes {
ts.add_from_folder(base_path.join(dir))?;
}
let extra_theme_set = Some(ts);

Ok(extra_theme_set)
}

/// Makes a url, taking into account that the base url might have a trailing slash
Expand Down
41 changes: 25 additions & 16 deletions components/config/src/highlighting.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use once_cell::sync::OnceCell;

use lazy_static::lazy_static;
use syntect::dumps::from_binary;
use syntect::easy::HighlightLines;
Expand All @@ -7,38 +9,45 @@ use syntect::parsing::SyntaxSet;
use crate::config::Config;

lazy_static! {
pub static ref SYNTAX_SET: SyntaxSet = {
pub static ref BUILTIN_SYNTAX_SET: SyntaxSet = {
let ss: SyntaxSet =
from_binary(include_bytes!("../../../sublime/syntaxes/newlines.packdump"));
ss
};
pub static ref THEME_SET: ThemeSet =
from_binary(include_bytes!("../../../sublime/themes/all.themedump"));
pub static ref BUILTIN_HIGHLIGHT_THEME_SET: ThemeSet = {
let ss: ThemeSet = from_binary(include_bytes!("../../../sublime/themes/all.themedump"));
ss
};
}

pub static EXTRA_SYNTAX_SET: OnceCell<SyntaxSet> = OnceCell::new();
pub static EXTRA_HIGHLIGHT_THEME_SET: OnceCell<ThemeSet> = OnceCell::new();

/// Returns the highlighter and whether it was found in the extra or not
pub fn get_highlighter(language: Option<&str>, config: &Config) -> (HighlightLines<'static>, bool) {
let theme = &THEME_SET.themes[config.highlight_theme()];
let mut in_extra = false;
pub fn get_highlighter(language: Option<&str>, config: &Config) -> HighlightLines<'static> {
let theme = if let Some(theme_set) = EXTRA_HIGHLIGHT_THEME_SET.get() {
theme_set
.themes
.get(config.highlight_theme())
.expect("extra highlight theme set does not contain configured highlight theme")
} else {
&BUILTIN_HIGHLIGHT_THEME_SET.themes[config.highlight_theme()]
};

if let Some(ref lang) = language {
let syntax = if let Some(ref extra) = config.markdown.extra_syntax_set {
let s = extra.find_syntax_by_token(lang);
if s.is_some() {
in_extra = true;
}
s
let syntax = if let Some(ref extra) = EXTRA_SYNTAX_SET.get() {
extra.find_syntax_by_token(lang)
} else {
// The JS syntax hangs a lot... the TS syntax is probably better anyway.
// https://github.com/getzola/zola/issues/1241
// https://github.com/getzola/zola/issues/1211
// https://github.com/getzola/zola/issues/1174
let hacked_lang = if *lang == "js" || *lang == "javascript" { "ts" } else { lang };
SYNTAX_SET.find_syntax_by_token(hacked_lang)
BUILTIN_SYNTAX_SET.find_syntax_by_token(hacked_lang)
}
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
(HighlightLines::new(syntax, theme), in_extra)
.unwrap_or_else(|| BUILTIN_SYNTAX_SET.find_syntax_plain_text());
HighlightLines::new(syntax, theme)
} else {
(HighlightLines::new(SYNTAX_SET.find_syntax_plain_text(), theme), false)
HighlightLines::new(BUILTIN_SYNTAX_SET.find_syntax_plain_text(), theme)
}
}
3 changes: 1 addition & 2 deletions components/rendering/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use syntect::html::{start_highlighted_html_snippet, IncludeBackground};

use crate::context::RenderContext;
use crate::table_of_contents::{make_table_of_contents, Heading};
use config::highlighting::THEME_SET;
use errors::{Error, Result};
use front_matter::InsertAnchor;
use utils::site::resolve_internal_link;
Expand Down Expand Up @@ -227,7 +226,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
return Event::Html("<pre><code>".into());
}

let theme = &THEME_SET.themes[context.config.highlight_theme()];
let theme = context.config.get_highlight_theme();
match kind {
cmark::CodeBlockKind::Indented => (),
cmark::CodeBlockKind::Fenced(fence_info) => {
Expand Down
20 changes: 7 additions & 13 deletions components/rendering/src/markdown/codeblock.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET};
use config::highlighting::{get_highlighter, BUILTIN_SYNTAX_SET, EXTRA_SYNTAX_SET};
use config::Config;
use std::cmp::min;
use std::collections::HashSet;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Color, Style, Theme};
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
use syntect::parsing::SyntaxSet;

use super::fence::{FenceSettings, Range};

pub struct CodeBlock<'config> {
pub struct CodeBlock {
highlighter: HighlightLines<'static>,
extra_syntax_set: Option<&'config SyntaxSet>,
background: IncludeBackground,
theme: &'static Theme,

Expand All @@ -21,17 +19,13 @@ pub struct CodeBlock<'config> {
num_lines: usize,
}

impl<'config> CodeBlock<'config> {
pub fn new(fence_info: &str, config: &'config Config, background: IncludeBackground) -> Self {
impl CodeBlock {
pub fn new(fence_info: &str, config: &Config, background: IncludeBackground) -> Self {
let fence_info = FenceSettings::new(fence_info);
let theme = &THEME_SET.themes[config.highlight_theme()];
let (highlighter, in_extra) = get_highlighter(fence_info.language, config);
let theme = config.get_highlight_theme();
let highlighter = get_highlighter(fence_info.language, config);
Self {
highlighter,
extra_syntax_set: match in_extra {
true => config.markdown.extra_syntax_set.as_ref(),
false => None,
},
background,
theme,

Expand All @@ -42,7 +36,7 @@ impl<'config> CodeBlock<'config> {

pub fn highlight(&mut self, text: &str) -> String {
let highlighted =
self.highlighter.highlight(text, self.extra_syntax_set.unwrap_or(&SYNTAX_SET));
self.highlighter.highlight(text, EXTRA_SYNTAX_SET.get().unwrap_or(&BUILTIN_SYNTAX_SET));
let line_boundaries = self.find_line_boundaries(&highlighted);

// First we make sure that `highlighted` is split at every line
Expand Down
1 change: 0 additions & 1 deletion components/site/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ impl Site {
let path = path.as_ref();
let config_file = config_file.as_ref();
let mut config = get_config(config_file);
config.load_extra_syntaxes(path)?;

if let Some(theme) = config.theme.clone() {
// Grab data from the extra section of the theme
Expand Down
2 changes: 2 additions & 0 deletions test_site/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ ignored_content = ["*/ignored.md"]

[markdown]
highlight_code = true
highlight_theme = "gruvbox"
extra_syntaxes = ["syntaxes"]
extra_highlight_themes = ["highlight_themes"]

[slugify]
paths = "on"
Expand Down
Loading

0 comments on commit 5bc9cd8

Please sign in to comment.