From 8e40720acaff065dd9bb64ed8e51c3d1f794e12e Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 24 Aug 2021 09:14:18 +0200 Subject: [PATCH 01/30] Next --- CHANGELOG.md | 3 +++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae72273a..133382d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.15.0 + + ## 0.14.1 (2021-08-24) - HTML minification now respects HTML spec (it still worked before because browsers can handle invalid HTML well and minifiers take advantage of it) diff --git a/Cargo.lock b/Cargo.lock index 30af561f..5148e9d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3423,7 +3423,7 @@ dependencies = [ [[package]] name = "zola" -version = "0.14.1" +version = "0.15.0" dependencies = [ "atty", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 33837707..02235212 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zola" -version = "0.14.1" +version = "0.15.0" authors = ["Vincent Prouillet "] edition = "2018" license = "MIT" From c1267e8929be99d923a5dae5a37d28bb02b3a165 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 24 Aug 2021 09:55:37 +0200 Subject: [PATCH 02/30] Remove number of processed images from logs Closes #1504 --- src/console.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/console.rs b/src/console.rs index 366ca1c6..84fcde0f 100644 --- a/src/console.rs +++ b/src/console.rs @@ -48,15 +48,14 @@ fn colorize(message: &str, color: &ColorSpec) { writeln!(&mut stdout).unwrap(); } -/// Display in the console the number of pages/sections in the site, and number of images to process +/// Display in the console the number of pages/sections in the site pub fn notify_site_size(site: &Site) { let library = site.library.read().unwrap(); println!( - "-> Creating {} pages ({} orphan), {} sections, and processing {} images", + "-> Creating {} pages ({} orphan) and {} sections", library.pages().len(), library.get_all_orphan_pages().len(), library.sections().len() - 1, // -1 since we do not count the index as a section there - site.num_img_ops(), ); } From 2be1b417d1a590b8260820e10fc13a1c3c23ff10 Mon Sep 17 00:00:00 2001 From: southerntofu Date: Wed, 1 Sep 2021 15:43:32 +0200 Subject: [PATCH 03/30] config.default_language is exposed to templates --- components/config/src/config/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index cc6b858c..37eba3e4 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -97,6 +97,7 @@ pub struct SerializedConfig<'a> { title: &'a Option, description: &'a Option, languages: HashMap<&'a String, &'a languages::LanguageOptions>, + default_language: &'a str, generate_feed: bool, feed_filename: &'a str, taxonomies: &'a [taxonomies::Taxonomy], @@ -291,6 +292,7 @@ impl Config { title: &options.title, description: &options.description, languages: self.languages.iter().filter(|(k, _)| k.as_str() != lang).collect(), + default_language: &self.default_language, generate_feed: options.generate_feed, feed_filename: &options.feed_filename, taxonomies: &options.taxonomies, From eceb1bd79d3b3ab5b3818039aa0d533bb61f3ad7 Mon Sep 17 00:00:00 2001 From: Gijs Burghoorn Date: Sat, 4 Sep 2021 08:10:02 +0200 Subject: [PATCH 04/30] Added prompt for when zola build output-dir mentions an existing directory. (#1558) * Next version * Added ask prompt for output-dir flag Added `ask_bool` prompt for `--output-dir` for when the output directory targeted already exists in the file system. [Issue: #1378] * Updated the documentation for #1378 * Added missing "sure" in prompt text * Added timeout to prompt + dirname * Fixed complication errors Co-authored-by: Vincent Prouillet --- CHANGELOG.md | 1 + Cargo.toml | 2 +- .../getting-started/cli-usage.md | 2 +- src/cmd/build.rs | 26 ++++++++++++++++++- src/prompt.rs | 20 ++++++++++++++ 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 133382d0..343dacd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog + ## 0.15.0 diff --git a/Cargo.toml b/Cargo.toml index 02235212..a737e008 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ termcolor = "1.0.4" url = "2" # Below is for the serve cmd hyper = { version = "0.14.1", default-features = false, features = ["runtime", "server", "http2", "http1"] } -tokio = { version = "1.0.1", default-features = false, features = ["rt", "fs"] } +tokio = { version = "1.0.1", default-features = false, features = ["rt", "fs", "time"] } percent-encoding = "2" notify = "4" ws = "0.9" diff --git a/docs/content/documentation/getting-started/cli-usage.md b/docs/content/documentation/getting-started/cli-usage.md index c7de069d..73b0cb22 100644 --- a/docs/content/documentation/getting-started/cli-usage.md +++ b/docs/content/documentation/getting-started/cli-usage.md @@ -46,7 +46,7 @@ $ zola build --base-url $DEPLOY_URL This is useful for example when you want to deploy previews of a site to a dynamic URL, such as Netlify deploy previews. -You can override the default output directory `public` by passing another value to the `output-dir` flag (if this directory already exists, it is deleted). +You can override the default output directory `public` by passing another value to the `output-dir` flag (if this directory already exists, the user will be prompted whether to replace the folder). ```bash $ zola build --output-dir $DOCUMENT_ROOT diff --git a/src/cmd/build.rs b/src/cmd/build.rs index 7a0e85cb..21dd456a 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,9 +1,12 @@ use std::path::Path; -use errors::Result; +use errors::{Error, Result}; use site::Site; use crate::console; +use crate::prompt::ask_bool_timeout; + +const BUILD_PROMPT_TIMEOUT_MILLIS: u64 = 10_000; pub fn build( root_dir: &Path, @@ -14,6 +17,27 @@ pub fn build( ) -> Result<()> { let mut site = Site::new(root_dir, config_file)?; if let Some(output_dir) = output_dir { + // Check whether output directory exists or not + // This way we don't replace already existing files. + if output_dir.exists() { + console::warn(&format!("The directory '{}' already exists. Building to this directory will delete files contained within this directory.", output_dir.display())); + + // Prompt the user to ask whether they want to continue. + let clear_dir = tokio::runtime::Runtime::new() + .expect("Tokio runtime failed to instantiate") + .block_on(ask_bool_timeout( + "Are you sure you want to continue?", + false, + std::time::Duration::from_millis(BUILD_PROMPT_TIMEOUT_MILLIS), + ))?; + + if !clear_dir { + return Err(Error::msg( + "Cancelled build process because output directory already exists.", + )); + } + } + site.set_output_path(output_dir); } if let Some(b) = base_url { diff --git a/src/prompt.rs b/src/prompt.rs index 134cb576..aec14fd4 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -1,8 +1,10 @@ use std::io::{self, BufRead, Write}; +use std::time::Duration; use url::Url; use errors::Result; +use crate::console; /// Wait for user input and return what they typed fn read_line() -> Result { @@ -32,6 +34,24 @@ pub fn ask_bool(question: &str, default: bool) -> Result { } } +/// Ask a yes/no question to the user with a timeout +pub async fn ask_bool_timeout(question: &str, default: bool, timeout: Duration) -> Result { + let (tx, rx) = tokio::sync::oneshot::channel(); + + let q = question.to_string(); + std::thread::spawn(move || { + tx.send(ask_bool(&q, default)).unwrap(); + }); + + match tokio::time::timeout(timeout, rx).await { + Err(_) => { + console::warn("\nWaited too long for response."); + Ok(default) + } + Ok(val) => val.expect("Tokio failed to properly execute"), + } +} + /// Ask a question to the user where they can write a URL pub fn ask_url(question: &str, default: &str) -> Result { print!("{} ({}): ", question, default); From 84b75f9725a6535434f7ccb1aa6ebf495852b1ee Mon Sep 17 00:00:00 2001 From: southerntofu <52931252+southerntofu@users.noreply.github.com> Date: Sat, 4 Sep 2021 06:10:33 +0000 Subject: [PATCH 05/30] [feature] Shortcodes and anchor-link.html can access lang context (#1609) * Pass lang to shortcodes context * Add tests for lang in shortcodes * Lang is passed to anchor-link.html template * Document passing lang to shortcodes/anchor-link.html Add a test to make sure lang can be overriden by passing an explicit argument to the shortcode, for usage in the markdown filter where the language context is not available. * Update docs for more information on shortcodes+i18n+markdown() filter Co-authored-by: southerntofu --- components/rendering/src/context.rs | 5 ++ components/rendering/src/markdown.rs | 1 + components/rendering/src/shortcode.rs | 7 ++- components/rendering/tests/markdown.rs | 63 +++++++++++++++++++ components/templates/src/filters.rs | 20 ++++++ docs/content/documentation/content/linking.md | 3 +- .../documentation/content/shortcodes.md | 23 ++++++- .../documentation/templates/overview.md | 5 +- 8 files changed, 123 insertions(+), 4 deletions(-) diff --git a/components/rendering/src/context.rs b/components/rendering/src/context.rs index aebdb431..44b8e74e 100644 --- a/components/rendering/src/context.rs +++ b/components/rendering/src/context.rs @@ -14,6 +14,7 @@ pub struct RenderContext<'a> { pub current_page_permalink: &'a str, pub permalinks: Cow<'a, HashMap>, pub insert_anchor: InsertAnchor, + pub lang: &'a str, } impl<'a> RenderContext<'a> { @@ -34,10 +35,13 @@ impl<'a> RenderContext<'a> { permalinks: Cow::Borrowed(permalinks), insert_anchor, config, + lang, } } // In use in the markdown filter + // NOTE: This RenderContext is not i18n-aware, see MarkdownFilter::filter for details + // If this function is ever used outside of MarkdownFilter, take this into consideration pub fn from_config(config: &'a Config) -> RenderContext<'a> { Self { tera: Cow::Owned(Tera::default()), @@ -46,6 +50,7 @@ impl<'a> RenderContext<'a> { permalinks: Cow::Owned(HashMap::new()), insert_anchor: InsertAnchor::None, config, + lang: &config.default_language, } } } diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index dd2dfdd2..c0078e75 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -342,6 +342,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result, ) -> Result { let mut tera_context = Context::new(); + + // nth and lang are inserted before passed arguments, so that they can be overriden explicitly + tera_context.insert("nth", &invocation_count); + tera_context.insert("lang", &context.lang); + for (key, value) in args.iter() { tera_context.insert(key, value); } @@ -114,7 +119,7 @@ fn render_shortcode( // Trimming right to avoid most shortcodes with bodies ending up with a HTML new line tera_context.insert("body", b.trim_end()); } - tera_context.insert("nth", &invocation_count); + tera_context.extend(context.tera_context.clone()); let mut template_name = format!("shortcodes/{}.md", name); diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs index 2818598f..e955e0dc 100644 --- a/components/rendering/tests/markdown.rs +++ b/components/rendering/tests/markdown.rs @@ -181,6 +181,47 @@ fn can_render_shortcode_with_markdown_char_in_args_value() { } } +#[test] +fn can_render_html_shortcode_with_lang() { + let permalinks_ctx = HashMap::new(); + let config = Config::default_for_test(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + tera.add_raw_template("shortcodes/i18nshortcode.html", "{{ lang }}").unwrap(); + let context = RenderContext::new( + &tera, + &config, + &config.default_language, + "", + &permalinks_ctx, + InsertAnchor::None, + ); + + let res = render_content("a{{ i18nshortcode() }}a", &context).unwrap(); + assert_eq!(res.body, "

