Skip to main content

laterite_web/theme/
build.rs

1//! A theme rendered to files: every page whose URL has no parameter, at its
2//! URL, through the same render path the site serves, plus the theme's text
3//! assets. The output is a plain directory for any static host.
4
5use super::engine::Engine;
6use super::router::Params;
7use super::{site, store, Kind, ThemeError};
8use crate::StaticSite;
9use laterite_core::Db;
10use std::path::Path;
11
12/// What a build wrote.
13#[derive(Debug, Default, Clone, PartialEq, Eq)]
14pub struct BuildReport {
15    /// The pages written, by URL.
16    pub pages: Vec<String>,
17    /// The assets written, by path under the output directory.
18    pub assets: Vec<String>,
19    /// The pages left out: a URL with parameters, or a hidden page.
20    pub skipped: Vec<String>,
21}
22
23/// Renders the theme `code` into `out_dir` as a site at `base_url`. A page
24/// at `/404` is also written as `404.html`, the file most static hosts serve
25/// for an unmatched URL.
26pub async fn build(
27    db: &Db,
28    code: &str,
29    out_dir: &Path,
30    base_url: &str,
31) -> Result<BuildReport, ThemeError> {
32    build_with(db, code, out_dir, base_url, &[], &[]).await
33}
34
35/// Like [`build`], with the `markup` and `components` modules registered, as
36/// the site has them.
37pub async fn build_with(
38    db: &Db,
39    code: &str,
40    out_dir: &Path,
41    base_url: &str,
42    markup: &[std::sync::Arc<dyn super::markup::Markup>],
43    components: &[&super::component::ComponentReg],
44) -> Result<BuildReport, ThemeError> {
45    let theme = store::find_theme(db, code)
46        .await?
47        .ok_or_else(|| ThemeError::NotFound(code.to_string()))?;
48    let origin = base_url.trim_end_matches('/').to_string();
49    let engine = Engine::build_with(db, code, {
50        let origin = origin.clone();
51        move |env| {
52            super::markup::install_all(markup, env);
53            super::component::install_markup(components, env);
54            site::add_url(env, origin);
55        }
56    })
57    .await?;
58    let mut out = StaticSite::new(out_dir, &origin)?;
59    let mut report = BuildReport::default();
60    let mut names: Vec<&str> = engine.routes().names();
61    names.sort_unstable();
62    for name in names {
63        let settings = engine.page(name).cloned().unwrap_or_default();
64        let pattern = match settings.pattern() {
65            Ok(p) => p,
66            Err(_) => continue,
67        };
68        if !pattern.is_static() || settings.hidden {
69            report.skipped.push(name.to_string());
70            continue;
71        }
72        let url = pattern.source().to_string();
73        let extras = site::run_components(
74            &engine,
75            components,
76            db,
77            &theme,
78            name,
79            &Params::new(),
80            &url,
81            false,
82        )
83        .await?;
84        let html = site::render_page_with(
85            &engine,
86            &theme,
87            name,
88            &Params::new(),
89            &url,
90            false,
91            &origin,
92            extras,
93        )
94        .map_err(ThemeError::Template)?;
95        out.page(&url, &html)?;
96        if url == "/404" {
97            out.file("404.html", &html)?;
98        }
99        report.pages.push(url);
100    }
101    report.pages.sort();
102    report.skipped.sort();
103    for template in engine.names() {
104        let Some(path) = template.strip_prefix("assets/") else {
105            continue;
106        };
107        let body = engine
108            .render(Kind::Asset, path, ())
109            .map_err(ThemeError::Template)?;
110        let target = format!("themes/{code}/assets/{path}");
111        out.file(&target, &body)?;
112        report.assets.push(target);
113    }
114    Ok(report)
115}