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