From e424803c507a73ac0f447f89bf2a9e4014a3405e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maro=C5=A1=20Grego?= Date: Mon, 7 Nov 2022 10:24:27 +0100 Subject: [PATCH] Use a thread-local buffer and a map of rendered pages for rendering --- Cargo.lock | 34 +++---- Cargo.toml | 9 +- README.md | 2 +- src/config.rs | 144 --------------------------- src/lib.rs | 10 +- src/main.rs | 249 +++++++++++++++++++++++++++++++++++++++------- src/page.rs | 40 +++++--- src/render.rs | 90 +++++++++++++++++ src/site.rs | 70 +++++++++++++ src/tasks.rs | 178 --------------------------------- src/taxonomies.rs | 22 ++-- src/types.rs | 11 +- 12 files changed, 445 insertions(+), 414 deletions(-) delete mode 100644 src/config.rs create mode 100644 src/render.rs create mode 100644 src/site.rs delete mode 100644 src/tasks.rs diff --git a/Cargo.lock b/Cargo.lock index da241f0..a312178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "blades" -version = "0.4.1" +version = "0.5.0-alpha" dependencies = [ "arrayvec", "beef", @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.15" +version = "4.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf8832993da70a4c6d13c581f4463c2bdda27b9bf1c5498dc4365543abe6d6f" +checksum = "8e67816e006b17427c9b4386915109b494fec2d929c63e3bd3561234cbf1bf1e" dependencies = [ "bitflags", "clap_lex", @@ -89,9 +89,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.13" +version = "4.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" +checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -212,9 +212,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" [[package]] name = "proc-macro-error" @@ -298,9 +298,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "ryu" @@ -310,9 +310,9 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "serde" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -339,9 +339,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ "itoa", "ryu", @@ -350,9 +350,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 242cca9..c801287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "blades" -version = "0.4.1" +version = "0.5.0-alpha" authors = ["Maroš Grego "] edition = "2021" description = "Blazing fast dead simple static site generator" @@ -9,7 +9,7 @@ keywords = ["website", "site", "generator"] categories = ["command-line-utilities", "web-programming"] license = "GPL-3.0-or-later" readme = "README.md" -homepage = "https://www.getblades.org" +homepage = "https://getblades.org" [dependencies] ramhorns = "0.14" @@ -19,7 +19,6 @@ serde = { version = "^1.0.126", features = ["derive"] } chrono = { version = "^0.4.19", features = ["std", "serde"], default_features = false } fnv = "1.0" hashbrown = { version = "0.12", features = ["inline-more", "serde"], default_features = false } -serde-cmd = "0.1.3" pulldown-cmark = { version = "0.9", default_features = false } cmark-syntax = "0.3" @@ -29,9 +28,10 @@ clap = { version = "4", optional = true, default_features = false, features = [" clap_derive = { version = "4", optional = true } thiserror = { version = "1", optional = true } serde_json = { version = "1", optional = true } +serde-cmd = { version = "0.1.3", optional = true } [features] -bin = ["toml", "clap", "clap_derive", "thiserror", "serde_json"] +bin = ["toml", "clap", "clap_derive", "thiserror", "serde_json", "serde-cmd"] mathml = ["cmark-syntax/latex2mathml"] default = ["bin", "mathml"] @@ -39,6 +39,7 @@ default = ["bin", "mathml"] lto = true opt-level = 3 strip = "debuginfo" +#panic = "abort" codegen-units = 1 [profile.bench] diff --git a/README.md b/README.md index d865be3..21a4c5a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ content using the provided templates. Thanks to [zero-copy](https://serde.rs/lifetimes.html#borrowing-data-in-a-derived-impl) deserialization and the [Ramhorns](https://github.com/maciejhirsz/ramhorns) templating engine, it renders the whole site in milliseconds, possibly more than -[10 times](https://github.com/grego/ssg-bench) faster than other generators like Hugo. +[20 times](https://github.com/grego/ssg-bench) faster than other generators like Hugo. It's made for easy setup and use. A static site generator should be a no brainer. It uses [mustache](https://mustache.github.io/mustache.5.html) templates with extremely minimal diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index a511296..0000000 --- a/src/config.rs +++ /dev/null @@ -1,144 +0,0 @@ -// Blades Copyright (C) 2021 Maroš Grego -// -// This file is part of Blades. This program comes with ABSOLUTELY NO WARRANTY; -// This is free software, and you are welcome to redistribute it under the -// conditions of the GNU General Public License version 3.0. -// -// You should have received a copy of the GNU General Public License -// along with Blades. If not, see -use crate::taxonomies::TaxonMeta; -use crate::types::{Any, HashMap}; - -use beef::lean::Cow; -use ramhorns::Content; -use serde::{Deserialize, Serialize}; -use serde_cmd::CmdBorrowed; - -// These are pre-defined since the life is easier when they are the same for every theme. -pub(crate) static TEMPLATE_DIR: &str = "templates"; -/// Where the assets will be copied from, relative to the site directrory. -pub(crate) static ASSET_SRC_DIR: &str = "assets"; - -/// Main configuration where all the site settings are set. -/// Razor deserializes it from a given TOML file. -#[derive(Default, Deserialize, Serialize)] -pub struct Config<'c> { - /// The directory of the content - #[serde(borrow, default = "default_content_dir")] - pub content_dir: Cow<'c, str>, - /// The directory where the output should be rendered to - #[serde(borrow, default = "default_output_dir")] - pub output_dir: Cow<'c, str>, - /// The directory where the themes are - #[serde(borrow, default = "default_theme_dir")] - pub theme_dir: Cow<'c, str>, - /// Name of the directory of a theme this site is using, empty if none. - #[serde(borrow, default, skip_serializing_if = "str::is_empty")] - pub theme: Cow<'c, str>, - /// Taxonomies of the site - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub taxonomies: HashMap<&'c str, TaxonMeta<'c>>, - /// Generate taxonomies not specified in the config? - #[serde(default = "default_true")] - pub implicit_taxonomies: bool, - - /// Information about the site usable in templates - #[serde(flatten)] - pub site: Site<'c>, - - /// Configuration of plugins for building the site. - #[serde(default, skip_serializing)] - pub plugins: Plugins<'c>, -} - -/// Information about the site usable in templates -#[derive(Content, Default, Deserialize, Serialize)] -pub struct Site<'c> { - /// Where the assets will be copied to, relative to the site directory. - #[serde(borrow, default = "default_assets")] - pub assets: Cow<'c, str>, - /// Title of the site - #[serde(borrow, default, skip_serializing_if = "str::is_empty")] - pub title: Cow<'c, str>, - /// Description of the site - #[serde(borrow, default, skip_serializing_if = "str::is_empty")] - pub description: Cow<'c, str>, - /// Keywords of the site - #[serde(borrow, default, skip_serializing_if = "str::is_empty")] - pub keywords: Cow<'c, str>, - /// A representative image of the site - #[serde(borrow, default, skip_serializing_if = "str::is_empty")] - pub image: Cow<'c, str>, - /// Language of the site - #[serde(borrow, default, skip_serializing_if = "str::is_empty")] - pub lang: Cow<'c, str>, - /// Name of the author of the site - #[serde(borrow, default)] - pub author: Cow<'c, str>, - /// Email of the webmaster - #[serde(borrow, default)] - pub email: Cow<'c, str>, - /// URL of the site - #[serde(borrow, default, skip_serializing_if = "str::is_empty")] - pub url: Cow<'c, str>, - - /// Generate a sitemap? - #[serde(default = "default_true")] - pub sitemap: bool, - /// Generate RSS feed? - #[serde(default = "default_true")] - pub rss: bool, - /// Generate Atom feed? - #[serde(default = "default_true")] - pub atom: bool, - - #[serde(flatten)] - #[ramhorns(flatten)] - pub extra: HashMap<&'c str, Any<'c>>, -} - -/// Plugins to use when building the site. -#[derive(Default, Deserialize)] -pub struct Plugins<'p> { - /// Plugins to get the input from, in the form of serialized list of pages. - #[serde(borrow, default)] - pub input: Box<[CmdBorrowed<'p>]>, - /// Plugins that transform the serialized list of pages. - #[serde(borrow, default)] - pub transform: Box<[CmdBorrowed<'p>]>, - /// Plugins that get the serialized list of pages and might do something with it. - #[serde(borrow, default)] - pub output: Box<[CmdBorrowed<'p>]>, - /// Plugins that transform the content of pages. - /// They are identified by their name and must be enabled for each page. - #[serde(borrow, default)] - pub content: HashMap<&'p str, CmdBorrowed<'p>>, - /// A list of names of content plugins that should be applied to every page. - #[serde(default)] - pub default: Box<[&'p str]>, -} - -#[inline] -const fn default_content_dir() -> Cow<'static, str> { - Cow::const_str("content") -} - -#[inline] -const fn default_output_dir() -> Cow<'static, str> { - Cow::const_str("public") -} - -#[inline] -const fn default_theme_dir() -> Cow<'static, str> { - Cow::const_str("themes") -} - -#[inline] -const fn default_assets() -> Cow<'static, str> { - Cow::const_str("assets") -} - -#[inline] -pub(crate) const fn default_true() -> bool { - true -} diff --git a/src/lib.rs b/src/lib.rs index 498dcf3..c344a42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,16 +16,16 @@ //! the `bin` feature gate, which is enabled by default. When using Blades as a library, they are not //! necessary, so it is recommended to import blades with `default_features = false`. #![warn(missing_docs)] -mod config; mod page; +mod render; +mod site; mod sources; -mod tasks; mod taxonomies; mod types; -pub use config::Config; pub use page::{Context, Page, Pages, Picture}; +pub use render::render_meta; +pub use site::Site; pub use sources::{Parser, Source, Sources}; -pub use tasks::{cleanup, colocate_assets, load_templates, render_meta}; pub use taxonomies::{TaxonMeta, Taxonomies, Taxonomy}; -pub use types::{Ancestors, Any, DateTime, HashMap, MutSet}; +pub use types::{Ancestors, Any, DateTime, HashMap}; diff --git a/src/main.rs b/src/main.rs index de050a5..c8cfb6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,19 +8,24 @@ // along with Blades. If not, see use blades::*; +use beef::lean::Cow; use clap::Parser as ClapParser; -use ramhorns::{Content, Template}; +use ramhorns::{Content, Ramhorns, Template}; +use serde::Deserialize; +use serde_cmd::CmdBorrowed; + use std::env::var; use std::ffi::OsStr; -use std::fs::{create_dir_all, read_to_string, write}; -use std::io::{stdin, stdout, BufRead, BufReader, Lines, Write}; -use std::path::Path; +use std::fs::{self, File}; +use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, ErrorKind, Lines, Write}; +use std::path::{self, Path, PathBuf}; use std::process::{Command, Output, Stdio}; use std::time::{Instant, SystemTime}; use std::{cmp, thread}; use thiserror::Error; static CONFIG_FILE: &str = "Blades.toml"; +const BUFFER_SIZE: usize = 16384; #[derive(clap_derive::Parser)] #[clap(version, about)] @@ -50,6 +55,83 @@ enum Cmd { Lazy, } +/// Main configuration where all the site settings are set. +/// Blades deserializes it from a given TOML file. +#[derive(Default, Deserialize)] +struct Config<'c> { + /// The directory of the content + #[serde(borrow, default = "default_content_dir")] + content_dir: Cow<'c, str>, + /// The directory where the output should be rendered to + #[serde(borrow, default = "default_output_dir")] + output_dir: Cow<'c, str>, + /// The directory where the themes are + #[serde(borrow, default = "default_theme_dir")] + theme_dir: Cow<'c, str>, + /// Name of the directory of a theme this site is using, empty if none. + #[serde(borrow, default)] + theme: Cow<'c, str>, + /// Taxonomies of the site + #[serde(default)] + taxonomies: HashMap<&'c str, TaxonMeta<'c>>, + /// Generate taxonomies not specified in the config? + #[serde(default = "default_true")] + implicit_taxonomies: bool, + + /// Information about the site usable in templates + #[serde(flatten)] + site: Site<'c>, + + /// Configuration of plugins for building the site. + #[serde(default)] + plugins: Plugins<'c>, +} + +/// Plugins to use when building the site. +#[derive(Default, Deserialize)] +struct Plugins<'p> { + /// Plugins to get the input from, in the form of serialized list of pages. + #[serde(borrow, default)] + input: Box<[CmdBorrowed<'p>]>, + /// Plugins that transform the serialized list of pages. + #[serde(borrow, default)] + transform: Box<[CmdBorrowed<'p>]>, + /// Plugins that get the serialized list of pages and might do something with it. + #[serde(borrow, default)] + output: Box<[CmdBorrowed<'p>]>, + /// Plugins that transform the content of pages. + /// They are identified by their name and must be enabled for each page. + #[serde(borrow, default)] + content: HashMap<&'p str, CmdBorrowed<'p>>, + /// A list of names of content plugins that should be applied to every page. + #[serde(default)] + default: Box<[&'p str]>, +} + +#[inline] +const fn default_content_dir() -> Cow<'static, str> { + Cow::const_str("content") +} + +#[inline] +const fn default_output_dir() -> Cow<'static, str> { + Cow::const_str("public") +} + +#[inline] +const fn default_theme_dir() -> Cow<'static, str> { + Cow::const_str("themes") +} + +#[inline] +const fn default_true() -> bool { + true +} + +/// Where the templates are located, relative to the site directrory. +static TEMPLATE_DIR: &str = "templates"; +/// Where the assets will be copied from, relative to the site directrory. +static ASSET_SRC_DIR: &str = "assets"; static FILELIST: &str = ".blades"; static OLD_THEME: &str = ".bladestheme"; @@ -223,13 +305,10 @@ fn init() -> Result<(), Error> { let title = next_line(&mut lines, "Name:")?; let author = next_line(&mut lines, "Author:")?; let config = MockConfig { title, author }; - Template::new(include_str!("templates/Blades.toml")) - .unwrap() - .render_to_file(CONFIG_FILE, &config) - .unwrap(); - write(".watch.toml", include_str!("templates/.watch.toml"))?; - create_dir_all("content")?; - create_dir_all("themes").map_err(Into::into) + Template::new(include_str!("templates/Blades.toml"))?.render_to_file(CONFIG_FILE, &config)?; + fs::write(".watch.toml", include_str!("templates/.watch.toml"))?; + fs::create_dir_all("content")?; + fs::create_dir_all("themes").map_err(Into::into) } /// Create a new page and edit it if the EDITOR variable is set @@ -242,7 +321,7 @@ fn new_page(config: &Config) -> Result<(), Error> { &mut lines, "Path (relative to the content directory):", )?); - create_dir_all(&path)?; + fs::create_dir_all(&path)?; let date: chrono::DateTime = SystemTime::now().into(); let date = date.format("%Y-%m-%d").to_string(); @@ -262,10 +341,7 @@ fn new_page(config: &Config) -> Result<(), Error> { } } let page = MockPage { title, slug, date }; - Template::new(include_str!("templates/page.toml")) - .unwrap() - .render_to_file(&path, &page) - .unwrap(); + Template::new(include_str!("templates/page.toml"))?.render_to_file(&path, &page)?; println!("{:?} created", &path); if let Ok(editor) = var("EDITOR") { @@ -276,6 +352,107 @@ fn new_page(config: &Config) -> Result<(), Error> { Ok(()) } +fn copy_dir(src: &mut PathBuf, dest: &mut PathBuf) -> Result<(), io::Error> { + let iter = match fs::read_dir(&src) { + Ok(iter) => iter, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e), + }; + fs::create_dir_all(&dest)?; + for entry in iter.filter_map(Result::ok) { + let file_type = entry.file_type()?; + let file_name = entry.file_name(); + src.push(&file_name); + dest.push(&file_name); + if file_type.is_file() { + fs::copy(&src, &dest)?; + } else if file_type.is_dir() { + copy_dir(src, dest)?; + } + src.pop(); + dest.pop(); + } + Ok(()) +} + +/// Place assets located in the `assets` directory or in the `assets` subdirectory of the theme, +/// if used, into a dedicated subdirectory of the output directory specified in the config +/// (defaults to `assets`, too). +fn colocate_assets(config: &Config) -> Result<(), io::Error> { + let mut output = Path::new(config.output_dir.as_ref()).join(config.site.assets.as_ref()); + match fs::remove_dir_all(&output) { + Ok(_) => Ok(()), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + }?; + let mut src = PathBuf::with_capacity(64); + if !config.theme.is_empty() { + src.push(config.theme_dir.as_ref()); + src.push(config.theme.as_ref()); + src.push(ASSET_SRC_DIR); + copy_dir(&mut src, &mut output)?; + src.clear(); + } + src.push(ASSET_SRC_DIR); + copy_dir(&mut src, &mut output) +} + +/// Load the templates from the directories specified by the config. +fn load_templates(config: &Config) -> Result { + fs::create_dir_all(TEMPLATE_DIR)?; + let mut templates = Ramhorns::from_folder(TEMPLATE_DIR)?; + if !config.theme.is_empty() { + let mut theme_path = Path::new(config.theme_dir.as_ref()).join(config.theme.as_ref()); + theme_path.push(TEMPLATE_DIR); + if theme_path.exists() { + templates.extend_from_folder(theme_path)?; + } + } + Ok(templates) +} + +/// Delete all the pages that were present in the previous render, but not the current one. +/// Then, write all the paths that were rendered to the file `filelist` +fn cleanup(mut rendered: HashMap, filelist: &str) -> Result<(), io::Error> { + if let Ok(f) = File::open(filelist) { + BufReader::new(f).lines().try_for_each(|filename| { + let filename = filename?; + if !rendered.contains_key(Path::new(&filename)) { + // Every directory has its index rendered + if let Some(dir) = filename.strip_suffix("index.html") { + if dir.ends_with(path::is_separator) { + return match fs::remove_dir_all(dir) { + Ok(_) => Ok(()), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + }; + } + } + match fs::remove_file(&filename) { + Ok(_) => Ok(()), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + } + } else { + Ok(()) + } + })?; + }; + + let f = File::create(filelist)?; + let mut f = BufWriter::new(f); + for (path, count) in rendered.drain() { + // It was already checked that the paths contain valid UTF-8 + let path = path.into_os_string().into_string().unwrap(); + writeln!(&mut f, "{}", path)?; + if count > 1 { + println!("{} paths render to {}", count, path); + } + } + + Ok(()) +} + /// The actual logic of task parallelisation. fn build(config: &Config) -> Result<(), Error> { const MIN_PER_THREAD: usize = 5; @@ -391,39 +568,41 @@ fn build(config: &Config) -> Result<(), Error> { config.implicit_taxonomies, ); - let rendered = MutSet::default(); let output_dir = config.output_dir.as_ref().as_ref(); - let context = Context( - &pages, - &config.site, - &taxonomies, - &templates, - &rendered, - output_dir, - ); - thread::scope(|s| { + let context = Context(&pages, &config.site, &taxonomies, &templates, output_dir); + let rendered = thread::scope(|s| { let mut threads = Vec::with_capacity(num_threads); for chunk in pages.chunks(per_thread) { threads.push(s.spawn(|| { + let mut rendered = HashMap::default(); + let mut buffer = Vec::with_capacity(BUFFER_SIZE); for page in chunk.iter() { - page.render(context)?; + page.render(context, &mut rendered, &mut buffer)?; } - Ok::<_, Error>(()) + Ok::<_, Error>(rendered) })); } + let mut rendered = HashMap::default(); + let mut buffer = Vec::with_capacity(BUFFER_SIZE); for (_, taxonomy) in taxonomies.iter() { - taxonomy.render(context)?; + taxonomy.render(context, &mut rendered, &mut buffer)?; for (n, l) in taxonomy.keys().iter() { - taxonomy.render_key(n, l, context)?; + taxonomy.render_key(n, l, context, &mut rendered, &mut buffer)?; } } - render_meta(&pages, &config.site, &taxonomies, output_dir)?; + render_meta(&pages, &config.site, &taxonomies, output_dir, &mut buffer)?; for thread in threads.drain(..) { - thread.join().unwind()?; + let mut other = thread.join().unwind()?; + for (path, count) in other.drain() { + rendered + .entry(path) + .and_modify(|c| *c += count) + .or_default(); + } } - Ok::<_, Error>(()) + Ok::<_, Error>(rendered) })?; cleanup(rendered, FILELIST)?; @@ -484,12 +663,12 @@ fn main() { Some(Cmd::Colocate) => colocate_assets(&config).map_err(Into::into), Some(Cmd::All) => build(&config).and_then(|_| colocate_assets(&config).map_err(Into::into)), Some(Cmd::Lazy) | None => build(&config).and_then(|_| { - if read_to_string(OLD_THEME) + if fs::read_to_string(OLD_THEME) .map(|old| old != config.theme) .unwrap_or(true) { colocate_assets(&config)?; - write(OLD_THEME, config.theme.as_ref()).map_err(Into::into) + fs::write(OLD_THEME, config.theme.as_ref()).map_err(Into::into) } else { Ok(()) } diff --git a/src/page.rs b/src/page.rs index 4045828..ad41cef 100644 --- a/src/page.rs +++ b/src/page.rs @@ -6,11 +6,11 @@ // // You should have received a copy of the GNU General Public License // along with Blades. If not, see -use crate::config::{default_true, Site}; +use crate::render::render; +use crate::site::{default_true, Site}; use crate::sources::{Parser, Source, Sources}; -use crate::tasks::render; use crate::taxonomies::{Classification, Taxonomies}; -use crate::types::{Ancestors, Any, DateTime, HashMap, MutSet}; +use crate::types::{Ancestors, Any, DateTime, HashMap}; use beef::lean::Cow; use ramhorns::{ @@ -168,7 +168,6 @@ pub struct Context<'p, 'r>( pub &'r Site<'p>, pub &'r Classification<'p, 'r>, pub &'r Ramhorns, - pub &'r MutSet, pub &'r Path, ); @@ -269,13 +268,14 @@ pub(crate) trait Paginate: Content + Sized { /// Render `self` into separate pages where each can view just a subslice of `self`'s subpages. fn render_paginated( &self, - mut first: usize, - last: usize, + range: Range, by: usize, path: &mut PathBuf, tpl: &Template, - rendered: &MutSet, - ) -> Result<(), Error> { + rendered: &mut HashMap, + buffer: &mut Vec, + ) -> Result<(), io::Error> { + let (mut first, last) = (range.start, range.end); let count = last - first; let by = min(by, count); let len = count / by + ((count % by != 0) as usize); @@ -284,13 +284,20 @@ pub(crate) trait Paginate: Content + Sized { &path, &self.paginate(first..(first + by), len, 1), rendered, + buffer, )?; for i in 0..len { path.pop(); path.push((i + 1).to_string()); path.set_extension("html"); let end = min(first + by, last); - render(tpl, &path, &self.paginate(first..end, len, i + 1), rendered)?; + render( + tpl, + &path, + &self.paginate(first..end, len, i + 1), + rendered, + buffer, + )?; first = end; } Ok(()) @@ -398,10 +405,13 @@ impl<'p> Page<'p> { } /// Render the page to the output directory specified by the config. + /// `buffer` is used to store the result before writing it to the disk and expected to be empty. #[inline] pub fn render( &self, - Context(all, site, classification, templates, rendered, output_dir): Context<'p, '_>, + Context(all, site, classification, templates, output_dir): Context<'p, '_>, + rendered: &mut HashMap, + buffer: &mut Vec, ) -> Result<(), Error> { let mut output = output_dir.join(self.path.as_ref()); output.push(self.slug.as_ref()); @@ -427,9 +437,9 @@ impl<'p> Page<'p> { let by = self.paginate_by.map(NonZeroUsize::get).unwrap_or(0); if by > 0 && self.pages.len() > by { let (start, end) = (self.pages.start, self.pages.end); - page.render_paginated(start, end, by, &mut output, template, rendered)? + page.render_paginated(start..end, by, &mut output, template, rendered, buffer)? } else if !self.pictures.is_empty() { - render(template, &output, &page, rendered)?; + render(template, &output, &page, rendered, buffer)?; if self.is_section { output.pop(); @@ -456,17 +466,17 @@ impl<'p> Page<'p> { }; output.push(pictures[i].pid.as_ref()); output.set_extension("html"); - render(template, &output, &page, rendered)?; + render(template, &output, &page, rendered, buffer)?; output.pop(); } } else { - render(template, output, &page, rendered)?; + render(template, output, &page, rendered, buffer)?; } for path in self.alternative_paths.iter() { let mut output = output_dir.join(path); output.push("index.html"); - render(template, output, &page, rendered)?; + render(template, output, &page, rendered, buffer)?; } Ok(()) } diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..f7200f6 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,90 @@ +// Blades Copyright (C) 2021 Maroš Grego +// +// This file is part of Blades. This program comes with ABSOLUTELY NO WARRANTY; +// This is free software, and you are welcome to redistribute it under the +// conditions of the GNU General Public License version 3.0. +// +// You should have received a copy of the GNU General Public License +// along with Blades. If not, see +use crate::page::{Page, PageList}; +use crate::site::Site; +use crate::taxonomies::{Classification, TaxonList}; +use crate::types::{DateTime, HashMap}; + +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use ramhorns::{Content, Template}; + +#[inline] +pub(crate) fn render( + template: &Template, + path: P, + content: &C, + rendered: &mut HashMap, + buffer: &mut Vec, +) -> Result<(), io::Error> +where + C: Content, + P: Into, +{ + let path = path.into(); + // Can't fail + let _ = template.render_to_writer(buffer, content); + fs::write(&path, &buffer)?; + buffer.clear(); + let count = rendered.entry(path).or_default(); + *count += 1; + Ok(()) +} + +#[derive(Content)] +struct Meta<'p, 'r>( + #[ramhorns(rename = "date")] DateTime, + #[ramhorns(rename = "pages")] PageList<'p, 'r>, + #[ramhorns(rename = "taxons")] TaxonList<'p, 'r>, + #[ramhorns(rename = "site")] &'r Site<'p>, +); + +impl<'p> Meta<'p, '_> { + #[inline] + fn render( + &self, + name: &str, + template: &str, + path: &Path, + buffer: &mut Vec, + ) -> Result<(), ramhorns::Error> { + let _ = Template::new(template)?.render_to_writer(buffer, self); + fs::write(path.join(name), &buffer)?; + buffer.clear(); + Ok(()) + } +} + +/// Render sitemap, Atom and RSS feeds if enabled in the config. +pub fn render_meta<'p>( + pages: &[Page<'p>], + site: &Site<'p>, + taxons: &Classification<'p, '_>, + output_dir: &Path, + buffer: &mut Vec, +) -> Result<(), ramhorns::Error> { + let pages = PageList::new(pages, 0..pages.len(), 0, &site.url); + let meta = Meta(DateTime::now(), pages, TaxonList(taxons), site); + + if site.sitemap { + let sitemap = include_str!("templates/sitemap.xml"); + meta.render("sitemap.xml", sitemap, output_dir, buffer)?; + } + if site.rss { + let rss = include_str!("templates/rss.xml"); + meta.render("rss.xml", rss, output_dir, buffer)?; + } + if site.atom { + let atom = include_str!("templates/atom.xml"); + meta.render("atom.xml", atom, output_dir, buffer)?; + } + Ok(()) +} diff --git a/src/site.rs b/src/site.rs new file mode 100644 index 0000000..5b161bb --- /dev/null +++ b/src/site.rs @@ -0,0 +1,70 @@ +// Blades Copyright (C) 2021 Maroš Grego +// +// This file is part of Blades. This program comes with ABSOLUTELY NO WARRANTY; +// This is free software, and you are welcome to redistribute it under the +// conditions of the GNU General Public License version 3.0. +// +// You should have received a copy of the GNU General Public License +// along with Blades. If not, see +use crate::types::{Any, HashMap}; + +use beef::lean::Cow; +use ramhorns::Content; +use serde::{Deserialize, Serialize}; + +/// Information about the site usable in templates +#[derive(Content, Default, Deserialize, Serialize)] +pub struct Site<'c> { + /// Where the assets will be copied to, relative to the site directory. + #[serde(borrow, default = "default_assets")] + pub assets: Cow<'c, str>, + /// Title of the site + #[serde(borrow, default, skip_serializing_if = "str::is_empty")] + pub title: Cow<'c, str>, + /// Description of the site + #[serde(borrow, default, skip_serializing_if = "str::is_empty")] + pub description: Cow<'c, str>, + /// Keywords of the site + #[serde(borrow, default, skip_serializing_if = "str::is_empty")] + pub keywords: Cow<'c, str>, + /// A representative image of the site + #[serde(borrow, default, skip_serializing_if = "str::is_empty")] + pub image: Cow<'c, str>, + /// Language of the site + #[serde(borrow, default, skip_serializing_if = "str::is_empty")] + pub lang: Cow<'c, str>, + /// Name of the author of the site + #[serde(borrow, default)] + pub author: Cow<'c, str>, + /// Email of the webmaster + #[serde(borrow, default)] + pub email: Cow<'c, str>, + /// URL of the site + #[serde(borrow, default, skip_serializing_if = "str::is_empty")] + pub url: Cow<'c, str>, + + /// Generate a sitemap? + #[serde(default = "default_true")] + pub sitemap: bool, + /// Generate RSS feed? + #[serde(default = "default_true")] + pub rss: bool, + /// Generate Atom feed? + #[serde(default = "default_true")] + pub atom: bool, + + /// Extra values provided by the user + #[serde(flatten)] + #[ramhorns(flatten)] + pub extra: HashMap<&'c str, Any<'c>>, +} + +#[inline] +const fn default_assets() -> Cow<'static, str> { + Cow::const_str("assets") +} + +#[inline] +pub(crate) const fn default_true() -> bool { + true +} diff --git a/src/tasks.rs b/src/tasks.rs deleted file mode 100644 index 52e0226..0000000 --- a/src/tasks.rs +++ /dev/null @@ -1,178 +0,0 @@ -// Blades Copyright (C) 2021 Maroš Grego -// -// This file is part of Blades. This program comes with ABSOLUTELY NO WARRANTY; -// This is free software, and you are welcome to redistribute it under the -// conditions of the GNU General Public License version 3.0. -// -// You should have received a copy of the GNU General Public License -// along with Blades. If not, see -use crate::config::{Config, Site, ASSET_SRC_DIR, TEMPLATE_DIR}; -use crate::page::{Page, PageList}; -use crate::taxonomies::{Classification, TaxonList}; -use crate::types::{DateTime, MutSet}; - -use std::fs::{copy, create_dir_all, read_dir, remove_dir_all, remove_file, File}; -use std::io::{self, BufRead, BufReader, BufWriter, ErrorKind, Write}; -use std::path::{is_separator, Path, PathBuf}; - -use ramhorns::{Content, Ramhorns, Template}; - -/// Load the templates from the directories specified by the config. -#[inline] -pub fn load_templates(config: &Config) -> Result { - create_dir_all(TEMPLATE_DIR)?; - let mut templates = Ramhorns::from_folder(TEMPLATE_DIR)?; - if !config.theme.is_empty() { - let mut theme_path = Path::new(config.theme_dir.as_ref()).join(config.theme.as_ref()); - theme_path.push(TEMPLATE_DIR); - if theme_path.exists() { - templates.extend_from_folder(theme_path)?; - } - } - Ok(templates) -} - -#[inline] -pub(crate) fn render( - template: &Template, - path: P, - content: &C, - rendered: &MutSet, -) -> Result<(), ramhorns::Error> -where - C: Content, - P: Into, -{ - let path = path.into(); - template.render_to_file(&path, content)?; - if let Some(path) = rendered.lock().unwrap().replace(path) { - println!("Warning: more paths render to {}", path.to_string_lossy()); - } - Ok(()) -} - -#[derive(Content)] -struct Meta<'p, 'r>( - #[ramhorns(rename = "date")] DateTime, - #[ramhorns(rename = "pages")] PageList<'p, 'r>, - #[ramhorns(rename = "taxons")] TaxonList<'p, 'r>, - #[ramhorns(rename = "site")] &'r Site<'p>, -); - -impl<'p> Meta<'p, '_> { - #[inline] - fn render(&self, name: &str, template: &str, path: &Path) -> Result<(), ramhorns::Error> { - Template::new(template)? - .render_to_file(path.join(name), self) - .map_err(Into::into) - } -} - -/// Render sitemap, Atom and RSS feeds if enabled in the config. -pub fn render_meta<'p>( - pages: &[Page<'p>], - site: &Site<'p>, - taxons: &Classification<'p, '_>, - output_dir: &Path, -) -> Result<(), ramhorns::Error> { - let pages = PageList::new(pages, 0..pages.len(), 0, &site.url); - let meta = Meta(DateTime::now(), pages, TaxonList(taxons), site); - - if site.sitemap { - let sitemap = include_str!("templates/sitemap.xml"); - meta.render("sitemap.xml", sitemap, output_dir)?; - } - if site.rss { - let rss = include_str!("templates/rss.xml"); - meta.render("rss.xml", rss, output_dir)?; - } - if site.atom { - let atom = include_str!("templates/atom.xml"); - meta.render("atom.xml", atom, output_dir)?; - } - Ok(()) -} - -fn copy_dir(src: &mut PathBuf, dest: &mut PathBuf) -> Result<(), io::Error> { - let iter = match read_dir(&src) { - Ok(iter) => iter, - Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()), - Err(e) => return Err(e), - }; - create_dir_all(&dest)?; - for entry in iter.filter_map(Result::ok) { - let file_type = entry.file_type()?; - let file_name = entry.file_name(); - src.push(&file_name); - dest.push(&file_name); - if file_type.is_file() { - copy(&src, &dest)?; - } else if file_type.is_dir() { - copy_dir(src, dest)?; - } - src.pop(); - dest.pop(); - } - Ok(()) -} - -/// Place assets located in the `assets` directory or in the `assets` subdirectory of the theme, -/// if used, into a dedicated subdirectory of the output directory specified in the config -/// (defaults to `assets`, too). -pub fn colocate_assets(config: &Config) -> Result<(), io::Error> { - let mut output = Path::new(config.output_dir.as_ref()).join(config.site.assets.as_ref()); - match remove_dir_all(&output) { - Ok(_) => Ok(()), - Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), - Err(e) => Err(e), - }?; - let mut src = PathBuf::with_capacity(64); - if !config.theme.is_empty() { - src.push(config.theme_dir.as_ref()); - src.push(config.theme.as_ref()); - src.push(ASSET_SRC_DIR); - copy_dir(&mut src, &mut output)?; - src.clear(); - } - src.push(ASSET_SRC_DIR); - copy_dir(&mut src, &mut output) -} - -/// Delete all the pages that were present in the previous render, but not the current one. -/// Then, write all the paths that were rendered to the file `filelist` -pub fn cleanup(rendered: MutSet, filelist: &str) -> Result<(), io::Error> { - let rendered = rendered.into_inner().unwrap(); - if let Ok(f) = File::open(filelist) { - BufReader::new(f).lines().try_for_each(|filename| { - let filename = filename?; - if !rendered.contains(Path::new(&filename)) { - // Every directory has its index rendered - if let Some(dir) = filename.strip_suffix("index.html") { - if dir.ends_with(is_separator) { - return match remove_dir_all(dir) { - Ok(_) => Ok(()), - Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), - Err(e) => Err(e), - }; - } - } - match remove_file(&filename) { - Ok(_) => Ok(()), - Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), - Err(e) => Err(e), - } - } else { - Ok(()) - } - })?; - }; - - let f = File::create(filelist)?; - let mut f = BufWriter::new(f); - for path in rendered { - // It was already checked that the paths contain valid UTF-8 - writeln!(&mut f, "{}", path.into_os_string().into_string().unwrap())?; - } - - Ok(()) -} diff --git a/src/taxonomies.rs b/src/taxonomies.rs index 8bfee06..6df8857 100644 --- a/src/taxonomies.rs +++ b/src/taxonomies.rs @@ -6,9 +6,9 @@ // // You should have received a copy of the GNU General Public License // along with Blades. If not, see -use crate::config::Site; use crate::page::{Context, Page, PageRef, Paginate, Pagination, Permalink}; -use crate::tasks::render; +use crate::render::render; +use crate::site::Site; use crate::types::HashMap; use arrayvec::ArrayVec; @@ -22,6 +22,7 @@ use std::collections::BTreeMap; use std::fs::create_dir_all; use std::num::NonZeroUsize; use std::ops::{Deref, Range}; +use std::path::PathBuf; const DEFAULT_TEMPLATE: &str = "taxonomy.html"; const DEFAULT_KEY_TEMPLATE: &str = "taxonomy_key.html"; @@ -224,10 +225,13 @@ impl<'t, 'r> Taxonomy<'t, 'r> { } /// Render this taxonomy into the output directory specified by the config. + /// `buffer` is used to store the result before writing it to the disk and expected to be empty. #[inline] pub fn render( &self, - Context(all, site, classification, templates, rendered, output_dir): Context<'t, '_>, + Context(all, site, classification, templates, output_dir): Context<'t, '_>, + rendered: &mut HashMap, + buffer: &mut Vec, ) -> Result<(), Error> { let mut path = output_dir.join(self.slug); create_dir_all(&path)?; @@ -242,16 +246,19 @@ impl<'t, 'r> Taxonomy<'t, 'r> { let template = templates .get(&self.taxonomy.template) .ok_or_else(|| Error::NotFound(self.taxonomy.template.as_ref().into()))?; - render(template, path, &contexted, rendered) + render(template, path, &contexted, rendered, buffer).map_err(Into::into) } /// Render one key of this taxonomy into the output directory specified by the config. + /// `buffer` is used to store the result before writing it to the disk and expected to be empty. #[inline] pub fn render_key( &self, title: &str, pages: &[PageLinked<'t, '_>], - Context(all, site, classification, templates, rendered, output_dir): Context<'t, '_>, + Context(all, site, classification, templates, output_dir): Context<'t, '_>, + rendered: &mut HashMap, + buffer: &mut Vec, ) -> Result<(), Error> { let mut output = output_dir.join(self.slug); output.push(title); @@ -277,10 +284,11 @@ impl<'t, 'r> Taxonomy<'t, 'r> { .get(&self.taxonomy.key_template) .ok_or_else(|| Error::NotFound(self.taxonomy.key_template.as_ref().into()))?; if by > 0 && pages.len() > by { - contexted.render_paginated(0, pages.len(), by, &mut output, template, rendered) + contexted.render_paginated(0..pages.len(), by, &mut output, template, rendered, buffer) } else { - render(template, output, &contexted, rendered) + render(template, output, &contexted, rendered, buffer) } + .map_err(Into::into) } } diff --git a/src/types.rs b/src/types.rs index ceaa1c1..821b0cd 100644 --- a/src/types.rs +++ b/src/types.rs @@ -14,17 +14,12 @@ use ramhorns::{Content, Section}; use serde::de::{self, Deserialize, Deserializer, Visitor}; use std::borrow::Borrow; -use std::collections::HashSet; use std::fmt; use std::hash::Hash; use std::ops::{Deref, DerefMut}; -use std::path::{is_separator, PathBuf}; -use std::sync::Mutex; +use std::path::is_separator; use std::time::SystemTime; -/// A set of all rendered paths. Behind a mutex, so it can be written from multiple threads. -pub type MutSet = Mutex>; - /// A hash map wrapper that can render fields directly by the hash. #[derive(Clone, serde::Deserialize, serde::Serialize)] #[serde(transparent)] @@ -35,8 +30,8 @@ pub struct HashMap(pub(crate) hashbrown::HashMap(#[serde(borrow)] pub Cow<'a, str>);