1use crate::escape::ascii_escape_json;
4use crate::page::{
5 I18nOpts, PageConfig, build_seam_data, flatten_for_slots, inject_data_script, inject_head_meta,
6 inject_html_lang, replace_asset_slots, strip_asset_slots,
7};
8
9pub fn render_page(
21 template: &str,
22 loader_data_json: &str,
23 config_json: &str,
24 i18n_opts_json: Option<&str>,
25) -> String {
26 let loader_data: serde_json::Value =
27 serde_json::from_str(loader_data_json).unwrap_or(serde_json::Value::Null);
28 let config: PageConfig = match serde_json::from_str(config_json) {
29 Ok(c) => c,
30 Err(_) => return template.to_string(),
31 };
32 let i18n_opts: Option<I18nOpts> = i18n_opts_json.and_then(|s| serde_json::from_str(s).ok());
33
34 let working = match config.page_assets {
39 Some(ref assets) => replace_asset_slots(template, assets),
40 None => strip_asset_slots(template),
41 };
42
43 let flat_data = flatten_for_slots(&loader_data);
45
46 let mut html = seam_injector::inject_no_script(&working, &flat_data);
48
49 if let Some(ref meta) = config.head_meta {
51 let injected_meta = seam_injector::inject_no_script(meta, &flat_data);
53 html = inject_head_meta(&html, &injected_meta);
54 }
55
56 if let Some(ref opts) = i18n_opts {
58 html = inject_html_lang(&html, &opts.locale);
59 }
60
61 let seam_data = build_seam_data(&loader_data, &config, i18n_opts.as_ref());
63 let json = serde_json::to_string(&seam_data).unwrap_or_default();
64 let escaped = ascii_escape_json(&json);
65 inject_data_script(&html, &config.data_id, &escaped)
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use serde_json::json;
72
73 fn simple_template() -> String {
74 r#"<html><head><meta charset="utf-8"><title>Test</title></head><body><p><!--seam:title--></p></body></html>"#.to_string()
75 }
76
77 #[test]
78 fn render_basic_page() {
79 let template = simple_template();
80 let data = json!({"title": "Hello"}).to_string();
81 let config = json!({"layout_chain": [], "data_id": "__data"}).to_string();
82
83 let result = render_page(&template, &data, &config, None);
84 assert!(result.contains("<p>Hello</p>"));
85 assert!(result.contains(r#"<script id="__data""#));
86 assert!(result.contains(r#""title":"Hello""#));
87 }
88
89 #[test]
90 fn render_with_layout() {
91 let template = simple_template();
92 let data = json!({"title": "Page", "nav": "NavData"}).to_string();
93 let config = json!({
94 "layout_chain": [{"id": "root", "loader_keys": ["nav"]}],
95 "data_id": "__data"
96 })
97 .to_string();
98
99 let result = render_page(&template, &data, &config, None);
100 assert!(result.contains(r#""_layouts""#), "missing _layouts key");
102 assert!(result.contains(r#""root""#), "missing root layout key");
103 assert!(result.contains(r#""title":"Page""#), "missing page-level title");
105 }
106
107 #[test]
108 fn render_with_i18n() {
109 let template = simple_template();
110 let data = json!({"title": "Hello"}).to_string();
111 let config = json!({"layout_chain": [], "data_id": "__data"}).to_string();
112 let i18n = json!({
113 "locale": "zh",
114 "default_locale": "en",
115 "messages": {"hello": "你好"}
116 })
117 .to_string();
118
119 let result = render_page(&template, &data, &config, Some(&i18n));
120 assert!(result.contains(r#"<html lang="zh""#));
121 assert!(result.contains(r#""_i18n""#));
122 }
123
124 #[test]
125 fn render_with_head_meta() {
126 let template = simple_template();
127 let data = json!({"title": "Hello"}).to_string();
128 let config = json!({
129 "layout_chain": [],
130 "data_id": "__data",
131 "head_meta": r#"<title><!--seam:title--></title>"#
132 })
133 .to_string();
134
135 let result = render_page(&template, &data, &config, None);
136 assert!(result.contains(r#"<meta charset="utf-8"><title>Hello</title>"#));
138 }
139
140 #[test]
141 fn render_invalid_config_returns_template() {
142 let template = "plain html";
143 let result = render_page(template, "{}", "invalid json", None);
144 assert_eq!(result, "plain html");
145 }
146
147 #[test]
148 fn render_with_page_assets() {
149 let template = concat!(
150 r#"<html><head><meta charset="utf-8">"#,
151 r#"<link rel="stylesheet" href="/_seam/static/main.css">"#,
152 "<!--seam:page-styles--><!--seam:prefetch-->",
153 "</head><body>",
154 r#"<div id="__seam"><p><!--seam:title--></p></div>"#,
155 r#"<script type="module" src="/_seam/static/main.js"></script>"#,
156 "<!--seam:page-scripts-->",
157 "</body></html>"
158 );
159 let data = json!({"title": "Hello"}).to_string();
160 let config = json!({
161 "layout_chain": [],
162 "data_id": "__data",
163 "page_assets": {
164 "styles": ["page-home.css"],
165 "scripts": ["page-home.js"],
166 "preload": ["shared.js"],
167 "prefetch": ["page-other.js"]
168 }
169 })
170 .to_string();
171
172 let result = render_page(template, &data, &config, None);
173
174 assert!(result.contains(r#"href="/_seam/static/page-home.css""#));
176 assert!(result.contains(r#"src="/_seam/static/page-home.js""#));
177 assert!(result.contains(r#"modulepreload"#));
178 assert!(result.contains(r#"prefetch"#));
179 assert!(!result.contains("<!--seam:page-styles-->"));
181 assert!(!result.contains("<!--seam:page-scripts-->"));
182 assert!(!result.contains("<!--seam:prefetch-->"));
183 assert!(result.contains("<p>Hello</p>"));
185 assert!(result.contains(r#"<script id="__data""#));
186 }
187
188 #[test]
189 fn render_without_page_assets_strips_slots() {
190 let template = concat!(
191 r#"<html><head><meta charset="utf-8">"#,
192 "<!--seam:page-styles--><!--seam:prefetch-->",
193 "</head><body>",
194 r#"<div id="__seam"><p><!--seam:title--></p></div>"#,
195 "<!--seam:page-scripts-->",
196 "</body></html>"
197 );
198 let data = json!({"title": "Hello"}).to_string();
199 let config = json!({"layout_chain": [], "data_id": "__data"}).to_string();
200
201 let result = render_page(template, &data, &config, None);
202
203 assert!(!result.contains("<!--seam:page-styles-->"));
205 assert!(!result.contains("<!--seam:page-scripts-->"));
206 assert!(!result.contains("<!--seam:prefetch-->"));
207 assert!(result.contains("<p>Hello</p>"));
209 }
210}