Skip to content

Commit e669879

Browse files
committed
Support custom syntax highlighting themes
Related to getzola#419 Introduces once_cell dependency to store SyntaxSets and ThemeSets 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)
1 parent 8c3ce7d commit e669879

File tree

15 files changed

+620
-115
lines changed

15 files changed

+620
-115
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/config/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ serde_derive = "1"
1212
chrono = "0.4"
1313
globset = "0.4"
1414
lazy_static = "1"
15+
once_cell = "1.8.0"
1516
# TODO: go back to version 4/5 once https://github.com/trishume/syntect/pull/337 is merged
1617
syntect = { git = "https://github.com/Keats/syntect.git", branch = "scopestack" }
1718
unic-langid = "0.9"

components/config/src/config/markup.rs

+22-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::path::Path;
22

33
use serde_derive::{Deserialize, Serialize};
4-
use syntect::parsing::{SyntaxSet, SyntaxSetBuilder};
4+
use syntect::{
5+
highlighting::ThemeSet,
6+
parsing::{SyntaxSet, SyntaxSetBuilder},
7+
};
58

69
use errors::Result;
710

@@ -43,28 +46,32 @@ pub struct Markdown {
4346
pub external_links_no_referrer: bool,
4447
/// Whether smart punctuation is enabled (changing quotes, dashes, dots etc in their typographic form)
4548
pub smart_punctuation: bool,
46-
47-
/// A list of directories to search for additional `.sublime-syntax` files in.
48-
pub extra_syntaxes: Vec<String>,
49-
/// The compiled extra syntaxes into a syntax set
50-
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
51-
pub extra_syntax_set: Option<SyntaxSet>,
49+
/// A list of directories to search for additional `.sublime-syntax` and `.tmTheme` files in.
50+
pub extra_syntaxes_and_themes: Vec<String>,
5251
}
5352

