use crate::{
runtime_component_artifact_json, runtime_computed_artifact_json, runtime_context_artifact_json,
runtime_effect_artifact_json, runtime_forms_artifact_json, template_manifest_json,
ResumeManifest, RuntimeComponentArtifact, RuntimeComputedArtifact, RuntimeContextArtifact,
RuntimeEffectArtifact, RuntimeFormsArtifact, RuntimeOpaqueArtifact, RuntimeResourceArtifact,
TemplateManifest,
};
#[must_use]
pub fn generate_standalone_page(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
) -> String {
generate_page(
title, body_html, manifest, None, None, None, None, None, None,
)
}
#[must_use]
pub fn generate_standalone_page_with_computed_runtime(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: &RuntimeComputedArtifact,
) -> String {
generate_page(
title,
body_html,
manifest,
Some(computed),
None,
None,
None,
None,
None,
)
}
#[must_use]
pub fn generate_standalone_page_with_effect_runtime(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: &RuntimeComputedArtifact,
effects: &RuntimeEffectArtifact,
) -> String {
generate_page(
title,
body_html,
manifest,
Some(computed),
None,
Some(effects),
None,
None,
None,
)
}
#[must_use]
pub fn generate_standalone_page_with_context_runtime(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: &RuntimeComputedArtifact,
context: &RuntimeContextArtifact,
effects: &RuntimeEffectArtifact,
) -> String {
generate_page(
title,
body_html,
manifest,
Some(computed),
Some(context),
Some(effects),
None,
None,
None,
)
}
#[must_use]
pub fn generate_standalone_page_with_component_runtime(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: &RuntimeComputedArtifact,
context: &RuntimeContextArtifact,
effects: &RuntimeEffectArtifact,
components: &RuntimeComponentArtifact,
) -> String {
generate_page(
title,
body_html,
manifest,
Some(computed),
Some(context),
Some(effects),
Some(components),
None,
None,
)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn generate_standalone_page_with_component_runtime_and_forms(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: &RuntimeComputedArtifact,
context: &RuntimeContextArtifact,
effects: &RuntimeEffectArtifact,
components: &RuntimeComponentArtifact,
forms: &RuntimeFormsArtifact,
) -> String {
generate_page(
title,
body_html,
manifest,
Some(computed),
Some(context),
Some(effects),
Some(components),
Some(forms),
None,
)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn generate_standalone_page_with_resume_runtime(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: &RuntimeComputedArtifact,
context: &RuntimeContextArtifact,
effects: &RuntimeEffectArtifact,
components: &RuntimeComponentArtifact,
forms: &RuntimeFormsArtifact,
resume: &ResumeManifest,
) -> String {
generate_page(
title,
body_html,
manifest,
Some(computed),
Some(context),
Some(effects),
Some(components),
Some(forms),
Some(resume),
)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn generate_standalone_page_with_resume_runtime_and_resources(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: &RuntimeComputedArtifact,
context: &RuntimeContextArtifact,
effects: &RuntimeEffectArtifact,
components: &RuntimeComponentArtifact,
forms: &RuntimeFormsArtifact,
resume: &ResumeManifest,
resources: &RuntimeResourceArtifact,
) -> String {
let page = generate_standalone_page_with_resume_runtime(
title, body_html, manifest, computed, context, effects, components, forms, resume,
);
let mut resource_script =
" <script type=\"application/json\" id=\"presolve-resources-runtime\">\n".to_string();
for line in crate::runtime_resource_artifact_json(resources).lines() {
resource_script.push_str(" ");
resource_script.push_str(&escape_script_json_line(line));
resource_script.push('\n');
}
resource_script.push_str(" </script>\n");
page.replacen(
" <script src=\"./runtime.js\" defer></script>",
&(resource_script + " <script src=\"./runtime.js\" defer></script>"),
1,
)
}
#[must_use]
pub fn embed_opaque_runtime_artifact(page: String, opaque: &RuntimeOpaqueArtifact) -> String {
let mut opaque_script =
" <script type=\"application/json\" id=\"presolve-opaque-runtime\">\n".to_string();
for line in crate::runtime_opaque_artifact_json(opaque).lines() {
opaque_script.push_str(" ");
opaque_script.push_str(&escape_script_json_line(line));
opaque_script.push('\n');
}
opaque_script.push_str(" </script>\n");
page.replacen(
" <script src=\"./runtime.js\" defer></script>",
&(opaque_script + " <script src=\"./runtime.js\" defer></script>"),
1,
)
}
#[allow(clippy::too_many_arguments)]
fn generate_page(
title: &str,
body_html: &str,
manifest: &TemplateManifest,
computed: Option<&RuntimeComputedArtifact>,
context: Option<&RuntimeContextArtifact>,
effects: Option<&RuntimeEffectArtifact>,
components: Option<&RuntimeComponentArtifact>,
forms: Option<&RuntimeFormsArtifact>,
resume: Option<&ResumeManifest>,
) -> String {
let manifest_json = template_manifest_json(manifest);
let mut output = String::new();
output.push_str("<!doctype html>\n");
output.push_str("<html lang=\"en\">\n");
output.push_str(" <head>\n");
output.push_str(" <meta charset=\"utf-8\">\n");
output.push_str(" <title>");
output.push_str(&escape_text(title));
output.push_str("</title>\n");
output.push_str(" </head>\n");
output.push_str(" <body>\n");
for line in body_html.lines() {
output.push_str(" ");
output.push_str(line);
output.push('\n');
}
output.push_str(" <script type=\"application/json\" id=\"presolve-template-manifest\">\n");
for line in manifest_json.lines() {
output.push_str(" ");
output.push_str(&escape_script_json_line(line));
output.push('\n');
}
output.push_str(" </script>\n");
if let Some(computed) = computed {
output
.push_str(" <script type=\"application/json\" id=\"presolve-computed-runtime\">\n");
for line in runtime_computed_artifact_json(computed).lines() {
output.push_str(" ");
output.push_str(&escape_script_json_line(line));
output.push('\n');
}
output.push_str(" </script>\n");
}
if let Some(context) = context {
output.push_str(" <script type=\"application/json\" id=\"presolve-context-runtime\">\n");
for line in runtime_context_artifact_json(context).lines() {
output.push_str(" ");
output.push_str(&escape_script_json_line(line));
output.push('\n');
}
output.push_str(" </script>\n");
}
if let Some(effects) = effects {
output.push_str(" <script type=\"application/json\" id=\"presolve-effect-runtime\">\n");
for line in runtime_effect_artifact_json(effects).lines() {
output.push_str(" ");
output.push_str(&escape_script_json_line(line));
output.push('\n');
}
output.push_str(" </script>\n");
}
if let Some(components) = components {
output
.push_str(" <script type=\"application/json\" id=\"presolve-component-runtime\">\n");
for line in runtime_component_artifact_json(components).lines() {
output.push_str(" ");
output.push_str(&escape_script_json_line(line));
output.push('\n');
}
output.push_str(" </script>\n");
}
if let Some(forms) = forms {
output.push_str(" <script type=\"application/json\" id=\"presolve-forms-runtime\">\n");
for line in runtime_forms_artifact_json(forms).lines() {
output.push_str(" ");
output.push_str(&escape_script_json_line(line));
output.push('\n');
}
output.push_str(" </script>\n");
}
if let Some(resume) = resume {
output.push_str(" <script type=\"application/json\" id=\"presolve-resume-runtime\">");
output.push_str(&crate::resume_manifest_json(resume));
output.push_str(" </script>\n");
}
output.push_str(" <script src=\"./runtime.js\" defer></script>\n");
output.push_str(" </body>\n");
output.push_str("</html>\n");
output
}
fn escape_text(value: &str) -> String {
let mut output = String::new();
for ch in value.chars() {
match ch {
'&' => output.push_str("&"),
'<' => output.push_str("<"),
'>' => output.push_str(">"),
ch => output.push(ch),
}
}
output
}
fn escape_script_json_line(value: &str) -> String {
value.replace("</script", "<\\/script")
}
#[cfg(test)]
mod tests {
use super::{embed_opaque_runtime_artifact, escape_script_json_line, escape_text};
#[test]
fn escapes_title_text() {
assert_eq!(escape_text("A < B & C > D"), "A < B & C > D");
}
#[test]
fn escapes_script_close_sequence() {
assert_eq!(escape_script_json_line(r#""</script>""#), r#""<\/script>""#);
}
#[test]
fn embeds_opaque_artifact_before_the_runtime_boot_script() {
let artifact: crate::RuntimeOpaqueArtifact = serde_json::from_str(
r#"{"schema_version":1,"activations":[{"id":"opaque:track","owner_component":"component:x","method":"component:x/method:track","package":"@acme/analytics","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","export":"trackPurchase","type_signature":"() -> void","runtime_module":"dist/track.js","execution_boundary":"client","resume_policy":"cold_fallback"}]}"#,
)
.unwrap();
let page = embed_opaque_runtime_artifact(
" <script src=\"./runtime.js\" defer></script>".to_string(),
&artifact,
);
assert!(page.contains("presolve-opaque-runtime"));
assert!(page.contains("trackPurchase"));
assert!(page.find("presolve-opaque-runtime") < page.find("./runtime.js"));
}
}