From 2a5062682b9cdc23a8c0716f29bd1e403d95879f Mon Sep 17 00:00:00 2001 From: trivernis Date: Thu, 29 Jun 2023 09:07:22 +0200 Subject: [PATCH] Add sass rendering and embedding --- Cargo.lock | 73 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 9 ++--- src/rendering/mod.rs | 30 ++++++++++++--- src/rendering/style.rs | 84 ++++++++++++++++++++++++++++++++++++------ 5 files changed, 174 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c973534..9b68d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "async-channel" version = "1.8.0" @@ -967,6 +973,12 @@ dependencies = [ "syn", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.6.2" @@ -996,6 +1008,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1006,6 +1028,39 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1323,6 +1378,23 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "rsass" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a43695dd28122f6c684273de89796a56a98e02e9694b8ab57b160fdc6e6d69af" +dependencies = [ + "arc-swap", + "fastrand", + "lazy_static", + "nom", + "num-bigint", + "num-integer", + "num-rational", + "num-traits", + "tracing", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1940,6 +2012,7 @@ dependencies = [ "futures", "globset", "miette", + "rsass", "serde", "tera", "tera-text-filters", diff --git a/Cargo.toml b/Cargo.toml index c0d5e65..8dbfe54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ comrak = { version = "0.18.0", features = ["emojis"] } futures = "0.3.28" globset = { version = "0.4.10", features = ["serde", "serde1"] } miette = { version = "5.9.0", features = ["serde", "fancy"] } +rsass = "0.27.0" serde = { version = "1.0.164", features = ["derive"] } tera = "1.19.0" tera-text-filters = "1.0.0" diff --git a/src/main.rs b/src/main.rs index 8d8a674..5ed617c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,4 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{path::Path, sync::Arc}; use args::BuildArgs; use clap::Parser; @@ -43,7 +40,7 @@ async fn build(args: &Args, _build_args: &BuildArgs, cfg: Config) -> Result<()> .read_content() .await?; - ContentRenderer::new(ctx).render_all(dirs).await?; + ContentRenderer::new(ctx).await?.render_all(dirs).await?; Ok(()) } @@ -53,7 +50,7 @@ fn build_context(base_path: &Path, config: &Config) -> Context { let content_dir = base_path.join(folders.content.unwrap_or("content".into())); let template_dir = base_path.join(folders.templates.unwrap_or("templates".into())); let output_dir = base_path.join(folders.output.unwrap_or("dist".into())); - let stylesheet_dir = base_path.join(folders.stylesheets.unwrap_or("style".into())); + let stylesheet_dir = base_path.join(folders.stylesheets.unwrap_or("styles".into())); Context { content_dir, diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs index a7644a0..9793b39 100644 --- a/src/rendering/mod.rs +++ b/src/rendering/mod.rs @@ -3,25 +3,34 @@ use std::{path::PathBuf, sync::Arc}; use futures::future; use miette::{IntoDiagnostic, Result}; use tera::{Context as TeraContext, Tera}; -use tokio::fs; +use tokio::{fs, sync::Mutex}; use crate::{ context::Context, data::{load_page, FolderData}, }; +use self::style::{load_stylesheets, Stylesheets}; + mod style; // renders content using the given template folder pub struct ContentRenderer { template_glob: String, ctx: Arc, + styles: Arc>, } impl ContentRenderer { - pub fn new(ctx: Arc) -> Self { + pub async fn new(ctx: Arc) -> Result { let template_glob = format!("{}/**/*", ctx.template_dir.to_string_lossy()); - Self { template_glob, ctx } + let styles = load_stylesheets(&ctx.stylesheet_dir).await?; + + Ok(Self { + template_glob, + ctx, + styles: Arc::new(Mutex::new(styles)), + }) } #[tracing::instrument(level = "trace", skip_all)] @@ -75,20 +84,31 @@ impl ContentRenderer { let page = load_page(&page_path).await?; let mut context = TeraContext::new(); let mut template_name = default_template; + let mut style_name = template_name.to_owned(); match page { crate::data::Page::Data(data) => { if let Some(tmpl) = data.metadata.template { - template_name = tmpl; + template_name = tmpl.to_owned(); + style_name = tmpl; } context.insert("data", &data.data); } crate::data::Page::Content(content) => context.insert("content", &content), } + { + let mut styles = self.styles.lock().await; + let style_embed = styles + .get_style_embed(&style_name, &self.ctx.output_dir) + .await?; + context.insert("style", &style_embed); + }; tracing::debug!("context = {context:?}"); - let html = tera.render(&template_name, &context).into_diagnostic()?; + let html = tera + .render(&format!("{template_name}.html"), &context) + .into_diagnostic()?; let rel_path = page_path .strip_prefix(&self.ctx.content_dir) .into_diagnostic()?; diff --git a/src/rendering/style.rs b/src/rendering/style.rs index bd9c7fe..35b5a1b 100644 --- a/src/rendering/style.rs +++ b/src/rendering/style.rs @@ -1,24 +1,37 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use async_walkdir::WalkDir; use futures::StreamExt; use miette::{IntoDiagnostic, Result}; +use rsass::output::Format; +use tokio::fs; + +const DEFAULT_SHEET_NAME: &str = "style"; +const EMBED_THRESHOLD: usize = 512; pub struct Stylesheets { - pub default_style: Option, - pub page_styles: HashMap, + page_styles: HashMap, + processed_styles: HashMap, } +#[tracing::instrument(level = "trace")] pub async fn load_stylesheets(base_dir: &PathBuf) -> Result { let mut entries = WalkDir::new(base_dir); let mut page_styles = HashMap::new(); + let empty_path = PathBuf::new(); while let Some(res) = entries.next().await { match res { Ok(entry) => { let entry_path = entry.path(); if entry_path.is_file() { + let rel_path = entry_path.strip_prefix(base_dir).into_diagnostic()?; + if let Some(file_name) = entry_path.file_stem() { + let file_name = rel_path.parent().unwrap_or(&empty_path).join(file_name); let file_name = file_name.to_string_lossy().into_owned(); page_styles.insert(file_name, entry_path.to_owned()); } @@ -27,17 +40,64 @@ pub async fn load_stylesheets(base_dir: &PathBuf) -> Result { Err(e) => return Err(e).into_diagnostic(), } } - - let mut default_style = None; - for name in ["style", "default", "stylesheet", "index"] { - if let Some(style) = page_styles.remove(name) { - default_style = Some(style); - break; - } - } + tracing::debug!("Styles {page_styles:?}"); Ok(Stylesheets { - default_style, page_styles, + processed_styles: HashMap::new(), }) } + +impl Stylesheets { + #[tracing::instrument(level = "trace", skip(self, out_dir))] + pub async fn get_style_embed(&mut self, name: &str, out_dir: &Path) -> Result { + let mut styles: Vec = Vec::with_capacity(2); + + if let Some(default_style) = self + .get_processed_style(DEFAULT_SHEET_NAME, out_dir) + .await? + { + styles.push(default_style); + } + if let Some(style) = self.get_processed_style(name, out_dir).await? { + styles.push(style); + } + + Ok(styles.join("")) + } + + #[tracing::instrument(level = "trace", skip(self, out_dir))] + async fn get_processed_style(&mut self, name: &str, out_dir: &Path) -> Result> { + if let Some(processed) = self.processed_styles.get(name) { + Ok(Some(processed.to_owned())) + } else if let Some(source) = self.page_styles.get(name) { + let format = Format { + style: rsass::output::Style::Compressed, + ..Default::default() + }; + let style_contents = rsass::compile_scss_path(source, format).into_diagnostic()?; + let style_html = if style_contents.len() < EMBED_THRESHOLD { + let utf_contents = String::from_utf8(style_contents).into_diagnostic()?; + + format!(r#""#) + } else { + let output_path = out_dir.join(name).with_extension("css"); + let parent = output_path.parent().unwrap(); + if !parent.exists() { + fs::create_dir_all(parent).await.into_diagnostic()?; + } + fs::write(output_path, style_contents) + .await + .into_diagnostic()?; + + format!(r#""#) + }; + self.processed_styles + .insert(name.to_owned(), style_html.to_owned()); + + Ok(Some(style_html)) + } else { + Ok(None) + } + } +}