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#[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#[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#[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#[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#[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#[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#[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#[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#[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("&"),
348 '<' => output.push_str("<"),
349 '>' => output.push_str(">"),
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 < B & C > 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}