Skip to main content

presolve_compiler/
document_template.rs

1//! Compiler-owned application document template projection.
2
3use std::fmt;
4
5/// Exact placeholders required by an `app/index.html` document template.
6pub const DOCUMENT_TEMPLATE_PLACEHOLDERS_V1: [&str; 3] =
7    ["{{ head }}", "{{ app }}", "{{ runtime }}"];
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DocumentTemplateErrorV1 {
11    pub code: &'static str,
12    pub message: String,
13}
14
15impl fmt::Display for DocumentTemplateErrorV1 {
16    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
17        write!(output, "{}: {}", self.code, self.message)
18    }
19}
20
21impl std::error::Error for DocumentTemplateErrorV1 {}
22
23/// Applies a validated `app/index.html` template to a compiler-generated page.
24///
25/// The template controls document framing only. Presolve extracts the existing
26/// compiler-owned head, rendered application, and runtime payload from the
27/// generated page and injects each into exactly one required placeholder.
28pub fn apply_document_template_v1(
29    template: &str,
30    generated_page: &str,
31    additional_head: &str,
32) -> Result<String, DocumentTemplateErrorV1> {
33    validate_template(template)?;
34    let (head, app, runtime) = split_generated_page(generated_page)?;
35    let head = format!("{head}{additional_head}");
36    Ok(template
37        .replacen("{{ head }}", &head, 1)
38        .replacen("{{ app }}", app, 1)
39        .replacen("{{ runtime }}", runtime, 1))
40}
41
42fn validate_template(template: &str) -> Result<(), DocumentTemplateErrorV1> {
43    if !template.trim_start().starts_with("<!doctype html>") || !template.contains("<html") {
44        return Err(DocumentTemplateErrorV1 {
45            code: "PSDOC1001_TEMPLATE_DOCUMENT_INVALID",
46            message: "app/index.html must declare an HTML document".into(),
47        });
48    }
49    for placeholder in DOCUMENT_TEMPLATE_PLACEHOLDERS_V1 {
50        if template.matches(placeholder).count() != 1 {
51            return Err(DocumentTemplateErrorV1 {
52                code: "PSDOC1002_TEMPLATE_PLACEHOLDER_COUNT",
53                message: format!("{placeholder} must occur exactly once"),
54            });
55        }
56    }
57    let mut remaining = template;
58    while let Some(start) = remaining.find("{{") {
59        let suffix = &remaining[start..];
60        let Some(end) = suffix.find("}}") else {
61            return Err(DocumentTemplateErrorV1 {
62                code: "PSDOC1003_TEMPLATE_PLACEHOLDER_INVALID",
63                message: "unclosed document-template placeholder".into(),
64            });
65        };
66        let placeholder = &suffix[..end + 2];
67        if !DOCUMENT_TEMPLATE_PLACEHOLDERS_V1.contains(&placeholder) {
68            return Err(DocumentTemplateErrorV1 {
69                code: "PSDOC1003_TEMPLATE_PLACEHOLDER_INVALID",
70                message: format!("unsupported document-template placeholder {placeholder}"),
71            });
72        }
73        remaining = &suffix[end + 2..];
74    }
75    Ok(())
76}
77
78fn split_generated_page(page: &str) -> Result<(&str, &str, &str), DocumentTemplateErrorV1> {
79    let head_start = "  <head>\n";
80    let head_end = "  </head>\n";
81    let body_start = "  <body>\n";
82    let runtime_start =
83        "    <script type=\"application/json\" id=\"presolve-template-manifest\">\n";
84    let body_end = "  </body>\n</html>\n";
85    let Some(head_after) = page.find(head_start).map(|index| index + head_start.len()) else {
86        return generated_page_error();
87    };
88    let Some(head_end_index) = page[head_after..]
89        .find(head_end)
90        .map(|index| head_after + index)
91    else {
92        return generated_page_error();
93    };
94    let body_after = head_end_index + head_end.len();
95    if !page[body_after..].starts_with(body_start) {
96        return generated_page_error();
97    }
98    let app_after = body_after + body_start.len();
99    let Some(runtime_index) = page[app_after..]
100        .find(runtime_start)
101        .map(|index| app_after + index)
102    else {
103        return generated_page_error();
104    };
105    let Some(body_end_index) = page[runtime_index..]
106        .find(body_end)
107        .map(|index| runtime_index + index)
108    else {
109        return generated_page_error();
110    };
111    Ok((
112        &page[head_after..head_end_index],
113        &page[app_after..runtime_index],
114        &page[runtime_index..body_end_index],
115    ))
116}
117
118fn generated_page_error<T>() -> Result<T, DocumentTemplateErrorV1> {
119    Err(DocumentTemplateErrorV1 {
120        code: "PSDOC1004_GENERATED_PAGE_INVALID",
121        message: "compiler-generated page does not have the expected document structure".into(),
122    })
123}
124
125#[cfg(test)]
126mod tests {
127    use super::apply_document_template_v1;
128
129    const PAGE: &str = "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Home</title>\n  </head>\n  <body>\n    <main>Home</main>\n    <script type=\"application/json\" id=\"presolve-template-manifest\">\n      {}\n    </script>\n    <script src=\"./runtime.js\" defer></script>\n  </body>\n</html>\n";
130
131    #[test]
132    fn projects_compiler_owned_parts_into_the_required_placeholders() {
133        let output = apply_document_template_v1(
134            "<!doctype html>\n<html lang=\"en\">\n<head>\n{{ head }}\n</head>\n<body>\n{{ app }}{{ runtime }}\n</body>\n</html>\n",
135            PAGE,
136            "    <link rel=\"stylesheet\" href=\"/app.css\">\n",
137        )
138        .unwrap();
139        assert!(output.contains("<head>\n    <meta charset"));
140        assert!(output.contains("href=\"/app.css\""));
141        assert!(output.contains("<main>Home</main>"));
142        assert!(output.contains("presolve-template-manifest"));
143        assert!(!output.contains("{{ head }}"));
144    }
145
146    #[test]
147    fn rejects_missing_or_unknown_placeholders() {
148        assert_eq!(
149            apply_document_template_v1(
150                "<!doctype html><html>{{ head }}{{ app }}</html>",
151                PAGE,
152                "",
153            )
154            .unwrap_err()
155            .code,
156            "PSDOC1002_TEMPLATE_PLACEHOLDER_COUNT"
157        );
158        assert_eq!(
159            apply_document_template_v1(
160                "<!doctype html><html>{{ head }}{{ app }}{{ runtime }}{{ title }}</html>",
161                PAGE,
162                "",
163            )
164            .unwrap_err()
165            .code,
166            "PSDOC1003_TEMPLATE_PLACEHOLDER_INVALID"
167        );
168    }
169}