Skip to main content

forge_codegen/
compiler.rs

1//! Compile Forge templates to Askama (compile-time, build.rs path) or to a
2//! MiniJinja-compatible runtime form used by Spark.
3
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use walkdir::WalkDir;
8
9use crate::lower::{lower, lower_with_target, LowerTarget};
10use crate::parser::tokenize;
11
12pub fn compile_source(source: &str) -> String {
13    let tokens = tokenize(source);
14    lower(&tokens)
15}
16
17/// Lower a Forge source string into MiniJinja-compatible syntax. Spark
18/// directives (`@spark`, `@sparkScripts`) emit function calls bound to
19/// MiniJinja's global function table; other directives behave the same as the
20/// Askama path for now.
21pub fn compile_source_runtime(source: &str) -> String {
22    let tokens = tokenize(source);
23    lower_with_target(&tokens, LowerTarget::MiniJinja)
24}
25
26pub fn compile_file(input: &Path, output: &Path) -> std::io::Result<()> {
27    let raw = fs::read_to_string(input)?;
28    let lowered = compile_source(&raw);
29    if let Some(parent) = output.parent() {
30        fs::create_dir_all(parent)?;
31    }
32    fs::write(output, lowered)?;
33    Ok(())
34}
35
36/// Walk `input_dir` for `*.forge.html` and emit a Rust source file that
37/// registers each template's raw source via `inventory::submit!` as a
38/// `spark::template::EmbeddedTemplate`. The user includes this from their
39/// crate root with:
40///
41/// ```ignore
42/// include!(concat!(env!("OUT_DIR"), "/spark_embedded_templates.rs"));
43/// ```
44///
45/// The included `inventory::submit!` blocks register the embedded sources;
46/// `spark::template::render` then resolves view paths from memory instead of
47/// hitting disk, enabling single-binary distribution.
48///
49/// Calling this is optional — apps that don't include the generated file
50/// continue to load templates from `resources/views/` at runtime.
51pub fn emit_embedded_registry(input_dir: &Path, output_rs: &Path) -> std::io::Result<()> {
52    if let Some(parent) = output_rs.parent() {
53        fs::create_dir_all(parent)?;
54    }
55
56    if !input_dir.exists() {
57        fs::write(
58            output_rs,
59            "// forge-codegen: input dir not found at build time, no templates embedded.\n",
60        )?;
61        return Ok(());
62    }
63
64    let abs_input = fs::canonicalize(input_dir)?;
65    let mut out = String::from(
66        "// Generated by forge_codegen::emit_embedded_registry — do not edit.\n",
67    );
68
69    for entry in WalkDir::new(&abs_input).into_iter().filter_map(|e| e.ok()) {
70        if !entry.file_type().is_file() {
71            continue;
72        }
73        let path = entry.path();
74        let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
75            continue;
76        };
77        if !file_name.ends_with(".forge.html") && !file_name.ends_with(".forge") {
78            continue;
79        }
80        let rel = path.strip_prefix(&abs_input).unwrap_or(path);
81        let rel_str = rel.to_string_lossy().replace('\\', "/");
82        let view_path = rel_str
83            .trim_end_matches(".forge.html")
84            .trim_end_matches(".forge")
85            .to_string();
86        let abs_str = path
87            .canonicalize()
88            .unwrap_or_else(|_| path.to_path_buf())
89            .to_string_lossy()
90            .to_string();
91        out.push_str(&format!(
92            "::anvilforge::inventory::submit! {{ ::anvilforge::spark::template::EmbeddedTemplate {{ view_path: {view:?}, source: include_str!({src:?}) }} }}\n",
93            view = view_path,
94            src = abs_str,
95        ));
96    }
97
98    fs::write(output_rs, out)?;
99    Ok(())
100}
101
102/// Walk `input_dir` for `*.forge.html` and write Askama-compatible `*.html`
103/// files into `output_dir`, preserving the relative layout.
104pub fn compile_dir(input_dir: &Path, output_dir: &Path) -> std::io::Result<Vec<PathBuf>> {
105    let mut written = Vec::new();
106    if !input_dir.exists() {
107        return Ok(written);
108    }
109    for entry in WalkDir::new(input_dir).into_iter().filter_map(|e| e.ok()) {
110        if !entry.file_type().is_file() {
111            continue;
112        }
113        let path = entry.path();
114        let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
115            continue;
116        };
117        if !file_name.ends_with(".forge.html") && !file_name.ends_with(".forge") {
118            continue;
119        }
120        let rel = path.strip_prefix(input_dir).unwrap_or(path);
121        let out_name = file_name
122            .replace(".forge.html", ".html")
123            .replace(".forge", ".html");
124        let mut out_path = output_dir.to_path_buf();
125        if let Some(parent) = rel.parent() {
126            out_path.push(parent);
127        }
128        out_path.push(out_name);
129        compile_file(path, &out_path)?;
130        written.push(out_path);
131    }
132    Ok(written)
133}