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

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

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(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?)

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(())