use std::path::{Path, PathBuf};
use clap::{Arg, ArgMatches, Command};
use crate::domain::usecases::site::build::BuildError;
use crate::domain::usecases::site::generate::{
generate_site, DrSource, GenerateError, GenerateSiteRequest, PageSource, SiteNavLink,
};
use crate::domain::usecases::site::overlay_assets::OverlayAssetsReader;
use crate::domain::usecases::site::readers::AssetsReader;
use crate::domain::usecases::site::templates::TemplateEngine;
use crate::infra::driven::site::embedded_assets_reader::EmbeddedDefaultThemeAssets;
use crate::infra::driven::site::fs_assets_reader::FsAssetsReader;
use crate::infra::driven::site::fs_pages_reader::FsPagesReader;
use crate::infra::driven::site::fs_site_writer::FsSiteWriter;
use crate::infra::driven::site::tera_adapter::TeraAdapter;
use super::super::errors::{die1, CliError};
use super::super::pages_renderer;
use super::super::Context;
pub(in super::super) fn site_subcommand() -> Command {
Command::new("site")
.about("Publish the workspace as a browsable static site")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("build")
.about("Render every entry in the workspace to HTML under <out>/")
.long_about(
"Render every decision record and issue in the workspace \
to a self-contained static site (HTML pages, indexes, \
companion files, embedded CSS) under the output \
directory. The default theme is embedded; pass --theme \
to override with a custom Tera template tree, or set \
`[site] theme = \"...\"` in cartulary.toml to record \
the choice project-wide.",
)
.arg(Arg::new("out").long("out").value_name("PATH").help(
"Output directory (wins over `[site] out` in cartulary.toml; \
defaults to site/)",
))
.arg(Arg::new("theme").long("theme").value_name("PATH").help(
"Override the embedded default theme with templates from PATH \
(wins over `[site] theme` in cartulary.toml)",
)),
)
}
pub(in super::super) fn execute_site(matches: &ArgMatches, ctx: &Context<'_>) {
match matches.subcommand() {
Some(("build", sub)) => execute_build(sub, ctx),
_ => {
die1(
CliError::new("unknown site subcommand").kind("validation"),
ctx.output_fmt,
);
}
}
}
fn execute_build(matches: &ArgMatches, ctx: &Context<'_>) {
let out_dir = matches
.get_one::<String>("out")
.map(PathBuf::from)
.or_else(|| ctx.config().site_out.clone())
.unwrap_or_else(|| ctx.root_dir.join("site"));
let theme_dir = matches
.get_one::<String>("theme")
.map(PathBuf::from)
.or_else(|| ctx.config().site_theme.clone());
if let Err(e) = run_build(ctx, &out_dir, theme_dir.as_deref()) {
die1(CliError::new(render_error(&e)), ctx.output_fmt);
}
}
fn run_build(
ctx: &Context<'_>,
out_dir: &Path,
theme_dir: Option<&Path>,
) -> Result<(), GenerateError> {
let config = ctx.config();
let dr_sources: Vec<DrSource> = config
.decision_kinds
.iter()
.map(|kind_cfg| {
let repo = ctx.decision_record_repository(kind_cfg);
DrSource {
kind: kind_cfg.kind.clone(),
repo: Box::new(repo),
}
})
.collect();
let issue_repo = ctx.issue_repository();
let page_sources: Vec<PageSource> = config
.docs
.iter()
.map(|docs_cfg| PageSource {
label: docs_cfg.name.clone(),
publish: docs_cfg.publish.clone(),
reader: Box::new(FsPagesReader::new(&docs_cfg.source)),
})
.collect();
let default_assets = EmbeddedDefaultThemeAssets;
let override_assets = theme_dir.map(FsAssetsReader::from_theme_dir);
let overlay = override_assets
.as_ref()
.map(|overlay| OverlayAssetsReader::new(&default_assets, overlay));
let assets: &dyn AssetsReader = match &overlay {
Some(overlay) => overlay,
None => &default_assets,
};
let theme: Box<dyn TemplateEngine> = match theme_dir {
Some(path) => Box::new(TeraAdapter::from_directory_with_default(path)?),
None => Box::new(TeraAdapter::from_default()?),
};
let writer = FsSiteWriter::new(out_dir);
let site_nav: Vec<SiteNavLink> = config
.site_nav
.iter()
.map(|n| SiteNavLink {
label: n.label.clone(),
url: n.url.clone(),
})
.collect();
let request = GenerateSiteRequest {
dr_sources,
issue_repo: &issue_repo,
page_sources,
assets,
theme: theme.as_ref(),
writer: &writer,
site_title: config.site_title.clone(),
site_nav,
};
let total = generate_site(request)?;
println!("Built {total} files → {}", out_dir.display());
Ok(())
}
fn render_error(err: &GenerateError) -> String {
match err {
GenerateError::Pages { docs_label, error } => {
format!("[docs.{docs_label}]: {}", pages_renderer::render(error))
}
GenerateError::Build(e @ BuildError::BrokenReference { .. }) => format!("{e}"),
GenerateError::Build(e) => format!("{e}"),
GenerateError::Other(e) => e.to_string(),
}
}