Skip to main content

presolve_compiler/
page_codegen.rs

1use crate::{
2    runtime_component_artifact_json, runtime_computed_artifact_json, runtime_context_artifact_json,
3    runtime_effect_artifact_json, runtime_forms_artifact_json, template_manifest_json,
4    ResumeManifest, RuntimeComponentArtifact, RuntimeComputedArtifact, RuntimeContextArtifact,
5    RuntimeEffectArtifact, RuntimeFormsArtifact, RuntimeOpaqueArtifact, RuntimeResourceArtifact,
6    TemplateManifest,
7};
8
9#[must_use]
10pub fn generate_standalone_page(
11    title: &str,
12    body_html: &str,
13    manifest: &TemplateManifest,
14) -> String {
15    generate_page(
16        title, body_html, manifest, None, None, None, None, None, None,
17    )
18}
19
20/// Generate a standalone page with compiler-generated computed runtime data.
21#[must_use]
22pub fn generate_standalone_page_with_computed_runtime(
23    title: &str,
24    body_html: &str,
25    manifest: &TemplateManifest,
26    computed: &RuntimeComputedArtifact,
27) -> String {
28    generate_page(
29        title,
30        body_html,
31        manifest,
32        Some(computed),
33        None,
34        None,
35        None,
36        None,
37        None,
38    )
39}
40
41/// Generate a standalone page with compiler-generated computed and effect runtime data.
42#[must_use]
43pub fn generate_standalone_page_with_effect_runtime(
44    title: &str,
45    body_html: &str,
46    manifest: &TemplateManifest,
47    computed: &RuntimeComputedArtifact,
48    effects: &RuntimeEffectArtifact,
49) -> String {
50    generate_page(
51        title,
52        body_html,
53        manifest,
54        Some(computed),
55        None,
56        Some(effects),
57        None,
58        None,
59        None,
60    )
61}
62
63/// Generate a standalone page with compiler-generated Context runtime data.
64#[must_use]
65pub fn generate_standalone_page_with_context_runtime(
66    title: &str,
67    body_html: &str,
68    manifest: &TemplateManifest,
69    computed: &RuntimeComputedArtifact,
70    context: &RuntimeContextArtifact,
71    effects: &RuntimeEffectArtifact,
72) -> String {
73    generate_page(
74        title,
75        body_html,
76        manifest,
77        Some(computed),
78        Some(context),
79        Some(effects),
80        None,
81        None,
82        None,
83    )
84}
85
86/// Generate a standalone page with all compiler-generated runtime artifacts.
87#[must_use]
88pub fn generate_standalone_page_with_component_runtime(
89    title: &str,
90    body_html: &str,
91    manifest: &TemplateManifest,
92    computed: &RuntimeComputedArtifact,
93    context: &RuntimeContextArtifact,
94    effects: &RuntimeEffectArtifact,
95    components: &RuntimeComponentArtifact,
96) -> String {
97    generate_page(
98        title,
99        body_html,
100        manifest,
101        Some(computed),
102        Some(context),
103        Some(effects),
104        Some(components),
105        None,
106        None,
107    )
108}
109
110/// Generate a standalone page with all compiler-generated runtime artifacts,
111/// including the I15 Forms artifact.
112#[must_use]
113#[allow(clippy::too_many_arguments)]
114pub fn generate_standalone_page_with_component_runtime_and_forms(
115    title: &str,
116    body_html: &str,
117    manifest: &TemplateManifest,
118    computed: &RuntimeComputedArtifact,
119    context: &RuntimeContextArtifact,
120    effects: &RuntimeEffectArtifact,
121    components: &RuntimeComponentArtifact,
122    forms: &RuntimeFormsArtifact,
123) -> String {
124    generate_page(
125        title,
126        body_html,
127        manifest,
128        Some(computed),
129        Some(context),
130        Some(effects),
131        Some(components),
132        Some(forms),
133        None,
134    )
135}
136
137/// Generate a standalone page with every runtime artifact and the exact J9
138/// resume-manifest bytes embedded for runtime consumption.
139#[must_use]
140#[allow(clippy::too_many_arguments)]
141pub fn generate_standalone_page_with_resume_runtime(
142    title: &str,
143    body_html: &str,
144    manifest: &TemplateManifest,
145    computed: &RuntimeComputedArtifact,
146    context: &RuntimeContextArtifact,
147    effects: &RuntimeEffectArtifact,
148    components: &RuntimeComponentArtifact,
149    forms: &RuntimeFormsArtifact,
150    resume: &ResumeManifest,
151) -> String {
152    generate_page(
153        title,
154        body_html,
155        manifest,
156        Some(computed),
157        Some(context),
158        Some(effects),
159        Some(components),
160        Some(forms),
161        Some(resume),
162    )
163}
164
165/// Generates the full runtime page with a Resource artifact embedded before
166/// the runtime boot script. The caller must supply the host-bound artifact.
167#[must_use]
168#[allow(clippy::too_many_arguments)]
169pub fn generate_standalone_page_with_resume_runtime_and_resources(
170    title: &str,
171    body_html: &str,
172    manifest: &TemplateManifest,
173    computed: &RuntimeComputedArtifact,
174    context: &RuntimeContextArtifact,
175    effects: &RuntimeEffectArtifact,
176    components: &RuntimeComponentArtifact,
177    forms: &RuntimeFormsArtifact,
178    resume: &ResumeManifest,
179    resources: &RuntimeResourceArtifact,
180) -> String {
181    let page = generate_standalone_page_with_resume_runtime(
182        title, body_html, manifest, computed, context, effects, components, forms, resume,
183    );
184    let mut resource_script =
185        "    <script type=\"application/json\" id=\"presolve-resources-runtime\">\n".to_string();
186    for line in crate::runtime_resource_artifact_json(resources).lines() {
187        resource_script.push_str("      ");
188        resource_script.push_str(&escape_script_json_line(line));
189        resource_script.push('\n');
190    }
191    resource_script.push_str("    </script>\n");
192    page.replacen(
193        "    <script src=\"./runtime.js\" defer></script>",
194        &(resource_script + "    <script src=\"./runtime.js\" defer></script>"),
195        1,
196    )
197}
198
199/// Embeds a validated opaque-terminal artifact immediately before the runtime
200/// boot script. The caller composes this with other compiler-owned page
201/// products; the function never inspects application source.
202#[must_use]
203pub fn embed_opaque_runtime_artifact(page: String, opaque: &RuntimeOpaqueArtifact) -> String {
204    let mut opaque_script =
205        "    <script type=\"application/json\" id=\"presolve-opaque-runtime\">\n".to_string();
206    for line in crate::runtime_opaque_artifact_json(opaque).lines() {
207        opaque_script.push_str("      ");
208        opaque_script.push_str(&escape_script_json_line(line));
209        opaque_script.push('\n');
210    }
211    opaque_script.push_str("    </script>\n");
212    page.replacen(
213        "    <script src=\"./runtime.js\" defer></script>",
214        &(opaque_script + "    <script src=\"./runtime.js\" defer></script>"),
215        1,
216    )
217}
218
219#[allow(clippy::too_many_arguments)]
220fn generate_page(
221    title: &str,
222    body_html: &str,
223    manifest: &TemplateManifest,
224    computed: Option<&RuntimeComputedArtifact>,
225    context: Option<&RuntimeContextArtifact>,
226    effects: Option<&RuntimeEffectArtifact>,
227    components: Option<&RuntimeComponentArtifact>,
228    forms: Option<&RuntimeFormsArtifact>,
229    resume: Option<&ResumeManifest>,
230) -> String {
231    let manifest_json = template_manifest_json(manifest);
232
233    let mut output = String::new();
234
235    output.push_str("<!doctype html>\n");
236    output.push_str("<html lang=\"en\">\n");
237    output.push_str("  <head>\n");
238    output.push_str("    <meta charset=\"utf-8\">\n");
239    output.push_str("    <title>");
240    output.push_str(&escape_text(title));
241    output.push_str("</title>\n");
242    output.push_str("  </head>\n");
243    output.push_str("  <body>\n");
244
245    for line in body_html.lines() {
246        output.push_str("    ");
247        output.push_str(line);
248        output.push('\n');
249    }
250
251    output.push_str("    <script type=\"application/json\" id=\"presolve-template-manifest\">\n");
252
253    for line in manifest_json.lines() {
254        output.push_str("      ");
255        output.push_str(&escape_script_json_line(line));
256        output.push('\n');
257    }
258
259    output.push_str("    </script>\n");
260    if let Some(computed) = computed {
261        output
262            .push_str("    <script type=\"application/json\" id=\"presolve-computed-runtime\">\n");
263        for line in runtime_computed_artifact_json(computed).lines() {
264            output.push_str("      ");
265            output.push_str(&escape_script_json_line(line));
266            output.push('\n');
267        }
268        output.push_str("    </script>\n");
269    }
270    if let Some(context) = context {
271        output.push_str("    <script type=\"application/json\" id=\"presolve-context-runtime\">\n");
272        for line in runtime_context_artifact_json(context).lines() {
273            output.push_str("      ");
274            output.push_str(&escape_script_json_line(line));
275            output.push('\n');
276        }
277        output.push_str("    </script>\n");
278    }
279    if let Some(effects) = effects {
280        output.push_str("    <script type=\"application/json\" id=\"presolve-effect-runtime\">\n");
281        for line in runtime_effect_artifact_json(effects).lines() {
282            output.push_str("      ");
283            output.push_str(&escape_script_json_line(line));
284            output.push('\n');
285        }
286        output.push_str("    </script>\n");
287    }
288    if let Some(components) = components {
289        output
290            .push_str("    <script type=\"application/json\" id=\"presolve-component-runtime\">\n");
291        for line in runtime_component_artifact_json(components).lines() {
292            output.push_str("      ");
293            output.push_str(&escape_script_json_line(line));
294            output.push('\n');
295        }
296        output.push_str("    </script>\n");
297    }
298    if let Some(forms) = forms {
299        output.push_str("    <script type=\"application/json\" id=\"presolve-forms-runtime\">\n");
300        for line in runtime_forms_artifact_json(forms).lines() {
301            output.push_str("      ");
302            output.push_str(&escape_script_json_line(line));
303            output.push('\n');
304        }
305        output.push_str("    </script>\n");
306    }
307    if let Some(resume) = resume {
308        output.push_str("    <script type=\"application/json\" id=\"presolve-resume-runtime\">");
309        output.push_str(&crate::resume_manifest_json(resume));
310        output.push_str("    </script>\n");
311    }
312    output.push_str("    <script src=\"./runtime.js\" defer></script>\n");
313    output.push_str("  </body>\n");
314    output.push_str("</html>\n");
315
316    output
317}
318
319fn escape_text(value: &str) -> String {
320    let mut output = String::new();
321
322    for ch in value.chars() {
323        match ch {
324            '&' => output.push_str("&amp;"),
325            '<' => output.push_str("&lt;"),
326            '>' => output.push_str("&gt;"),
327            ch => output.push(ch),
328        }
329    }
330
331    output
332}
333
334fn escape_script_json_line(value: &str) -> String {
335    value.replace("</script", "<\\/script")
336}
337
338#[cfg(test)]
339mod tests {
340    use super::{embed_opaque_runtime_artifact, escape_script_json_line, escape_text};
341
342    #[test]
343    fn escapes_title_text() {
344        assert_eq!(escape_text("A < B & C > D"), "A &lt; B &amp; C &gt; D");
345    }
346
347    #[test]
348    fn escapes_script_close_sequence() {
349        assert_eq!(escape_script_json_line(r#""</script>""#), r#""<\/script>""#);
350    }
351
352    #[test]
353    fn embeds_opaque_artifact_before_the_runtime_boot_script() {
354        let artifact: crate::RuntimeOpaqueArtifact = serde_json::from_str(
355            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"}]}"#,
356        )
357        .unwrap();
358        let page = embed_opaque_runtime_artifact(
359            "    <script src=\"./runtime.js\" defer></script>".to_string(),
360            &artifact,
361        );
362        assert!(page.contains("presolve-opaque-runtime"));
363        assert!(page.contains("trackPurchase"));
364        assert!(page.find("presolve-opaque-runtime") < page.find("./runtime.js"));
365    }
366}