1use std::any::{Any, TypeId};
2use std::collections::HashSet;
3use std::sync::{Arc, Mutex};
4
5use once_cell::sync::Lazy;
6
7use crate::*;
8
9pub enum SsrNode {
10 Element {
11 tag: Cow<'static, str>,
12 attributes: Vec<(Cow<'static, str>, Cow<'static, str>)>,
13 bool_attributes: Vec<(Cow<'static, str>, bool)>,
14 children: Vec<Self>,
15 inner_html: Option<Box<Cow<'static, str>>>,
17 hk_key: Option<HydrationKey>,
18 },
19 TextDynamic {
20 text: Cow<'static, str>,
21 },
22 TextStatic {
23 text: Cow<'static, str>,
24 },
25 Marker,
26 Dynamic {
31 view: Arc<Mutex<View<Self>>>,
32 },
33 SuspenseMarker {
34 key: u32,
35 },
36}
37
38impl From<SsrNode> for View<SsrNode> {
39 fn from(node: SsrNode) -> Self {
40 View::from_node(node)
41 }
42}
43
44impl ViewNode for SsrNode {
45 fn append_child(&mut self, child: Self) {
46 match self {
47 Self::Element { children, .. } => {
48 children.push(child);
49 }
50 _ => panic!("can only append child to an element"),
51 }
52 }
53
54 fn create_dynamic_view<U: Into<View<Self>> + 'static>(
55 mut f: impl FnMut() -> U + 'static,
56 ) -> View<Self> {
57 if TypeId::of::<U>() == TypeId::of::<String>() {
61 let text = (Box::new(f()) as Box<dyn Any>)
62 .downcast::<String>()
63 .unwrap();
64 View::from(SsrNode::TextDynamic {
65 text: (*text).into(),
66 })
67 } else {
68 let start = Self::create_marker_node();
69 let end = Self::create_marker_node();
70 let view = f().into();
71 View::from((start, view, end))
72 }
73 }
74}
75
76impl ViewHtmlNode for SsrNode {
77 fn create_element(tag: Cow<'static, str>) -> Self {
78 let hk_key = if IS_HYDRATING.get() {
79 let reg: HydrationRegistry = use_context();
80 Some(reg.next_key())
81 } else {
82 None
83 };
84 Self::Element {
85 tag,
86 attributes: Vec::new(),
87 bool_attributes: Vec::new(),
88 children: Vec::new(),
89 inner_html: None,
90 hk_key,
91 }
92 }
93
94 fn create_element_ns(_namespace: &str, tag: Cow<'static, str>) -> Self {
95 Self::create_element(tag)
97 }
98
99 fn create_text_node(text: Cow<'static, str>) -> Self {
100 Self::TextStatic { text }
101 }
102
103 fn create_dynamic_text_node(text: Cow<'static, str>) -> Self {
104 Self::TextDynamic { text }
105 }
106
107 fn create_marker_node() -> Self {
108 Self::Marker
109 }
110
111 fn set_attribute(&mut self, name: Cow<'static, str>, value: StringAttribute) {
112 match self {
113 Self::Element { attributes, .. } => {
114 if let Some(value) = value.evaluate() {
115 attributes.push((name, value))
116 }
117 }
118 _ => panic!("can only set attribute on an element"),
119 }
120 }
121
122 fn set_bool_attribute(&mut self, name: Cow<'static, str>, value: BoolAttribute) {
123 match self {
124 Self::Element {
125 bool_attributes, ..
126 } => bool_attributes.push((name, value.evaluate())),
127 _ => panic!("can only set attribute on an element"),
128 }
129 }
130
131 fn set_property(&mut self, _name: Cow<'static, str>, _value: MaybeDyn<JsValue>) {
132 }
134
135 fn set_event_handler(
136 &mut self,
137 _name: Cow<'static, str>,
138 _handler: impl FnMut(web_sys::Event) + 'static,
139 ) {
140 }
142
143 fn set_inner_html(&mut self, inner_html: Cow<'static, str>) {
144 match self {
145 Self::Element {
146 inner_html: slot, ..
147 } => *slot = Some(Box::new(inner_html)),
148 _ => panic!("can only set inner_html on an element"),
149 }
150 }
151
152 fn as_web_sys(&self) -> &web_sys::Node {
153 panic!("`as_web_sys()` is not supported in SSR mode")
154 }
155
156 fn from_web_sys(_node: web_sys::Node) -> Self {
157 panic!("`from_web_sys()` is not supported in SSR mode")
158 }
159}
160
161static VOID_ELEMENTS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
163 [
164 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
165 "source", "track", "wbr", "command", "keygen", "menuitem",
166 ]
167 .into_iter()
168 .collect()
169});
170
171pub(crate) fn render_recursive(node: &SsrNode, buf: &mut String) {
173 match node {
174 SsrNode::Element {
175 tag,
176 attributes,
177 bool_attributes,
178 children,
179 inner_html,
180 hk_key,
181 } => {
182 buf.push('<');
183 buf.push_str(tag);
184 for (name, value) in attributes {
185 buf.push(' ');
186 buf.push_str(name);
187 buf.push_str("=\"");
188 html_escape::encode_double_quoted_attribute_to_string(value, buf);
189 buf.push('"');
190 }
191 for (name, value) in bool_attributes {
192 if *value {
193 buf.push(' ');
194 buf.push_str(name);
195 }
196 }
197
198 if let Some(hk_key) = hk_key {
199 buf.push_str(" data-hk=\"");
200 buf.push_str(&hk_key.to_string());
201 buf.push('"');
202 }
203 buf.push('>');
204
205 let is_void = VOID_ELEMENTS.contains(tag.as_ref());
206
207 if is_void {
208 assert!(
209 children.is_empty() && inner_html.is_none(),
210 "void elements cannot have children or inner_html"
211 );
212 return;
213 }
214 if let Some(inner_html) = inner_html {
215 assert!(
216 children.is_empty(),
217 "inner_html and children are mutually exclusive"
218 );
219 buf.push_str(inner_html);
220 } else {
221 for child in children {
222 render_recursive(child, buf);
223 }
224 }
225
226 if !is_void {
227 buf.push_str("</");
228 buf.push_str(tag);
229 buf.push('>');
230 }
231 }
232 SsrNode::TextDynamic { text } => {
233 buf.push_str("<!--t-->"); html_escape::encode_text_to_string(text, buf);
235 buf.push_str("<!-->"); }
237 SsrNode::TextStatic { text } => {
238 html_escape::encode_text_to_string(text, buf);
239 }
240 SsrNode::Marker => {
241 buf.push_str("<!--/-->");
242 }
243 SsrNode::Dynamic { view } => {
244 render_recursive_view(&view.lock().unwrap(), buf);
245 }
246 SsrNode::SuspenseMarker { key } => {
247 buf.push_str("<!--sycamore-suspense-");
248 buf.push_str(&key.to_string());
249 buf.push_str("-->");
250 }
251 }
252}
253
254pub(crate) fn render_recursive_view(view: &View, buf: &mut String) {
256 for node in &view.nodes {
257 render_recursive(node, buf);
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use expect_test::{expect, Expect};
264
265 use super::*;
266 use crate::tags::*;
267
268 fn check<T: Into<View>>(view: impl FnOnce() -> T, expect: Expect) {
269 let actual = render_to_string(move || view().into());
270 expect.assert_eq(&actual);
271 }
272
273 #[test]
274 fn hello_world() {
275 check(move || "Hello, world!", expect!["Hello, world!"]);
276 }
277
278 #[test]
279 fn render_escaped_text() {
280 check(
281 move || "<script>alert('xss')</script>",
282 expect!["<script>alert('xss')</script>"],
283 );
284 }
285
286 #[test]
287 fn render_inner_html() {
288 check(
289 move || div().dangerously_set_inner_html("<p>hello</p>"),
290 expect![[r#"<div data-hk="0.0"><p>hello</p></div>"#]],
291 );
292 }
293
294 #[test]
295 fn render_void_element() {
296 check(br, expect![[r#"<br data-hk="0.0">"#]]);
297 check(
298 move || input().value("value"),
299 expect![[r#"<input value="value" data-hk="0.0">"#]],
300 );
301 }
302
303 #[test]
304 fn fragments() {
305 check(
306 move || (p().children("1"), p().children("2"), p().children("3")),
307 expect![[r#"<p data-hk="0.0">1</p><p data-hk="0.1">2</p><p data-hk="0.2">3</p>"#]],
308 );
309 }
310
311 #[test]
312 fn indexed() {
313 check(
314 move || {
315 sycamore_macro::view! {
316 ul {
317 Indexed(
318 list=vec![1, 2],
319 view=|i| sycamore_macro::view! { li { (i) } },
320 )
321 }
322 }
323 },
324 expect![[r#"<ul data-hk="0.0"><li data-hk="0.1">1</li><li data-hk="0.2">2</li></ul>"#]],
325 );
326 }
327
328 #[test]
329 fn bind() {
330 check(
332 move || {
333 let value = create_signal(String::new());
334 sycamore_macro::view! {
335 input(bind:value=value)
336 }
337 },
338 expect![[r#"<input data-hk="0.0">"#]],
339 );
340 }
341
342 #[test]
343 fn svg_element() {
344 check(
345 move || {
346 sycamore_macro::view! {
347 svg(xmlns="http://www.w2.org/2000/svg") {
348 rect()
349 }
350 }
351 },
352 expect![[
353 r#"<svg xmlns="http://www.w2.org/2000/svg" data-hk="0.0"><rect data-hk="0.1"></rect></svg>"#
354 ]],
355 );
356 check(
357 move || {
358 sycamore_macro::view! {
359 svg_a()
360 }
361 },
362 expect![[r#"<a data-hk="0.0"></a>"#]],
363 );
364 }
365}