1use alloc::collections::BTreeMap;
7use alloc::string::String;
8use alloc::vec::Vec;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ShadowRootMode {
13 Open,
14 Closed,
15}
16
17impl ShadowRootMode {
18 pub fn from_str(s: &str) -> Option<Self> {
19 match s {
20 "open" => Some(Self::Open),
21 "closed" => Some(Self::Closed),
22 _ => None,
23 }
24 }
25
26 pub fn as_str(&self) -> &'static str {
27 match self {
28 Self::Open => "open",
29 Self::Closed => "closed",
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ShadowRootData {
37 pub root_node_idx: usize,
39 pub mode: ShadowRootMode,
41 pub host_idx: usize,
43}
44
45impl ShadowRootData {
46 pub fn new(root_node_idx: usize, mode: ShadowRootMode, host_idx: usize) -> Self {
47 Self {
48 root_node_idx,
49 mode,
50 host_idx,
51 }
52 }
53
54 pub fn is_accessible_via_element(&self) -> bool {
56 self.mode == ShadowRootMode::Open
57 }
58}
59
60#[derive(Debug, Clone, Default)]
62pub struct ShadowTreeRegistry {
63 roots_by_host: BTreeMap<usize, ShadowRootData>,
65 hosts_by_root: BTreeMap<usize, usize>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum AttachShadowError {
71 AlreadyHasShadowRoot,
72 InvalidMode,
73 DisallowedTag,
74}
75
76const DISALLOWED_TAGS: &[&str] = &[
78 "button", "input", "textarea", "select", "option",
79 "table", "tr", "td", "th", "tbody", "thead", "tfoot",
80 "video", "audio", "canvas", "iframe", "embed", "object",
81 "img", "br", "hr", "wbr",
82];
83
84pub fn is_tag_allowed_for_shadow_root(tag_name: &str) -> bool {
86 let tag = tag_name.to_ascii_lowercase();
87 if tag.contains('-') {
89 return true;
90 }
91 match tag.as_str() {
94 "article" | "aside" | "blockquote" | "body" | "div" | "footer" |
95 "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "header" | "main" |
96 "nav" | "p" | "section" | "span" => true,
97 _ => !DISALLOWED_TAGS.contains(&tag.as_str()),
98 }
99}
100
101impl ShadowTreeRegistry {
102 pub fn new() -> Self {
103 Self {
104 roots_by_host: BTreeMap::new(),
105 hosts_by_root: BTreeMap::new(),
106 }
107 }
108
109 pub fn attach_shadow(
111 &mut self,
112 host_idx: usize,
113 tag_name: &str,
114 mode: ShadowRootMode,
115 new_root_idx: usize,
116 ) -> Result<ShadowRootData, AttachShadowError> {
117 if !is_tag_allowed_for_shadow_root(tag_name) {
118 return Err(AttachShadowError::DisallowedTag);
119 }
120 if self.roots_by_host.contains_key(&host_idx) {
121 return Err(AttachShadowError::AlreadyHasShadowRoot);
122 }
123 let data = ShadowRootData::new(new_root_idx, mode, host_idx);
124 self.roots_by_host.insert(host_idx, data.clone());
125 self.hosts_by_root.insert(new_root_idx, host_idx);
126 Ok(data)
127 }
128
129 pub fn get_by_host(&self, host_idx: usize) -> Option<&ShadowRootData> {
131 self.roots_by_host.get(&host_idx)
132 }
133
134 pub fn get_host_by_root(&self, root_idx: usize) -> Option<usize> {
136 self.hosts_by_root.get(&root_idx).copied()
137 }
138
139 pub fn is_shadow_root(&self, node_idx: usize) -> bool {
141 self.hosts_by_root.contains_key(&node_idx)
142 }
143
144 pub fn len(&self) -> usize {
146 self.roots_by_host.len()
147 }
148
149 pub fn is_empty(&self) -> bool {
150 self.roots_by_host.is_empty()
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct SlotProjectionItem {
157 pub node_idx: usize,
158 pub is_slotted: bool,
159}
160
161pub fn project_slotted_children(
168 shadow_children: &[(usize, bool, Option<String>)],
169 light_children: &[(usize, Option<String>)],
170) -> Vec<usize> {
171 let mut result = Vec::new();
172
173 for &(s_idx, is_slot, ref s_name) in shadow_children {
174 if is_slot {
175 let target_name = s_name.as_deref().unwrap_or("");
177 let mut slotted_any = false;
178 for &(l_idx, ref l_slot_attr) in light_children {
179 let attr_val = l_slot_attr.as_deref().unwrap_or("");
180 if attr_val == target_name {
181 result.push(l_idx);
182 slotted_any = true;
183 }
184 }
185 let _ = slotted_any;
188 } else {
189 result.push(s_idx);
191 }
192 }
193
194 result
195}