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,
6    RuntimePackageInvocationArtifact, RuntimeResourceArtifact, 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/// Embeds the compiler-authorized decorator-free package invocation registry
220/// metadata immediately before runtime boot.
221#[must_use]
222pub fn embed_package_invocation_runtime_artifact(
223    page: String,
224    package_invocations: &RuntimePackageInvocationArtifact,
225) -> String {
226    let mut artifact_script =
227        "    <script type=\"application/json\" id=\"presolve-package-invocations-runtime\">\n"
228            .to_string();
229    for line in crate::runtime_package_invocation_artifact_json(package_invocations).lines() {
230        artifact_script.push_str("      ");
231        artifact_script.push_str(&escape_script_json_line(line));
232        artifact_script.push('\n');
233    }
234    artifact_script.push_str("    </script>\n");
235    page.replacen(
236        "    <script src=\"./runtime.js\" defer></script>",
237        &(artifact_script + "    <script src=\"./runtime.js\" defer></script>"),
238        1,
239    )
240}
241
242#[allow(clippy::too_many_arguments)]
243fn generate_page(
244    title: &str,
245    body_html: &str,
246    manifest: &TemplateManifest,
247    computed: Option<&RuntimeComputedArtifact>,
248    context: Option<&RuntimeContextArtifact>,
249    effects: Option<&RuntimeEffectArtifact>,
250    components: Option<&RuntimeComponentArtifact>,
251    forms: Option<&RuntimeFormsArtifact>,
252    resume: Option<&ResumeManifest>,
253) -> String {
254    let manifest_json = template_manifest_json(manifest);
255
256    let mut output = String::new();
257
258    output.push_str("<!doctype html>\n");
259    output.push_str("<html lang=\"en\">\n");
260    output.push_str("  <head>\n");
261    output.push_str("    <meta charset=\"utf-8\">\n");
262    output.push_str("    <title>");
263    output.push_str(&escape_text(title));
264    output.push_str("</title>\n");
265    output.push_str("  </head>\n");
266    output.push_str("  <body>\n");
267
268    for line in body_html.lines() {
269        output.push_str("    ");
270        output.push_str(line);
271        output.push('\n');
272    }
273
274    output.push_str("    <script type=\"application/json\" id=\"presolve-template-manifest\">\n");
275
276    for line in manifest_json.lines() {
277        output.push_str("      ");
278        output.push_str(&escape_script_json_line(line));
279        output.push('\n');
280    }
281
282    output.push_str("    </script>\n");
283    if let Some(computed) = computed {
284        output
285            .push_str("    <script type=\"application/json\" id=\"presolve-computed-runtime\">\n");
286        for line in runtime_computed_artifact_json(computed).lines() {
287            output.push_str("      ");
288            output.push_str(&escape_script_json_line(line));
289            output.push('\n');
290        }
291        output.push_str("    </script>\n");
292    }
293    if let Some(context) = context {
294        output.push_str("    <script type=\"application/json\" id=\"presolve-context-runtime\">\n");
295        for line in runtime_context_artifact_json(context).lines() {
296            output.push_str("      ");
297            output.push_str(&escape_script_json_line(line));
298            output.push('\n');
299        }
300        output.push_str("    </script>\n");
301    }
302    if let Some(effects) = effects {
303        output.push_str("    <script type=\"application/json\" id=\"presolve-effect-runtime\">\n");
304        for line in runtime_effect_artifact_json(effects).lines() {
305            output.push_str("      ");
306            output.push_str(&escape_script_json_line(line));
307            output.push('\n');
308        }
309        output.push_str("    </script>\n");
310    }
311    if let Some(components) = components {
312        output
313            .push_str("    <script type=\"application/json\" id=\"presolve-component-runtime\">\n");
314        for line in runtime_component_artifact_json(components).lines() {
315            output.push_str("      ");
316            output.push_str(&escape_script_json_line(line));
317            output.push('\n');
318        }
319        output.push_str("    </script>\n");
320    }
321    if let Some(forms) = forms {
322        output.push_str("    <script type=\"application/json\" id=\"presolve-forms-runtime\">\n");
323        for line in runtime_forms_artifact_json(forms).lines() {
324            output.push_str("      ");
325            output.push_str(&escape_script_json_line(line));
326            output.push('\n');
327        }
328        output.push_str("    </script>\n");
329    }
330    if let Some(resume) = resume {
331        output.push_str("    <script type=\"application/json\" id=\"presolve-resume-runtime\">");
332        output.push_str(&crate::resume_manifest_json(resume));
333        output.push_str("    </script>\n");
334    }
335    output.push_str("    <script src=\"./runtime.js\" defer></script>\n");
336    output.push_str("  </body>\n");
337    output.push_str("</html>\n");
338
339    output
340}
341
342fn escape_text(value: &str) -> String {
343    let mut output = String::new();
344
345    for ch in value.chars() {
346        match ch {
347            '&' => output.push_str("&amp;"),
348            '<' => output.push_str("&lt;"),
349            '>' => output.push_str("&gt;"),
350            ch => output.push(ch),
351        }
352    }
353
354    output
355}
356
357fn escape_script_json_line(value: &str) -> String {
358    value.replace("</script", "<\\/script")
359}
360
361#[cfg(test)]
362mod tests {
363    use super::{embed_opaque_runtime_artifact, escape_script_json_line, escape_text};
364
365    #[test]
366    fn escapes_title_text() {
367        assert_eq!(escape_text("A < B & C > D"), "A &lt; B &amp; C &gt; D");
368    }
369
370    #[test]
371    fn escapes_script_close_sequence() {
372        assert_eq!(escape_script_json_line(r#""</script>""#), r#""<\/script>""#);
373    }
374
375    #[test]
376    fn embeds_opaque_artifact_before_the_runtime_boot_script() {
377        let artifact: crate::RuntimeOpaqueArtifact = serde_json::from_str(
378            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"}]}"#,
379        )
380        .unwrap();
381        let page = embed_opaque_runtime_artifact(
382            "    <script src=\"./runtime.js\" defer></script>".to_string(),
383            &artifact,
384        );
385        assert!(page.contains("presolve-opaque-runtime"));
386        assert!(page.contains("trackPurchase"));
387        assert!(page.find("presolve-opaque-runtime") < page.find("./runtime.js"));
388    }
389}