From e432f906a1e818a9210e297939a28270d5e09e1b Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 19 Aug 2021 13:43:45 -0700 Subject: [PATCH] Add Sass support --- Cargo.lock | 231 +++++++++++++++++++++- Cargo.toml | 1 + site/config.yaml | 1 + site/{static/site.css => sass/index.scss} | 8 +- site/templates/base.hbs | 2 +- src/builder.rs | 216 ++++++++++++++++++++ src/lib.rs | 188 +----------------- src/serving.rs | 61 ++++-- src/util.rs | 22 +++ 9 files changed, 525 insertions(+), 205 deletions(-) rename site/{static/site.css => sass/index.scss} (76%) create mode 100644 src/builder.rs create mode 100644 src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 9f77f3c..17bf5f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom 0.2.3", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -17,12 +28,32 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "anyhow" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + [[package]] name = "autocfg" version = "0.1.7" @@ -41,6 +72,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "beef" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed554bd50246729a1ec158d08aa3235d1b69d94ad120ebe187e28894787e736" + [[package]] name = "bitflags" version = "1.3.2" @@ -117,6 +154,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -126,6 +178,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "codemap" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" + [[package]] name = "cpufeatures" version = "0.1.5" @@ -146,7 +204,7 @@ dependencies = [ "dtoa-short", "itoa", "matches", - "phf", + "phf 0.7.24", "proc-macro2", "procedural-masquerade", "quote", @@ -440,6 +498,25 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "grass" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82317908bc4204532d098390f8e041693aaeab95cf7351f774bdacf253b1c8ed" +dependencies = [ + "beef", + "clap", + "codemap", + "indexmap", + "lasso", + "num-bigint", + "num-rational", + "num-traits", + "once_cell", + "phf 0.9.0", + "rand 0.8.4", +] + [[package]] name = "gray_matter" version = "0.2.0" @@ -493,7 +570,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash", + "ahash 0.4.7", ] [[package]] @@ -501,6 +578,9 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.4", +] [[package]] name = "headers" @@ -685,6 +765,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "lasso" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8647c8a01e5f7878eacb2c323c4c949fdb63773110f0686c7810769874b7e0a" +dependencies = [ + "hashbrown 0.11.2", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -904,6 +993,48 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.0.1", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg 1.0.1", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -914,6 +1045,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "opaque-debug" version = "0.2.3" @@ -981,7 +1118,18 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" dependencies = [ - "phf_shared", + "phf_shared 0.7.24", +] + +[[package]] +name = "phf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" +dependencies = [ + "phf_macros", + "phf_shared 0.9.0", + "proc-macro-hack", ] [[package]] @@ -990,8 +1138,8 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.7.24", + "phf_shared 0.7.24", ] [[package]] @@ -1000,17 +1148,50 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" dependencies = [ - "phf_shared", + "phf_shared 0.7.24", "rand 0.6.5", ] +[[package]] +name = "phf_generator" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082" +dependencies = [ + "phf_shared 0.9.0", + "rand 0.8.4", +] + +[[package]] +name = "phf_macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86" +dependencies = [ + "phf_generator 0.9.1", + "phf_shared 0.9.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" dependencies = [ - "siphasher", + "siphasher 0.2.3", +] + +[[package]] +name = "phf_shared" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9" +dependencies = [ + "siphasher 0.3.6", ] [[package]] @@ -1385,7 +1566,7 @@ dependencies = [ "fxhash", "log", "matches", - "phf", + "phf 0.7.24", "phf_codegen", "precomputed-hash", "servo_arc", @@ -1489,6 +1670,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" +[[package]] +name = "siphasher" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1" + [[package]] name = "slab" version = "0.4.4" @@ -1520,6 +1707,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "syn" version = "1.0.74" @@ -1545,6 +1738,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thin-slice" version = "0.1.1" @@ -1768,6 +1970,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -1792,6 +2000,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.3" @@ -1930,6 +2144,7 @@ dependencies = [ "extract-frontmatter", "fs_extra", "futures", + "grass", "gray_matter", "handlebars", "hotwatch", diff --git a/Cargo.toml b/Cargo.toml index c7502dc..cdcf2a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1" extract-frontmatter = "2.1" fs_extra = "1.2" futures = { version = "0.3", optional = true } +grass = "0.10" gray_matter = "0.2" handlebars = "4.1" hotwatch = { version = "0.4", optional = true } diff --git a/site/config.yaml b/site/config.yaml index 7f3005e..8e929d7 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -1,3 +1,4 @@ base_url: "https://zoey.dev" title: zoey.dev description: "Zoey's site." +sass_styles: [index.scss] diff --git a/site/static/site.css b/site/sass/index.scss similarity index 76% rename from site/static/site.css rename to site/sass/index.scss index bf52da3..600e3a6 100644 --- a/site/static/site.css +++ b/site/sass/index.scss @@ -11,11 +11,11 @@ header.main-header { a { text-decoration: none; -} -a:hover, -a:focus { - text-decoration: underline; + &:hover, + &:focus { + text-decoration: underline; + } } main.page { diff --git a/site/templates/base.hbs b/site/templates/base.hbs index 65bc59e..1e77d8c 100644 --- a/site/templates/base.hbs +++ b/site/templates/base.hbs @@ -4,7 +4,7 @@ {{{head}}} - + diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..d5f8a8a --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,216 @@ +//! Module containing the site builder. + +use std::{path::PathBuf, str::FromStr}; + +use anyhow::Context; +use gray_matter::{engine::YAML, Matter}; +use handlebars::Handlebars; +use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings}; +use pulldown_cmark::{Options, Parser}; +use serde::Serialize; +use walkdir::WalkDir; +use warp::hyper::Uri; + +use crate::{util, PageMetadata, Site, SASS_PATH, STATIC_PATH}; + +/// Struct containing data to be sent to templates when rendering them. +#[derive(Debug, Serialize)] +struct TemplateData<'a> { + /// The rendered page. + pub page: &'a str, +} + +/// Struct used to build the site. +pub struct SiteBuilder<'a> { + /// The matter instance used to extract front matter. + matter: Matter, + /// The Handlebars registry used to render templates. + pub(crate) reg: Handlebars<'a>, + /// The site info used to build the site. + pub site: Site, + /// The path to the build directory. + pub build_path: PathBuf, + /// Whether the site is going to be served locally with the dev server. + serving: bool, +} + +impl<'a> SiteBuilder<'a> { + /// Creates a new site builder. + pub fn new(site: Site, serving: bool) -> Self { + let mut build_path = match &site.config.build { + Some(build) => site.site_path.join(build), + _ => site.site_path.join("build"), + }; + if serving { + build_path = site.site_path.join("build"); + } + + Self { + matter: Matter::new(), + reg: Handlebars::new(), + site, + build_path, + serving, + } + } + + /// Prepares the site builder for use. + pub fn prepare(mut self) -> anyhow::Result { + let build_static_path = self.build_path.join(STATIC_PATH); + if std::fs::try_exists(&self.build_path) + .context("Failed check if build directory exists")? + { + if build_static_path.exists() { + std::fs::remove_dir_all(&build_static_path) + .context("Failed to remove old static directory")?; + } + for entry in WalkDir::new(&self.build_path) { + let entry = entry?; + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext == "html" { + std::fs::remove_file(path).with_context(|| { + format!("Failed to remove file at {}", path.display()) + })?; + } + } + } + } else { + std::fs::create_dir(&self.build_path).context("Failed to create build directory")?; + } + + for (template_name, template_path) in &self.site.template_index { + self.reg + .register_template_file(template_name, template_path) + .context("Failed to register template file")?; + } + + let static_path = self.site.site_path.join(STATIC_PATH); + if static_path.exists() { + fs_extra::copy_items( + &[static_path], + &self.build_path, + &fs_extra::dir::CopyOptions::default(), + ) + .context("Failed to copy static directory")?; + } + + Ok(self) + } + + /// Builds a page. + pub fn build_page(&self, page_name: &str) -> anyhow::Result<()> { + let page_path = self.site.page_index.get(page_name).unwrap(); + + let input = std::fs::read_to_string(page_path) + .with_context(|| format!("Failed to read page at {}", page_path.display()))?; + let page = self.matter.parse(&input); + let page_metadata = if let Some(data) = page.data { + data.deserialize()? + } else { + PageMetadata::default() + }; + + let parser = Parser::new_ext(&page.content, Options::all()); + let mut page_html = String::new(); + pulldown_cmark::html::push_html(&mut page_html, parser); + + let out = self.reg.render( + &page_metadata.template.unwrap_or_else(|| "base".to_string()), + &TemplateData { page: &page_html }, + )?; + + let title = match &page_metadata.title { + Some(page_title) => format!("{} / {}", self.site.config.title, page_title), + _ => self.site.config.title.clone(), + }; + + let mut output = Vec::new(); + let mut rewriter = HtmlRewriter::new( + Settings { + element_content_handlers: vec![ + element!("head", |el| { + el.prepend(r#""#, ContentType::Html); + el.append(&format!("{}", title), ContentType::Html); + if self.serving { + el.append( + &format!(r#""#, STATIC_PATH), + ContentType::Html, + ); + } else { + el.append( + &format!(r#""#, &self.site.config.base_url), + ContentType::Html, + ); + } + + Ok(()) + }), + element!("a", |el| { + if let Some(href) = el.get_attribute("href") { + if let Ok(uri) = Uri::from_str(&href) { + if uri.host().is_some() { + el.set_attribute("rel", "noopener noreferrer")?; + el.set_attribute("target", "_blank")?; + } + } + } + + Ok(()) + }), + ], + ..Default::default() + }, + |c: &[u8]| output.extend_from_slice(c), + ); + + rewriter.write(out.as_bytes())?; + rewriter.end()?; + + let out = String::from_utf8(output)?; + + let out_path = self.build_path.join(page_name).with_extension("html"); + std::fs::create_dir_all(out_path.parent().unwrap()) + .with_context(|| format!("Failed to create directory for page {}", page_name))?; + std::fs::write(&out_path, out).with_context(|| { + format!( + "Failed to create page file at {} for page {}", + out_path.display(), + page_name + ) + })?; + + Ok(()) + } + + /// Builds the Sass styles in the site. + pub fn build_sass(&self) -> anyhow::Result<()> { + let styles_path = self.build_path.join("styles"); + if self.serving { + util::remove_dir_contents(&styles_path) + .context("Failed to remove old contents of styles directory")?; + } + let sass_path = self.site.site_path.join(SASS_PATH); + for sheet in &self.site.config.sass_styles { + let sheet_path = sass_path.join(sheet); + if let Some(sheet_path) = sheet_path.to_str() { + match grass::from_path(sheet_path, &grass::Options::default()) { + Ok(css) => { + std::fs::write(styles_path.join(sheet).with_extension("css"), css) + .with_context(|| { + format!("Failed to write new CSS file for Sass: {:?}", sheet) + })?; + } + Err(e) => eprintln!( + "Failed to compile Sass stylesheet at {:?}: {}", + sheet_path, e + ), + } + } else { + eprintln!("Sass stylesheet contains invalid UTF-8: {:?}", sheet_path); + } + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 6ef8411..60bd90d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,26 @@ #![feature(path_try_exists)] #![feature(async_closure)] +mod builder; #[cfg(feature = "serve")] pub mod serving; +mod util; use std::{ collections::HashMap, path::{Path, PathBuf}, - str::FromStr, }; use anyhow::Context; -use gray_matter::{engine::yaml::YAML, matter::Matter}; -use handlebars::Handlebars; -use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings}; -use pulldown_cmark::{Options, Parser}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use walkdir::WalkDir; -use warp::hyper::Uri; + +use builder::SiteBuilder; const PAGES_PATH: &str = "pages"; const TEMPLATES_PATH: &str = "templates"; const STATIC_PATH: &str = "static"; +const SASS_PATH: &str = "sass"; /// Struct for the site's configuration. #[derive(Debug, Deserialize)] @@ -34,19 +33,14 @@ pub struct SiteConfig { pub description: String, /// The site's build directory. Defaults to /build if not specified. pub build: Option, + /// A list of Sass stylesheets that will be built. + pub sass_styles: Vec, } /// Struct for the front matter in templates. (nothing here yet) #[derive(Debug, Default, Deserialize)] pub struct TemplateMetadata {} -/// Struct containing data to be sent to templates when rendering them. -#[derive(Debug, Serialize)] -struct TemplateData<'a> { - /// The rendered page. - pub page: &'a str, -} - /// Struct for the front matter in pages. #[derive(Debug, Default, Deserialize)] pub struct PageMetadata { @@ -131,6 +125,7 @@ impl Site { let builder = SiteBuilder::new(self, false).prepare()?; builder.site.build_all_pages(&builder)?; + builder.build_sass()?; Ok(()) } @@ -144,168 +139,3 @@ impl Site { Ok(()) } } - -/// Struct used to build the site. -struct SiteBuilder<'a> { - /// The matter instance used to extract front matter. - matter: Matter, - /// The Handlebars registry used to render templates. - reg: Handlebars<'a>, - /// The site info used to build the site. - site: Site, - /// The path to the build directory. - build_path: PathBuf, - /// Whether the site is going to be served locally with the dev server. - serving: bool, -} - -impl<'a> SiteBuilder<'a> { - /// Creates a new site builder. - pub fn new(site: Site, serving: bool) -> Self { - let mut build_path = match &site.config.build { - Some(build) => site.site_path.join(build), - _ => site.site_path.join("build"), - }; - if serving { - build_path = site.site_path.join("build"); - } - - Self { - matter: Matter::new(), - reg: Handlebars::new(), - site, - build_path, - serving, - } - } - - /// Prepares the site builder for use. - pub fn prepare(mut self) -> anyhow::Result { - if std::fs::try_exists(&self.build_path) - .context("Failed check if build directory exists")? - { - std::fs::remove_dir_all(self.build_path.join(STATIC_PATH)) - .context("Failed to remove static directory")?; - for entry in WalkDir::new(&self.build_path) { - let entry = entry?; - let path = entry.path(); - if let Some(ext) = path.extension() { - if ext == "html" { - std::fs::remove_file(path).with_context(|| { - format!("Failed to remove file at {}", path.display()) - })?; - } - } - } - } else { - std::fs::create_dir(&self.build_path).context("Failed to create build directory")?; - } - - for (template_name, template_path) in &self.site.template_index { - self.reg - .register_template_file(template_name, template_path) - .context("Failed to register template file")?; - } - - fs_extra::copy_items( - &[self.site.site_path.join(STATIC_PATH)], - &self.build_path, - &fs_extra::dir::CopyOptions::default(), - ) - .context("Failed to copy static directory")?; - - if self.serving { - std::fs::write( - self.build_path.join(format!("{}/_dev.js", STATIC_PATH)), - include_str!("./refresh_websocket.js"), - )?; - } - - Ok(self) - } - - /// Builds a page. - pub fn build_page(&self, page_name: &str) -> anyhow::Result<()> { - let page_path = self.site.page_index.get(page_name).unwrap(); - - let input = std::fs::read_to_string(page_path) - .with_context(|| format!("Failed to read page at {}", page_path.display()))?; - let page = self.matter.parse(&input); - let page_metadata = if let Some(data) = page.data { - data.deserialize()? - } else { - PageMetadata::default() - }; - - let parser = Parser::new_ext(&page.content, Options::all()); - let mut page_html = String::new(); - pulldown_cmark::html::push_html(&mut page_html, parser); - - let out = self.reg.render( - &page_metadata.template.unwrap_or_else(|| "base".to_string()), - &TemplateData { page: &page_html }, - )?; - - let title = match &page_metadata.title { - Some(page_title) => format!("{} / {}", self.site.config.title, page_title), - _ => self.site.config.title.clone(), - }; - - let mut output = Vec::new(); - let mut rewriter = HtmlRewriter::new( - Settings { - element_content_handlers: vec![ - element!("head", |el| { - el.prepend(r#""#, ContentType::Html); - el.append(&format!("{}", title), ContentType::Html); - if self.serving { - el.append( - &format!(r#""#, STATIC_PATH), - ContentType::Html, - ); - } else { - el.append( - &format!(r#""#, &self.site.config.base_url), - ContentType::Html, - ); - } - - Ok(()) - }), - element!("a", |el| { - if let Some(href) = el.get_attribute("href") { - if let Ok(uri) = Uri::from_str(&href) { - if uri.host().is_some() { - el.set_attribute("rel", "noopener noreferrer")?; - el.set_attribute("target", "_blank")?; - } - } - } - - Ok(()) - }), - ], - ..Default::default() - }, - |c: &[u8]| output.extend_from_slice(c), - ); - - rewriter.write(out.as_bytes())?; - rewriter.end()?; - - let out = String::from_utf8(output)?; - - let out_path = self.build_path.join(page_name).with_extension("html"); - std::fs::create_dir_all(out_path.parent().unwrap()) - .with_context(|| format!("Failed to create directory for page {}", page_name))?; - std::fs::write(&out_path, out).with_context(|| { - format!( - "Failed to create page file at {} for page {}", - out_path.display(), - page_name - ) - })?; - - Ok(()) - } -} diff --git a/src/serving.rs b/src/serving.rs index 502e17f..3646572 100644 --- a/src/serving.rs +++ b/src/serving.rs @@ -7,6 +7,7 @@ use std::{ sync::{Arc, Mutex}, }; +use anyhow::Context; use futures::SinkExt; use hotwatch::{Event, Hotwatch}; use warp::{ @@ -17,7 +18,7 @@ use warp::{ Filter, }; -use crate::{Site, SiteBuilder, PAGES_PATH, STATIC_PATH, TEMPLATES_PATH}; +use crate::{Site, SiteBuilder, PAGES_PATH, SASS_PATH, STATIC_PATH, TEMPLATES_PATH}; fn with_build_path( build_path: PathBuf, @@ -44,6 +45,9 @@ fn create( relative_path: &Path, build: bool, ) -> anyhow::Result<()> { + if path.is_dir() { + return Ok(()); + } if let Ok(page_path) = relative_path.strip_prefix(PAGES_PATH) { let (_page_name, page_name_str) = get_name(page_path); @@ -66,25 +70,40 @@ fn create( let new_config = serde_yaml::from_str(&std::fs::read_to_string(path)?)?; builder.site.config = new_config; builder.site.build_all_pages(builder)?; + } else if let Ok(_sass_path) = relative_path.strip_prefix(SASS_PATH) { + if build { + builder.build_sass().context("Failed to rebuild Sass")?; + } } Ok(()) } /// Removes an existing resource. -fn remove(builder: &mut SiteBuilder, _path: &Path, relative_path: &Path) -> anyhow::Result<()> { +fn remove(builder: &mut SiteBuilder, path: &Path, relative_path: &Path) -> anyhow::Result<()> { + if path.is_dir() { + return Ok(()); + } if let Ok(page_path) = relative_path.strip_prefix(PAGES_PATH) { let (page_name, page_name_str) = get_name(page_path); builder.site.page_index.remove(&page_name_str); - std::fs::remove_file(builder.build_path.join(page_name.with_extension("html")))?; + std::fs::remove_file(builder.build_path.join(page_name.with_extension("html"))) + .with_context(|| format!("Failed to remove page at {:?}", path))?; } else if let Ok(template_path) = relative_path.strip_prefix(TEMPLATES_PATH) { let (_template_name, template_name_str) = get_name(template_path); builder.site.template_index.remove(&template_name_str); builder.reg.unregister_template(&template_name_str); - builder.site.build_all_pages(builder)?; + builder + .site + .build_all_pages(builder) + .context("Failed to rebuild pages")?; } else if let Ok(_static_path) = relative_path.strip_prefix(STATIC_PATH) { - std::fs::remove_file(builder.build_path.join(relative_path))?; + let to_remove = builder.build_path.join(relative_path); + std::fs::remove_file(&to_remove) + .with_context(|| format!("Failed to remove file at {:?}", to_remove))?; + } else if let Ok(_sass_path) = relative_path.strip_prefix(SASS_PATH) { + builder.build_sass().context("Failed to rebuild Sass")?; } Ok(()) @@ -110,6 +129,7 @@ impl Site { eprintln!("Failed to build page {}: {}", page_name, e); } } + builder.build_sass().context("Failed to build Sass")?; // Map of websocket connections let peers: Arc>> = @@ -120,7 +140,6 @@ impl Site { let hw_peers = peers.clone(); hotwatch .watch(site.site_path.clone(), move |event| { - let site_path = builder.site.site_path.canonicalize().unwrap(); let peers = hw_peers.clone(); match (|| match event { @@ -128,7 +147,7 @@ impl Site { if skip_path(&builder, &path) { Ok(false) } else { - let rel = rel(&path, &site_path)?; + let rel = rel(&path, &builder.site.site_path)?; println!("CHANGED - {:?}", rel); create(&mut builder, &path, &rel, true)?; Ok::<_, anyhow::Error>(true) @@ -138,7 +157,7 @@ impl Site { if skip_path(&builder, &path) { Ok(false) } else { - let rel = rel(&path, &site_path)?; + let rel = rel(&path, &builder.site.site_path)?; println!("CREATED - {:?}", rel); create(&mut builder, &path, &rel, true)?; Ok(true) @@ -148,7 +167,7 @@ impl Site { if skip_path(&builder, &path) { Ok(false) } else { - let rel = rel(&path, &site_path)?; + let rel = rel(&path, &builder.site.site_path)?; println!("REMOVED - {:?}", rel); remove(&mut builder, &path, &rel)?; Ok(true) @@ -158,8 +177,8 @@ impl Site { if skip_path(&builder, &old) && skip_path(&builder, &new) { Ok(false) } else { - let old_rel = rel(&old, &site_path)?; - let new_rel = rel(&new, &site_path)?; + let old_rel = rel(&old, &builder.site.site_path)?; + let new_rel = rel(&new, &builder.site.site_path)?; println!("RENAMED - {:?} -> {:?}", old_rel, new_rel); create(&mut builder, &new, &new_rel, false)?; remove(&mut builder, &old, &old_rel)?; @@ -218,6 +237,13 @@ impl Site { -> Result { // Serve static files let p = &path.as_str()[1..]; + + if p == "static/_dev.js" { + let res = + Response::new(include_str!("./refresh_websocket.js").into()); + return Ok(res); + } + let mut p = build_path.join(p); if !p.exists() { @@ -228,8 +254,17 @@ impl Site { } if p.exists() { - let body = std::fs::read_to_string(&p).unwrap(); - let res = Response::new(body.into()); + let mut res = Response::new("".into()); + match std::fs::read_to_string(&p) { + Ok(body) => { + *res.body_mut() = body.into(); + } + Err(e) => { + eprintln!("{}", e); + *res.body_mut() = format!("Failed to load: {}", e).into(); + *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + } + } return Ok(res); } Err(warp::reject()) diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..8625a9e --- /dev/null +++ b/src/util.rs @@ -0,0 +1,22 @@ +//! Module containing various utilities. + +use std::path::Path; + +/// Simple helper to remove the contents of a directory without removing the directory itself. +pub fn remove_dir_contents(path: &Path) -> anyhow::Result<()> { + if !path.exists() { + std::fs::create_dir_all(path)?; + return Ok(()); + } + for entry in path.read_dir()? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + std::fs::remove_file(&path)?; + } else { + std::fs::remove_dir_all(&path)?; + } + } + + Ok(()) +}