aena

\n"); +} + +#[test] +fn can_render_md_shortcode_with_lang() { + let permalinks_ctx = HashMap::new(); + let config = Config::default_for_test(); + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + tera.add_raw_template("shortcodes/i18nshortcode.md", "![Book cover in {{ lang }}](cover.{{ lang }}.png)").unwrap(); + let context = RenderContext::new( + &tera, + &config, + &config.default_language, + "", + &permalinks_ctx, + InsertAnchor::None, + ); + + let res = render_content("{{ i18nshortcode() }}", &context).unwrap(); + assert_eq!(res.body, "

\"Book

\n"); +} + + #[test] fn can_render_body_shortcode_with_markdown_char_in_name() { let permalinks_ctx = HashMap::new(); @@ -689,6 +730,28 @@ fn can_insert_anchor_with_other_special_chars() { ); } +#[test] +fn can_insert_anchor_with_lang() { + let mut tera = Tera::default(); + tera.extend(&ZOLA_TERA).unwrap(); + tera.add_raw_template("anchor-link.html", "({{ lang }})").unwrap(); + let permalinks_ctx = HashMap::new(); + let config = Config::default_for_test(); + let context = RenderContext::new( + &tera, + &config, + &config.default_language, + "", + &permalinks_ctx, + InsertAnchor::Right, + ); + let res = render_content("# Hello", &context).unwrap(); + assert_eq!( + res.body, + "

Hello(en)

\n" + ); +} + #[test] fn can_make_toc() { let permalinks_ctx = HashMap::new(); diff --git a/components/templates/src/filters.rs b/components/templates/src/filters.rs index 806feffe..a1c65734 100644 --- a/components/templates/src/filters.rs +++ b/components/templates/src/filters.rs @@ -33,6 +33,10 @@ impl MarkdownFilter { impl TeraFilter for MarkdownFilter { fn filter(&self, value: &Value, args: &HashMap) -> TeraResult { + // NOTE: RenderContext below is not aware of the current language + // However, it should not be a problem because the surrounding tera + // template has language context, and will most likely call a piece of + // markdown respecting language preferences. let mut context = RenderContext::from_config(&self.config); context.permalinks = Cow::Borrowed(&self.permalinks); context.tera = Cow::Borrowed(&self.tera); @@ -123,6 +127,22 @@ mod tests { assert_eq!(result.unwrap(), to_value(&"

Hey

