1use crate::component::Component;
4use crate::vdom::{VElement, VNode, VText};
5use std::collections::HashMap;
6
7pub struct SSRRenderer {
9 html: String,
11 state: HashMap<String, String>,
13 hydration_script: String,
15}
16
17impl SSRRenderer {
18 pub fn new() -> Self {
20 Self {
21 html: String::new(),
22 state: HashMap::new(),
23 hydration_script: String::new(),
24 }
25 }
26
27 pub fn render_to_string<C: Component>(&mut self, component: C) -> String {
29 let vnode = component.render();
30 self.render_vnode(&vnode);
31 self.html.clone()
32 }
33
34 pub fn render_to_document<C: Component>(&mut self, component: C, title: &str) -> String {
36 let body_html = self.render_to_string(component);
37
38 format!(
39 r#"<!DOCTYPE html>
40<html lang="en">
41<head>
42 <meta charset="UTF-8">
43 <meta name="viewport" content="width=device-width, initial-scale=1.0">
44 <title>{}</title>
45 <script id="__WINDJAMMER_STATE__" type="application/json">
46 {}
47 </script>
48</head>
49<body>
50 <div id="app">{}</div>
51 <script>
52 {}
53 </script>
54</body>
55</html>"#,
56 Self::escape_html(title),
57 serde_json::to_string(&self.state).unwrap_or_default(),
58 body_html,
59 self.get_hydration_script()
60 )
61 }
62
63 pub fn get_hydration_script(&self) -> String {
65 if !self.hydration_script.is_empty() {
66 return self.hydration_script.clone();
67 }
68
69 r#"
71 (function() {
72 // Hydration: Attach event listeners to server-rendered HTML
73 const state = JSON.parse(document.getElementById('__WINDJAMMER_STATE__').textContent);
74
75 // Store state for client-side hydration
76 window.__WINDJAMMER_HYDRATION_STATE__ = state;
77
78 // Mark as hydrated
79 document.getElementById('app').setAttribute('data-hydrated', 'true');
80
81 console.log('Windjammer: Hydration complete', state);
82 })();
83 "#
84 .to_string()
85 }
86
87 fn render_vnode(&mut self, vnode: &VNode) {
89 match vnode {
90 VNode::Element(element) => self.render_element(element),
91 VNode::Text(text) => self.render_text(text),
92 VNode::Component(_) => {
93 self.html.push_str("<!-- Component not expanded -->");
95 }
96 VNode::Empty => {}
97 }
98 }
99
100 fn render_element(&mut self, element: &VElement) {
102 self.html.push('<');
104 self.html.push_str(&element.tag);
105
106 for (key, value) in &element.attrs {
108 self.html.push(' ');
109 self.html.push_str(&Self::escape_attribute(key));
110 self.html.push_str("=\"");
111 self.html.push_str(&Self::escape_attribute(value));
112 self.html.push('"');
113 }
114
115 if element.children.is_empty() && Self::is_void_element(&element.tag) {
117 self.html.push_str(" />");
118 return;
119 }
120
121 self.html.push('>');
122
123 for child in &element.children {
125 self.render_vnode(child);
126 }
127
128 self.html.push_str("</");
130 self.html.push_str(&element.tag);
131 self.html.push('>');
132 }
133
134 fn render_text(&mut self, text: &VText) {
136 self.html.push_str(&Self::escape_html(&text.content));
137 }
138
139 fn is_void_element(tag: &str) -> bool {
141 matches!(
142 tag,
143 "area"
144 | "base"
145 | "br"
146 | "col"
147 | "embed"
148 | "hr"
149 | "img"
150 | "input"
151 | "link"
152 | "meta"
153 | "param"
154 | "source"
155 | "track"
156 | "wbr"
157 )
158 }
159
160 fn escape_html(text: &str) -> String {
162 text.replace('&', "&")
163 .replace('<', "<")
164 .replace('>', ">")
165 .replace('"', """)
166 .replace('\'', "'")
167 }
168
169 fn escape_attribute(text: &str) -> String {
171 text.replace('&', "&")
172 .replace('"', """)
173 .replace('<', "<")
174 .replace('>', ">")
175 }
176
177 pub fn add_state(&mut self, key: String, value: String) {
179 self.state.insert(key, value);
180 }
181
182 pub fn set_hydration_script(&mut self, script: String) {
184 self.hydration_script = script;
185 }
186}
187
188impl Default for SSRRenderer {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194pub struct StreamingSSRRenderer {
196 chunk_size: usize,
197 chunks: Vec<String>,
198}
199
200impl StreamingSSRRenderer {
201 pub fn new(chunk_size: usize) -> Self {
203 Self {
204 chunk_size,
205 chunks: Vec::new(),
206 }
207 }
208
209 pub fn render_vnode(&mut self, vnode: &VNode) -> Vec<String> {
211 self.chunks.clear();
212 let mut buffer = String::new();
213
214 self.render_vnode_to_buffer(vnode, &mut buffer);
215
216 for chunk in buffer.as_bytes().chunks(self.chunk_size) {
218 self.chunks.push(String::from_utf8_lossy(chunk).to_string());
219 }
220
221 self.chunks.clone()
222 }
223
224 #[allow(clippy::only_used_in_recursion)]
225 fn render_vnode_to_buffer(&self, vnode: &VNode, buffer: &mut String) {
226 match vnode {
227 VNode::Element(element) => {
228 buffer.push('<');
229 buffer.push_str(&element.tag);
230
231 for (key, value) in &element.attrs {
232 buffer.push(' ');
233 buffer.push_str(key);
234 buffer.push_str("=\"");
235 buffer.push_str(&SSRRenderer::escape_attribute(value));
236 buffer.push('"');
237 }
238
239 if element.children.is_empty() && SSRRenderer::is_void_element(&element.tag) {
240 buffer.push_str(" />");
241 } else {
242 buffer.push('>');
243 for child in &element.children {
244 self.render_vnode_to_buffer(child, buffer);
245 }
246 buffer.push_str("</");
247 buffer.push_str(&element.tag);
248 buffer.push('>');
249 }
250 }
251 VNode::Text(text) => {
252 buffer.push_str(&SSRRenderer::escape_html(&text.content));
253 }
254 VNode::Component(_) => {
255 buffer.push_str("<!-- Component -->");
256 }
257 VNode::Empty => {}
258 }
259 }
260}
261
262pub struct Hydration {
264 state: HashMap<String, String>,
265}
266
267impl Hydration {
268 pub fn from_state(state_json: &str) -> Result<Self, String> {
270 let state: HashMap<String, String> =
271 serde_json::from_str(state_json).map_err(|e| e.to_string())?;
272 Ok(Self { state })
273 }
274
275 pub fn get(&self, key: &str) -> Option<&String> {
277 self.state.get(key)
278 }
279
280 pub fn is_hydrated(&self) -> bool {
282 !self.state.is_empty()
283 }
284
285 #[allow(dead_code)]
287 fn render_to_document_with_html(&self, title: &str, body_html: &str) -> String {
288 format!(
289 r#"<!DOCTYPE html>
290<html lang="en">
291<head>
292 <meta charset="UTF-8">
293 <meta name="viewport" content="width=device-width, initial-scale=1.0">
294 <title>{}</title>
295</head>
296<body>
297 {}
298</body>
299</html>"#,
300 title, body_html
301 )
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn test_ssr_render_simple_element() {
311 let mut renderer = SSRRenderer::new();
312 let vnode = VNode::Element(VElement::new("div").child(VNode::Text(VText::new("Hello"))));
313
314 renderer.render_vnode(&vnode);
315 assert_eq!(renderer.html, "<div>Hello</div>");
316 }
317
318 #[test]
319 fn test_ssr_render_with_attributes() {
320 let mut renderer = SSRRenderer::new();
321 let vnode = VNode::Element(
322 VElement::new("div")
323 .attr("class", "container")
324 .child(VNode::Text(VText::new("Content"))),
325 );
326
327 renderer.render_vnode(&vnode);
328 assert!(renderer.html.contains("class=\"container\""));
329 assert!(renderer.html.contains("Content"));
330 }
331
332 #[test]
333 fn test_ssr_render_void_element() {
334 let mut renderer = SSRRenderer::new();
335 let vnode = VNode::Element(VElement::new("br"));
336
337 renderer.render_vnode(&vnode);
338 assert_eq!(renderer.html, "<br />");
339 }
340
341 #[test]
342 fn test_ssr_escape_html() {
343 let mut renderer = SSRRenderer::new();
344 let vnode = VNode::Element(
345 VElement::new("div").child(VNode::Text(VText::new("<script>alert('xss')</script>"))),
346 );
347
348 renderer.render_vnode(&vnode);
349 assert!(renderer.html.contains("<script>"));
350 assert!(!renderer.html.contains("<script>"));
351 }
352
353 #[test]
354 fn test_ssr_nested_elements() {
355 let mut renderer = SSRRenderer::new();
356 let vnode = VNode::Element(
357 VElement::new("div")
358 .child(VNode::Element(
359 VElement::new("p").child(VNode::Text(VText::new("Nested"))),
360 ))
361 .child(VNode::Element(
362 VElement::new("span").child(VNode::Text(VText::new("Content"))),
363 )),
364 );
365
366 renderer.render_vnode(&vnode);
367 assert!(renderer.html.contains("<div>"));
368 assert!(renderer.html.contains("<p>Nested</p>"));
369 assert!(renderer.html.contains("<span>Content</span>"));
370 assert!(renderer.html.contains("</div>"));
371 }
372
373 #[test]
374 fn test_ssr_document_generation() {
375 let mut renderer = SSRRenderer::new();
376
377 let vnode = VNode::Element(VElement::new("h1").child(VNode::Text(VText::new("Hello SSR"))));
379 renderer.render_vnode(&vnode);
380
381 assert!(renderer.html.contains("<h1>Hello SSR</h1>"));
383 }
384
385 #[test]
386 fn test_streaming_ssr() {
387 let mut renderer = StreamingSSRRenderer::new(10); let vnode = VNode::Element(
389 VElement::new("div").child(VNode::Text(VText::new("This is a longer text"))),
390 );
391
392 let chunks = renderer.render_vnode(&vnode);
393 assert!(!chunks.is_empty());
394
395 let combined: String = chunks.into_iter().collect();
397 assert!(combined.contains("<div>"));
398 assert!(combined.contains("This is a longer text"));
399 assert!(combined.contains("</div>"));
400 }
401
402 #[test]
403 fn test_hydration_state() {
404 let mut renderer = SSRRenderer::new();
405 renderer.add_state("count".to_string(), "42".to_string());
406 renderer.add_state("name".to_string(), "Alice".to_string());
407
408 let state_json = serde_json::to_string(&renderer.state).unwrap();
409 let hydration = Hydration::from_state(&state_json).unwrap();
410
411 assert_eq!(hydration.get("count"), Some(&"42".to_string()));
412 assert_eq!(hydration.get("name"), Some(&"Alice".to_string()));
413 assert!(hydration.is_hydrated());
414 }
415
416 #[test]
417 fn test_attribute_escaping() {
418 let mut renderer = SSRRenderer::new();
419 let vnode = VNode::Element(
420 VElement::new("input")
421 .attr("value", "Test \"quoted\" value")
422 .attr("data-test", "<script>"),
423 );
424
425 renderer.render_vnode(&vnode);
426 assert!(renderer.html.contains("""));
427 assert!(renderer.html.contains("<script>"));
428 }
429}