rework generation to support multiple files; introduce a more proper CLI

This commit is contained in:
liquidex 2023-08-19 17:52:13 +02:00
parent 27414d4254
commit 7e84005a6b
9 changed files with 302 additions and 49 deletions

60
Cargo.lock generated
View file

@ -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]]

View file

@ -3,5 +3,6 @@ members = ["crates/*"]
resolver = "2"
[workspace.dependencies]
log = "0.4.20"
treehouse-format = { path = "crates/treehouse-format" }

1
content/secret.tree Normal file
View file

@ -0,0 +1 @@
- He is behind the tree.

View file

@ -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 }

View file

@ -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(Serialize)]
pub struct TemplateData {
pub tree: String,
#[derive(Default)]
struct Generator {
tree_files: Vec<PathBuf>,
}
pub fn regenerate() -> anyhow::Result<()> {
let _ = std::fs::remove_dir_all("target/site");
std::fs::create_dir_all("target/site")?;
type Files = SimpleFiles<String, String>;
type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId;
copy_dir("static", "target/site/static")?;
pub struct Diagnosis {
pub files: Files,
pub diagnostics: Vec<Diagnostic<FileId>>,
}
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<FileId> {
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<usize>,
column: Option<usize>,
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<Diagnosis> {
let mut diagnosis = Diagnosis {
files: Files::new(),
diagnostics: vec![],
};
let mut handlebars = Handlebars::new();
handlebars.register_template_file("template/index.hbs", "template/index.hbs")?;
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 root_file = std::fs::read_to_string("content/tree/index.tree")?;
let parse_result = Roots::parse(&mut treehouse_format::pull::Parser {
input: &root_file,
input: source,
position: 0,
});
match parse_result {
Ok(roots) => {
let mut tree = String::new();
branches_to_html(&mut tree, &roots.branches, &root_file);
branches_to_html(&mut tree, &roots.branches, source);
let index_html = handlebars.render("template/index.hbs", &TemplateData { tree })?;
std::fs::write("target/site/index.html", index_html)?;
}
let template_data = TemplateData { tree };
let templated_html = match handlebars.render("tree", &template_data) {
Ok(html) => 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 {
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: None,
code: Some("tree".into()),
message: error.kind.to_string(),
labels: vec![Label {
style: LabelStyle::Primary,
file_id: (),
file_id,
range: error.range,
message: String::new(),
}],
notes: vec![],
};
codespan_reporting::term::emit(&mut writer.lock(), &config, &files, &diagnostic)?;
}),
}
}
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(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)?;
info!("copying static directory to target directory");
copy_dir(dirs.static_dir, dirs.target_dir.join("static"))?;
info!("generating standalone pages");
let mut generator = Generator::default();
generator.add_directory_rec(dirs.content_dir)?;
let diagnosis = generator.generate_all_files(dirs)?;
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?)

View file

@ -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<dyn std::error::Error>> {
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(())

View file

@ -10,7 +10,7 @@ body {
body {
background-color: rgb(255, 253, 246);
color: #333;
color: #55423e;
}
/* Set up typography */