diff --git a/Cargo.lock b/Cargo.lock index d80de3a..792b7b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.3.2" @@ -290,6 +299,19 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.2" @@ -447,6 +469,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.27" @@ -731,6 +759,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1068,13 +1125,16 @@ dependencies = [ "clap", "codespan-reporting", "copy_dir", + "env_logger", "handlebars", + "log", "pulldown-cmark", "serde", "tokio", "tower-http", "tower-livereload", "treehouse-format", + "walkdir", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d36d5e8..9996aa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ members = ["crates/*"] resolver = "2" [workspace.dependencies] +log = "0.4.20" treehouse-format = { path = "crates/treehouse-format" } diff --git a/content/tree/index.tree b/content/index.tree similarity index 100% rename from content/tree/index.tree rename to content/index.tree diff --git a/content/secret.tree b/content/secret.tree new file mode 100644 index 0000000..270dc86 --- /dev/null +++ b/content/secret.tree @@ -0,0 +1 @@ +- He is behind the tree. diff --git a/crates/treehouse/Cargo.toml b/crates/treehouse/Cargo.toml index df4753d..6ba7d61 100644 --- a/crates/treehouse/Cargo.toml +++ b/crates/treehouse/Cargo.toml @@ -4,17 +4,22 @@ version = "0.1.0" edition = "2021" [dependencies] + +treehouse-format = { workspace = true } + anyhow = "1.0.75" axum = "0.6.20" clap = { version = "4.3.22", features = ["derive"] } codespan-reporting = "0.11.1" copy_dir = "0.1.3" +env_logger = "0.10.0" +log = { workspace = true } handlebars = "4.3.7" pulldown-cmark = { version = "0.9.3", default-features = false } serde = { version = "1.0.183", features = ["derive"] } tokio = { version = "1.32.0", features = ["full"] } tower-http = { version = "0.4.3", features = ["fs"] } tower-livereload = "0.8.0" +walkdir = "2.3.3" -treehouse-format = { workspace = true } diff --git a/crates/treehouse/src/cli/regenerate.rs b/crates/treehouse/src/cli/regenerate.rs index 7f7e072..cf1f863 100644 --- a/crates/treehouse/src/cli/regenerate.rs +++ b/crates/treehouse/src/cli/regenerate.rs @@ -1,74 +1,242 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, Context}; use axum::Router; use codespan_reporting::{ diagnostic::{Diagnostic, Label, LabelStyle, Severity}, - files::SimpleFile, + files::{Files as _, SimpleFiles}, term::termcolor::{ColorChoice, StandardStream}, }; use copy_dir::copy_dir; use handlebars::Handlebars; +use log::{debug, info}; use serde::Serialize; use tower_http::services::ServeDir; use tower_livereload::LiveReloadLayer; use treehouse_format::ast::Roots; +use walkdir::WalkDir; use crate::html::tree::branches_to_html; +#[derive(Default)] +struct Generator { + tree_files: Vec, +} + +type Files = SimpleFiles; +type FileId = >::FileId; + +pub struct Diagnosis { + pub files: Files, + pub diagnostics: Vec>, +} + +impl Generator { + fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> { + for entry in WalkDir::new(directory) { + let entry = entry?; + if entry.path().extension() == Some(OsStr::new("tree")) { + self.tree_files.push(entry.path().to_owned()); + } + } + Ok(()) + } + + fn register_template( + handlebars: &mut Handlebars<'_>, + diagnosis: &mut Diagnosis, + name: &str, + path: &Path, + ) -> anyhow::Result { + let source = std::fs::read_to_string(path) + .with_context(|| format!("cannot read template file {path:?}"))?; + let file_id = diagnosis + .files + .add(path.to_string_lossy().into_owned(), source); + let file = diagnosis + .files + .get(file_id) + .expect("file was just added to the list"); + let source = file.source(); + if let Err(error) = handlebars.register_template_string(name, source) { + Self::wrangle_handlebars_error_into_diagnostic( + diagnosis, + file_id, + error.line_no, + error.column_no, + error.reason().to_string(), + )?; + } + Ok(file_id) + } + + fn wrangle_handlebars_error_into_diagnostic( + diagnosis: &mut Diagnosis, + file_id: FileId, + line: Option, + column: Option, + message: String, + ) -> anyhow::Result<()> { + if let (Some(line), Some(column)) = (line, column) { + let line_range = diagnosis + .files + .line_range(file_id, line) + .expect("file was added to the list"); + diagnosis.diagnostics.push(Diagnostic { + severity: Severity::Error, + code: Some("template".into()), + message, + labels: vec![Label { + style: LabelStyle::Primary, + file_id, + range: line_range.start + column..line_range.start + column + 1, + message: String::new(), + }], + notes: vec![], + }) + } else { + let file = diagnosis + .files + .get(file_id) + .expect("file should already be in the list"); + bail!("template error in {}: {message}", file.name()); + } + Ok(()) + } + + fn generate_all_files(&self, dirs: &Dirs<'_>) -> anyhow::Result { + let mut diagnosis = Diagnosis { + files: Files::new(), + diagnostics: vec![], + }; + + let mut handlebars = Handlebars::new(); + let tree_template = Self::register_template( + &mut handlebars, + &mut diagnosis, + "tree", + &dirs.template_dir.join("tree.hbs"), + )?; + + for path in &self.tree_files { + let utf8_filename = path.to_string_lossy(); + let target_file = path.strip_prefix(dirs.content_dir).unwrap_or(path); + let target_path = if target_file == OsStr::new("index.tree") { + dirs.target_dir.join("index.html") + } else { + dirs.target_dir.join(target_file).with_extension("html") + }; + debug!("generating: {path:?} -> {target_path:?}"); + + let source = match std::fs::read_to_string(path) { + Ok(source) => source, + Err(error) => { + diagnosis.diagnostics.push(Diagnostic { + severity: Severity::Error, + code: None, + message: format!("{utf8_filename}: cannot read file: {error}"), + labels: vec![], + notes: vec![], + }); + continue; + } + }; + let file_id = diagnosis.files.add(utf8_filename.into_owned(), source); + let source = diagnosis + .files + .get(file_id) + .expect("file was just added to the list") + .source(); + + let parse_result = Roots::parse(&mut treehouse_format::pull::Parser { + input: source, + position: 0, + }); + + match parse_result { + Ok(roots) => { + let mut tree = String::new(); + branches_to_html(&mut tree, &roots.branches, source); + + let template_data = TemplateData { tree }; + let templated_html = match handlebars.render("tree", &template_data) { + Ok(html) => html, + Err(error) => { + Self::wrangle_handlebars_error_into_diagnostic( + &mut diagnosis, + tree_template, + error.line_no, + error.column_no, + error.desc, + )?; + continue; + } + }; + + std::fs::write(target_path, templated_html)?; + } + Err(error) => diagnosis.diagnostics.push(Diagnostic { + severity: Severity::Error, + code: Some("tree".into()), + message: error.kind.to_string(), + labels: vec![Label { + style: LabelStyle::Primary, + file_id, + range: error.range, + message: String::new(), + }], + notes: vec![], + }), + } + } + + Ok(diagnosis) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Dirs<'a> { + pub target_dir: &'a Path, + pub static_dir: &'a Path, + pub template_dir: &'a Path, + pub content_dir: &'a Path, +} + #[derive(Serialize)] pub struct TemplateData { pub tree: String, } -pub fn regenerate() -> anyhow::Result<()> { - let _ = std::fs::remove_dir_all("target/site"); - std::fs::create_dir_all("target/site")?; +pub fn regenerate(dirs: &Dirs<'_>) -> anyhow::Result<()> { + info!("cleaning target directory"); + let _ = std::fs::remove_dir_all(dirs.target_dir); + std::fs::create_dir_all(dirs.target_dir)?; - copy_dir("static", "target/site/static")?; + info!("copying static directory to target directory"); + copy_dir(dirs.static_dir, dirs.target_dir.join("static"))?; - let mut handlebars = Handlebars::new(); - handlebars.register_template_file("template/index.hbs", "template/index.hbs")?; + info!("generating standalone pages"); + let mut generator = Generator::default(); + generator.add_directory_rec(dirs.content_dir)?; + let diagnosis = generator.generate_all_files(dirs)?; - let root_file = std::fs::read_to_string("content/tree/index.tree")?; - let parse_result = Roots::parse(&mut treehouse_format::pull::Parser { - input: &root_file, - position: 0, - }); - - match parse_result { - Ok(roots) => { - let mut tree = String::new(); - branches_to_html(&mut tree, &roots.branches, &root_file); - - let index_html = handlebars.render("template/index.hbs", &TemplateData { tree })?; - - std::fs::write("target/site/index.html", index_html)?; - } - Err(error) => { - let writer = StandardStream::stderr(ColorChoice::Auto); - let config = codespan_reporting::term::Config::default(); - let files = SimpleFile::new("index.tree", &root_file); - let diagnostic = Diagnostic { - severity: Severity::Error, - code: None, - message: error.kind.to_string(), - labels: vec![Label { - style: LabelStyle::Primary, - file_id: (), - range: error.range, - message: String::new(), - }], - notes: vec![], - }; - codespan_reporting::term::emit(&mut writer.lock(), &config, &files, &diagnostic)?; - } + let writer = StandardStream::stderr(ColorChoice::Auto); + let config = codespan_reporting::term::Config::default(); + for diagnostic in &diagnosis.diagnostics { + codespan_reporting::term::emit(&mut writer.lock(), &config, &diagnosis.files, diagnostic) + .context("could not emit diagnostic")?; } Ok(()) } -pub fn regenerate_or_report_error() { - eprintln!("regenerating"); +pub fn regenerate_or_report_error(dirs: &Dirs<'_>) { + info!("regenerating site content"); - match regenerate() { + match regenerate(dirs) { Ok(_) => (), Err(error) => eprintln!("error: {error:?}"), } @@ -80,7 +248,7 @@ pub async fn web_server() -> anyhow::Result<()> { #[cfg(debug_assertions)] let app = app.layer(LiveReloadLayer::new()); - eprintln!("serving on port 8080"); + info!("serving on port 8080"); Ok(axum::Server::bind(&([0, 0, 0, 0], 8080).into()) .serve(app.into_make_service()) .await?) diff --git a/crates/treehouse/src/main.rs b/crates/treehouse/src/main.rs index 0f1c3fd..1c8d839 100644 --- a/crates/treehouse/src/main.rs +++ b/crates/treehouse/src/main.rs @@ -1,8 +1,11 @@ +use std::path::Path; + use clap::Parser; use cli::{ - regenerate::{self, regenerate_or_report_error}, + regenerate::{self, regenerate_or_report_error, Dirs}, Command, ProgramArgs, }; +use log::{error, info}; mod cli; mod html; @@ -12,7 +15,18 @@ async fn fallible_main() -> anyhow::Result<()> { match args.command { Command::Regenerate(regenerate_args) => { - regenerate_or_report_error(); + let dirs = Dirs { + target_dir: Path::new("target/site"), + + // NOTE: These are intentionally left unconfigurable from within treehouse.toml + // because this is is one of those things that should be consistent between sites. + static_dir: Path::new("static"), + template_dir: Path::new("template"), + content_dir: Path::new("content"), + }; + info!("regenerating using directories: {dirs:#?}"); + + regenerate_or_report_error(&dirs); if regenerate_args.serve { regenerate::web_server().await?; @@ -25,9 +39,13 @@ async fn fallible_main() -> anyhow::Result<()> { #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::Builder::new() + .filter_module("treehouse", log::LevelFilter::Debug) + .init(); + match fallible_main().await { Ok(_) => (), - Err(error) => eprintln!("fatal: {error:?}"), + Err(error) => error!("fatal: {error:?}"), } Ok(()) diff --git a/static/css/main.css b/static/css/main.css index bc3252c..0bdf82f 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -10,7 +10,7 @@ body { body { background-color: rgb(255, 253, 246); - color: #333; + color: #55423e; } /* Set up typography */ diff --git a/template/index.hbs b/template/tree.hbs similarity index 100% rename from template/index.hbs rename to template/tree.hbs