diff --git a/Cargo.lock b/Cargo.lock index 924f079..bcf7b32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +38,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -245,6 +263,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "codemap" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" + [[package]] name = "colorchoice" version = "1.0.3" @@ -561,8 +585,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -595,11 +621,38 @@ dependencies = [ "walkdir", ] +[[package]] +name = "grass" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a68216437ef68f0738e48d6c7bb9e6e6a92237e001b03d838314b068f33c94" +dependencies = [ + "getrandom", + "grass_compiler", +] + +[[package]] +name = "grass_compiler" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" +dependencies = [ + "codemap", + "indexmap", + "lasso", + "once_cell", + "phf", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "html5ever" @@ -889,6 +942,15 @@ dependencies = [ "libc", ] +[[package]] +name = "lasso" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" +dependencies = [ + "hashbrown", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1156,6 +1218,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros", "phf_shared 0.11.2", ] @@ -1189,6 +1252,19 @@ dependencies = [ "rand", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.10.0" @@ -1821,6 +1897,7 @@ dependencies = [ "debounced", "env_logger", "futures", + "grass", "html5ever", "log", "markup5ever_rcdom", diff --git a/Cargo.toml b/Cargo.toml index 7ca9bb1..7680a1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ compute_graph = { path = "crates/compute_graph" } debounced = "0.2.0" env_logger = "0.11.6" futures = "0.3.31" +grass = { version = "0.13.4", default-features = false } html5ever = "0.27.0" log = "0.4.22" markup5ever_rcdom = "0.3.0" diff --git a/site_test/css/main.scss b/site_test/css/main.scss new file mode 100644 index 0000000..b6f6b6d --- /dev/null +++ b/site_test/css/main.scss @@ -0,0 +1,5 @@ +.foo { + .bar { + color: red; + } +} diff --git a/src/generator/css.rs b/src/generator/css.rs new file mode 100644 index 0000000..94ef7b0 --- /dev/null +++ b/src/generator/css.rs @@ -0,0 +1,90 @@ +use std::{cell::RefCell, collections::HashSet, io::Write, path::PathBuf, rc::Rc}; + +use compute_graph::{ + InvalidationSignal, + builder::GraphBuilder, + rule::{Input, InputVisitable, Rule}, + synchronicity::Asynchronous, +}; +use grass::{Fs, Options, OutputStyle}; +use log::error; + +use super::{ + FileWatcher, + util::{content_path, output_writer}, +}; + +pub fn make_graph( + builder: &mut GraphBuilder<(), Asynchronous>, + watcher: Rc>, +) -> Input<()> { + let invalidate_css_box = Rc::new(RefCell::new(None)); + let (css, invalidate_css) = builder.add_invalidatable_rule(CompileScss { + watcher, + watched: HashSet::new(), + invalidate: Rc::clone(&invalidate_css_box), + }); + invalidate_css_box.replace(Some(invalidate_css)); + css +} + +#[derive(InputVisitable)] +struct CompileScss { + watcher: Rc>, + watched: HashSet, + invalidate: Rc>>, +} +impl Rule for CompileScss { + type Output = (); + fn evaluate(&mut self) -> Self::Output { + let read_files = RefCell::new(vec![]); + let fs = TrackingFs(&read_files); + let style = if cfg!(debug_assertions) { + OutputStyle::Expanded + } else { + OutputStyle::Compressed + }; + let options = Options::default().fs(&fs).style(style); + let result = grass::from_path(content_path("css/main.scss"), &options); + let mut watcher = self.watcher.borrow_mut(); + for file in read_files.take() { + if !self.watched.contains(&file) { + self.watched.insert(file.clone()); + let signal = self.invalidate.borrow().clone().unwrap(); + watcher.watch(file, move || signal.invalidate()); + } + } + match result { + Ok(s) => { + output_writer("css/main.css") + .expect("css writer") + .write_all(s.as_bytes()) + .expect("writing css"); + } + Err(e) => { + error!("Error compiling sass: {:?}", e); + } + } + } +} + +#[derive(Debug)] +struct TrackingFs<'a>(&'a RefCell>); +impl<'a> Fs for TrackingFs<'a> { + fn is_file(&self, path: &std::path::Path) -> bool { + path.is_file() + } + + fn is_dir(&self, path: &std::path::Path) -> bool { + path.is_dir() + } + + fn read(&self, path: &std::path::Path) -> std::io::Result> { + self.0.borrow_mut().push(path.to_owned()); + std::fs::read(path) + } + + fn canonicalize(&self, path: &std::path::Path) -> std::io::Result { + std::fs::canonicalize(path) + } +} diff --git a/src/generator/markdown.rs b/src/generator/markdown.rs index 4e3fad9..d074e16 100644 --- a/src/generator/markdown.rs +++ b/src/generator/markdown.rs @@ -18,6 +18,7 @@ pub fn parse<'a>(s: &'a str) -> impl Iterator> { options.insert(Options::ENABLE_STRIKETHROUGH); options.insert(Options::ENABLE_SMART_PUNCTUATION); let parser = Parser::new_ext(s, options); + // TODO: revisit which of these stages are necessary, remove unused (and url crate dep) let heading_anchors = heading_anchors::new(parser); let link_decorations = link_decorations::new(heading_anchors); // note backrefs need to come before defs, because the defs stage replaces the diff --git a/src/generator/mod.rs b/src/generator/mod.rs index 8f8c25f..046b0ed 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -1,4 +1,5 @@ mod archive; +mod css; mod markdown; mod posts; mod tags; @@ -41,30 +42,23 @@ fn make_graph(watcher: Rc>) -> anyhow::Result, -// posts: Input<()>, -// } -// impl Rule for Output { -// type Output = (); -// fn evaluate(&mut self) -> Self::Output {} -// } diff --git a/src/generator/util/file_watcher.rs b/src/generator/util/file_watcher.rs index 6549c6a..064bc8e 100644 --- a/src/generator/util/file_watcher.rs +++ b/src/generator/util/file_watcher.rs @@ -1,6 +1,8 @@ use std::{ + cell::RefCell, collections::HashMap, path::{Path, PathBuf}, + rc::Rc, }; use notify::{EventHandler, Watcher}; @@ -10,14 +12,12 @@ use crate::generator::util::content_base_path; pub struct FileWatcher { handlers: HashMap ()>>, - watcher: Option, } impl FileWatcher { pub fn new() -> Self { Self { handlers: HashMap::new(), - watcher: None, } } @@ -35,20 +35,18 @@ impl FileWatcher { } } - pub async fn start(&mut self) -> anyhow::Result<()> { - assert!(self.watcher.is_none()); + pub async fn start(self_: Rc>) -> anyhow::Result<()> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::>(); let mut watcher = notify::recommended_watcher(AsyncEventHandler(tx))?; watcher.watch(&content_base_path(), notify::RecursiveMode::Recursive)?; - self.watcher = Some(watcher); let mut absolute_content_parent = content_base_path().canonicalize()?; absolute_content_parent.pop(); while let Some(result) = rx.recv().await { if let Ok(ev) = result { - self.handle_event(ev, &absolute_content_parent); + self_.borrow().handle_event(ev, &absolute_content_parent); } } diff --git a/src/main.rs b/src/main.rs index 8ccbbd8..f9d7c7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,15 +48,14 @@ async fn main() { if matches.contains_id("watch") { let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<()>(); - let mut watcher = watcher.borrow_mut(); - watcher.watch(content_base_path(), move || { + watcher.borrow_mut().watch(content_base_path(), move || { tx.send(()).expect("sending regenerate signal"); }); let mut debounced = - debounced(UnboundedReceiverStream::new(rx), Duration::from_millis(500)); + debounced(UnboundedReceiverStream::new(rx), Duration::from_millis(100)); - let watch = watcher.start().fuse(); + let watch = FileWatcher::start(watcher).fuse(); let regenerate = async move { while let Some(_) = debounced.next().await {