rework generation to support multiple files; introduce a more proper CLI
This commit is contained in:
parent
27414d4254
commit
7e84005a6b
60
Cargo.lock
generated
60
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -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
1
content/secret.tree
Normal file
|
@ -0,0 +1 @@
|
|||
- He is behind the tree.
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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<PathBuf>,
|
||||
}
|
||||
|
||||
type Files = SimpleFiles<String, String>;
|
||||
type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId;
|
||||
|
||||
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();
|
||||
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?)
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -10,7 +10,7 @@ body {
|
|||
|
||||
body {
|
||||
background-color: rgb(255, 253, 246);
|
||||
color: #333;
|
||||
color: #55423e;
|
||||
}
|
||||
|
||||
/* Set up typography */
|
||||
|
|
Loading…
Reference in a new issue