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