diff --git a/crates/zensical/src/lib.rs b/crates/zensical/src/lib.rs index 26107f0..ca8ebe3 100644 --- a/crates/zensical/src/lib.rs +++ b/crates/zensical/src/lib.rs @@ -37,6 +37,7 @@ use pyo3::{ pyfunction, pymodule, wrap_pyfunction, Bound, FromPyObject, PyResult, Python, }; +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process; use std::time::{Duration, Instant}; @@ -57,9 +58,10 @@ mod workflow; use compat::mkdocs::plugin::meta; use config::Config; +use path::SourcePath; use server::{create_server, ServeOptions}; use watcher::Watcher; -use workflow::{create_workflow, Configuration, Input}; +use workflow::{create_workflow, has_snippets, Configuration, Input}; // ---------------------------------------------------------------------------- // Enums @@ -272,6 +274,7 @@ fn run(config_file: &PathBuf, mode: Mode) -> PyResult { docs: config.docs_root().clone(), context: config.project.docs_dir.clone(), }); + let mut snippets = BTreeMap::new(); // Start the event loop. Each debounced watcher batch is admitted as one // source revision and fully settled before the next batch is accepted. @@ -284,6 +287,33 @@ fn run(config_file: &PathBuf, mode: Mode) -> PyResult { metadata.prepare(&changes).map_err(|error| { PyRuntimeError::new_err(format!("{error:#}")) })?; + let mut dependents = BTreeMap::from_iter(dependents); + + // Until snippet dependencies are tracked, rebuild every page + // containing a marker whenever a source changes in serve mode + if serve { + dependents.extend(snippets.clone()); + for change in &changes { + let key = match change { + Change::Insert(key, _) | Change::Remove(key) => key, + }; + + // Explicit changes win over retained page entries + dependents.remove(key); + snippets.remove(key); + if let Change::Insert(_, source) = change + && key[0].context() == config.project.docs_dir + && let Ok(path) = + key[0].location().parse::() + && path.extension() == Some("md") + && !path.is_hidden() + && has_snippets(&fs::read_to_string(&**source)?) + { + snippets.insert(key.clone(), source.clone()); + } + } + } + let mut revision = input .begin() .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; diff --git a/crates/zensical/src/workflow.rs b/crates/zensical/src/workflow.rs index 13fc449..20df4a7 100644 --- a/crates/zensical/src/workflow.rs +++ b/crates/zensical/src/workflow.rs @@ -68,7 +68,7 @@ use cached::cached; /// Regular expression to detect use of snippets static SNIPPET_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^[ \t]*-+8<-+").expect("invariant")); + LazyLock::new(|| Regex::new(r"(?m)^[ \t]*-+8<-+").expect("invariant")); // ---------------------------------------------------------------------------- // Structs @@ -445,6 +445,11 @@ fn route_markdown( }) } +/// Returns whether Markdown contains a snippet marker. +pub fn has_snippets(data: &str) -> bool { + SNIPPET_RE.is_match(data.strip_prefix('\u{FEFF}').unwrap_or(data)) +} + /// Create a stream to process routed Markdown files. fn process_markdown( config: &Config, plugins: &plugin::Settings, @@ -468,7 +473,7 @@ fn process_markdown( // Don't cache page if it inserts (pymdownx) snippets. // This is a hack while waiting for CommonMark (AST) and components, // as well as topic-based authoring functionality. - if SNIPPET_RE.is_match(&data) { + if has_snippets(&data) { render_markdown(id, route, data, plugins.clone(), resolved) } else { cached(