\n").unwrap()); } + #[test] + fn markdown_filter_override_lang() { + // We're checking that we can use a workaround to explicitly provide `lang` in markdown filter from tera, + // because otherwise markdown filter shortcodes are not aware of the current language + // NOTE: This should also work for `nth` although i don't see a reason to do that + let args = HashMap::new(); + let config = Config::default(); + let permalinks = HashMap::new(); + let mut tera = super::load_tera(&PathBuf::new(), &config).map_err(tera::Error::msg).unwrap(); + tera.add_raw_template("shortcodes/explicitlang.html", "a{{ lang }}a").unwrap(); + let filter = MarkdownFilter { config, permalinks, tera }; + let result = filter.filter(&to_value(&"{{ explicitlang(lang='jp') }}").unwrap(), &args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), to_value(&"ajpa").unwrap()); + } + #[test] fn markdown_filter_inline() { let mut args = HashMap::new(); diff --git a/docs/content/documentation/content/linking.md b/docs/content/documentation/content/linking.md index 3497fe0a..ecc232a9 100644 --- a/docs/content/documentation/content/linking.md +++ b/docs/content/documentation/content/linking.md @@ -39,11 +39,12 @@ This option is set at the section level: the `insert_anchor_links` variable on t The default template is very basic and will need CSS tweaks in your project to look decent. If you want to change the anchor template, it can be easily overwritten by -creating an `anchor-link.html` file in the `templates` directory. +creating an `anchor-link.html` file in the `templates` directory. [Here](https://github.com/getzola/zola/blob/master/components/templates/src/builtins/anchor-link.html) you can find the default template. The anchor link template has the following variables: - `id`: the heading's id after applying the rules defined by `slugify.anchors` +- `lang`: the current language, unless called from the `markdown` template filter, in which case it will always be `en` - `level`: the heading level (between 1 and 6) ## Internal links diff --git a/docs/content/documentation/content/shortcodes.md b/docs/content/documentation/content/shortcodes.md index defa896f..139d205c 100644 --- a/docs/content/documentation/content/shortcodes.md +++ b/docs/content/documentation/content/shortcodes.md @@ -134,10 +134,19 @@ If you want to have some content that looks like a shortcode but not have Zola t you will need to escape it by using `{%/*` and `*/%}` instead of `{%` and `%}`. You won't need to escape anything else until the closing tag. +## Shortcode context + +Every shortcode can access some variables, beyond what you explicitly passed as parameter. These variables are explained in the following subsections: + +- invocation count (`nth`) +- current language (`lang`), unless called from the `markdown` template filter (in which case it will always be the same value as `default_language` in configuration, or `en` when it is unset) + +When one of these variables conflict with a variable passed as argument, the argument value will be used. + ### Invocation Count Every shortcode context is passed in a variable named `nth` that tracks how many times a particular shortcode has -been invoked in a Markdown file. Given a shortcode `true_statement.html` template: +been invoked in the current Markdown file. Given a shortcode `true_statement.html` template: ```jinja2

{{ value }} is equal to {{ nth }}.

