Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inject site config during Sass compilation #2242

Open
wants to merge 12 commits into
base: next
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

## 0.18.0 (unreleased)

## 0.17.2 (2023-03-19)

- Fix one more invalid error with colocated directories
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions components/config/src/config/markup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<ThemeSet>>,
/// Add loading="lazy" decoding="async" to img tags. When turned on, the alt text must be plain text. Defaults to false
pub lazy_async_image: bool,
}

impl Markdown {
Expand Down Expand Up @@ -204,6 +206,7 @@ impl Default for Markdown {
extra_syntaxes_and_themes: vec![],
extra_syntax_set: None,
extra_theme_set: Arc::new(None),
lazy_async_image: false,
}
}
}
13 changes: 13 additions & 0 deletions components/config/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ pub struct SerializedConfig<'a> {
build_search_index: bool,
extra: &'a HashMap<String, Toml>,
markdown: &'a markup::Markdown,
search: search::SerializedSearch<'a>,
}

#[derive(Serialize)]
pub struct SassConfig<'a> {
base_url: &'a str,
theme: &'a Option<String>,
extra: &'a HashMap<String, Toml>,
}

impl Config {
Expand Down Expand Up @@ -331,8 +339,13 @@ impl Config {
build_search_index: options.build_search_index,
extra: &self.extra,
markdown: &self.markdown,
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
Expand Down
11 changes: 11 additions & 0 deletions components/config/src/config/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
2 changes: 1 addition & 1 deletion components/libs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 8 additions & 5 deletions components/link_checker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<RwLock<HashMap<String, Result>>>> =
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<Client> = 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 {
{
Expand All @@ -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<u8> = vec![];
Expand Down
32 changes: 28 additions & 4 deletions components/markdown/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ pub fn markdown_to_html(

let mut stop_next_end_p = false;

let lazy_async_image = context.config.markdown.lazy_async_image;

let mut opts = Options::empty();
let mut has_summary = false;
opts.insert(Options::ENABLE_TABLES);
Expand Down Expand Up @@ -387,13 +389,35 @@ pub fn markdown_to_html(
events.push(Event::Html("</code></pre>\n".into()));
}
Event::Start(Tag::Image(link_type, src, title)) => {
if is_colocated_asset_link(&src) {
let link = if is_colocated_asset_link(&src) {
let link = format!("{}{}", context.current_page_permalink, &*src);
events.push(Event::Start(Tag::Image(link_type, link.into(), title)));
link.into()
} else {
events.push(Event::Start(Tag::Image(link_type, src, title)));
}
src
};

events.push(if lazy_async_image {
let mut img_before_alt: String = "<img src=\"".to_string();
cmark::escape::escape_href(&mut img_before_alt, &link)
.expect("Could not write to buffer");
if !title.is_empty() {
img_before_alt
.write_str("\" title=\"")
.expect("Could not write to buffer");
cmark::escape::escape_href(&mut img_before_alt, &title)
.expect("Could not write to buffer");
}
img_before_alt.write_str("\" alt=\"").expect("Could not write to buffer");
Event::Html(img_before_alt.into())
} else {
Event::Start(Tag::Image(link_type, link, title))
});
}
Event::End(Tag::Image(..)) => events.push(if lazy_async_image {
Event::Html("\" loading=\"lazy\" decoding=\"async\" />".into())
} else {
event
}),
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"));
events.push(Event::Start(Tag::Link(link_type, "#".into(), title)));
Expand Down
33 changes: 33 additions & 0 deletions components/markdown/tests/img.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
mod common;
use config::Config;

#[test]
fn can_transform_image() {
let cases = vec![
"![haha](https://example.com/abc.jpg)",
"![](https://example.com/abc.jpg)",
"![ha\"h>a](https://example.com/abc.jpg)",
"![__ha__*ha*](https://example.com/abc.jpg)",
"![ha[ha](https://example.com)](https://example.com/abc.jpg)",
];

let body = common::render(&cases.join("\n")).unwrap().body;
insta::assert_snapshot!(body);
}

#[test]
fn can_add_lazy_loading_and_async_decoding() {
let cases = vec![
"![haha](https://example.com/abc.jpg)",
"![](https://example.com/abc.jpg)",
"![ha\"h>a](https://example.com/abc.jpg)",
"![__ha__*ha*](https://example.com/abc.jpg)",
"![ha[ha](https://example.com)](https://example.com/abc.jpg)",
];

let mut config = Config::default_for_test();
config.markdown.lazy_async_image = true;

let body = common::render_with_config(&cases.join("\n"), config).unwrap().body;
insta::assert_snapshot!(body);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: components/markdown/tests/img.rs
expression: body
---
<p><img src="https://example.com/abc.jpg" alt="haha" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="ha&quot;h&gt;a" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="<strong>ha</strong><em>ha</em>" loading="lazy" decoding="async" />
<img src="https://example.com/abc.jpg" alt="ha<a href="https://example.com">ha</a>" loading="lazy" decoding="async" /></p>

10 changes: 10 additions & 0 deletions components/markdown/tests/snapshots/img__can_transform_image.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: components/markdown/tests/img.rs
expression: body
---
<p><img src="https://example.com/abc.jpg" alt="haha" />
<img src="https://example.com/abc.jpg" alt="" />
<img src="https://example.com/abc.jpg" alt="ha&quot;h&gt;a" />
<img src="https://example.com/abc.jpg" alt="haha" />
<img src="https://example.com/abc.jpg" alt="haha" /></p>

2 changes: 1 addition & 1 deletion components/site/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ include = ["src/**/*"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tempfile = "3"

errors = { path = "../errors" }
config = { path = "../config" }
Expand All @@ -20,5 +21,4 @@ libs = { path = "../libs" }
content = { path = "../content" }

[dev-dependencies]
tempfile = "3"
path-slash = "0.2"
12 changes: 4 additions & 8 deletions components/site/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
}
Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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));
Expand Down
37 changes: 29 additions & 8 deletions components/site/src/sass.rs → components/site/src/sass/mod.rs
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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<TempDir> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the name of that function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we take the config document and construct a directory that contains files that are ultimately added as dependencies when compiling Sass. the Sass compiler asks for a "load dir" during compilation, so instead of providing a single file we must provide a single directory containing a single file. so the function name is basically "builds a dependencies directory from the config document". happy to rename, do you have a suggestion?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write_temp_config_file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that works for me. seems odd to me that a function would be named that but return a directory instead of a file, but whatever.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah fair enough create_config_load_path?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like it

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of writing it to the sass directory so people can easily see what there is? It would be still overwritten on each serve/build but we can maybe not delete it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're saying write it to the user's sass source directory? i figured that would be undesirable because then maybe it gets erroneously checked in. but i'm open to it if that seems better.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be ok to check it in? It's not going to change very often

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the more i think about it, the more i prefer not having the injected config in source. i don't particularly like it being checked in when the author didn't write it and when it's liable to get overwritten by the Zola compiler. that creates a scenario where the author is looking through the sass directory and erroneously edits that file, only to have their edits confusingly wiped out on the next zola build. we could solve that with a warning comment at the top of that file, but i prefer just hiding the source since the author isn't writing it. i added site docs explaining this feature, in my opinion that makes clear to the author what's going on. what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to not add it the directory but it's a bit annoying that you can't see it to look at what exactly is defined

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any suggestions on how to surface this content to the site builder without putting it in the directory? i could create a non-tmp directory in /tmp and log the location of it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I'm not too sure. Maybe it's ok to just show an example of how it looks like in the docs

}

fn is_partial_scss(entry: &DirEntry) -> bool {
entry.file_name().to_str().map(|s| s.starts_with('_')).unwrap_or(false)
}
Expand Down
Loading