5453
impl Markdown {
55-
/// Attempt to load any extra syntax found in the extra syntaxes of the config
56-
pub fn load_extra_syntaxes(&mut self, base_path: &Path) -> Result<()> {
57-
if self.extra_syntaxes.is_empty() {
58-
return Ok(());
54+
/// Attempt to load any extra syntaxes and themes found in the extra_syntaxes_and_themes folders
55+
pub fn load_extra_syntaxes_and_themes(
56+
&self,
57+
base_path: &Path,
58+
) -> Result<(Option<SyntaxSet>, Option<ThemeSet>)> {
59+
if self.extra_syntaxes_and_themes.is_empty() {
60+
return Ok((None, None));
5961
}
6062

6163
let mut ss = SyntaxSetBuilder::new();
62-
for dir in &self.extra_syntaxes {
64+
let mut ts = ThemeSet::new();
65+
for dir in &self.extra_syntaxes_and_themes {
6366
ss.add_from_folder(base_path.join(dir), true)?;
67+
ts.add_from_folder(base_path.join(dir))?;
6468
}
65-
self.extra_syntax_set = Some(ss.build());
69+
let ss = ss.build();
6670

67-
Ok(())
71+
Ok((
72+
if ss.syntaxes().is_empty() { None } else { Some(ss) },
73+
if ts.themes.is_empty() { None } else { Some(ts) },
74+
))
6875
}
6976

7077
pub fn has_external_link_tweaks(&self) -> bool {
@@ -110,8 +117,7 @@ impl Default for Markdown {
110117
external_links_no_follow: false,
111118
external_links_no_referrer: false,
112119
smart_punctuation: false,
113-
extra_syntaxes: Vec::new(),
114-
extra_syntax_set: None,
120+
extra_syntaxes_and_themes: vec![],
115121
}
116122
}
117123
}

components/config/src/config/mod.rs

+57-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use globset::{Glob, GlobSet, GlobSetBuilder};
1212
use serde_derive::{Deserialize, Serialize};
1313
use toml::Value as Toml;
1414

15-
use crate::highlighting::THEME_SET;
15+
use crate::highlighting::{
16+
get_highlight_theme_by_name, BUILTIN_HIGHLIGHT_THEME_SET, EXTRA_HIGHLIGHT_THEME_SET,
17+
EXTRA_SYNTAX_SET,
18+
};
1619
use crate::theme::Theme;
1720
use errors::{bail, Error, Result};
1821
use utils::fs::read_file;
@@ -117,15 +120,6 @@ impl Config {
117120
bail!("A base URL is required in config.toml with key `base_url`");
118121
}
119122

120-
if config.markdown.highlight_theme != "css" {
121-
if !THEME_SET.themes.contains_key(&config.markdown.highlight_theme) {
122-
bail!(
123-
"Highlight theme {} defined in config does not exist.",
124-
config.markdown.highlight_theme
125-
);
126-
}
127-
}
128-
129123
languages::validate_code(&config.default_language)?;
130124
for code in config.languages.keys() {
131125
languages::validate_code(&code)?;
@@ -165,7 +159,59 @@ impl Config {
165159
let path = path.as_ref();
166160
let content =
167161
read_file(path).map_err(|e| errors::Error::chain("Failed to load config", e))?;
168-
Config::parse(&content)
162+
163+
let config = Config::parse(&content)?;
164+
let config_dir = path.parent().unwrap();
165+
166+
if config.markdown.highlight_theme != "css" {
167+
config.init_extra_syntaxes_and_highlight_themes(config_dir)?;
168+
}
169+
170+
Ok(config)
171+
}
172+
173+
// Initialise static once cells: EXTRA_SYNTAX_SET and EXTRA_HIGHLIGHT_THEME_SET
174+
// They can only be initialised once, when building a new site the existing values are reused
175+
fn init_extra_syntaxes_and_highlight_themes(&self, path: &Path) -> Result<()> {
176+
let (loaded_extra_syntaxes, loaded_extra_highlight_themes) =
177+
self.markdown.load_extra_syntaxes_and_themes(path)?;
178+
179+
if let Some(extra_syntax_set) = loaded_extra_syntaxes {
180+
if EXTRA_SYNTAX_SET.get().is_none() {
181+
EXTRA_SYNTAX_SET.set(extra_syntax_set).unwrap();
182+
}
183+
}
184+
if let Some(extra_highlight_theme_set) = loaded_extra_highlight_themes {
185+
if EXTRA_HIGHLIGHT_THEME_SET.get().is_none() {
186+
EXTRA_HIGHLIGHT_THEME_SET.set(extra_highlight_theme_set).unwrap();
187+
}
188+
}
189+
190+
// validate that the chosen highlight_theme exists in the loaded highlight theme sets
191+
if !BUILTIN_HIGHLIGHT_THEME_SET.themes.contains_key(&self.markdown.highlight_theme) {
192+
if let Some(extra) = EXTRA_HIGHLIGHT_THEME_SET.get() {
193+
if !extra.themes.contains_key(&self.markdown.highlight_theme) {
194+
bail!(
195+
"Highlight theme {} not found in the extra theme set",
196+
self.markdown.highlight_theme
197+
)
198+
}
199+
} else {
200+
bail!("Highlight theme {} not available.\n\
201+
You can load custom themes by configuring `extra_syntaxes_and_themes` to include a list of folders containing '.tmTheme' files", self.markdown.highlight_theme)
202+
}
203+
}
204+
205+
Ok(())
206+
}
207+
208+
/// Gets the configured highlight theme from the BUILTIN_HIGHLIGHT_THEME_SET or the EXTRA_HIGHLIGHT_THEME_SET
209+
pub fn get_highlight_theme(&self) -> Option<&'static syntect::highlighting::Theme> {
210+
if self.markdown.highlight_theme == "css" {
211+
None
212+
} else {
213+
Some(get_highlight_theme_by_name(&self.markdown.highlight_theme))
214+
}
169215
}
170216

171217
/// Makes a url, taking into account that the base url might have a trailing slash

components/config/src/highlighting.rs

+60-53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use lazy_static::lazy_static;
2+
use once_cell::sync::OnceCell;
23
use syntect::dumps::from_binary;
34
use syntect::highlighting::{Theme, ThemeSet};
45
use syntect::parsing::{SyntaxReference, SyntaxSet};
@@ -7,17 +8,28 @@ use crate::config::Config;
78
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
89

910
lazy_static! {
10-
pub static ref SYNTAX_SET: SyntaxSet = {
11+
pub static ref BUILTIN_SYNTAX_SET: SyntaxSet = {
1112
let ss: SyntaxSet =
1213
from_binary(include_bytes!("../../../sublime/syntaxes/newlines.packdump"));
1314
ss
1415
};
15-
pub static ref THEME_SET: ThemeSet =
16+
pub static ref BUILTIN_HIGHLIGHT_THEME_SET: ThemeSet =
1617
from_binary(include_bytes!("../../../sublime/themes/all.themedump"));
1718
}
1819

1920
pub const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "z-" };
2021

22+
pub static EXTRA_SYNTAX_SET: OnceCell<SyntaxSet> = OnceCell::new();
23+
pub static EXTRA_HIGHLIGHT_THEME_SET: OnceCell<ThemeSet> = OnceCell::new();
24+
25+
/// Gets an arbitrary theme from the BUILTIN_HIGHLIGHT_THEME_SET or the EXTRA_HIGHLIGHT_THEME_SET
26+
pub fn get_highlight_theme_by_name(theme_name: &str) -> &'static syntect::highlighting::Theme {
27+
&EXTRA_HIGHLIGHT_THEME_SET
28+
.get()
29+
.and_then(|ts| ts.themes.get(theme_name))
30+
.unwrap_or_else(|| &BUILTIN_HIGHLIGHT_THEME_SET.themes[theme_name])
31+
}
32+
2133
#[derive(Clone, Debug, PartialEq, Eq)]
2234
pub enum HighlightSource {
2335
/// One of the built-in Zola syntaxes
@@ -30,66 +42,61 @@ pub enum HighlightSource {
3042
NotFound,
3143
}
3244

33-
pub struct SyntaxAndTheme<'config> {
34-
pub syntax: &'config SyntaxReference,
35-
pub syntax_set: &'config SyntaxSet,
45+
impl HighlightSource {
46+
pub fn syntax_set(&self) -> &'static SyntaxSet {
47+
match self {
48+
HighlightSource::Extra => EXTRA_SYNTAX_SET.get().unwrap(),
49+
_ => &BUILTIN_SYNTAX_SET,
50+
}
51+
}
52+
}
53+
54+
pub struct SyntaxAndTheme {
55+
pub syntax: &'static SyntaxReference,
56+
// pub syntax_set: &'static SyntaxSet,
3657
/// None if highlighting via CSS
37-
pub theme: Option<&'config Theme>,
58+
pub theme: Option<&'static Theme>,
3859
pub source: HighlightSource,
3960
}
4061

41-
pub fn resolve_syntax_and_theme<'config>(
42-
language: Option<&'_ str>,
43-
config: &'config Config,
44-
) -> SyntaxAndTheme<'config> {
45-
let theme = if config.markdown.highlight_theme != "css" {
46-
Some(&THEME_SET.themes[&config.markdown.highlight_theme])
47-
} else {
48-
None
49-
};
62+
impl SyntaxAndTheme {
63+
pub fn syntax_set(&self) -> &'static SyntaxSet {
64+
self.source.syntax_set()
65+
}
66+
}
5067

51-
if let Some(ref lang) = language {
52-
if let Some(ref extra_syntaxes) = config.markdown.extra_syntax_set {
53-
if let Some(syntax) = extra_syntaxes.find_syntax_by_token(lang) {
54-
return SyntaxAndTheme {
55-
syntax,
56-
syntax_set: extra_syntaxes,
57-
theme,
58-
source: HighlightSource::Extra,
59-
};
60-
}
61-
}
62-
// The JS syntax hangs a lot... the TS syntax is probably better anyway.
63-
// https://github.com/getzola/zola/issues/1241
64-
// https://github.com/getzola/zola/issues/1211
65-
// https://github.com/getzola/zola/issues/1174
66-
let hacked_lang = if *lang == "js" || *lang == "javascript" { "ts" } else { lang };
67-
if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(hacked_lang) {
68-
SyntaxAndTheme {
69-
syntax,
70-
syntax_set: &SYNTAX_SET as &SyntaxSet,
71-
theme,
72-
source: HighlightSource::BuiltIn,
73-
}
74-
} else {
75-
SyntaxAndTheme {
76-
syntax: SYNTAX_SET.find_syntax_plain_text(),
77-
syntax_set: &SYNTAX_SET as &SyntaxSet,
78-
theme,
79-
source: HighlightSource::NotFound,
80-
}
81-
}
68+
pub fn resolve_syntax_and_theme(language: Option<&str>, config: &Config) -> SyntaxAndTheme {
69+
let theme = config.get_highlight_theme();
70+
71+
let mut source = HighlightSource::Plain;
72+
if let Some(lang) = language {
73+
let syntax = EXTRA_SYNTAX_SET
74+
.get()
75+
.and_then(|extra| {
76+
source = HighlightSource::Extra;
77+
extra.find_syntax_by_token(lang)
78+
})
79+
.or_else(|| {
80+
// The JS syntax hangs a lot... the TS syntax is probably better anyway.
81+
// https://github.com/getzola/zola/issues/1241
82+
// https://github.com/getzola/zola/issues/1211
83+
// https://github.com/getzola/zola/issues/1174
84+
let hacked_lang = if lang == "js" || lang == "javascript" { "ts" } else { lang };
85+
source = HighlightSource::BuiltIn;
86+
BUILTIN_SYNTAX_SET.find_syntax_by_token(hacked_lang)
87+
})
88+
.unwrap_or_else(|| {
89+
source = HighlightSource::NotFound;
90+
BUILTIN_SYNTAX_SET.find_syntax_plain_text()
91+
});
92+
93+
SyntaxAndTheme { syntax, theme, source }
8294
} else {
83-
SyntaxAndTheme {
84-
syntax: SYNTAX_SET.find_syntax_plain_text(),
85-
syntax_set: &SYNTAX_SET as &SyntaxSet,
86-
theme,
87-
source: HighlightSource::Plain,
88-
}
95+
SyntaxAndTheme { syntax: BUILTIN_SYNTAX_SET.find_syntax_plain_text(), theme, source }
8996
}
9097
}
9198

9299
pub fn export_theme_css(theme_name: &str) -> String {
93-
let theme = &THEME_SET.themes[theme_name];
100+
let theme = get_highlight_theme_by_name(theme_name);
94101
css_for_theme_with_class_style(theme, CLASS_STYLE)
95102
}

components/imageproc/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ impl ResizeOp {
140140
}
141141
Fit(w, h) => {
142142
if orig_w <= w && orig_h <= h {
143-
return res; // ie. no-op
143+
return res; // ie. no-op
144144
}
145145

146146
let orig_w_h = orig_w as u64 * h as u64;

components/rendering/benches/all.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fn bench_render_content_with_highlighting(b: &mut test::Bencher) {
8686
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
8787
let permalinks_ctx = HashMap::new();
8888
let config = Config::default();
89-
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
89+
let context = RenderContext::new(&tera, &config, "", "", &permalinks_ctx, InsertAnchor::None);
9090
b.iter(|| render_content(CONTENT, &context).unwrap());
9191
}
9292

@@ -97,7 +97,7 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) {
9797
let permalinks_ctx = HashMap::new();
9898
let mut config = Config::default();
9999
config.markdown.highlight_code = false;
100-
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
100+
let context = RenderContext::new(&tera, &config, "", "", &permalinks_ctx, InsertAnchor::None);
101101
b.iter(|| render_content(CONTENT, &context).unwrap());
102102
}
103103

@@ -108,7 +108,7 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) {
108108
let mut config = Config::default();
109109
config.markdown.highlight_code = false;
110110
let permalinks_ctx = HashMap::new();
111-
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
111+
let context = RenderContext::new(&tera, &config, "", "", &permalinks_ctx, InsertAnchor::None);
112112

113113
b.iter(|| render_content(&content2, &context).unwrap());
114114
}
@@ -119,7 +119,7 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) {
119119
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
120120
let config = Config::default();
121121
let permalinks_ctx = HashMap::new();
122-
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
122+
let context = RenderContext::new(&tera, &config, "", "", &permalinks_ctx, InsertAnchor::None);
123123

124124
b.iter(|| render_shortcodes(CONTENT, &context));
125125
}
@@ -132,7 +132,7 @@ fn bench_render_content_no_shortcode_with_emoji(b: &mut test::Bencher) {
132132
config.markdown.highlight_code = false;
133133
config.markdown.render_emoji = true;
134134
let permalinks_ctx = HashMap::new();
135-
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
135+
let context = RenderContext::new(&tera, &config, "", "", &permalinks_ctx, InsertAnchor::None);
136136

137137
b.iter(|| render_content(&content2, &context).unwrap());
138138
}

0 commit comments

Comments
 (0)