1#[doc(hidden)]
4pub mod key;
5#[doc(hidden)]
6pub mod listeners;
7#[doc(hidden)]
8pub mod vcomp;
9#[doc(hidden)]
10pub mod vlist;
11#[doc(hidden)]
12pub mod vnode;
13#[doc(hidden)]
14pub mod vportal;
15#[doc(hidden)]
16pub mod vraw;
17#[doc(hidden)]
18pub mod vsuspense;
19#[doc(hidden)]
20pub mod vtag;
21#[doc(hidden)]
22pub mod vtext;
23
24use std::hint::unreachable_unchecked;
25use std::rc::Rc;
26
27use indexmap::IndexMap;
28use wasm_bindgen::JsValue;
29
30#[doc(inline)]
31pub use self::key::Key;
32#[doc(inline)]
33pub use self::listeners::*;
34#[doc(inline)]
35pub use self::vcomp::{VChild, VComp};
36#[doc(hidden)]
37pub use self::vlist::FullyKeyedState;
38#[doc(inline)]
39pub use self::vlist::VList;
40#[doc(inline)]
41pub use self::vnode::VNode;
42#[doc(inline)]
43pub use self::vportal::VPortal;
44#[doc(inline)]
45pub use self::vraw::VRaw;
46#[doc(inline)]
47pub use self::vsuspense::VSuspense;
48#[doc(inline)]
49pub use self::vtag::VTag;
50#[doc(inline)]
51pub use self::vtext::VText;
52
53pub type AttrValue = implicit_clone::unsync::IString;
55
56#[cfg(any(feature = "ssr", feature = "hydration"))]
57mod feat_ssr_hydration {
58 #[cfg(debug_assertions)]
59 type ComponentName = &'static str;
60 #[cfg(not(debug_assertions))]
61 type ComponentName = std::marker::PhantomData<()>;
62
63 #[cfg(feature = "hydration")]
64 use std::borrow::Cow;
65
66 pub enum Collectable {
70 Component(ComponentName),
71 Raw,
72 Suspense,
73 }
74
75 impl Collectable {
76 #[cfg(not(debug_assertions))]
77 #[inline(always)]
78 pub fn for_component<T: 'static>() -> Self {
79 use std::marker::PhantomData;
80 let _comp_type: PhantomData<T> = PhantomData;
84 Self::Component(PhantomData)
85 }
86
87 #[cfg(debug_assertions)]
88 pub fn for_component<T: 'static>() -> Self {
89 let comp_name = std::any::type_name::<T>();
90 Self::Component(comp_name)
91 }
92
93 pub fn open_start_mark(&self) -> &'static str {
94 match self {
95 Self::Component(_) => "<[",
96 Self::Raw => "<#",
97 Self::Suspense => "<?",
98 }
99 }
100
101 pub fn close_start_mark(&self) -> &'static str {
102 match self {
103 Self::Component(_) => "</[",
104 Self::Raw => "</#",
105 Self::Suspense => "</?",
106 }
107 }
108
109 pub fn end_mark(&self) -> &'static str {
110 match self {
111 Self::Component(_) => "]>",
112 Self::Raw => ">",
113 Self::Suspense => ">",
114 }
115 }
116
117 #[cfg(feature = "hydration")]
118 pub fn name(&self) -> Cow<'static, str> {
119 match self {
120 #[cfg(debug_assertions)]
121 Self::Component(m) => format!("Component({m})").into(),
122 #[cfg(not(debug_assertions))]
123 Self::Component(_) => "Component".into(),
124 Self::Raw => "Raw".into(),
125 Self::Suspense => "Suspense".into(),
126 }
127 }
128 }
129}
130
131#[cfg(any(feature = "ssr", feature = "hydration"))]
132pub(crate) use feat_ssr_hydration::*;
133
134#[cfg(feature = "ssr")]
135mod feat_ssr {
136 use std::fmt::Write;
137
138 use super::*;
139 use crate::platform::fmt::BufWriter;
140
141 impl Collectable {
142 pub(crate) fn write_open_tag(&self, w: &mut BufWriter) {
143 let _ = w.write_str("<!--");
144 let _ = w.write_str(self.open_start_mark());
145
146 #[cfg(debug_assertions)]
147 match self {
148 Self::Component(type_name) => {
149 let _ = w.write_str(type_name);
150 }
151 Self::Raw => {}
152 Self::Suspense => {}
153 }
154
155 let _ = w.write_str(self.end_mark());
156 let _ = w.write_str("-->");
157 }
158
159 pub(crate) fn write_close_tag(&self, w: &mut BufWriter) {
160 let _ = w.write_str("<!--");
161 let _ = w.write_str(self.close_start_mark());
162
163 #[cfg(debug_assertions)]
164 match self {
165 Self::Component(type_name) => {
166 let _ = w.write_str(type_name);
167 }
168 Self::Raw => {}
169 Self::Suspense => {}
170 }
171
172 let _ = w.write_str(self.end_mark());
173 let _ = w.write_str("-->");
174 }
175 }
176}
177
178#[expect(missing_docs)]
180#[derive(PartialEq, Clone, Debug)]
181pub enum AttributeOrProperty {
182 Attribute(AttrValue),
183 Property(JsValue),
184}
185
186fn is_valid_attr_name(attr: &str) -> bool {
187 !attr.is_empty()
211 && attr.chars().all(|c| match c {
212 c if c.is_control() => false,
213 '/' | '"' | '\'' | '>' | '=' => false,
214 c if c.is_whitespace() => false,
215 '\u{10000}'.. => false,
222 _ => true,
223 })
224}
225
226#[track_caller]
227fn validate_attr_name(attr: &str) {
228 assert!(
229 is_valid_attr_name(attr),
230 "{attr:?} is not a valid attribute name"
231 );
232}
233
234#[derive(PartialEq, Clone, Debug)]
236#[non_exhaustive]
237pub enum Attributes {
238 #[doc(hidden)]
239 #[deprecated = "Attribute names are not validated. Use one of the conversion functions"]
240 Static(&'static [(&'static str, AttributeOrProperty)]),
241
242 #[doc(hidden)]
243 #[deprecated = "Attribute names are not validated. Use one of the conversion functions"]
244 Dynamic {
245 keys: &'static [&'static str],
247
248 values: Box<[Option<AttributeOrProperty>]>,
251 },
252
253 #[doc(hidden)]
254 #[deprecated = "Attribute names are not validated. Use one of the conversion functions"]
255 IndexMap(Rc<IndexMap<AttrValue, AttributeOrProperty>>),
256}
257
258impl Attributes {
259 #[track_caller]
264 pub fn from_static(statics: &'static [(&'static str, AttributeOrProperty)]) -> Self {
265 for &(key, _) in statics {
266 validate_attr_name(key); }
268 Self::from_static_unchecked(statics)
269 }
270
271 pub fn from_static_unchecked(statics: &'static [(&'static str, AttributeOrProperty)]) -> Self {
274 #[expect(deprecated)]
275 Self::Static(statics)
276 }
277
278 #[track_caller]
284 pub fn from_dynamic_values(
285 keys: &'static [&'static str],
286 values: Box<[Option<AttributeOrProperty>]>,
287 ) -> Self {
288 for &key in keys {
289 validate_attr_name(key); }
291 Self::from_dynamic_values_unchecked(keys, values)
292 }
293
294 pub fn from_dynamic_values_unchecked(
297 keys: &'static [&'static str],
298 values: Box<[Option<AttributeOrProperty>]>,
299 ) -> Self {
300 #[expect(deprecated)]
301 Self::Dynamic { keys, values }
302 }
303
304 #[track_caller]
307 pub fn from_index_map(map: Rc<IndexMap<AttrValue, AttributeOrProperty>>) -> Self {
308 for (key, _) in map.iter() {
309 validate_attr_name(key); }
311 Self::from_index_map_unchecked(map)
312 }
313
314 pub fn from_index_map_unchecked(map: Rc<IndexMap<AttrValue, AttributeOrProperty>>) -> Self {
317 #[expect(deprecated)]
318 Self::IndexMap(map)
319 }
320
321 pub fn is_valid_attr_key(name: &str) -> bool {
329 is_valid_attr_name(name)
330 }
331
332 pub fn new() -> Self {
334 Self::default()
335 }
336
337 pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'a str, &'a str)> + 'a> {
342 #[expect(deprecated)]
343 match self {
344 Self::Static(arr) => Box::new(arr.iter().filter_map(|(k, v)| match v {
345 AttributeOrProperty::Attribute(v) => Some((*k, v.as_ref())),
346 AttributeOrProperty::Property(_) => None,
347 })),
348 Self::Dynamic { keys, values } => {
349 Box::new(keys.iter().zip(values.iter()).filter_map(|(k, v)| match v {
350 Some(AttributeOrProperty::Attribute(v)) => Some((*k, v.as_ref())),
351 _ => None,
352 }))
353 }
354 Self::IndexMap(m) => Box::new(m.iter().filter_map(|(k, v)| match v {
355 AttributeOrProperty::Attribute(v) => Some((k.as_ref(), v.as_ref())),
356 _ => None,
357 })),
358 }
359 }
360
361 #[doc(hidden)]
364 #[deprecated = "Attribute names are not validated. Use `get_mut_index_map_unchecked` to signal \
365 this properly and validate your modifications."]
366 pub fn get_mut_index_map(&mut self) -> &mut IndexMap<AttrValue, AttributeOrProperty> {
367 self.get_mut_index_map_unchecked()
368 }
369
370 pub fn get_mut_index_map_unchecked(&mut self) -> &mut IndexMap<AttrValue, AttributeOrProperty> {
375 macro_rules! unpack {
376 () => {
377 #[expect(deprecated)]
378 match self {
379 Self::IndexMap(m) => Rc::make_mut(m),
380 _ => unsafe { unreachable_unchecked() },
382 }
383 };
384 }
385
386 #[expect(deprecated)]
387 match self {
388 Self::IndexMap(m) => Rc::make_mut(m),
389 Self::Static(arr) => {
390 *self = Self::IndexMap(Rc::new(
391 arr.iter().map(|(k, v)| ((*k).into(), v.clone())).collect(),
392 ));
393 unpack!()
394 }
395 Self::Dynamic { keys, values } => {
396 *self = Self::from_index_map_unchecked(Rc::new(
397 std::mem::take(values)
398 .iter_mut()
399 .zip(keys.iter())
400 .filter_map(|(v, k)| v.take().map(|v| (AttrValue::from(*k), v)))
401 .collect(),
402 ));
403 unpack!()
404 }
405 }
406 }
407}
408
409impl From<IndexMap<AttrValue, AttrValue>> for Attributes {
410 fn from(map: IndexMap<AttrValue, AttrValue>) -> Self {
411 let v = map
412 .into_iter()
413 .map(|(k, v)| (k, AttributeOrProperty::Attribute(v)))
414 .collect();
415 Self::from_index_map(Rc::new(v))
416 }
417}
418
419impl From<IndexMap<&'static str, AttrValue>> for Attributes {
420 fn from(v: IndexMap<&'static str, AttrValue>) -> Self {
421 let v = v
422 .into_iter()
423 .map(|(k, v)| (AttrValue::Static(k), (AttributeOrProperty::Attribute(v))))
424 .collect();
425 Self::from_index_map(Rc::new(v))
426 }
427}
428
429impl From<IndexMap<&'static str, JsValue>> for Attributes {
430 fn from(v: IndexMap<&'static str, JsValue>) -> Self {
431 let v = v
432 .into_iter()
433 .map(|(k, v)| (AttrValue::Static(k), (AttributeOrProperty::Property(v))))
434 .collect();
435 Self::from_index_map(Rc::new(v))
436 }
437}
438
439impl Default for Attributes {
440 fn default() -> Self {
441 Self::from_static(&[])
442 }
443}