From 8d16d1701ce7a8ec38508326bd7889f2cd8dc202 Mon Sep 17 00:00:00 2001
From: Vincent Prouillet
Date: Sun, 19 Mar 2023 20:37:13 +0100
Subject: [PATCH 01/12] Next version
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdb28810d3..9891777789 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
# Changelog
+## 0.18.0 (unreleased)
+
## 0.17.2 (2023-03-19)
- Fix one more invalid error with colocated directories
From b00ae4261bfb2d613583d51a1ce877b157c8740b Mon Sep 17 00:00:00 2001
From: Ever
Date: Fri, 7 Apr 2023 03:28:20 +0800
Subject: [PATCH 02/12] print error message when no config file found (#2168)
fixed #2195
---
src/main.rs | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 6f3fcdfeb7..983f7b2aa7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use std::time::Instant;
use cli::{Cli, Command};
+use errors::anyhow;
use utils::net::{get_available_port, port_is_available};
use clap::{CommandFactory, Parser};
@@ -13,10 +14,17 @@ mod messages;
mod prompt;
fn get_config_file_path(dir: &Path, config_path: &Path) -> (PathBuf, PathBuf) {
- let root_dir = dir
- .ancestors()
- .find(|a| a.join(config_path).exists())
- .unwrap_or_else(|| panic!("could not find directory containing config file"));
+ let root_dir = dir.ancestors().find(|a| a.join(config_path).exists()).unwrap_or_else(|| {
+ messages::unravel_errors(
+ "",
+ &anyhow!(
+ "{} not found in current directory or ancestors, current_dir is {}",
+ config_path.display(),
+ dir.display()
+ ),
+ );
+ std::process::exit(1);
+ });
// if we got here we found root_dir so config file should exist so we can unwrap safely
let config_file = root_dir
From 1ed722c076e801c7ec3d62209d40315b4353b811 Mon Sep 17 00:00:00 2001
From: Marcel
Date: Thu, 6 Apr 2023 23:21:01 +0200
Subject: [PATCH 03/12] Speedup "zola check" command by reusing the Client
(#2171)
* Reuse Client when checking urls and add timeout for requests
---
components/link_checker/src/lib.rs | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/components/link_checker/src/lib.rs b/components/link_checker/src/lib.rs
index 3b3551afd6..ab44259217 100644
--- a/components/link_checker/src/lib.rs
+++ b/components/link_checker/src/lib.rs
@@ -30,6 +30,13 @@ pub fn message(res: &Result) -> String {
// Keep history of link checks so a rebuild doesn't have to check again
static LINKS: Lazy>>> =
Lazy::new(|| Arc::new(RwLock::new(HashMap::new())));
+// Make sure to create only a single Client so that we can reuse the connections
+static CLIENT: Lazy = Lazy::new(|| {
+ Client::builder()
+ .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
+ .build()
+ .expect("reqwest client build")
+});
pub fn check_url(url: &str, config: &LinkChecker) -> Result {
{
@@ -44,15 +51,11 @@ pub fn check_url(url: &str, config: &LinkChecker) -> Result {
headers.append(ACCEPT, "*/*".parse().unwrap());
// TODO: pass the client to the check_url, do not pass the config
- let client = Client::builder()
- .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
- .build()
- .expect("reqwest client build");
let check_anchor = !config.skip_anchor_prefixes.iter().any(|prefix| url.starts_with(prefix));
// Need to actually do the link checking
- let res = match client.get(url).headers(headers).send() {
+ let res = match CLIENT.get(url).headers(headers).send() {
Ok(ref mut response) if check_anchor && has_anchor(url) => {
let body = {
let mut buf: Vec = vec![];
From 6a5c241152fead78bc6f08db18739d4aa0070ed9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=8F=E2=80=8FDave?=
<47663767+cydave@users.noreply.github.com>
Date: Thu, 20 Apr 2023 17:44:19 +0000
Subject: [PATCH 04/12] Implement replace_re filter (#2163)
* Implement replace_re filter
* Cargo fmt
* add regex caching
* cargo fmt
* update docs, update unit test
* rename replace_re -> regex_replace
---
components/templates/src/filters.rs | 70 ++++++++++++++++++-
components/templates/src/lib.rs | 1 +
.../documentation/templates/overview.md | 8 +++
3 files changed, 78 insertions(+), 1 deletion(-)
diff --git a/components/templates/src/filters.rs b/components/templates/src/filters.rs
index b9b53d3767..432f2b3d22 100644
--- a/components/templates/src/filters.rs
+++ b/components/templates/src/filters.rs
@@ -1,9 +1,12 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::hash::BuildHasher;
+use std::sync::{Arc, Mutex};
use config::Config;
+
use libs::base64::engine::{general_purpose::STANDARD as standard_b64, Engine};
+use libs::regex::Regex;
use libs::tera::{
to_value, try_get_value, Error as TeraError, Filter as TeraFilter, Result as TeraResult, Tera,
Value,
@@ -78,6 +81,53 @@ pub fn base64_decode(
Ok(to_value(as_str).unwrap())
}
+#[derive(Debug)]
+pub struct RegexReplaceFilter {
+ re_cache: Arc>>,
+}
+
+impl RegexReplaceFilter {
+ pub fn new() -> Self {
+ return Self { re_cache: Arc::new(Mutex::new(HashMap::new())) };
+ }
+}
+
+impl TeraFilter for RegexReplaceFilter {
+ fn filter(&self, value: &Value, args: &HashMap) -> TeraResult {
+ let text = try_get_value!("regex_replace", "value", String, value);
+ let pattern = match args.get("pattern") {
+ Some(val) => try_get_value!("regex_replace", "pattern", String, val),
+ None => {
+ return Err(TeraError::msg(
+ "Filter `regex_replace` expected an arg called `pattern`",
+ ))
+ }
+ };
+ let rep = match args.get("rep") {
+ Some(val) => try_get_value!("regex_replace", "rep", String, val),
+ None => {
+ return Err(TeraError::msg("Filter `regex_replace` expected an arg called `rep`"))
+ }
+ };
+
+ let mut cache = self.re_cache.lock().expect("re_cache lock");
+ let replaced = {
+ match cache.get(&pattern) {
+ Some(pat) => pat.replace_all(&text, &rep),
+ None => {
+ let pat = Regex::new(&pattern)
+ .map_err(|e| format!("`regex_replace`: failed to compile regex: {}", e))?;
+ let replaced = pat.replace_all(&text, &rep);
+ cache.insert(pattern, pat);
+ replaced
+ }
+ }
+ };
+
+ Ok(to_value(replaced).unwrap())
+ }
+}
+
#[derive(Debug)]
pub struct NumFormatFilter {
default_language: String,
@@ -114,7 +164,9 @@ mod tests {
use libs::tera::{to_value, Filter, Tera};
- use super::{base64_decode, base64_encode, MarkdownFilter, NumFormatFilter};
+ use super::{
+ base64_decode, base64_encode, MarkdownFilter, NumFormatFilter, RegexReplaceFilter,
+ };
use config::Config;
#[test]
@@ -251,6 +303,22 @@ mod tests {
}
}
+ #[test]
+ fn regex_replace_filter() {
+ let value = "Springsteen, Bruce";
+ let expected = "Bruce Springsteen";
+ let pattern = r"(?P[^,\s]+),\s+(?P\S+)";
+ let rep = "$first $last";
+ let mut args = HashMap::new();
+ args.insert("pattern".to_string(), to_value(pattern).unwrap());
+ args.insert("rep".to_string(), to_value(rep).unwrap());
+ let regex_replace = RegexReplaceFilter::new();
+ let result = regex_replace.filter(&to_value(value).unwrap(), &args);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), to_value(expected).unwrap());
+ assert!(regex_replace.re_cache.lock().unwrap().contains_key(pattern));
+ }
+
#[test]
fn num_format_filter() {
let tests = vec![
diff --git a/components/templates/src/lib.rs b/components/templates/src/lib.rs
index 6441ff4999..59ab2abaf9 100644
--- a/components/templates/src/lib.rs
+++ b/components/templates/src/lib.rs
@@ -28,6 +28,7 @@ pub static ZOLA_TERA: Lazy = Lazy::new(|| {
.unwrap();
tera.register_filter("base64_encode", filters::base64_encode);
tera.register_filter("base64_decode", filters::base64_decode);
+ tera.register_filter("regex_replace", filters::RegexReplaceFilter::new());
tera
});
diff --git a/docs/content/documentation/templates/overview.md b/docs/content/documentation/templates/overview.md
index 20c823070d..7eca41ecb1 100644
--- a/docs/content/documentation/templates/overview.md
+++ b/docs/content/documentation/templates/overview.md
@@ -86,6 +86,14 @@ Encode the variable to base64.
### base64_decode
Decode the variable from base64.
+### regex_replace
+Replace text via regular expressions.
+
+```jinja2
+{{ "World Hello" | regex_replace(pattern=`(?P\w+), (?P\w+)`, rep=`$greeting $subject`) }}
+
+```
+
### num_format
Format a number into its string representation.
From c48b61ad72b73bd94e6c3a361a66a36900f40898 Mon Sep 17 00:00:00 2001
From: Jeremy Kerr
Date: Mon, 24 Apr 2023 05:56:30 +0800
Subject: [PATCH 05/12] templates: add base URL for feed content (#2190)
Relative links in the entry content do not currently have a base URI, so
will be resolved relative to the feed URI:
Given an entry with the content:
And URIS of:
* entry: https://example.org/blog/some-entry/
* feed: https://example.org/atom.xml
The link URI will end up as:
https://example.org/some-resource.bin
rather than the URI that ends up resolved in the rendered page:
https://example.org/blog/some-entry/some-resource.bin
The atom and RSS formats allow for an xml:base attribute (itself
specified in [1]) to provide a base URI of a subset of a document. This
change adds xml:base attributes to each entry, using the page permalink.
This gives us something equivalent to:
]]>
[1]: https://www.w3.org/TR/xmlbase/
Signed-off-by: Jeremy Kerr
---
components/templates/src/builtins/atom.xml | 2 +-
components/templates/src/builtins/rss.xml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/components/templates/src/builtins/atom.xml b/components/templates/src/builtins/atom.xml
index 8b2d331e5c..2f16095563 100644
--- a/components/templates/src/builtins/atom.xml
+++ b/components/templates/src/builtins/atom.xml
@@ -40,7 +40,7 @@
{% if page.summary %}
{{ page.summary }}
{% else %}
- {{ page.content }}
+ {{ page.content }}
{% endif %}
{%- endfor %}
diff --git a/components/templates/src/builtins/rss.xml b/components/templates/src/builtins/rss.xml
index 1c09e4cf4d..a684a48af9 100644
--- a/components/templates/src/builtins/rss.xml
+++ b/components/templates/src/builtins/rss.xml
@@ -33,7 +33,7 @@
{{ page.permalink | escape_xml | safe }}
{{ page.permalink | escape_xml | safe }}
- {% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}
+ {% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}
{%- endfor %}
From 7edf92bc8f2b45a2da7a68ae117325eacc76d451 Mon Sep 17 00:00:00 2001
From: Jake G <106644+Jieiku@users.noreply.github.com>
Date: Fri, 28 Apr 2023 12:50:54 -0700
Subject: [PATCH 06/12] Fix multi-lingual json index (#2197)
* Fix multi-ligual json index
* multi-lingual search Fix cargo fmt
---
components/site/src/lib.rs | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs
index b40e0b5c76..f2cb1951f5 100644
--- a/components/site/src/lib.rs
+++ b/components/site/src/lib.rs
@@ -787,11 +787,7 @@ impl Site {
}
fn index_for_lang(&self, lang: &str) -> Result<()> {
- let index_json = search::build_index(
- &self.config.default_language,
- &self.library.read().unwrap(),
- &self.config,
- )?;
+ let index_json = search::build_index(lang, &self.library.read().unwrap(), &self.config)?;
let (path, content) = match &self.config.search.index_format {
IndexFormat::ElasticlunrJson => {
let path = self.output_path.join(format!("search_index.{}.json", lang));
From 74483096ca266408d7c8dca8105518048ef17b38 Mon Sep 17 00:00:00 2001
From: Andrew Langmeier
Date: Mon, 1 May 2023 15:40:29 -0400
Subject: [PATCH 07/12] Add search.index_format into the serialized config
(#2165) (#2196)
* Add search into the serialized config (#2165)
* Only expose index_format
* Create config.search struct
* cargo fmt
---
components/config/src/config/mod.rs | 2 ++
components/config/src/config/search.rs | 11 +++++++++++
2 files changed, 13 insertions(+)
diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs
index e0674276dd..2cda172127 100644
--- a/components/config/src/config/mod.rs
+++ b/components/config/src/config/mod.rs
@@ -109,6 +109,7 @@ pub struct SerializedConfig<'a> {
build_search_index: bool,
extra: &'a HashMap,
markdown: &'a markup::Markdown,
+ search: search::SerializedSearch<'a>,
}
impl Config {
@@ -331,6 +332,7 @@ impl Config {
build_search_index: options.build_search_index,
extra: &self.extra,
markdown: &self.markdown,
+ search: self.search.serialize(),
}
}
}
diff --git a/components/config/src/config/search.rs b/components/config/src/config/search.rs
index c96a874847..26c78f7464 100644
--- a/components/config/src/config/search.rs
+++ b/components/config/src/config/search.rs
@@ -44,3 +44,14 @@ impl Default for Search {
}
}
}
+
+impl Search {
+ pub fn serialize(&self) -> SerializedSearch {
+ SerializedSearch { index_format: &self.index_format }
+ }
+}
+
+#[derive(Serialize)]
+pub struct SerializedSearch<'a> {
+ pub index_format: &'a IndexFormat,
+}
From 4f779801b44e0b101a9020a5d99ae5026a1b12ee Mon Sep 17 00:00:00 2001
From: Vincent Prouillet
Date: Tue, 2 May 2023 13:39:13 +0200
Subject: [PATCH 08/12] Fix typo in error message
---
components/site/src/lib.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs
index f2cb1951f5..2d26b1545e 100644
--- a/components/site/src/lib.rs
+++ b/components/site/src/lib.rs
@@ -370,7 +370,7 @@ impl Site {
if self.config.build_search_index && !index_section.meta.in_search_index {
bail!(
"You have enabled search in the config but disabled it in the index section: \
- either turn off the search in the config or remote `in_search_index = true` from the \
+ either turn off the search in the config or remove `in_search_index = true` from the \
section front-matter."
)
}
From 1321a83e495d3bc6f8cb13cc5823c9a0a1d477b0 Mon Sep 17 00:00:00 2001
From: Andrew Langmeier
Date: Sat, 6 May 2023 10:02:29 -0400
Subject: [PATCH 09/12] Hard link serve panic fix (#2210)
* Fix hard link panic and add better error info to std:fs errors
* cargo fmt
* Remove erroneously committed config change
* Remove console import; Use with context to provide additional error info
* improve error wording
---
components/utils/src/fs.rs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/components/utils/src/fs.rs b/components/utils/src/fs.rs
index 8a53df1480..c29edba689 100644
--- a/components/utils/src/fs.rs
+++ b/components/utils/src/fs.rs
@@ -86,7 +86,12 @@ pub fn copy_file_if_needed(src: &Path, dest: &Path, hard_link: bool) -> Result<(
}
if hard_link {
- std::fs::hard_link(src, dest)?
+ if dest.exists() {
+ std::fs::remove_file(dest)
+ .with_context(|| format!("Error removing file: {:?}", dest))?;
+ }
+ std::fs::hard_link(src, dest)
+ .with_context(|| format!("Error hard linking file, src: {:?}, dst: {:?}", src, dest))?;
} else {
let src_metadata = metadata(src)
.with_context(|| format!("Failed to get metadata of {}", src.display()))?;
From b5a90dba5d12ea6760c3aa18fec40f8af4d7cbc7 Mon Sep 17 00:00:00 2001
From: sinofp
Date: Sat, 6 May 2023 22:04:33 +0100
Subject: [PATCH 10/12] Add support for lazy loading images (#2211)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add optional decoding="async" loading="lazy" for img
In theory, they can make the page load faster and show content faster.
There’s one problem: CommonMark allows arbitrary inline elements in alt text.
If I want to get the correct alt text, I need to match every inline event.
I think most people will only use plain text, so I only match Event::Text.
* Add very basic test for img
This is the reason why we should use plain text when lazy_async_image is enabled.
* Explain lazy_async_image in documentation
* Add test with empty alt and special characters
I totaly forgot one can leave the alt text empty.
I thought I need to eliminate the alt attribute in that case,
but actually empty alt text is better than not having an alt attribute at all:
https://www.w3.org/TR/WCAG20-TECHS/H67.html
https://www.boia.org/blog/images-that-dont-need-alternative-text-still-need-alt-attributes
Thus I will leave the empty alt text.
Another test is added to ensure alt text is properly escaped.
I will remove the redundant escaping code after this commit.
* Remove manually escaping alt text
After removing the if-else inside the arm of Event::Text(text),
the alt text is still escaped.
Indeed they are redundant.
* Use insta for snapshot testing
`cargo insta review` looks cool!
I wanted to dedup the cases variable,
but my Rust skill is not good enough to declare a global vector.
---
components/config/src/config/markup.rs | 3 ++
components/markdown/src/markdown.rs | 32 +++++++++++++++---
components/markdown/tests/img.rs | 33 +++++++++++++++++++
...n_add_lazy_loading_and_async_decoding.snap | 10 ++++++
.../snapshots/img__can_transform_image.snap | 10 ++++++
.../getting-started/configuration.md | 5 +++
6 files changed, 89 insertions(+), 4 deletions(-)
create mode 100644 components/markdown/tests/img.rs
create mode 100644 components/markdown/tests/snapshots/img__can_add_lazy_loading_and_async_decoding.snap
create mode 100644 components/markdown/tests/snapshots/img__can_transform_image.snap
diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs
index 7cd03520c8..b1ac7fa22f 100644
--- a/components/config/src/config/markup.rs
+++ b/components/config/src/config/markup.rs
@@ -51,6 +51,8 @@ pub struct Markdown {
/// The compiled extra themes into a theme set
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
pub extra_theme_set: Arc" loading="lazy" decoding="async" />
+
diff --git a/components/markdown/tests/snapshots/img__can_transform_image.snap b/components/markdown/tests/snapshots/img__can_transform_image.snap
new file mode 100644
index 0000000000..5ad51f586a
--- /dev/null
+++ b/components/markdown/tests/snapshots/img__can_transform_image.snap
@@ -0,0 +1,10 @@
+---
+source: components/markdown/tests/img.rs
+expression: body
+---
+
+
+
+
+
+
diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md
index b2ff781f6a..ac85b72426 100644
--- a/docs/content/documentation/getting-started/configuration.md
+++ b/docs/content/documentation/getting-started/configuration.md
@@ -122,6 +122,11 @@ external_links_no_referrer = false
# For example, `...` into `…`, `"quote"` into `“curly”` etc
smart_punctuation = false
+# Whether to set decoding="async" and loading="lazy" for all images
+# When turned on, the alt text must be plain text.
+# For example, `![xx](...)` is ok but `![*x*x](...)` isn’t ok
+lazy_async_image = false
+
# Configuration of the link checker.
[link_checker]
# Skip link checking for external URLs that start with these prefixes
From 961d07d535dab8fa58c882c6e30e1db02c5f0a30 Mon Sep 17 00:00:00 2001
From: Christopher Greenwood
Date: Fri, 7 Jul 2023 08:41:12 -0700
Subject: [PATCH 11/12] move sass.rs into sass folder
this will improve module structure, in preparation for adding some
serde code in another sub-module.
---
components/site/src/{sass.rs => sass/mod.rs} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename components/site/src/{sass.rs => sass/mod.rs} (100%)
diff --git a/components/site/src/sass.rs b/components/site/src/sass/mod.rs
similarity index 100%
rename from components/site/src/sass.rs
rename to components/site/src/sass/mod.rs
From 86532cda896000a1af6f5bcf5841eb08c646f5ad Mon Sep 17 00:00:00 2001
From: Christopher Greenwood
Date: Fri, 7 Jul 2023 08:38:34 -0700
Subject: [PATCH 12/12] inject site config during Sass compilation
it is helpful to be able to use site config during Sass compilation,
for example to allow a site owner to set header color in a Sass
template. in this change we serialize a subset of the site's config
to a `.scss` file as a Sass map literal, allowing config content to
be referenced in site Sass or theme Sass.
this commit involves bumping the `grass` dependency to version
`1.13.0`. we do this so that we can use nested map retrieval in
Sass files, such as `map.get($config, extra, background_color)`.
---
Cargo.lock | 8 +-
components/config/src/config/mod.rs | 11 +
components/libs/Cargo.toml | 2 +-
components/site/Cargo.toml | 2 +-
components/site/src/lib.rs | 4 +-
components/site/src/sass/mod.rs | 37 +-
components/site/src/sass/serde.rs | 477 +++++++++++++++++++++
docs/content/documentation/content/sass.md | 30 ++
src/cmd/serve.rs | 2 +-
test_site/config.toml | 3 +
test_site/sass/blog.scss | 5 +-
11 files changed, 563 insertions(+), 18 deletions(-)
create mode 100644 components/site/src/sass/serde.rs
diff --git a/Cargo.lock b/Cargo.lock
index ebbfb6efb8..eadefc16ce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1190,18 +1190,18 @@ dependencies = [
[[package]]
name = "grass"
-version = "0.12.3"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4bfa010e6f9fe2f40727b4aedf67aa54e0439c57f855458efb1f43d730a028f"
+checksum = "bc543656cb7df951b74b696766d878ab92192df27637839ec3056737950265f2"
dependencies = [
"grass_compiler",
]
[[package]]
name = "grass_compiler"
-version = "0.12.3"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abe05b48c9c96f5ec64ad9af20c9016a8d57ec8b979e0f6dbdd9747f32b16df3"
+checksum = "187adfc0b34289c7f8f3819453ce9da3177c3d73f40ac74bb17faba578813d45"
dependencies = [
"codemap",
"indexmap",
diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs
index 2cda172127..932ab43ef9 100644
--- a/components/config/src/config/mod.rs
+++ b/components/config/src/config/mod.rs
@@ -112,6 +112,13 @@ pub struct SerializedConfig<'a> {
search: search::SerializedSearch<'a>,
}
+#[derive(Serialize)]
+pub struct SassConfig<'a> {
+ base_url: &'a str,
+ theme: &'a Option,
+ extra: &'a HashMap,
+}
+
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
@@ -335,6 +342,10 @@ impl Config {
search: self.search.serialize(),
}
}
+
+ pub fn sass_config(&self) -> SassConfig {
+ SassConfig { base_url: &self.base_url, theme: &self.theme, extra: &self.extra }
+ }
}
// merge TOML data that can be a table, or anything else
diff --git a/components/libs/Cargo.toml b/components/libs/Cargo.toml
index 2eb8bf8afa..0028b5a8e4 100644
--- a/components/libs/Cargo.toml
+++ b/components/libs/Cargo.toml
@@ -27,7 +27,7 @@ rayon = "1"
regex = "1"
relative-path = "1"
reqwest = { version = "0.11", default-features = false, features = ["blocking"] }
-grass = {version = "0.12.1", default-features = false, features = ["random"]}
+grass = {version = "0.13.0", default-features = false, features = ["random"]}
serde_json = "1"
serde_yaml = "0.9"
sha2 = "0.10"
diff --git a/components/site/Cargo.toml b/components/site/Cargo.toml
index 3388f2dc88..9474265599 100644
--- a/components/site/Cargo.toml
+++ b/components/site/Cargo.toml
@@ -7,6 +7,7 @@ include = ["src/**/*"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
+tempfile = "3"
errors = { path = "../errors" }
config = { path = "../config" }
@@ -20,5 +21,4 @@ libs = { path = "../libs" }
content = { path = "../content" }
[dev-dependencies]
-tempfile = "3"
path-slash = "0.2"
diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs
index 2d26b1545e..33b11712bc 100644
--- a/components/site/src/lib.rs
+++ b/components/site/src/lib.rs
@@ -708,13 +708,13 @@ impl Site {
if let Some(ref theme) = self.config.theme {
let theme_path = self.base_path.join("themes").join(theme);
if theme_path.join("sass").exists() {
- sass::compile_sass(&theme_path, &self.output_path)?;
+ sass::compile_sass(&theme_path, &self.output_path, &self.config)?;
start = log_time(start, "Compiled theme Sass");
}
}
if self.config.compile_sass {
- sass::compile_sass(&self.base_path, &self.output_path)?;
+ sass::compile_sass(&self.base_path, &self.output_path, &self.config)?;
start = log_time(start, "Compiled own Sass");
}
diff --git a/components/site/src/sass/mod.rs b/components/site/src/sass/mod.rs
index 87f5c7dd8c..4a6c846ef4 100644
--- a/components/site/src/sass/mod.rs
+++ b/components/site/src/sass/mod.rs
@@ -1,24 +1,27 @@
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
+use config::Config;
use libs::globset::Glob;
use libs::grass::{from_path as compile_file, Options, OutputStyle};
use libs::walkdir::{DirEntry, WalkDir};
+use tempfile::{tempdir, TempDir};
use crate::anyhow;
-use errors::{bail, Result};
+use errors::{bail, Context, Result};
use utils::fs::{create_file, ensure_directory_exists};
-pub fn compile_sass(base_path: &Path, output_path: &Path) -> Result<()> {
+mod serde;
+
+pub fn compile_sass(base_path: &Path, output_path: &Path, config: &Config) -> Result<()> {
ensure_directory_exists(output_path)?;
- let sass_path = {
- let mut sass_path = PathBuf::from(base_path);
- sass_path.push("sass");
- sass_path
- };
+ let sass_path = PathBuf::from(base_path).join("sass");
+
+ let dependencies_dir = build_dependencies_dir_from_config(config)?;
- let options = Options::default().style(OutputStyle::Compressed);
+ let options =
+ Options::default().style(OutputStyle::Compressed).load_path(dependencies_dir.path());
let files = get_non_partial_scss(&sass_path);
let mut compiled_paths = Vec::new();
@@ -52,6 +55,24 @@ pub fn compile_sass(base_path: &Path, output_path: &Path) -> Result<()> {
Ok(())
}
+/// write out a subset of the Zola config document to a temporary SCSS file
+/// as an SCSS map variable literal. this will allow parts of the site's
+/// config to be usable during Sass compilation. this enables theme configuration
+/// like allowing the site owner to change header color. this function returns
+/// a tempdir holding a single `.scss` file. the tempdir should then be used as
+/// a load directory above when compiling the site's Sass files. the tempdir
+/// and contained `.scss` file will be deleted on drop of the returned `TempDir`
+/// struct, which should happen after Sass compilation finishes.
+fn build_dependencies_dir_from_config(config: &Config) -> Result {
+ let dir = tempdir().context("failed to create tempdir for SASS dependencies")?;
+
+ let config_serialized = serde::serialize_config(config)?;
+
+ std::fs::write(dir.path().join("zola.scss"), format!("$config: {}", config_serialized))?;
+
+ Ok(dir)
+}
+
fn is_partial_scss(entry: &DirEntry) -> bool {
entry.file_name().to_str().map(|s| s.starts_with('_')).unwrap_or(false)
}
diff --git a/components/site/src/sass/serde.rs b/components/site/src/sass/serde.rs
new file mode 100644
index 0000000000..0b75de2c1b
--- /dev/null
+++ b/components/site/src/sass/serde.rs
@@ -0,0 +1,477 @@
+use config::Config;
+use errors::{Context, Result};
+
+use serde::{Serialize, Serializer};
+
+use std::fmt::Display;
+
+/// write the site's `Config` struct out as a SCSS map literal. this
+/// first converts the `Config` to a `SassConfig` to only retain and
+/// serialize the keys in the config document that we want to end up
+/// in the resulting SCSS map.
+pub(crate) fn serialize_config(config: &Config) -> Result {
+ let sass_config = config.sass_config();
+ let mut ser = SassMapSerializer::default();
+
+ serde::Serialize::serialize(&sass_config, &mut ser)
+ .context("failed to serialize Zola config document")?;
+
+ Ok(ser.output)
+}
+
+/// custom serde `Serializer` that serializes a structure as an SCSS
+/// map literal. the primary difference between an SCSS map literal
+/// and JSON is that SCSS uses parentheses like `("key": "value")`
+/// instead of `{"key": "value"}` to express maps/dictionaries.
+#[derive(Default)]
+struct SassMapSerializer {
+ output: String,
+}
+
+#[derive(Debug)]
+struct SassMapSerializerError(String);
+
+type SassMapSerializerResult = std::result::Result<(), SassMapSerializerError>;
+
+impl serde::ser::Error for SassMapSerializerError {
+ fn custom(msg: T) -> Self {
+ SassMapSerializerError(msg.to_string())
+ }
+}
+
+impl Display for SassMapSerializerError {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ formatter.write_fmt(format_args!("SassMapSerializerError({})", self.0))
+ }
+}
+
+impl std::error::Error for SassMapSerializerError {}
+
+impl<'a> Serializer for &'a mut SassMapSerializer {
+ type Ok = ();
+ type Error = SassMapSerializerError;
+
+ type SerializeSeq = Self;
+ type SerializeTuple = Self;
+ type SerializeTupleStruct = Self;
+ type SerializeTupleVariant = Self;
+ type SerializeMap = Self;
+ type SerializeStruct = Self;
+ type SerializeStructVariant = Self;
+
+ fn serialize_bool(self, v: bool) -> std::result::Result {
+ self.output += if v { "true" } else { "false" };
+ Ok(())
+ }
+
+ fn serialize_i8(self, v: i8) -> std::result::Result {
+ self.serialize_i64(i64::from(v))
+ }
+
+ fn serialize_i16(self, v: i16) -> std::result::Result {
+ self.serialize_i64(i64::from(v))
+ }
+
+ fn serialize_i32(self, v: i32) -> std::result::Result {
+ self.serialize_i64(i64::from(v))
+ }
+
+ fn serialize_i64(self, v: i64) -> std::result::Result {
+ self.output += &v.to_string();
+ Ok(())
+ }
+
+ fn serialize_u8(self, v: u8) -> std::result::Result {
+ self.serialize_u64(u64::from(v))
+ }
+
+ fn serialize_u16(self, v: u16) -> std::result::Result {
+ self.serialize_u64(u64::from(v))
+ }
+
+ fn serialize_u32(self, v: u32) -> std::result::Result {
+ self.serialize_u64(u64::from(v))
+ }
+
+ fn serialize_u64(self, v: u64) -> std::result::Result {
+ self.output += &v.to_string();
+ Ok(())
+ }
+
+ fn serialize_f32(self, v: f32) -> std::result::Result {
+ self.serialize_f64(f64::from(v))
+ }
+
+ fn serialize_f64(self, v: f64) -> std::result::Result {
+ self.output += &v.to_string();
+ Ok(())
+ }
+
+ /// serialize any chars as if they were single-character strings
+ fn serialize_char(self, v: char) -> std::result::Result {
+ self.serialize_str(&v.to_string())
+ }
+
+ fn serialize_str(self, v: &str) -> std::result::Result {
+ self.output += "\"";
+ self.output += v;
+ self.output += "\"";
+ Ok(())
+ }
+
+ /// not implemented, as the type being serialized here is TOML-based, which
+ /// has no native byte type
+ fn serialize_bytes(self, _v: &[u8]) -> std::result::Result {
+ unimplemented!()
+ }
+
+ /// treat None as null
+ fn serialize_none(self) -> std::result::Result {
+ self.output += "null";
+ Ok(())
+ }
+
+ /// treat Some(T) just as an instance of T itself
+ fn serialize_some(self, value: &T) -> std::result::Result
+ where
+ T: serde::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ /// treat the unit struct `()` as None/null
+ fn serialize_unit(self) -> std::result::Result {
+ self.serialize_none()
+ }
+
+ /// treat the unit struct `()` as None/null
+ fn serialize_unit_struct(
+ self,
+ _name: &'static str,
+ ) -> std::result::Result {
+ self.serialize_none()
+ }
+
+ /// for a unit variant like `MyEnum::A`, just serialize the variant name
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> std::result::Result {
+ self.serialize_str(variant)
+ }
+
+ /// for a newtype struct like `Dollars(u8)`, just serialize as
+ /// the wrapped type
+ fn serialize_newtype_struct(
+ self,
+ _name: &'static str,
+ value: &T,
+ ) -> std::result::Result
+ where
+ T: serde::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ /// for a newtype variant like `Currency::Dollars(u8)`, just serialize as
+ /// the wrapped type
+ fn serialize_newtype_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ value: &T,
+ ) -> std::result::Result
+ where
+ T: serde::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ /// arrays/sequences are serialized as in JSON
+ fn serialize_seq(
+ self,
+ _len: Option,
+ ) -> std::result::Result {
+ self.output += "[";
+ Ok(self)
+ }
+
+ /// treat a tuple the same way we treat an array/sequence
+ fn serialize_tuple(self, len: usize) -> std::result::Result {
+ self.serialize_seq(Some(len))
+ }
+
+ /// treat a tuple struct like `Rgb(u8, u8, u8)` the same way we treat
+ /// an array/sequence
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> std::result::Result {
+ self.serialize_seq(Some(len))
+ }
+
+ /// treat a tuple variant like `Color::Rgb(u8, u8, u8)` the same way
+ /// we treat an array/sequence
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ len: usize,
+ ) -> std::result::Result {
+ self.serialize_seq(Some(len))
+ }
+
+ /// serialize maps as `("key": "value")`
+ fn serialize_map(
+ self,
+ _len: Option,
+ ) -> std::result::Result {
+ self.output += "(";
+ Ok(self)
+ }
+
+ /// treat a struct with named members as a map
+ fn serialize_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> std::result::Result {
+ self.serialize_map(Some(len))
+ }
+
+ /// treat a struct variant like `Color::Rgb { r: u8, g: u8, b: u8}`
+ /// as a map
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ len: usize,
+ ) -> std::result::Result {
+ self.serialize_map(Some(len))
+ }
+}
+
+impl<'a> serde::ser::SerializeSeq for &'a mut SassMapSerializer {
+ // Must match the `Ok` type of the serializer.
+ type Ok = ();
+ // Must match the `Error` type of the serializer.
+ type Error = SassMapSerializerError;
+
+ // Serialize a single element of the sequence.
+ fn serialize_element(&mut self, value: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ // output a comma for all but the first element
+ if !self.output.ends_with('[') {
+ self.output += ",";
+ }
+ value.serialize(&mut **self)
+ }
+
+ // Close the sequence.
+ fn end(self) -> SassMapSerializerResult {
+ self.output += "]";
+ Ok(())
+ }
+}
+
+// Same thing but for tuples.
+impl<'a> serde::ser::SerializeTuple for &'a mut SassMapSerializer {
+ type Ok = ();
+ type Error = SassMapSerializerError;
+
+ fn serialize_element(&mut self, value: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ // output a comma for all but the first element
+ if !self.output.ends_with('[') {
+ self.output += ",";
+ }
+ value.serialize(&mut **self)
+ }
+
+ fn end(self) -> SassMapSerializerResult {
+ self.output += "]";
+ Ok(())
+ }
+}
+
+// Same thing but for tuple structs.
+impl<'a> serde::ser::SerializeTupleStruct for &'a mut SassMapSerializer {
+ type Ok = ();
+ type Error = SassMapSerializerError;
+
+ fn serialize_field(&mut self, value: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ // output a comma for all but the first element
+ if !self.output.ends_with('[') {
+ self.output += ",";
+ }
+ value.serialize(&mut **self)
+ }
+
+ fn end(self) -> SassMapSerializerResult {
+ self.output += "]";
+ Ok(())
+ }
+}
+
+// Same thing but for tuple variants.
+impl<'a> serde::ser::SerializeTupleVariant for &'a mut SassMapSerializer {
+ type Ok = ();
+ type Error = SassMapSerializerError;
+
+ fn serialize_field(&mut self, value: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ // output a comma for all but the first element
+ if !self.output.ends_with('[') {
+ self.output += ",";
+ }
+ value.serialize(&mut **self)
+ }
+
+ fn end(self) -> SassMapSerializerResult {
+ self.output += "]";
+ Ok(())
+ }
+}
+
+impl<'a> serde::ser::SerializeMap for &'a mut SassMapSerializer {
+ type Ok = ();
+ type Error = SassMapSerializerError;
+
+ fn serialize_key(&mut self, key: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ if !self.output.ends_with('(') {
+ self.output += ",";
+ }
+ key.serialize(&mut **self)
+ }
+
+ fn serialize_value(&mut self, value: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ self.output += ":";
+ value.serialize(&mut **self)
+ }
+
+ fn end(self) -> SassMapSerializerResult {
+ self.output += ")";
+ Ok(())
+ }
+}
+
+// Same thing but for structs, where the keys are static string field names.
+impl<'a> serde::ser::SerializeStruct for &'a mut SassMapSerializer {
+ type Ok = ();
+ type Error = SassMapSerializerError;
+
+ fn serialize_field(&mut self, key: &'static str, value: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ if !self.output.ends_with('(') {
+ self.output += ",";
+ }
+ key.serialize(&mut **self)?;
+ self.output += ":";
+ value.serialize(&mut **self)
+ }
+
+ fn end(self) -> SassMapSerializerResult {
+ self.output += ")";
+ Ok(())
+ }
+}
+
+// Same thing but for struct variants, where we ignore the variant name.
+impl<'a> serde::ser::SerializeStructVariant for &'a mut SassMapSerializer {
+ type Ok = ();
+ type Error = SassMapSerializerError;
+
+ fn serialize_field(&mut self, key: &'static str, value: &T) -> SassMapSerializerResult
+ where
+ T: ?Sized + Serialize,
+ {
+ if !self.output.ends_with('(') {
+ self.output += ",";
+ }
+ key.serialize(&mut **self)?;
+ self.output += ":";
+ value.serialize(&mut **self)
+ }
+
+ fn end(self) -> SassMapSerializerResult {
+ self.output += ")";
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_primitives() {
+ verify_serialization(true, "true");
+ verify_serialization(64, "64");
+ verify_serialization(-123, "-123");
+ verify_serialization(567.89, "567.89");
+ verify_serialization('t', "\"t\"");
+ verify_serialization("abc", "\"abc\"");
+ verify_serialization(Option::::None, "null");
+ verify_serialization((), "null");
+ }
+
+ #[test]
+ fn test_arrays() {
+ verify_serialization(&[123, 456, 789], "[123,456,789]");
+ verify_serialization((123, 456, 789), "[123,456,789]");
+ }
+
+ #[test]
+ fn test_struct() {
+ #[derive(Serialize)]
+ struct Inner {
+ a: Vec,
+ b: (String, f64),
+ }
+
+ #[derive(Serialize)]
+ struct Outer {
+ a: i32,
+ b: String,
+ c: Inner,
+ }
+
+ let val = Outer {
+ a: 42,
+ b: "abc".to_string(),
+ c: Inner { a: vec![6, 7, 8], b: ("def".to_string(), 123.45) },
+ };
+
+ let expected = "(\"a\":42,\"b\":\"abc\",\"c\":(\"a\":[6,7,8],\"b\":[\"def\",123.45]))";
+ verify_serialization(val, expected);
+ }
+
+ fn verify_serialization(val: T, expected: &str) {
+ let mut ser = SassMapSerializer::default();
+ val.serialize(&mut ser).unwrap();
+ assert_eq!(ser.output, expected);
+ }
+}
diff --git a/docs/content/documentation/content/sass.md b/docs/content/documentation/content/sass.md
index 1c08bbd742..d258f043d3 100644
--- a/docs/content/documentation/content/sass.md
+++ b/docs/content/documentation/content/sass.md
@@ -43,3 +43,33 @@ Files with the `scss` extension use "Sassy CSS" syntax,
while files with the `sass` extension use the "indented" syntax: .
Zola will return an error if `scss` and `sass` files with the same
base name exist in the same folder to avoid confusion -- see the example above.
+
+## Site configuration in Sass
+
+Zola supports referencing a subset of the site's configuration (defined in its `config.toml`) in Sass files. An example where this might be useful is a theme author allowing the site owner to configure theme colors by defining certain keys in their site `config.toml`. At build time, Zola generates a Sass file containing the appropriate config expressed as a [Sass map literal](https://sass-lang.com/documentation/values/maps/) and makes that Sass file available to theme-defined and user-defined Sass files when they are compiled. The config keys that are currently exposed are:
+
+1. `base_url`
+2. `theme`
+3. Everything in `extra`
+
+### Example
+
+`config.toml`
+```
+title = "My Test Site"
+base_url = "https://replace-this-with-your-url.com"
+compile_sass = true
+
+[extra.sass]
+background_color = "red"
+```
+
+`style.scss`
+```
+@use 'zola';
+@use 'sass:map';
+
+body {
+ background: map.get(zola.$config, extra, sass, background_color);
+}
+```
\ No newline at end of file
diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs
index 52b1d585b9..1b050eae78 100644
--- a/src/cmd/serve.rs
+++ b/src/cmd/serve.rs
@@ -465,7 +465,7 @@ pub fn serve(
console::info(&msg);
rebuild_done_handling(
&broadcaster,
- compile_sass(&site.base_path, &site.output_path),
+ compile_sass(&site.base_path, &site.output_path, &site.config),
&partial_path.to_string_lossy(),
);
};
diff --git a/test_site/config.toml b/test_site/config.toml
index e3fcbfd64d..77268e0535 100644
--- a/test_site/config.toml
+++ b/test_site/config.toml
@@ -33,5 +33,8 @@ skip_anchor_prefixes = [
"https://github.com/rust-lang/rust/blob/",
]
+[extra.sass]
+background_color = "red"
+
[extra.author]
name = "Vincent Prouillet"
diff --git a/test_site/sass/blog.scss b/test_site/sass/blog.scss
index 5a0036151d..277344a406 100644
--- a/test_site/sass/blog.scss
+++ b/test_site/sass/blog.scss
@@ -1,5 +1,8 @@
+@use 'zola';
+@use 'sass:map';
+
body {
- background: red;
+ background: map.get(zola.$config, extra, sass, background_color);
.container {
background: blue;