@@ -152,6 +161,18 @@ It could be used in our Markdown as follows: This is useful when implementing custom markup for features such as sidenotes or end notes. +### Current language + +**NOTE:** When calling a shortcode from within the `markdown` template filter, the `lang` variable will always be `en`. If you feel like you need that, please consider using template macros instead. If you really need that, you can rewrite your Markdown content to pass `lang` as argument to the shortcode. + +Every shortcode can access the current language in the `lang` variable in the context. This is useful for presenting/filtering information in a shortcode depending in a per-language manner. For example, to display a per-language book cover for the current page in a shortcode called `bookcover.md`: + +```jinja2 +![Book cover in {{ lang }}](cover.{{ lang }}.png) +``` + +You can then use it in your Markdown like so: `{{/* bookcover() */}}` + ## Built-in shortcodes Zola comes with a few built-in shortcodes. If you want to override a default shortcode template, diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md index 1cfc83a2..5cfeeac2 100644 --- a/docs/content/documentation/templates/overview.md +++ b/docs/content/documentation/templates/overview.md @@ -64,7 +64,10 @@ Zola adds a few filters in addition to [those](https://tera.netlify.com/docs/#fi in Tera. ### markdown -Converts the given variable to HTML using Markdown. Please note that shortcodes evaluated by this filter cannot access the current rendering context. `config` will be available, but accessing `section` or `page` (among others) from a shortcode called within the `markdown` filter will prevent your site from building. See [this discussion](https://github.com/getzola/zola/pull/1358). +Converts the given variable to HTML using Markdown. There are a few differences compared to page/section Markdown rendering: + +- shortcodes evaluated by this filter cannot access the current rendering context: `config` will be available, but accessing `section` or `page` (among others) from a shortcode called within the `markdown` filter will prevent your site from building (see [this discussion](https://github.com/getzola/zola/pull/1358)) +- `lang` in shortcodes will always be equal to the site's `default_lang` (or `en` otherwise) ; it should not be a problem, but if it is in most cases, but if you need to use language-aware shortcodes in this filter, please refer to the [Shortcode context](@/documentation/content/shortcodes.md#shortcode-context) section of the docs. By default, the filter will wrap all text in a paragraph. To disable this behaviour, you can pass `true` to the inline argument: From 4086b0755a84e82645de3f43aab7460a1a5da6d6 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Sat, 4 Sep 2021 08:23:03 +0200 Subject: [PATCH 06/30] Support colocating subfolders (#1582) * Support colocating subfolders * Adjust tests --- Cargo.lock | 1 + components/library/Cargo.toml | 1 + components/library/src/content/mod.rs | 45 +++++++++++------------ components/library/src/content/page.rs | 2 +- components/library/src/content/section.rs | 2 +- components/rendering/src/markdown.rs | 17 --------- components/rendering/tests/markdown.rs | 4 +- components/site/src/lib.rs | 4 +- 8 files changed, 30 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5148e9d6..7615a68f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1261,6 +1261,7 @@ dependencies = [ "tera", "toml", "utils", + "walkdir", ] [[package]] diff --git a/components/library/Cargo.toml b/components/library/Cargo.toml index bfaac903..d7122e0e 100644 --- a/components/library/Cargo.toml +++ b/components/library/Cargo.toml @@ -14,6 +14,7 @@ serde_derive = "1" regex = "1" lazy_static = "1" lexical-sort = "0.3" +walkdir = "2" front_matter = { path = "../front_matter" } config = { path = "../config" } diff --git a/components/library/src/content/mod.rs b/components/library/src/content/mod.rs index 161a1ab9..377a8601 100644 --- a/components/library/src/content/mod.rs +++ b/components/library/src/content/mod.rs @@ -3,9 +3,10 @@ mod page; mod section; mod ser; -use std::fs::read_dir; use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + pub use self::file_info::FileInfo; pub use self::page::Page; pub use self::section::Section; @@ -32,7 +33,7 @@ pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool { pub fn find_related_assets(path: &Path, config: &Config) -> Vec { let mut assets = vec![]; - for entry in read_dir(path).unwrap().filter_map(std::result::Result::ok) { + for entry in WalkDir::new(path).into_iter().filter_map(std::result::Result::ok) { let entry_path = entry.path(); if entry_path.is_file() { match entry_path.extension() { @@ -46,18 +47,11 @@ pub fn find_related_assets(path: &Path, config: &Config) -> Vec { } if let Some(ref globset) = config.ignored_content_globset { - // `find_related_assets` only scans the immediate directory (it is not recursive) so our - // filtering only needs to work against the file_name component, not the full suffix. If - // `find_related_assets` was changed to also return files in subdirectories, we could - // use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter - // against the remaining path. Note that the current behaviour effectively means that - // the `ignored_content` setting in the config file is limited to single-file glob - // patterns (no "**" patterns). assets = assets .into_iter() - .filter(|path| match path.file_name() { - None => false, - Some(file) => !globset.is_match(file), + .filter(|p| match p.strip_prefix(path) { + Err(_) => false, + Ok(file) => !globset.is_match(file), }) .collect(); } @@ -68,7 +62,7 @@ pub fn find_related_assets(path: &Path, config: &Config) -> Vec { #[cfg(test)] mod tests { use super::*; - use std::fs::File; + use std::fs::{File, create_dir}; use config::Config; use tempfile::tempdir; @@ -76,17 +70,22 @@ mod tests { #[test] fn can_find_related_assets() { let tmp_dir = tempdir().expect("create temp dir"); - File::create(tmp_dir.path().join("index.md")).unwrap(); - File::create(tmp_dir.path().join("example.js")).unwrap(); - File::create(tmp_dir.path().join("graph.jpg")).unwrap(); - File::create(tmp_dir.path().join("fail.png")).unwrap(); + let path = tmp_dir.path(); + File::create(path.join("index.md")).unwrap(); + File::create(path.join("example.js")).unwrap(); + File::create(path.join("graph.jpg")).unwrap(); + File::create(path.join("fail.png")).unwrap(); + create_dir(path.join("subdir")).expect("create subdir temp dir"); + File::create(path.join("subdir").join("index.md")).unwrap(); + File::create(path.join("subdir").join("example.js")).unwrap(); - let assets = find_related_assets(tmp_dir.path(), &Config::default()); - assert_eq!(assets.len(), 3); - assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3); - assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1); - assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1); - assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1); + let assets = find_related_assets(path, &Config::default()); + assert_eq!(assets.len(), 4); + assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 4); + assert_eq!(assets.iter().filter(|p| p.strip_prefix(path).unwrap() == Path::new("example.js")).count(), 1); + assert_eq!(assets.iter().filter(|p| p.strip_prefix(path).unwrap() == Path::new("graph.jpg")).count(), 1); + assert_eq!(assets.iter().filter(|p| p.strip_prefix(path).unwrap() == Path::new("fail.png")).count(), 1); + assert_eq!(assets.iter().filter(|p| p.strip_prefix(path).unwrap() == Path::new("subdir/example.js")).count(), 1); } #[test] diff --git a/components/library/src/content/page.rs b/components/library/src/content/page.rs index c8bcd0aa..ab0f1cf5 100644 --- a/components/library/src/content/page.rs +++ b/components/library/src/content/page.rs @@ -276,7 +276,7 @@ impl Page { fn serialize_assets(&self, base_path: &Path) -> Vec { self.assets .iter() - .filter_map(|asset| asset.file_name()) + .filter_map(|asset| asset.strip_prefix(&self.file.path.parent().unwrap()).ok()) .filter_map(|filename| filename.to_str()) .map(|filename| { let mut path = self.file.path.clone(); diff --git a/components/library/src/content/section.rs b/components/library/src/content/section.rs index 5f9e93bf..4aa3c05a 100644 --- a/components/library/src/content/section.rs +++ b/components/library/src/content/section.rs @@ -195,7 +195,7 @@ impl Section { fn serialize_assets(&self) -> Vec { self.assets .iter() - .filter_map(|asset| asset.file_name()) + .filter_map(|asset| asset.strip_prefix(&self.file.path.parent().unwrap()).ok()) .filter_map(|filename| filename.to_str()) .map(|filename| format!("{}{}", self.path, filename)) .collect() diff --git a/components/rendering/src/markdown.rs b/components/rendering/src/markdown.rs index c0078e75..3605a6d3 100644 --- a/components/rendering/src/markdown.rs +++ b/components/rendering/src/markdown.rs @@ -74,13 +74,6 @@ fn starts_with_schema(s: &str) -> bool { PATTERN.is_match(s) } -/// Colocated asset links refers to the files in the same directory, -/// there it should be a filename only -fn is_colocated_asset_link(link: &str) -> bool { - !link.contains('/') // http://, ftp://, ../ etc - && !starts_with_schema(link) -} - /// Returns whether a link starts with an HTTP(s) scheme. fn is_external_link(link: &str) -> bool { link.starts_with("http:") || link.starts_with("https:") @@ -111,8 +104,6 @@ fn fix_link( return Err(format!("Relative link {} not found.", link).into()); } } - } else if is_colocated_asset_link(link) { - format!("{}{}", context.current_page_permalink, link) } else { if is_external_link(link) { external_links.push(link.to_owned()); @@ -222,14 +213,6 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result\n".into()) } - Event::Start(Tag::Image(link_type, src, title)) => { - if is_colocated_asset_link(&src) { - let link = format!("{}{}", context.current_page_permalink, &*src); - return Event::Start(Tag::Image(link_type, link.into(), title)); - } - - Event::Start(Tag::Image(link_type, src, title)) - } Event::Start(Tag::Link(link_type, link, title)) if link.is_empty() => { error = Some(Error::msg("There is a link that is missing a URL")); Event::Start(Tag::Link(link_type, "#".into(), title)) diff --git a/components/rendering/tests/markdown.rs b/components/rendering/tests/markdown.rs index e955e0dc..164e3b7f 100644 --- a/components/rendering/tests/markdown.rs +++ b/components/rendering/tests/markdown.rs @@ -998,7 +998,7 @@ fn can_make_permalinks_with_colocated_assets_for_link() { InsertAnchor::None, ); let res = render_content("[an image](image.jpg)", &context).unwrap(); - assert_eq!(res.body, "

an image

\n"); + assert_eq!(res.body, "

an image

\n"); } #[test] @@ -1016,7 +1016,7 @@ fn can_make_permalinks_with_colocated_assets_for_image() { let res = render_content("![alt text](image.jpg)", &context).unwrap(); assert_eq!( res.body, - "

\"alt

\n" + "

\"alt

