mod atom; mod dir_helper; mod include_static_helper; mod simple_template; mod tree; use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc}; use atom::FeedDir; use dir_helper::DirHelper; use handlebars::{handlebars_helper, Handlebars}; use include_static_helper::IncludeStaticHelper; use serde::Serialize; use tracing::{error, info_span, instrument}; use crate::{ config::Config, dirs::Dirs, fun::seasons::Season, sources::Sources, vfs::{ self, AnchoredAtExt, Cd, Content, ContentCache, Dir, DynDir, Entries, HtmlCanonicalize, MemDir, Overlay, ToDynDir, VPath, VPathBuf, }, }; #[derive(Serialize)] struct BaseTemplateData<'a> { config: &'a Config, import_map: String, season: Option, dev: bool, feeds: Vec, } impl<'a> BaseTemplateData<'a> { fn new(sources: &'a Sources) -> Self { Self { config: &sources.config, import_map: serde_json::to_string_pretty(&sources.import_map) .expect("import map should be serializable to JSON"), season: Season::current(), dev: cfg!(debug_assertions), feeds: sources.treehouse.feeds_by_name.keys().cloned().collect(), } } } fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> { let mut handlebars = Handlebars::new(); handlebars_helper!(cat: |a: String, b: String| a + &b); handlebars.register_helper("cat", Box::new(cat)); handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone()))); handlebars.register_helper( "include_static", Box::new(IncludeStaticHelper::new(static_)), ); handlebars } #[instrument(skip(handlebars))] fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) { vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| { if path.extension() == Some("hbs") { if let Some(content) = vfs::query::(dir, path).and_then(|c| c.string().ok()) { let _span = info_span!("register_template", ?path).entered(); if let Err(err) = handlebars.register_template_string(path.as_str(), content) { error!("in template: {err}"); } } } ControlFlow::Continue(()) }); } struct TreehouseDir { dirs: Arc, sources: Arc, handlebars: Arc>, dir_index: DirIndex, } impl TreehouseDir { fn new( dirs: Arc, sources: Arc, handlebars: Arc>, dir_index: DirIndex, ) -> Self { Self { dirs, sources, handlebars, dir_index, } } #[instrument("TreehouseDir::dir", skip(self))] fn dir(&self, path: &VPath) -> Vec { // NOTE: This does not include simple templates, because that's not really needed right now. let mut index = &self.dir_index; for component in path.segments() { if let Some(child) = index.children.get(component) { index = child; } else { // There cannot possibly be any entries under an invalid path. // Bail early. return vec![]; } } index .children .values() .map(|child| child.full_path.clone()) .collect() } #[instrument("TreehouseDir::content", skip(self))] fn content(&self, path: &VPath) -> Option { let path = if path.is_root() { VPath::new_const("index") } else { path }; self.sources .treehouse .files_by_tree_path .get(path) .map(|&file_id| { Content::new( "text/html", tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id) .into(), ) }) .or_else(|| { if path.file_name().is_some_and(|s| !s.starts_with('_')) { let template_name = path.with_extension("hbs"); if self.handlebars.has_template(template_name.as_str()) { return Some(Content::new( "text/html", simple_template::generate_or_error( &self.sources, &self.handlebars, template_name.as_str(), ) .into(), )); } } None }) } } impl Dir for TreehouseDir { fn query(&self, path: &VPath, query: &mut vfs::Query) { query.provide(|| Entries(self.dir(path))); query.try_provide(|| self.content(path)); } } impl fmt::Debug for TreehouseDir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("TreehouseDir") } } /// Acceleration structure for `dir` operations on [`TreehouseDir`]s. #[derive(Debug, Default)] struct DirIndex { full_path: VPathBuf, children: HashMap, } impl DirIndex { #[instrument(name = "DirIndex::new", skip(paths))] pub fn new<'a>(paths: impl Iterator) -> Self { let mut root = DirIndex::default(); for path in paths { let mut parent = &mut root; let mut full_path = VPath::ROOT.to_owned(); for segment in path.segments() { full_path.push(segment); let child = parent .children .entry(segment.to_owned()) .or_insert_with(|| DirIndex { full_path: full_path.clone(), children: HashMap::new(), }); parent = child; } } root } } pub fn target(dirs: Arc, sources: Arc) -> DynDir { let mut handlebars = create_handlebars(&sources.config.site, dirs.static_.clone()); load_templates(&mut handlebars, &dirs.template); let handlebars = Arc::new(handlebars); let mut root = MemDir::new(); root.add( VPath::new("feed"), ContentCache::new(FeedDir::new( dirs.clone(), sources.clone(), handlebars.clone(), )) .to_dyn(), ); root.add(VPath::new("static"), dirs.static_.clone()); root.add( VPath::new("robots.txt"), Cd::new(dirs.static_.clone(), VPathBuf::new("robots.txt")).to_dyn(), ); let dir_index = DirIndex::new(sources.treehouse.files_by_tree_path.keys().map(|x| &**x)); let tree_view = TreehouseDir::new(dirs, sources, handlebars, dir_index); let tree_view = ContentCache::new(tree_view); tree_view.warm_up(); let tree_view = HtmlCanonicalize::new(tree_view); Overlay::new(tree_view.to_dyn(), root.to_dyn()) .anchored_at(VPath::ROOT.to_owned()) .to_dyn() }