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