1use std::marker::PhantomData;
4use std::mem;
5use std::ops::{Deref, DerefMut};
6use std::rc::Rc;
7
8use wasm_bindgen::JsValue;
9use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
10
11use super::{AttrValue, AttributeOrProperty, Attributes, Key, Listener, Listeners, VNode};
12use crate::html::{ImplicitClone, IntoPropValue, NodeRef};
13
14pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
16
17pub const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML";
19
20pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
22
23#[derive(Debug, Eq, PartialEq)]
25pub(crate) struct Value<T>(Option<AttrValue>, PhantomData<T>);
26
27impl<T> Clone for Value<T> {
28 fn clone(&self) -> Self {
29 Self::new(self.0.clone())
30 }
31}
32
33impl<T> ImplicitClone for Value<T> {}
34
35impl<T> Default for Value<T> {
36 fn default() -> Self {
37 Self::new(None)
38 }
39}
40
41impl<T> Value<T> {
42 fn new(value: Option<AttrValue>) -> Self {
45 Value(value, PhantomData)
46 }
47
48 pub(crate) fn set(&mut self, value: Option<AttrValue>) {
51 self.0 = value;
52 }
53}
54
55impl<T> Deref for Value<T> {
56 type Target = Option<AttrValue>;
57
58 fn deref(&self) -> &Self::Target {
59 &self.0
60 }
61}
62
63#[derive(Debug, Clone, ImplicitClone, Default, Eq, PartialEq)]
66pub(crate) struct InputFields {
67 pub(crate) value: Value<InputElement>,
70 pub(crate) checked: Option<bool>,
76}
77
78impl Deref for InputFields {
79 type Target = Value<InputElement>;
80
81 fn deref(&self) -> &Self::Target {
82 &self.value
83 }
84}
85
86impl DerefMut for InputFields {
87 fn deref_mut(&mut self) -> &mut Self::Target {
88 &mut self.value
89 }
90}
91
92impl InputFields {
93 fn new(value: Option<AttrValue>, checked: Option<bool>) -> Self {
95 Self {
96 value: Value::new(value),
97 checked,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Default)]
103pub(crate) struct TextareaFields {
104 pub(crate) value: Value<TextAreaElement>,
107 #[allow(unused)] pub(crate) defaultvalue: Option<AttrValue>,
111}
112
113#[derive(Debug, Clone, ImplicitClone)]
116pub(crate) enum VTagInner {
117 Input(InputFields),
121 Textarea(TextareaFields),
125 Other {
127 tag: AttrValue,
129 children: VNode,
131 },
132}
133
134#[derive(Debug, Clone, ImplicitClone)]
138pub struct VTag {
139 pub(crate) inner: VTagInner,
141 pub(crate) listeners: Listeners,
143 pub node_ref: NodeRef,
145 pub attributes: Attributes,
147 pub key: Option<Key>,
148}
149
150impl VTag {
151 pub fn new(tag: impl Into<AttrValue>) -> Self {
153 let tag = tag.into();
154 let lowercase_tag = tag.to_ascii_lowercase();
155 Self::new_base(
156 match &*lowercase_tag {
157 "input" => VTagInner::Input(Default::default()),
158 "textarea" => VTagInner::Textarea(Default::default()),
159 _ => VTagInner::Other {
160 tag,
161 children: Default::default(),
162 },
163 },
164 Default::default(),
165 Default::default(),
166 Default::default(),
167 Default::default(),
168 )
169 }
170
171 #[doc(hidden)]
180 pub fn __new_input(
181 value: Option<AttrValue>,
182 checked: Option<bool>,
183 node_ref: NodeRef,
184 key: Option<Key>,
185 attributes: Attributes,
187 listeners: Listeners,
188 ) -> Self {
189 VTag::new_base(
190 VTagInner::Input(InputFields::new(
191 value,
192 checked,
195 )),
196 node_ref,
197 key,
198 attributes,
199 listeners,
200 )
201 }
202
203 #[doc(hidden)]
212 pub fn __new_textarea(
213 value: Option<AttrValue>,
214 defaultvalue: Option<AttrValue>,
215 node_ref: NodeRef,
216 key: Option<Key>,
217 attributes: Attributes,
219 listeners: Listeners,
220 ) -> Self {
221 VTag::new_base(
222 VTagInner::Textarea(TextareaFields {
223 value: Value::new(value),
224 defaultvalue,
225 }),
226 node_ref,
227 key,
228 attributes,
229 listeners,
230 )
231 }
232
233 #[doc(hidden)]
240 pub fn __new_other(
241 tag: AttrValue,
242 node_ref: NodeRef,
243 key: Option<Key>,
244 attributes: Attributes,
246 listeners: Listeners,
247 children: VNode,
248 ) -> Self {
249 VTag::new_base(
250 VTagInner::Other { tag, children },
251 node_ref,
252 key,
253 attributes,
254 listeners,
255 )
256 }
257
258 #[inline]
260 fn new_base(
261 inner: VTagInner,
262 node_ref: NodeRef,
263 key: Option<Key>,
264 attributes: Attributes,
265 listeners: Listeners,
266 ) -> Self {
267 VTag {
268 inner,
269 attributes,
270 listeners,
271 node_ref,
272 key,
273 }
274 }
275
276 pub fn tag(&self) -> &str {
278 match &self.inner {
279 VTagInner::Input { .. } => "input",
280 VTagInner::Textarea { .. } => "textarea",
281 VTagInner::Other { tag, .. } => tag.as_ref(),
282 }
283 }
284
285 pub fn add_child(&mut self, child: VNode) {
287 if let VTagInner::Other { children, .. } = &mut self.inner {
288 children.to_vlist_mut().add_child(child)
289 }
290 }
291
292 pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) {
294 if let VTagInner::Other { children: dst, .. } = &mut self.inner {
295 dst.to_vlist_mut().add_children(children)
296 }
297 }
298
299 pub fn children(&self) -> Option<&VNode> {
302 match &self.inner {
303 VTagInner::Other { children, .. } => Some(children),
304 _ => None,
305 }
306 }
307
308 pub fn children_mut(&mut self) -> Option<&mut VNode> {
311 match &mut self.inner {
312 VTagInner::Other { children, .. } => Some(children),
313 _ => None,
314 }
315 }
316
317 pub fn into_children(self) -> Option<VNode> {
320 match self.inner {
321 VTagInner::Other { children, .. } => Some(children),
322 _ => None,
323 }
324 }
325
326 pub fn value(&self) -> Option<&AttrValue> {
330 match &self.inner {
331 VTagInner::Input(f) => f.as_ref(),
332 VTagInner::Textarea(TextareaFields { value, .. }) => value.as_ref(),
333 VTagInner::Other { .. } => None,
334 }
335 }
336
337 pub fn set_value(&mut self, value: impl IntoPropValue<Option<AttrValue>>) {
341 match &mut self.inner {
342 VTagInner::Input(f) => {
343 f.set(value.into_prop_value());
344 }
345 VTagInner::Textarea(TextareaFields { value: dst, .. }) => {
346 dst.set(value.into_prop_value());
347 }
348 VTagInner::Other { .. } => (),
349 }
350 }
351
352 pub fn checked(&self) -> Option<bool> {
356 match &self.inner {
357 VTagInner::Input(f) => f.checked,
358 _ => None,
359 }
360 }
361
362 pub fn set_checked(&mut self, value: bool) {
366 if let VTagInner::Input(f) = &mut self.inner {
367 f.checked = Some(value);
368 }
369 }
370
371 pub fn preserve_checked(&mut self) {
375 if let VTagInner::Input(f) = &mut self.inner {
376 f.checked = None;
377 }
378 }
379
380 pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
385 self.attributes.get_mut_index_map().insert(
386 AttrValue::Static(key),
387 AttributeOrProperty::Attribute(value.into()),
388 );
389 }
390
391 pub fn add_property(&mut self, key: &'static str, value: impl Into<JsValue>) {
395 self.attributes.get_mut_index_map().insert(
396 AttrValue::Static(key),
397 AttributeOrProperty::Property(value.into()),
398 );
399 }
400
401 pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
406 self.attributes = attrs.into();
407 }
408
409 #[doc(hidden)]
410 pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) {
411 self.attributes.get_mut_index_map().insert(
412 AttrValue::from(key),
413 AttributeOrProperty::Attribute(value.into_prop_value()),
414 );
415 }
416
417 pub fn add_listener(&mut self, listener: Rc<dyn Listener>) -> bool {
420 match &mut self.listeners {
421 Listeners::None => {
422 self.set_listeners([Some(listener)].into());
423 true
424 }
425 Listeners::Pending(listeners) => {
426 let mut listeners = mem::take(listeners).into_vec();
427 listeners.push(Some(listener));
428
429 self.set_listeners(listeners.into());
430 true
431 }
432 }
433 }
434
435 pub fn set_listeners(&mut self, listeners: Box<[Option<Rc<dyn Listener>>]>) {
437 self.listeners = Listeners::Pending(listeners);
438 }
439}
440
441impl PartialEq for VTag {
442 fn eq(&self, other: &VTag) -> bool {
443 use VTagInner::*;
444
445 (match (&self.inner, &other.inner) {
446 (Input(l), Input(r)) => l == r,
447 (Textarea (TextareaFields{ value: value_l, .. }), Textarea (TextareaFields{ value: value_r, .. })) => value_l == value_r,
448 (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r,
449 _ => false,
450 }) && self.listeners.eq(&other.listeners)
451 && self.attributes == other.attributes
452 && match (&self.inner, &other.inner) {
454 (Other { children: ch_l, .. }, Other { children: ch_r, .. }) => ch_l == ch_r,
455 _ => true,
456 }
457 }
458}
459
460#[cfg(feature = "ssr")]
461mod feat_ssr {
462 use std::fmt::Write;
463
464 use super::*;
465 use crate::feat_ssr::VTagKind;
466 use crate::html::AnyScope;
467 use crate::platform::fmt::BufWriter;
468 use crate::virtual_dom::VText;
469
470 static VOID_ELEMENTS: &[&str; 15] = &[
472 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
473 "source", "track", "wbr", "textarea",
474 ];
475
476 impl VTag {
477 pub(crate) async fn render_into_stream(
478 &self,
479 w: &mut BufWriter,
480 parent_scope: &AnyScope,
481 hydratable: bool,
482 ) {
483 let _ = w.write_str("<");
484 let _ = w.write_str(self.tag());
485
486 let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| {
487 let _ = w.write_str(" ");
488 let _ = w.write_str(name);
489
490 if let Some(m) = val {
491 let _ = w.write_str("=\"");
492 let _ = w.write_str(&html_escape::encode_double_quoted_attribute(m));
493 let _ = w.write_str("\"");
494 }
495 };
496
497 if let VTagInner::Input(InputFields { value, checked }) = &self.inner {
498 if let Some(value) = value.as_deref() {
499 write_attr(w, "value", Some(value));
500 }
501
502 if *checked == Some(true) {
505 write_attr(w, "checked", None);
506 }
507 }
508
509 for (k, v) in self.attributes.iter() {
510 write_attr(w, k, Some(v));
511 }
512
513 let _ = w.write_str(">");
514
515 match &self.inner {
516 VTagInner::Input(_) => {}
517 VTagInner::Textarea(TextareaFields {
518 value,
519 defaultvalue,
520 }) => {
521 if let Some(def) = value.as_ref().or(defaultvalue.as_ref()) {
522 VText::new(def.clone())
523 .render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
524 .await;
525 }
526
527 let _ = w.write_str("</textarea>");
528 }
529 VTagInner::Other { tag, children } => {
530 let lowercase_tag = tag.to_ascii_lowercase();
531 if !VOID_ELEMENTS.contains(&lowercase_tag.as_ref()) {
532 children
533 .render_into_stream(w, parent_scope, hydratable, tag.into())
534 .await;
535
536 let _ = w.write_str("</");
537 let _ = w.write_str(tag);
538 let _ = w.write_str(">");
539 } else {
540 debug_assert!(
542 match children {
543 VNode::VList(m) => m.is_empty(),
544 _ => false,
545 },
546 "{tag} cannot have any children!"
547 );
548 }
549 }
550 }
551 }
552 }
553}
554
555#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
556#[cfg(feature = "ssr")]
557#[cfg(test)]
558mod ssr_tests {
559 use tokio::test;
560
561 use crate::LocalServerRenderer as ServerRenderer;
562 use crate::prelude::*;
563
564 #[cfg_attr(not(target_os = "wasi"), test)]
565 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
566 async fn test_simple_tag() {
567 #[component]
568 fn Comp() -> Html {
569 html! { <div></div> }
570 }
571
572 let s = ServerRenderer::<Comp>::new()
573 .hydratable(false)
574 .render()
575 .await;
576
577 assert_eq!(s, "<div></div>");
578 }
579
580 #[cfg_attr(not(target_os = "wasi"), test)]
581 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
582 async fn test_simple_tag_with_attr() {
583 #[component]
584 fn Comp() -> Html {
585 html! { <div class="abc"></div> }
586 }
587
588 let s = ServerRenderer::<Comp>::new()
589 .hydratable(false)
590 .render()
591 .await;
592
593 assert_eq!(s, r#"<div class="abc"></div>"#);
594 }
595
596 #[cfg_attr(not(target_os = "wasi"), test)]
597 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
598 async fn test_simple_tag_with_content() {
599 #[component]
600 fn Comp() -> Html {
601 html! { <div>{"Hello!"}</div> }
602 }
603
604 let s = ServerRenderer::<Comp>::new()
605 .hydratable(false)
606 .render()
607 .await;
608
609 assert_eq!(s, r#"<div>Hello!</div>"#);
610 }
611
612 #[cfg_attr(not(target_os = "wasi"), test)]
613 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
614 async fn test_simple_tag_with_nested_tag_and_input() {
615 #[component]
616 fn Comp() -> Html {
617 html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
618 }
619
620 let s = ServerRenderer::<Comp>::new()
621 .hydratable(false)
622 .render()
623 .await;
624
625 assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
626 }
627
628 #[cfg_attr(not(target_os = "wasi"), test)]
629 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
630 async fn test_textarea() {
631 #[component]
632 fn Comp() -> Html {
633 html! { <textarea value="teststring" /> }
634 }
635
636 let s = ServerRenderer::<Comp>::new()
637 .hydratable(false)
638 .render()
639 .await;
640
641 assert_eq!(s, r#"<textarea>teststring</textarea>"#);
642 }
643
644 #[cfg_attr(not(target_os = "wasi"), test)]
645 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
646 async fn test_textarea_w_defaultvalue() {
647 #[component]
648 fn Comp() -> Html {
649 html! { <textarea defaultvalue="teststring" /> }
650 }
651
652 let s = ServerRenderer::<Comp>::new()
653 .hydratable(false)
654 .render()
655 .await;
656
657 assert_eq!(s, r#"<textarea>teststring</textarea>"#);
658 }
659
660 #[cfg_attr(not(target_os = "wasi"), test)]
661 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
662 async fn test_value_precedence_over_defaultvalue() {
663 #[component]
664 fn Comp() -> Html {
665 html! { <textarea defaultvalue="defaultvalue" value="value" /> }
666 }
667
668 let s = ServerRenderer::<Comp>::new()
669 .hydratable(false)
670 .render()
671 .await;
672
673 assert_eq!(s, r#"<textarea>value</textarea>"#);
674 }
675
676 #[cfg_attr(not(target_os = "wasi"), test)]
677 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
678 async fn test_escaping_in_style_tag() {
679 #[component]
680 fn Comp() -> Html {
681 html! { <style>{"body > a {color: #cc0;}"}</style> }
682 }
683
684 let s = ServerRenderer::<Comp>::new()
685 .hydratable(false)
686 .render()
687 .await;
688
689 assert_eq!(s, r#"<style>body > a {color: #cc0;}</style>"#);
690 }
691
692 #[cfg_attr(not(target_os = "wasi"), test)]
693 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
694 async fn test_escaping_in_script_tag() {
695 #[component]
696 fn Comp() -> Html {
697 html! { <script>{"foo.bar = x < y;"}</script> }
698 }
699
700 let s = ServerRenderer::<Comp>::new()
701 .hydratable(false)
702 .render()
703 .await;
704
705 assert_eq!(s, r#"<script>foo.bar = x < y;</script>"#);
706 }
707
708 #[cfg_attr(not(target_os = "wasi"), test)]
709 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
710 async fn test_multiple_vtext_in_style_tag() {
711 #[component]
712 fn Comp() -> Html {
713 let one = "html { background: black } ";
714 let two = "body > a { color: white } ";
715 html! {
716 <style>
717 {one}
718 {two}
719 </style>
720 }
721 }
722
723 let s = ServerRenderer::<Comp>::new()
724 .hydratable(false)
725 .render()
726 .await;
727
728 assert_eq!(
729 s,
730 r#"<style>html { background: black } body > a { color: white } </style>"#
731 );
732 }
733}