use std::fmt;
pub const DOCUMENT_TEMPLATE_PLACEHOLDERS_V1: [&str; 3] =
["{{ head }}", "{{ app }}", "{{ runtime }}"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentTemplateErrorV1 {
pub code: &'static str,
pub message: String,
}
impl fmt::Display for DocumentTemplateErrorV1 {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(output, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for DocumentTemplateErrorV1 {}
pub fn apply_document_template_v1(
template: &str,
generated_page: &str,
additional_head: &str,
) -> Result<String, DocumentTemplateErrorV1> {
validate_template(template)?;
let (head, app, runtime) = split_generated_page(generated_page)?;
let head = format!("{head}{additional_head}");
Ok(template
.replacen("{{ head }}", &head, 1)
.replacen("{{ app }}", app, 1)
.replacen("{{ runtime }}", runtime, 1))
}
fn validate_template(template: &str) -> Result<(), DocumentTemplateErrorV1> {
if !template.trim_start().starts_with("<!doctype html>") || !template.contains("<html") {
return Err(DocumentTemplateErrorV1 {
code: "PSDOC1001_TEMPLATE_DOCUMENT_INVALID",
message: "app/index.html must declare an HTML document".into(),
});
}
for placeholder in DOCUMENT_TEMPLATE_PLACEHOLDERS_V1 {
if template.matches(placeholder).count() != 1 {
return Err(DocumentTemplateErrorV1 {
code: "PSDOC1002_TEMPLATE_PLACEHOLDER_COUNT",
message: format!("{placeholder} must occur exactly once"),
});
}
}
let mut remaining = template;
while let Some(start) = remaining.find("{{") {
let suffix = &remaining[start..];
let Some(end) = suffix.find("}}") else {
return Err(DocumentTemplateErrorV1 {
code: "PSDOC1003_TEMPLATE_PLACEHOLDER_INVALID",
message: "unclosed document-template placeholder".into(),
});
};
let placeholder = &suffix[..end + 2];
if !DOCUMENT_TEMPLATE_PLACEHOLDERS_V1.contains(&placeholder) {
return Err(DocumentTemplateErrorV1 {
code: "PSDOC1003_TEMPLATE_PLACEHOLDER_INVALID",
message: format!("unsupported document-template placeholder {placeholder}"),
});
}
remaining = &suffix[end + 2..];
}
Ok(())
}
fn split_generated_page(page: &str) -> Result<(&str, &str, &str), DocumentTemplateErrorV1> {
let head_start = " <head>\n";
let head_end = " </head>\n";
let body_start = " <body>\n";
let runtime_start =
" <script type=\"application/json\" id=\"presolve-template-manifest\">\n";
let body_end = " </body>\n</html>\n";
let Some(head_after) = page.find(head_start).map(|index| index + head_start.len()) else {
return generated_page_error();
};
let Some(head_end_index) = page[head_after..]
.find(head_end)
.map(|index| head_after + index)
else {
return generated_page_error();
};
let body_after = head_end_index + head_end.len();
if !page[body_after..].starts_with(body_start) {
return generated_page_error();
}
let app_after = body_after + body_start.len();
let Some(runtime_index) = page[app_after..]
.find(runtime_start)
.map(|index| app_after + index)
else {
return generated_page_error();
};
let Some(body_end_index) = page[runtime_index..]
.find(body_end)
.map(|index| runtime_index + index)
else {
return generated_page_error();
};
Ok((
&page[head_after..head_end_index],
&page[app_after..runtime_index],
&page[runtime_index..body_end_index],
))
}
fn generated_page_error<T>() -> Result<T, DocumentTemplateErrorV1> {
Err(DocumentTemplateErrorV1 {
code: "PSDOC1004_GENERATED_PAGE_INVALID",
message: "compiler-generated page does not have the expected document structure".into(),
})
}
#[cfg(test)]
mod tests {
use super::apply_document_template_v1;
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";
#[test]
fn projects_compiler_owned_parts_into_the_required_placeholders() {
let output = apply_document_template_v1(
"<!doctype html>\n<html lang=\"en\">\n<head>\n{{ head }}\n</head>\n<body>\n{{ app }}{{ runtime }}\n</body>\n</html>\n",
PAGE,
" <link rel=\"stylesheet\" href=\"/app.css\">\n",
)
.unwrap();
assert!(output.contains("<head>\n <meta charset"));
assert!(output.contains("href=\"/app.css\""));
assert!(output.contains("<main>Home</main>"));
assert!(output.contains("presolve-template-manifest"));
assert!(!output.contains("{{ head }}"));
}
#[test]
fn rejects_missing_or_unknown_placeholders() {
assert_eq!(
apply_document_template_v1(
"<!doctype html><html>{{ head }}{{ app }}</html>",
PAGE,
"",
)
.unwrap_err()
.code,
"PSDOC1002_TEMPLATE_PLACEHOLDER_COUNT"
);
assert_eq!(
apply_document_template_v1(
"<!doctype html><html>{{ head }}{{ app }}{{ runtime }}{{ title }}</html>",
PAGE,
"",
)
.unwrap_err()
.code,
"PSDOC1003_TEMPLATE_PLACEHOLDER_INVALID"
);
}
}