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