1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use crate::{BaseDocument, LocalName, NodeData, NodeId, QualName, ns};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum State {
13 Undefined,
14 Upgrading,
15 Custom,
16 Failed,
17}
18
19struct Candidate {
20 name: Arc<str>,
21 local_name: LocalName,
22 state: State,
23}
24
25struct Definition {
26 local_name: LocalName,
27 observed: Arc<HashSet<String>>,
28}
29
30#[derive(Clone)]
31pub enum ReactionKind {
32 Upgrade,
33 Connected,
34 Disconnected,
35 Moved,
36 Attribute {
37 name: QualName,
38 old_value: Option<String>,
39 new_value: Option<String>,
40 },
41 Adopted {
42 old_document: NodeId,
43 new_document: NodeId,
44 },
45}
46
47#[derive(Clone)]
48pub struct Reaction {
49 pub node: NodeId,
50 pub name: Arc<str>,
51 pub kind: ReactionKind,
52}
53
54#[derive(Default)]
55pub struct Registry {
56 candidates: HashMap<NodeId, Candidate>,
57 names: HashMap<Arc<str>, HashSet<NodeId>>,
58 definitions: HashMap<Arc<str>, Definition>,
59 reactions: Vec<Reaction>,
60}
61
62impl BaseDocument {
63 pub(crate) fn register_script_custom_element(&mut self, node_id: NodeId) {
64 let Some(element) = self.nodes[node_id].element_data() else {
65 return;
66 };
67 if element.name.ns != ns!(html) {
68 return;
69 }
70 let name: Arc<str> = if element.name.local.as_ref().contains('-') {
71 Arc::from(element.name.local.as_ref())
72 } else if let Some(name) = element.attr(LocalName::from("is")) {
73 Arc::from(name)
74 } else {
75 return;
76 };
77 let local_name = element.name.local.clone();
78 self.nodes[node_id].custom_element_subtree_count = 1;
79 self.script_custom_elements
80 .names
81 .entry(Arc::clone(&name))
82 .or_default()
83 .insert(node_id);
84 self.script_custom_elements.candidates.insert(
85 node_id,
86 Candidate {
87 name,
88 local_name,
89 state: State::Undefined,
90 },
91 );
92 if self.script_custom_element_is_defined(node_id) {
93 self.record_script_custom_element(node_id, ReactionKind::Upgrade);
94 }
95 }
96
97 pub(crate) fn forget_script_custom_element(&mut self, node_id: NodeId) {
98 if let Some(candidate) = self.script_custom_elements.candidates.remove(&node_id) {
99 if let Some(nodes) = self.script_custom_elements.names.get_mut(&candidate.name) {
100 nodes.remove(&node_id);
101 if nodes.is_empty() {
102 self.script_custom_elements.names.remove(&candidate.name);
103 }
104 }
105 }
106 }
107
108 pub fn script_custom_element_name(&self, node_id: NodeId) -> Option<&str> {
109 self.script_custom_elements
110 .candidates
111 .get(&node_id)
112 .map(|candidate| candidate.name.as_ref())
113 }
114
115 pub fn script_custom_element_state(&self, node_id: NodeId) -> State {
116 self.script_custom_elements
117 .candidates
118 .get(&node_id)
119 .map_or(State::Undefined, |candidate| candidate.state)
120 }
121
122 pub fn set_script_custom_element_state(&mut self, node_id: NodeId, state: State) {
123 if let Some(candidate) = self.script_custom_elements.candidates.get_mut(&node_id) {
124 candidate.state = state;
125 }
126 }
127
128 pub fn script_custom_element_is_defined(&self, node_id: NodeId) -> bool {
129 let Some(candidate) = self.script_custom_elements.candidates.get(&node_id) else {
130 return false;
131 };
132 self.script_custom_elements
133 .definitions
134 .get(&candidate.name)
135 .is_some_and(|definition| definition.local_name == candidate.local_name)
136 }
137
138 pub fn script_node_is_connected(&self, node_id: NodeId) -> bool {
140 let mut current = Some(node_id);
141 while let Some(id) = current {
142 let Some(node) = self.get_node(id) else {
143 return false;
144 };
145 if matches!(node.data, NodeData::Document(_)) {
146 return true;
147 }
148 current = node.parent;
149 }
150 false
151 }
152
153 fn record_script_custom_element(&mut self, node_id: NodeId, kind: ReactionKind) {
154 if let Some(candidate) = self.script_custom_elements.candidates.get(&node_id) {
155 self.script_custom_elements.reactions.push(Reaction {
156 node: node_id,
157 name: Arc::clone(&candidate.name),
158 kind,
159 });
160 }
161 }
162
163 pub fn take_script_custom_element_reactions(&mut self) -> Vec<Reaction> {
164 let mut reactions = std::mem::take(&mut self.script_custom_elements.reactions);
165 let root = self.root_node_id;
169 reactions.retain(|reaction| {
170 !matches!(reaction.kind, ReactionKind::Upgrade)
171 || self
172 .nodes
173 .get(reaction.node)
174 .is_some_and(|node| node.owner_document == Some(root))
175 });
176 reactions
177 }
178
179 pub fn define_script_custom_element(
182 &mut self,
183 name: &str,
184 local_name: LocalName,
185 observed: Arc<HashSet<String>>,
186 ) -> Vec<NodeId> {
187 let name: Arc<str> = Arc::from(name);
188 self.script_custom_elements.definitions.insert(
189 Arc::clone(&name),
190 Definition {
191 local_name: local_name.clone(),
192 observed,
193 },
194 );
195 let mut matches: Vec<_> = self
196 .script_custom_elements
197 .names
198 .get(&name)
199 .into_iter()
200 .flatten()
201 .copied()
202 .filter(|id| {
203 let candidate = &self.script_custom_elements.candidates[id];
204 candidate.state == State::Undefined
205 && candidate.local_name == local_name
206 && self
207 .get_node(*id)
208 .is_some_and(|node| node.owner_document == Some(self.root_node_id))
209 && self.script_node_is_connected(*id)
210 })
211 .map(|id| (self.script_tree_order_key(id), id))
212 .collect();
213 matches.sort_by(|left, right| left.0.cmp(&right.0));
214 matches.into_iter().map(|(_, id)| id).collect()
215 }
216
217 fn script_tree_order_key(&self, node_id: NodeId) -> Vec<usize> {
218 let mut key = Vec::new();
219 let mut current = node_id;
220 while let Some(parent_id) = self.get_node(current).and_then(|node| node.parent) {
221 let Some(parent) = self.get_node(parent_id) else {
222 break;
223 };
224 let index = parent
226 .children
227 .iter()
228 .position(|id| *id == current)
229 .map_or(0, |index| index + 1);
230 key.push(index);
231 current = parent_id;
232 }
233 key.reverse();
234 key
235 }
236
237 pub fn script_custom_element_candidates(&self, root: NodeId) -> Vec<NodeId> {
240 if self
241 .get_node(root)
242 .is_none_or(|node| node.custom_element_subtree_count == 0)
243 {
244 return Vec::new();
245 }
246 let mut found = Vec::new();
247 let mut stack = vec![root];
248 while let Some(id) = stack.pop() {
249 let Some(node) = self.get_node(id) else {
250 continue;
251 };
252 if node.custom_element_subtree_count == 0 {
253 continue;
254 }
255 if self.script_custom_elements.candidates.contains_key(&id) {
256 found.push(id);
257 }
258 stack.extend(node.children.iter().rev().copied());
259 #[cfg(feature = "shadow-dom")]
260 if let Some(shadow_root) = node.shadow_root_id() {
261 stack.push(shadow_root);
262 }
263 }
264 found
265 }
266
267 fn change_script_custom_element_count(
268 &mut self,
269 mut ancestor: Option<NodeId>,
270 count: usize,
271 add: bool,
272 ) {
273 if count == 0 {
274 return;
275 }
276 while let Some(id) = ancestor {
277 let Some(node) = self.get_node_mut(id) else {
278 break;
279 };
280 if add {
281 node.custom_element_subtree_count += count;
282 } else {
283 node.custom_element_subtree_count -= count;
284 }
285 ancestor = node.parent;
286 }
287 }
288
289 pub(crate) fn detach_script_custom_element_subtree(&mut self, root: NodeId) {
291 let Some(node) = self.get_node(root) else {
292 return;
293 };
294 let count = node.custom_element_subtree_count;
295 let parent = node.parent;
296 if count == 0 {
297 return;
298 }
299 if parent.is_some() && self.script_node_is_connected(root) {
300 for id in self.script_custom_element_candidates(root) {
301 if matches!(
302 self.script_custom_element_state(id),
303 State::Custom | State::Upgrading
304 ) {
305 self.record_script_custom_element(id, ReactionKind::Disconnected);
306 }
307 }
308 }
309 self.change_script_custom_element_count(parent, count, false);
310 }
311
312 fn take_pending_disconnect(&mut self, node_id: NodeId) -> bool {
314 let Some(index) = self
315 .script_custom_elements
316 .reactions
317 .iter()
318 .rposition(|reaction| {
319 reaction.node == node_id && matches!(reaction.kind, ReactionKind::Disconnected)
320 })
321 else {
322 return false;
323 };
324 self.script_custom_elements.reactions.remove(index);
325 true
326 }
327
328 pub(crate) fn attach_script_custom_element_subtree(&mut self, root: NodeId, parent: NodeId) {
330 let owner = self.nodes[parent].owner_document.unwrap_or(parent);
331 let same_document = self.nodes[root].owner_document == Some(owner);
332 self.adopt_script_subtree(root, owner);
333 let count = self.nodes[root].custom_element_subtree_count;
334 self.change_script_custom_element_count(Some(parent), count, true);
335 if count == 0 || !self.script_node_is_connected(parent) {
336 return;
337 }
338 for id in self.script_custom_element_candidates(root) {
339 match self.script_custom_element_state(id) {
340 State::Custom | State::Upgrading => {
341 if same_document && self.take_pending_disconnect(id) {
345 self.record_script_custom_element(id, ReactionKind::Moved);
346 } else {
347 self.record_script_custom_element(id, ReactionKind::Connected);
348 }
349 }
350 State::Undefined
351 if self.nodes[id].owner_document == Some(self.root_node_id)
352 && self.script_custom_element_is_defined(id) =>
353 {
354 self.record_script_custom_element(id, ReactionKind::Upgrade);
355 }
356 _ => {}
357 }
358 }
359 }
360
361 pub fn adopt_script_subtree(&mut self, root: NodeId, new_document: NodeId) {
364 if self.get_node(root).is_none_or(|node| {
365 matches!(node.data, NodeData::Document(_)) || node.owner_document == Some(new_document)
366 }) {
367 return;
368 }
369 let mut stack = vec![root];
370 while let Some(id) = stack.pop() {
371 let Some(node) = self.get_node_mut(id) else {
372 continue;
373 };
374 let old_document = node.owner_document;
375 let attribute_id = node
376 .element_data()
377 .and_then(|element| element.id.as_ref())
378 .map(ToString::to_string);
379 node.owner_document = Some(new_document);
380 stack.extend(node.children.iter().rev().copied());
381 if let Some(contents) = node
382 .element_data()
383 .and_then(|element| element.template_contents)
384 {
385 stack.push(contents);
386 }
387 #[cfg(feature = "shadow-dom")]
388 if let Some(shadow_root) = node.shadow_root_id() {
389 stack.push(shadow_root);
390 }
391 if let Some(attribute_id) = attribute_id {
392 if let Some(old_document) = old_document {
393 self.remove_from_document_id_map(old_document, &attribute_id, id);
394 }
395 self.add_to_document_id_map(new_document, &attribute_id, id);
396 }
397 if matches!(
398 self.script_custom_element_state(id),
399 State::Custom | State::Upgrading
400 ) && let Some(old_document) = old_document
401 && old_document != new_document
402 {
403 self.record_script_custom_element(
404 id,
405 ReactionKind::Adopted {
406 old_document,
407 new_document,
408 },
409 );
410 }
411 }
412 }
413
414 pub(crate) fn record_script_custom_element_attribute(
417 &mut self,
418 node_id: NodeId,
419 name: &QualName,
420 new_value: Option<&str>,
421 ) {
422 if self.script_custom_element_state(node_id) != State::Custom {
426 return;
427 }
428 let candidate = &self.script_custom_elements.candidates[&node_id];
429 let Some(definition) = self.script_custom_elements.definitions.get(&candidate.name) else {
430 return;
431 };
432 let local: &str = name.local.as_ref();
433 if !definition.observed.contains(local) {
434 return;
435 }
436 let old_value = self.nodes[node_id]
437 .element_data()
438 .and_then(|element| {
439 element.attrs.iter().find(|attribute| {
440 attribute.name.ns == name.ns && attribute.name.local == name.local
441 })
442 })
443 .map(|attribute| attribute.value.to_string());
444 if old_value.is_none() && new_value.is_none() {
445 return;
446 }
447 self.record_script_custom_element(
448 node_id,
449 ReactionKind::Attribute {
450 name: name.clone(),
451 old_value,
452 new_value: new_value.map(str::to_owned),
453 },
454 );
455 }
456}