\n" ); } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 040a6b1f..17d0059f 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -595,7 +595,7 @@ impl Site { self.copy_asset( asset_path, ¤t_path - .join(asset_path.file_name().expect("Couldn't get filename from page asset")), + .join(asset_path.strip_prefix(&page.file.path.parent().unwrap()).expect("Couldn't get filename from page asset")), )?; } @@ -988,7 +988,7 @@ impl Site { self.copy_asset( asset_path, &output_path.join( - asset_path.file_name().expect("Failed to get asset filename for section"), + asset_path.strip_prefix(§ion.file.path.parent().unwrap()).expect("Failed to get asset filename for section"), ), )?; } From b503d5cc86544ebebea631a8892fdfe560ffdd49 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 4 Sep 2021 16:25:03 +1000 Subject: [PATCH 07/30] feat: add required argument to taxonomy functions (#1598) --- .../templates/src/global_fns/content.rs | 26 ++++++++++++++----- .../documentation/templates/overview.md | 4 +++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/components/templates/src/global_fns/content.rs b/components/templates/src/global_fns/content.rs index 952cb54e..fafb2262 100644 --- a/components/templates/src/global_fns/content.rs +++ b/components/templates/src/global_fns/content.rs @@ -40,10 +40,17 @@ impl TeraFn for GetTaxonomyUrl { let lang = optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string") .unwrap_or_else(|| self.default_lang.clone()); + let required = optional_arg!( + bool, + args.get("required"), + "`get_taxonomy_url`: `required` must be a boolean (true or false)" + ) + .unwrap_or(true); - let container = match self.taxonomies.get(&format!("{}-{}", kind, lang)) { - Some(c) => c, - None => { + let container = match (self.taxonomies.get(&format!("{}-{}", kind, lang)), required) { + (Some(c), _) => c, + (None, false) => return Ok(Value::Null), + (None, true) => { return Err(format!( "`get_taxonomy_url` received an unknown taxonomy as kind: {}", kind @@ -154,14 +161,21 @@ impl TeraFn for GetTaxonomy { args.get("kind"), "`get_taxonomy` requires a `kind` argument with a string value" ); + let required = optional_arg!( + bool, + args.get("required"), + "`get_taxonomy`: `required` must be a boolean (true or false)" + ) + .unwrap_or(true); let lang = optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string") .unwrap_or_else(|| self.default_lang.clone()); - match self.taxonomies.get(&format!("{}-{}", kind, lang)) { - Some(t) => Ok(to_value(t.to_serialized(&self.library.read().unwrap())).unwrap()), - None => { + match (self.taxonomies.get(&format!("{}-{}", kind, lang)), required) { + (Some(t), _) => Ok(to_value(t.to_serialized(&self.library.read().unwrap())).unwrap()), + (None, false) => Ok(Value::Null), + (None, true) => { Err(format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into()) } } diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md index 5cfeeac2..a16a0752 100644 --- a/docs/content/documentation/templates/overview.md +++ b/docs/content/documentation/templates/overview.md @@ -156,6 +156,8 @@ the value should be the same as the one in the front matter, not the slugified v `lang` (optional) default to `config.default_language` in config.toml +`required` (optional) if a taxonomy is defined but there isn't any content that uses it then throw an error. Defaults to true. + ### `get_taxonomy` Gets the whole taxonomy of a specific kind. @@ -172,6 +174,8 @@ items: Array; `lang` (optional) default to `config.default_language` in config.toml +`required` (optional) if a taxonomy is defined but there isn't any content that uses it then throw an error. Defaults to true. + See the [Taxonomies documentation](@/documentation/templates/taxonomies.md) for a full documentation of those types. ### `get_url` From f0b131838fee5edfb7441ec14a9691e9340831c9 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 11 Sep 2021 17:31:34 +1000 Subject: [PATCH 08/30] fix: crash on config change (#1616) --- Cargo.lock | 1 + Cargo.toml | 1 + src/cmd/serve.rs | 38 +++++++++++++++++++------------------- src/main.rs | 4 +++- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7615a68f..96a1c806 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3438,6 +3438,7 @@ dependencies = [ "mime_guess", "notify", "open", + "pathdiff", "percent-encoding", "relative-path", "same-file", diff --git a/Cargo.toml b/Cargo.toml index a737e008..8175ddda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ ctrlc = "3" open = "2" globset = "0.4" relative-path = "1" +pathdiff = "0.2" serde_json = "1.0" # For mimetype detection in serve mode mime_guess = "2.0" diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 8591615f..f08f57fe 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -40,6 +40,7 @@ use ws::{Message, Sender, WebSocket}; use errors::{Error as ZolaError, Result}; use globset::GlobSet; +use pathdiff::diff_paths; use relative_path::{RelativePath, RelativePathBuf}; use site::sass::compile_sass; use site::{Site, SITE_CONTENT}; @@ -300,12 +301,14 @@ pub fn serve( return Err(format!("Cannot start server on address {}.", address).into()); } - let config_path = config_file.to_str().unwrap_or("config.toml"); + let config_path = PathBuf::from(config_file); + let config_path_rel = diff_paths(&config_path, &root_dir).unwrap_or(config_path.clone()); - // An array of (path, bool, bool) where the path should be watched for changes, and the boolean value - // indicates whether this file/folder must exist for zola serve to operate + // An array of (path, WatchMode) where the path should be watched for changes, + // and the WatchMode value indicates whether this file/folder must exist for + // zola serve to operate let watch_this = vec![ - (config_path, WatchMode::Required), + (config_path_rel.to_str().unwrap_or("config.toml"), WatchMode::Required), ("content", WatchMode::Required), ("sass", WatchMode::Condition(site.config.compile_sass)), ("static", WatchMode::Optional), @@ -518,7 +521,7 @@ pub fn serve( ); let start = Instant::now(); - match detect_change_kind(root_dir, &path, config_path) { + match detect_change_kind(root_dir, &path, &config_path) { (ChangeKind::Content, _) => { console::info(&format!("-> Content changed {}", path.display())); @@ -644,13 +647,10 @@ fn is_temp_file(path: &Path) -> bool { /// Detect what changed from the given path so we have an idea what needs /// to be reloaded -fn detect_change_kind(pwd: &Path, path: &Path, config_filename: &str) -> (ChangeKind, PathBuf) { +fn detect_change_kind(pwd: &Path, path: &Path, config_path: &Path) -> (ChangeKind, PathBuf) { let mut partial_path = PathBuf::from("/"); partial_path.push(path.strip_prefix(pwd).unwrap_or(path)); - let mut partial_config_path = PathBuf::from("/"); - partial_config_path.push(config_filename); - let change_kind = if partial_path.starts_with("/templates") { ChangeKind::Templates } else if partial_path.starts_with("/themes") { @@ -661,7 +661,7 @@ fn detect_change_kind(pwd: &Path, path: &Path, config_filename: &str) -> (Change ChangeKind::StaticFiles } else if partial_path.starts_with("/sass") { ChangeKind::Sass - } else if partial_path == partial_config_path { + } else if path == config_path { ChangeKind::Config } else { unreachable!("Got a change in an unexpected path: {}", partial_path.display()); @@ -709,43 +709,43 @@ mod tests { (ChangeKind::Templates, PathBuf::from("/templates/hello.html")), Path::new("/home/vincent/site"), Path::new("/home/vincent/site/templates/hello.html"), - "config.toml", + Path::new("/home/vincent/site/config.toml"), ), ( (ChangeKind::Themes, PathBuf::from("/themes/hello.html")), Path::new("/home/vincent/site"), Path::new("/home/vincent/site/themes/hello.html"), - "config.toml", + Path::new("/home/vincent/site/config.toml"), ), ( (ChangeKind::StaticFiles, PathBuf::from("/static/site.css")), Path::new("/home/vincent/site"), Path::new("/home/vincent/site/static/site.css"), - "config.toml", + Path::new("/home/vincent/site/config.toml"), ), ( (ChangeKind::Content, PathBuf::from("/content/posts/hello.md")), Path::new("/home/vincent/site"), Path::new("/home/vincent/site/content/posts/hello.md"), - "config.toml", + Path::new("/home/vincent/site/config.toml"), ), ( (ChangeKind::Sass, PathBuf::from("/sass/print.scss")), Path::new("/home/vincent/site"), Path::new("/home/vincent/site/sass/print.scss"), - "config.toml", + Path::new("/home/vincent/site/config.toml"), ), ( (ChangeKind::Config, PathBuf::from("/config.toml")), Path::new("/home/vincent/site"), Path::new("/home/vincent/site/config.toml"), - "config.toml", + Path::new("/home/vincent/site/config.toml"), ), ( (ChangeKind::Config, PathBuf::from("/config.staging.toml")), Path::new("/home/vincent/site"), Path::new("/home/vincent/site/config.staging.toml"), - "config.staging.toml", + Path::new("/home/vincent/site/config.staging.toml"), ), ]; @@ -760,7 +760,7 @@ mod tests { let expected = (ChangeKind::Templates, PathBuf::from("/templates/hello.html")); let pwd = Path::new(r#"C:\\Users\johan\site"#); let path = Path::new(r#"C:\\Users\johan\site\templates\hello.html"#); - let config_filename = "config.toml"; + let config_filename = Path::new(r#"C:\\Users\johan\site\config.toml"#); assert_eq!(expected, detect_change_kind(pwd, path, config_filename)); } @@ -769,7 +769,7 @@ mod tests { let expected = (ChangeKind::Templates, PathBuf::from("/templates/hello.html")); let pwd = Path::new("/home/johan/site"); let path = Path::new("templates/hello.html"); - let config_filename = "config.toml"; + let config_filename = Path::new("config.toml"); assert_eq!(expected, detect_change_kind(pwd, path, config_filename)); } } diff --git a/src/main.rs b/src/main.rs index 925d36d3..00991967 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,9 @@ fn main() { .unwrap_or_else(|_| panic!("Cannot find root directory: {}", path)), }; let config_file = match matches.value_of("config") { - Some(path) => PathBuf::from(path), + Some(path) => PathBuf::from(path) + .canonicalize() + .unwrap_or_else(|_| panic!("Cannot find config file: {}", path)), None => root_dir.join("config.toml"), }; From 23064f57c8d45534bdf4d757f4e8263415f35e3d Mon Sep 17 00:00:00 2001 From: David Date: Mon, 13 Sep 2021 20:08:48 +0100 Subject: [PATCH 09/30] Support custom syntax highlighting themes (#1499) Related to #419 Gruvbox tmTheme added to test_site, it is taken from https://github.com/Colorsublime/Colorsublime-Themes (MIT licensed) --- components/config/src/config/markup.rs | 99 ++++- components/config/src/config/mod.rs | 22 +- components/config/src/highlighting.rs | 17 +- components/rendering/benches/all.rs | 15 +- .../rendering/src/codeblock/highlight.rs | 2 +- components/site/src/lib.rs | 5 +- .../content/syntax-highlighting.md | 41 +- .../getting-started/configuration.md | 3 + test_site/config.toml | 3 +- test_site/content/posts/extra_syntax.md | 6 + .../highlight_themes/custom_gruvbox.tmTheme | 394 ++++++++++++++++++ 11 files changed, 556 insertions(+), 51 deletions(-) create mode 100644 test_site/highlight_themes/custom_gruvbox.tmTheme diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index b6fa0e2d..5396b2db 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -1,9 +1,15 @@ -use std::path::Path; +use std::{path::Path, sync::Arc}; use serde_derive::{Deserialize, Serialize}; -use syntect::parsing::{SyntaxSet, SyntaxSetBuilder}; +use syntect::{ + highlighting::{Theme, ThemeSet}, + html::css_for_theme_with_class_style, + parsing::{SyntaxSet, SyntaxSetBuilder}, +}; -use errors::Result; +use errors::{bail, Result}; + +use crate::highlighting::{CLASS_STYLE, THEME_SET}; pub const DEFAULT_HIGHLIGHT_THEME: &str = "base16-ocean-dark"; @@ -43,26 +49,92 @@ pub struct Markdown { pub external_links_no_referrer: bool, /// Whether smart punctuation is enabled (changing quotes, dashes, dots etc in their typographic form) pub smart_punctuation: bool, - - /// A list of directories to search for additional `.sublime-syntax` files in. - pub extra_syntaxes: Vec, + /// A list of directories to search for additional `.sublime-syntax` and `.tmTheme` files in. + pub extra_syntaxes_and_themes: 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, + /// The compiled extra themes into a theme set + #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need + pub extra_theme_set: Arc>, } impl Markdown { - /// Attempt to load any extra syntax found in the extra syntaxes of the config - pub fn load_extra_syntaxes(&mut self, base_path: &Path) -> Result<()> { - if self.extra_syntaxes.is_empty() { - return Ok(()); + /// Gets the configured highlight theme from the THEME_SET or the config's extra_theme_set + /// Returns None if the configured highlighting theme is set to use css + pub fn get_highlight_theme(&self) -> Option<&Theme> { + if self.highlight_theme == "css" { + None + } else { + Some(self.get_highlight_theme_by_name(&self.highlight_theme)) + } + } + + /// Gets an arbitrary theme from the THEME_SET or the extra_theme_set + pub fn get_highlight_theme_by_name<'config>(&'config self, theme_name: &str) -> &'config Theme { + (*self.extra_theme_set) + .as_ref() + .and_then(|ts| ts.themes.get(theme_name)) + .unwrap_or_else(|| &THEME_SET.themes[theme_name]) + } + + /// Attempt to load any extra syntaxes and themes found in the extra_syntaxes_and_themes folders + pub fn load_extra_syntaxes_and_highlight_themes( + &self, + base_path: &Path, + ) -> Result<(Option, Option)> { + if self.extra_syntaxes_and_themes.is_empty() { + return Ok((None, None)); } let mut ss = SyntaxSetBuilder::new(); - for dir in &self.extra_syntaxes { + let mut ts = ThemeSet::new(); + for dir in &self.extra_syntaxes_and_themes { ss.add_from_folder(base_path.join(dir), true)?; + ts.add_from_folder(base_path.join(dir))?; + } + let ss = ss.build(); + + Ok(( + if ss.syntaxes().is_empty() { None } else { Some(ss) }, + if ts.themes.is_empty() { None } else { Some(ts) }, + )) + } + + pub fn export_theme_css(&self, theme_name: &str) -> String { + let theme = self.get_highlight_theme_by_name(theme_name); + css_for_theme_with_class_style(theme, CLASS_STYLE) + } + + pub fn init_extra_syntaxes_and_highlight_themes(&mut self, path: &Path) -> Result<()> { + if self.highlight_theme == "css" { + return Ok(()); + } + + let (loaded_extra_syntaxes, loaded_extra_highlight_themes) = + self.load_extra_syntaxes_and_highlight_themes(path)?; + + if let Some(extra_syntax_set) = loaded_extra_syntaxes { + self.extra_syntax_set = Some(extra_syntax_set); + } + if let Some(extra_theme_set) = loaded_extra_highlight_themes { + self.extra_theme_set = Arc::new(Some(extra_theme_set)); + } + + // validate that the chosen highlight_theme exists in the loaded highlight theme sets + if !THEME_SET.themes.contains_key(&self.highlight_theme) { + if let Some(extra) = &*self.extra_theme_set { + if !extra.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_syntaxes_and_themes` to include a list of folders containing '.tmTheme' files", self.highlight_theme) + } } - self.extra_syntax_set = Some(ss.build()); Ok(()) } @@ -110,8 +182,9 @@ impl Default for Markdown { external_links_no_follow: false, external_links_no_referrer: false, smart_punctuation: false, - extra_syntaxes: Vec::new(), + extra_syntaxes_and_themes: vec![], extra_syntax_set: None, + extra_theme_set: Arc::new(None), } } } diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index 37eba3e4..4f6d5bb9 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -12,7 +12,6 @@ use globset::{Glob, GlobSet, GlobSetBuilder}; use serde_derive::{Deserialize, Serialize}; use toml::Value as Toml; -use crate::highlighting::THEME_SET; use crate::theme::Theme; use errors::{bail, Error, Result}; use utils::fs::read_file; @@ -106,6 +105,7 @@ pub struct SerializedConfig<'a> { } impl Config { + // any extra syntax and highlight themes have been loaded and validated already by the from_file method before parsing the config /// Parses a string containing TOML to our Config struct /// Any extra parameter will end up in the extra field pub fn parse(content: &str) -> Result { @@ -118,15 +118,6 @@ impl Config { bail!("A base URL is required in config.toml with key `base_url`"); } - if config.markdown.highlight_theme != "css" - && !THEME_SET.themes.contains_key(&config.markdown.highlight_theme) - { - bail!( - "Highlight theme {} defined in config does not exist.", - config.markdown.highlight_theme - ); - } - languages::validate_code(&config.default_language)?; for code in config.languages.keys() { languages::validate_code(code)?; @@ -166,7 +157,16 @@ impl Config { let path = path.as_ref(); let content = read_file(path).map_err(|e| errors::Error::chain("Failed to load config", e))?; - Config::parse(&content) + + let mut config = Config::parse(&content)?; + let config_dir = path + .parent() + .ok_or(Error::msg("Failed to find directory containing the config file."))?; + + // this is the step at which missing extra syntax and highlighting themes are raised as errors + config.markdown.init_extra_syntaxes_and_highlight_themes(config_dir)?; + + Ok(config) } /// 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 08cbe85e..40e778a3 100644 --- a/components/config/src/highlighting.rs +++ b/components/config/src/highlighting.rs @@ -1,10 +1,12 @@ use lazy_static::lazy_static; use syntect::dumps::from_binary; use syntect::highlighting::{Theme, ThemeSet}; +use syntect::html::ClassStyle; use syntect::parsing::{SyntaxReference, SyntaxSet}; use crate::config::Config; -use syntect::html::{css_for_theme_with_class_style, ClassStyle}; + +pub const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "z-" }; lazy_static! { pub static ref SYNTAX_SET: SyntaxSet = { @@ -16,8 +18,6 @@ lazy_static! { from_binary(include_bytes!("../../../sublime/themes/all.themedump")); } -pub const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "z-" }; - #[derive(Clone, Debug, PartialEq, Eq)] pub enum HighlightSource { /// One of the built-in Zola syntaxes @@ -42,11 +42,7 @@ pub fn resolve_syntax_and_theme<'config>( language: Option<&'_ str>, config: &'config Config, ) -> SyntaxAndTheme<'config> { - let theme = if config.markdown.highlight_theme != "css" { - Some(&THEME_SET.themes[&config.markdown.highlight_theme]) - } else { - None - }; + let theme = config.markdown.get_highlight_theme(); if let Some(ref lang) = language { if let Some(ref extra_syntaxes) = config.markdown.extra_syntax_set { @@ -88,8 +84,3 @@ pub fn resolve_syntax_and_theme<'config>( } } } - -pub fn export_theme_css(theme_name: &str) -> String { - let theme = &THEME_SET.themes[theme_name]; - css_for_theme_with_class_style(theme, CLASS_STYLE) -} diff --git a/components/rendering/benches/all.rs b/components/rendering/benches/all.rs index dcf080d0..ec5afbe9 100644 --- a/components/rendering/benches/all.rs +++ b/components/rendering/benches/all.rs @@ -106,10 +106,11 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) { let mut config = Config::default(); config.markdown.highlight_code = false; let current_page_permalink = ""; + let lang = ""; let context = RenderContext::new( &tera, &config, - "", + lang, current_page_permalink, &permalinks_ctx, InsertAnchor::None, @@ -117,7 +118,6 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) { b.iter(|| render_content(CONTENT, &context).unwrap()); } -#[bench] fn bench_render_content_no_shortcode(b: &mut test::Bencher) { let tera = Tera::default(); let content2 = CONTENT.replace(r#"{{ youtube(id="my_youtube_id") }}"#, ""); @@ -125,10 +125,11 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) { config.markdown.highlight_code = false; let permalinks_ctx = HashMap::new(); let current_page_permalink = ""; + let lang = ""; let context = RenderContext::new( &tera, &config, - "", + lang, current_page_permalink, &permalinks_ctx, InsertAnchor::None, @@ -144,16 +145,15 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) { let config = Config::default(); let permalinks_ctx = HashMap::new(); let current_page_permalink = ""; + let lang = ""; let context = RenderContext::new( &tera, &config, - "", + lang, current_page_permalink, &permalinks_ctx, InsertAnchor::None, ); - - b.iter(|| render_shortcodes(CONTENT, &context)); } #[bench] @@ -165,10 +165,11 @@ fn bench_render_content_no_shortcode_with_emoji(b: &mut test::Bencher) { config.markdown.render_emoji = true; let permalinks_ctx = HashMap::new(); let current_page_permalink = ""; + let lang = ""; let context = RenderContext::new( &tera, &config, - "", + lang, current_page_permalink, &permalinks_ctx, InsertAnchor::None, diff --git a/components/rendering/src/codeblock/highlight.rs b/components/rendering/src/codeblock/highlight.rs index bdf4e8ff..4d8048b5 100644 --- a/components/rendering/src/codeblock/highlight.rs +++ b/components/rendering/src/codeblock/highlight.rs @@ -26,7 +26,7 @@ pub(crate) struct ClassHighlighter<'config> { } impl<'config> ClassHighlighter<'config> { - pub fn new(syntax: &'config SyntaxReference, syntax_set: &'config SyntaxSet) -> Self { + pub fn new(syntax: &SyntaxReference, syntax_set: &'config SyntaxSet) -> Self { let parse_state = ParseState::new(syntax); Self { syntax_set, open_spans: 0, parse_state, scope_stack: ScopeStack::new() } } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 17d0059f..e86cd166 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -14,7 +14,6 @@ use rayon::prelude::*; use tera::{Context, Tera}; use walkdir::{DirEntry, WalkDir}; -use config::highlighting::export_theme_css; use config::{get_config, Config}; use errors::{bail, Error, Result}; use front_matter::InsertAnchor; @@ -74,7 +73,7 @@ impl Site { let path = path.as_ref(); let config_file = config_file.as_ref(); let mut config = get_config(config_file)?; - config.markdown.load_extra_syntaxes(path)?; + config.markdown.load_extra_syntaxes_and_highlight_themes(path)?; if let Some(theme) = config.theme.clone() { // Grab data from the extra section of the theme @@ -691,7 +690,7 @@ impl Site { for t in &self.config.markdown.highlight_themes_css { let p = self.static_path.join(&t.filename); if !p.exists() { - let content = export_theme_css(&t.theme); + let content = &self.config.markdown.export_theme_css(&t.theme); create_file(&p, &content)?; } } diff --git a/docs/content/documentation/content/syntax-highlighting.md b/docs/content/documentation/content/syntax-highlighting.md index 3eadfd21..40301b9a 100644 --- a/docs/content/documentation/content/syntax-highlighting.md +++ b/docs/content/documentation/content/syntax-highlighting.md @@ -150,7 +150,7 @@ Here is a full list of supported languages and their short names: Note: due to some issues with the JavaScript syntax, the TypeScript syntax will be used instead. If you want to highlight a language not on this list, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola). -Alternatively, the `extra_syntaxes` configuration option can be used to add additional syntax files. +Alternatively, the `extra_syntaxes_and_themes` configuration option can be used to add additional syntax (and theme) files. If your site source is laid out as follows: @@ -169,7 +169,7 @@ If your site source is laid out as follows: └── ... ``` -you would set your `extra_syntaxes` to `["syntaxes", "syntaxes/Sublime-Language1"]` to load `lang1.sublime-syntax` and `lang2.sublime-syntax`. +you would set your `extra_syntaxes_and_themes` to `["syntaxes", "syntaxes/Sublime-Language1"]` to load `lang1.sublime-syntax` and `lang2.sublime-syntax`. ## Inline VS classed highlighting @@ -347,3 +347,40 @@ Line 2 and 7 are comments that are not shown in the final output. When line numbers are active, the code block is turned into a table with one row and two cells. The first cell contains the line number and the second cell contains the code. Highlights are done via the `` HTML tag. When a line with line number is highlighted two `` tags are created: one around the line number(s) and one around the code. + +## Custom Highlighting Themes + +The default *theme* for syntax highlighting is called `base16-ocean-dark`, you can choose another theme from the built in set of highlight themes using the `highlight_theme` configuration option. +For example, this documentation site currently uses the `kronuz` theme, which is built in. + +``` +[markdown] +highlight_code = true +highlight_theme = "kronuz" +``` + +Alternatively, the `extra_syntaxes_and_themes` configuration option can be used to add additional theme files. +You can load your own highlight theme from a TextMate `.tmTheme` file. + +It works the same way as adding extra syntaxes. It should contain a list of paths to folders containing the .tmTheme files you want to include. +You would then set `highlight_theme` to the name of one of these files, without the `.tmTheme` extension. + +If your site source is laid out as follows: + +``` +. +├── config.toml +├── content/ +│   └── ... +├── static/ +│   └── ... +├── highlight_themes/ +│   ├── MyGroovyTheme/ +│   │   └── theme1.tmTheme +│   ├── theme2.tmTheme +└── templates/ + └── ... +``` + +you would set your `extra_highlight_themes` to `["highlight_themes", "highlight_themes/MyGroovyTheme"]` to load `theme1.tmTheme` and `theme2.tmTheme`. +Then choose one of them to use, say theme1, by setting `highlight_theme = theme1`. diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index 8b4bd034..ff5cf753 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -236,6 +236,9 @@ Zola currently has the following highlight themes available: Zola uses the Sublime Text themes, making it very easy to add more. If you want a theme not listed above, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola). +Alternatively you can use the `extra_syntaxes_and_themes` configuration option to load your own custom themes from a .tmTheme file. +See [Syntax Highlighting](@/syntax-highlighting.md) for more details. + ## Slugification strategies By default, Zola will turn every path, taxonomies and anchors to a slug, an ASCII representation with no special characters. diff --git a/test_site/config.toml b/test_site/config.toml index cf989d60..7093a779 100644 --- a/test_site/config.toml +++ b/test_site/config.toml @@ -13,7 +13,8 @@ ignored_content = ["*/ignored.md"] [markdown] highlight_code = true -extra_syntaxes = ["syntaxes"] +highlight_theme = "custom_gruvbox" +extra_syntaxes_and_themes = ["syntaxes", "highlight_themes"] [slugify] paths = "on" diff --git a/test_site/content/posts/extra_syntax.md b/test_site/content/posts/extra_syntax.md index dfdc5afe..eabdadd1 100644 --- a/test_site/content/posts/extra_syntax.md +++ b/test_site/content/posts/extra_syntax.md @@ -10,6 +10,12 @@ for (int i = 0; ; i++ ) { } ``` +``` +for (int i = 0; ; i++ ) { + if (i < 10) +} +``` + ```c for (int i = 0; ; i++ ) { if (i < 10) diff --git a/test_site/highlight_themes/custom_gruvbox.tmTheme b/test_site/highlight_themes/custom_gruvbox.tmTheme new file mode 100644 index 00000000..94c0c092 --- /dev/null +++ b/test_site/highlight_themes/custom_gruvbox.tmTheme @@ -0,0 +1,394 @@ + + + + + name + Gruvbox-N + settings + + + settings + + background + #1a1a1a + 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 From 2e884e06cb5c9e9652450ffc9caafe91ea0d3e88 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 13 Sep 2021 21:13:51 +0200 Subject: [PATCH 10/30] Remove duplicate call to load syntax/themes --- components/config/src/config/markup.rs | 2 +- components/site/src/lib.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index 5396b2db..f92b8ed2 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -71,7 +71,7 @@ impl Markdown { } /// Gets an arbitrary theme from the THEME_SET or the extra_theme_set - pub fn get_highlight_theme_by_name<'config>(&'config self, theme_name: &str) -> &'config Theme { + pub fn get_highlight_theme_by_name(&self, theme_name: &str) -> &Theme { (*self.extra_theme_set) .as_ref() .and_then(|ts| ts.themes.get(theme_name)) diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index e86cd166..10c8a05c 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.markdown.load_extra_syntaxes_and_highlight_themes(path)?; if let Some(theme) = config.theme.clone() { // Grab data from the extra section of the theme From 7a0cf261bf0891baf00edb1ee95aa0d6419c2e8e Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 13 Sep 2021 21:16:55 +0200 Subject: [PATCH 11/30] Update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 343dacd8..d5abe608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ # Changelog -## 0.15.0 +## 0.15.0 (unreleased) +- Fix config file watching +- Support custom syntax highlighting themes +- Add a `required` argument to taxonomy template functions to allow them to return empty taxonomies +- Support colocating subfolders +- shorcodes and `anchor-link.html` can now access the `lang` context +- Add prompt before replacing the output directory with `zola build` if the `output-dir` flag is given ## 0.14.1 (2021-08-24) From aaa25cee2af2ec0485796bf77b11ee311722d0de Mon Sep 17 00:00:00 2001 From: Miguel de Moura <9093796+migueldemoura@users.noreply.github.com> Date: Sat, 2 Oct 2021 10:07:04 +0100 Subject: [PATCH 12/30] Fix clippy warnings (#1618) Fixes clippy warnings for the `needless_borrow` lint. https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow Removes the unused `starts_with_schema` function (dead code since 4086b075). --- .../rendering/src/codeblock/highlight.rs | 2 +- components/rendering/src/markdown.rs | 35 ------------------- .../templates/src/global_fns/load_data.rs | 10 ++++-- 3 files changed, 9 insertions(+), 38 deletions(-) diff --git a/components/rendering/src/codeblock/highlight.rs b/components/rendering/src/codeblock/highlight.rs index 4d8048b5..1262e26f 100644 --- a/components/rendering/src/codeblock/highlight.rs +++ b/components/rendering/src/codeblock/highlight.rs @@ -236,7 +236,7 @@ mod tests { let syntax_and_theme = resolve_syntax_and_theme(Some("py"), &config); let mut highlighter = SyntaxHighlighter::new(false, syntax_and_theme); let mut out = String::new(); - for line in LinesWithEndings::from(&code) { + for line in LinesWithEndings::from(code) { out.push_str(&highlighter.highlight_line(line)); } assert!(!out.contains("