diff --git a/Cargo.lock b/Cargo.lock index 5225d59f91..e5558c5ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,7 @@ dependencies = [ "errors", "globset", "lazy_static", + "once_cell", "serde", "serde_derive", "syntect", @@ -1645,9 +1646,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.5.2" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "onig" @@ -2971,7 +2972,6 @@ dependencies = [ "minify-html", "percent-encoding", "serde", - "serde_derive", "slug", "tempfile", "tera", diff --git a/components/config/Cargo.toml b/components/config/Cargo.toml index 43bbcddbdf..441de64413 100644 --- a/components/config/Cargo.toml +++ b/components/config/Cargo.toml @@ -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" } diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index 3feb5cfccf..fefed06bd1 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -1,5 +1,4 @@ use serde_derive::{Deserialize, Serialize}; -use syntect::parsing::SyntaxSet; pub const DEFAULT_HIGHLIGHT_THEME: &str = "base16-ocean-dark"; @@ -25,9 +24,8 @@ pub struct Markdown { /// A list of directories to search for additional `.sublime-syntax` files in. pub extra_syntaxes: Vec, - /// The compiled extra syntaxes into a syntax set - #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need - pub extra_syntax_set: Option, + /// A list of directories to search for additional `.tmTheme` files in. + pub extra_highlight_themes: Vec, } impl Markdown { @@ -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![], } } } diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index 743c7df5b7..adf3ca11f4 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -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"; @@ -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) } @@ -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 @@ -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 @@ -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> { 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> { + 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 diff --git a/components/config/src/highlighting.rs b/components/config/src/highlighting.rs index 2d733d10f7..d13637ef85 100644 --- a/components/config/src/highlighting.rs +++ b/components/config/src/highlighting.rs @@ -1,3 +1,5 @@ +use once_cell::sync::OnceCell; + use lazy_static::lazy_static; use syntect::dumps::from_binary; use syntect::easy::HighlightLines; @@ -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 = OnceCell::new(); +pub static EXTRA_HIGHLIGHT_THEME_SET: OnceCell = 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) } } diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index 760e64be3d..d48d061a68 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -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; @@ -227,7 +226,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result".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) => { diff --git a/components/rendering/src/markdown/codeblock.rs b/components/rendering/src/markdown/codeblock.rs index b5120c7bdb..7a45b95cbe 100644 --- a/components/rendering/src/markdown/codeblock.rs +++ b/components/rendering/src/markdown/codeblock.rs @@ -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, @@ -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, @@ -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 diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 53720e57a8..0a4a734658 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -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 diff --git a/test_site/config.toml b/test_site/config.toml index cf989d6049..2a9c936e5d 100644 --- a/test_site/config.toml +++ b/test_site/config.toml @@ -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" diff --git a/test_site/highlight_themes/gruvbox.tmTheme b/test_site/highlight_themes/gruvbox.tmTheme new file mode 100644 index 0000000000..5ba426303b --- /dev/null +++ b/test_site/highlight_themes/gruvbox.tmTheme @@ -0,0 +1,394 @@ + + + + + name + Gruvbox-N + settings + + + settings + + background + #282828 + caret + #908476 + foreground + #EAD4AF + invisibles + #3B3836 + lineHighlight + #3B3836 + selection + #3B3836 + + + + name + Comment + scope + comment + settings + + foreground + #908476 + + + + name + String + scope + string + settings + + foreground + #AAB11E + + + + name + Separator + scope + punctuation.separator.key-value + settings + + foreground + #CF8498 + + + + name + Constant + scope + constant + settings + + foreground + #CC869B + + + + name + Variable + scope + variable + settings + + foreground + #EAD4AF + + + + name + Other variable objct + scope + variable.other.object + settings + + foreground + #CAB990 + + + + name + Other variable class + scope + variable.other.class, variable.other.constant + settings + + foreground + #F1C050 + + + + name + Object property + scope + meta.property.object, entity.name.tag + settings + + foreground + #EAD4AF + + + + name + Arrows + scope + meta.function, meta.function.static.arrow, meta.function.arrow + settings + + foreground + #EAD4AF + + + + name + Keyword + scope + keyword, string.regexp punctuation.definition + settings + + foreground + #FB4938 + + + + name + Storage + scope + storage, storage.type + settings + + foreground + #FB4938 + + + + name + Inline link + scope + markup.underline.link + settings + + foreground + #FB4938 + + + + name + Class name + scope + entity.name.class, entity.name.type.class + settings + + foreground + #BABC52 + + + + name + Inherited class + scope + entity.other.inherited-class, tag.decorator, tag.decorator entity.name.tag + settings + + foreground + #7BA093 + + + + name + Function name + scope + entity.name.function, meta.function entity.name.function + settings + + foreground + #8AB572 + + + + name + Function argument + scope + variable.parameter, meta.function storage.type + settings + + foreground + #FD971F + + + + name + Tag name + scope + entity.name.tag + settings + + foreground + #FB4938 + fontStyle + italic + + + + name + Tag attribute + scope + entity.other.attribute-name + settings + + foreground + #8AB572 + fontStyle + italic + + + + name + Library class/type + scope + support.type, support.class, support.function, variable.language, support.constant, string.regexp keyword.control + settings + + foreground + #F1C050 + + + + name + Template string element + scope + punctuation.template-string.element, string.regexp punctuation.definition.group, constant.character.escape + settings + + foreground + #8AB572 + + + + name + Invalid + scope + invalid + settings + + background + #FB4938 + fontStyle + + foreground + #F8F8F0 + + + + name + Invalid deprecated + scope + invalid.deprecated + settings + + background + #FD971F + foreground + #F8F8F0 + + + + name + Operator + scope + keyword.operator, keyword.operator.logical, meta.property-name, meta.brace, punctuation.definition.parameters.begin, punctuation.definition.parameters.end, keyword.other.parenthesis + settings + + foreground + #CAB990 + + + + name + Special operator + scope + keyword.operator.ternary + settings + + foreground + #7BA093 + + + + name + Separator + scope + punctuation.separator.parameter + settings + + foreground + #EAD4AF + + + + name + Module + scope + keyword.operator.module + settings + + foreground + #FB4938 + + + + name + SublimeLinter Error + scope + sublimelinter.mark.error + settings + + foreground + #D02000 + + + + name + SublimeLinter Warning + scope + sublimelinter.mark.warning + settings + + foreground + #DDB700 + + + + name + SublimeLinter Gutter Mark + scope + sublimelinter.gutter-mark + settings + + foreground + #FFFFFF + + + + name + Diff inserted + scope + markup.inserted + settings + + foreground + #70c060 + + + + name + Diff changed + scope + markup.changed + settings + + foreground + #DDB700 + + + + name + Diff deleted + scope + markup.deleted + settings + + foreground + #FB4938 + + + + uuid + D8D5E82E-3D5B-46B5-B38E-8C841C21347D + colorSpaceName + sRGB + + \ No newline at end of file