Skip to main content

blitz_dom/
mutator.rs

1use blitz_traits::net::RequestDestination;
2use blitz_traits::node_id::NodeId;
3use rustc_hash::FxHashSet;
4use std::collections::HashSet;
5use std::mem;
6use std::ops::{Deref, DerefMut};
7
8use crate::document::make_device;
9use crate::layout::damage::ALL_DAMAGE;
10use crate::net::{ImageHandler, ResourceHandler, StylesheetHandler};
11use crate::node::{CanvasData, NodeFlags, SpecialElementData};
12use crate::util::ImageType;
13use crate::{
14    Attribute, BaseDocument, Document, ElementData, Node, NodeData, QualName, local_name, qual_name,
15};
16use blitz_traits::shell::Viewport;
17use markup5ever::ns;
18use selectors::matching::ElementSelectorFlags;
19use style::Atom;
20use style::invalidation::element::restyle_hints::RestyleHint;
21use style::stylesheets::OriginSet;
22use thin_vec::ThinVec;
23
24macro_rules! tag_and_attr {
25    ($tag:tt, $attr:tt) => {
26        (&local_name!($tag), &local_name!($attr))
27    };
28}
29
30#[derive(Debug, Clone)]
31pub enum AppendTextErr {
32    /// The node is not a text node
33    NotTextNode,
34}
35
36/// Operations that happen almost immediately, but are deferred within a
37/// function for borrow-checker reasons.
38enum SpecialOp {
39    LoadImage(NodeId),
40    LoadIframe(NodeId),
41    LoadStylesheet(NodeId),
42    UnloadStylesheet(NodeId),
43    LoadCustomPaintSource(NodeId),
44    ProcessButtonInput(NodeId),
45    UnloadSubDocument(NodeId),
46    #[cfg(feature = "custom-widget")]
47    UnloadCustomWidget(NodeId),
48    #[cfg(feature = "shadow-dom")]
49    UpgradeCustomElement(NodeId),
50    #[cfg(feature = "shadow-dom")]
51    DisconnectCustomElement(NodeId),
52}
53
54pub struct DocumentMutator<'doc> {
55    /// Document is public as an escape hatch, but users of this API should ideally avoid using it
56    /// and prefer exposing additional functionality in DocumentMutator.
57    pub doc: &'doc mut BaseDocument,
58
59    eager_op_queue: Vec<SpecialOp>,
60
61    // Tracked nodes for deferred processing when mutations have completed
62    title_node: Option<NodeId>,
63    style_nodes: HashSet<NodeId>,
64    form_nodes: HashSet<NodeId>,
65
66    /// Whether an element/attribute that affect animation status has been seen
67    recompute_is_animating: bool,
68
69    /// Whether any mutation that affects rendered output has been performed
70    mutations_occurred: bool,
71
72    /// Deferred custom-element attribute-change notifications: (host_id, attr
73    /// name, old value, new value). Drained and dispatched on flush.
74    #[cfg(feature = "shadow-dom")]
75    custom_element_attr_changes: Vec<(NodeId, QualName, Option<String>, Option<String>)>,
76
77    /// The (latest) node which has been mounted in and had autofocus=true, if any
78    #[cfg(feature = "autofocus")]
79    node_to_autofocus: Option<NodeId>,
80}
81
82impl Drop for DocumentMutator<'_> {
83    fn drop(&mut self) {
84        self.flush(); // Defined at bottom of file
85        self.doc.mutator_redraw_pending |= self.mutations_occurred;
86        self.doc.mutator_depth -= 1;
87        if self.doc.mutator_depth == 0 && mem::take(&mut self.doc.mutator_redraw_pending) {
88            self.doc.shell_provider.request_redraw();
89        }
90    }
91}
92
93impl DocumentMutator<'_> {
94    pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
95        doc.mutator_depth += 1;
96        DocumentMutator {
97            doc,
98            eager_op_queue: Vec::new(),
99            title_node: None,
100            style_nodes: HashSet::new(),
101            form_nodes: HashSet::new(),
102            recompute_is_animating: false,
103            mutations_occurred: false,
104            #[cfg(feature = "shadow-dom")]
105            custom_element_attr_changes: Vec::new(),
106            #[cfg(feature = "autofocus")]
107            node_to_autofocus: None,
108        }
109    }
110
111    // Attribute changes can affect sibling and :has() matches on the parent.
112    // Keep the wide hint conditional so unrelated siblings need not restyle.
113    fn restyle_parent_for_attribute_change(&mut self, parent_id: Option<NodeId>) {
114        let Some(parent_id) = parent_id else {
115            return;
116        };
117        let flags = self.doc.nodes[parent_id].selector_flags().get();
118        let child_dependent = ElementSelectorFlags::HAS_SLOW_SELECTOR
119            | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
120            | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR
121            | ElementSelectorFlags::HAS_EMPTY_SELECTOR
122            | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR
123            | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
124            | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING;
125        if flags.intersects(child_dependent) {
126            if let Some(mut data) = self.doc.nodes[parent_id]
127                .stylo_element_data_opt_mut()
128                .and_then(|s| s.get_mut())
129            {
130                data.hint |= RestyleHint::restyle_subtree();
131            }
132        }
133    }
134
135    // Query methods
136
137    pub fn node_has_parent(&self, node_id: NodeId) -> bool {
138        self.doc.nodes[node_id].parent.is_some()
139    }
140
141    pub fn previous_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
142        self.doc.nodes[node_id].backward(1).map(|node| node.id)
143    }
144
145    pub fn next_sibling_id(&self, node_id: NodeId) -> Option<NodeId> {
146        self.doc.nodes[node_id].forward(1).map(|node| node.id)
147    }
148
149    pub fn parent_id(&self, node_id: NodeId) -> Option<NodeId> {
150        self.doc.nodes[node_id].parent
151    }
152
153    pub fn last_child_id(&self, node_id: NodeId) -> Option<NodeId> {
154        self.doc.nodes[node_id].children.last().copied()
155    }
156
157    pub fn child_ids(&self, node_id: NodeId) -> ThinVec<NodeId> {
158        self.doc.nodes[node_id].children.clone()
159    }
160
161    pub fn element_name(&self, node_id: NodeId) -> Option<&QualName> {
162        self.doc.nodes[node_id].element_data().map(|el| &el.name)
163    }
164
165    pub fn node_at_path(&self, start_node_id: NodeId, path: &[u8]) -> NodeId {
166        let mut current = &self.doc.nodes[start_node_id];
167        for i in path {
168            let new_id = current.children[*i as usize];
169            current = &self.doc.nodes[new_id];
170        }
171        current.id
172    }
173
174    // Node creation methods
175
176    pub fn create_comment_node(&mut self, contents: &str) -> NodeId {
177        self.doc.create_node(NodeData::Comment {
178            contents: contents.to_string(),
179        })
180    }
181
182    pub fn create_text_node(&mut self, text: &str) -> NodeId {
183        self.doc.create_text_node(text)
184    }
185
186    /// A `DocumentFragment`: a parentless container that never appears in a box
187    /// tree and whose children are what gets inserted when it is.
188    ///
189    /// jQuery builds one during initialisation, so without this the library
190    /// throws before it defines `jQuery` and every page depending on it loses
191    /// its scripting. Eight sites in a hundred-site corpus failed that way, and
192    /// they reported it as `jQuery is not defined` — the missing method itself
193    /// says only `not a callable function`, which names nothing.
194    pub fn create_document_fragment(&mut self) -> NodeId {
195        self.doc.create_node(NodeData::DocumentFragment)
196    }
197
198    /// A template's contents: a fragment owned by the inert template contents document, so
199    /// what the parser puts in it is adopted there and never upgraded.
200    fn create_template_contents(&mut self) -> NodeId {
201        let contents = self.create_document_fragment();
202        let owner = self.doc.template_contents_owner();
203        self.doc.nodes[contents].owner_document = Some(owner);
204        contents
205    }
206
207    /// A detached HTML document: `html > head > title`, and `body`.
208    ///
209    /// This backs `document.implementation.createHTMLDocument`, which jQuery
210    /// calls during initialisation to decide whether it can parse markup
211    /// through a second document. Reading `createHTMLDocument` off an absent
212    /// `implementation` threw before jQuery had assigned itself to `window`,
213    /// so every site depending on it reported `jQuery is not defined` -- a
214    /// global that was never missing.
215    ///
216    /// The node lives in this document's arena but is attached to nothing, so
217    /// it never lays out or paints. That is what makes it cheap: no second
218    /// `BaseDocument`, no second style engine, no second arena.
219    pub fn create_html_document(&mut self, title: &str) -> NodeId {
220        let doc_id = self.create_document("text/html");
221
222        let html = self.create_element(html_tag("html"), Vec::new());
223        let head = self.create_element(html_tag("head"), Vec::new());
224        let title_el = self.create_element(html_tag("title"), Vec::new());
225        let body = self.create_element(html_tag("body"), Vec::new());
226
227        // An empty title argument still gets an element, because the DOM has
228        // one either way; it simply has no text child.
229        if !title.is_empty() {
230            let text = self.create_text_node(title);
231            self.append_children(title_el, &[text]);
232        }
233
234        self.append_children(head, &[title_el]);
235        self.append_children(html, &[head, body]);
236        self.append_children(doc_id, &[html]);
237
238        doc_id
239    }
240
241    /// An empty, detached document in the existing arena.
242    pub fn create_document(&mut self, content_type: &'static str) -> NodeId {
243        let mut data = crate::node::DocumentData::default();
244        data.content_type = content_type;
245        self.doc.create_node(NodeData::Document(Box::new(data)))
246    }
247
248    /// Use the native adoption path, including custom element reactions.
249    pub fn adopt_node(&mut self, node_id: NodeId, document_id: NodeId) {
250        self.doc.adopt_script_subtree(node_id, document_id);
251    }
252
253    pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
254        let is_template = name.ns == ns!(html) && name.local == local_name!("template");
255        let mut data = ElementData::new(name, attrs);
256        data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
257        if is_template {
258            data.template_contents = Some(self.create_template_contents());
259        }
260
261        let id = self.doc.create_node(NodeData::Element(Box::new(data)));
262        self.doc.note_stash_subtree(id);
263        let node = self.doc.get_node_mut(id).unwrap();
264
265        // Initialise style data
266        *node.stylo_element_data_mut().ensure_init_mut() = style::data::ElementData {
267            damage: ALL_DAMAGE,
268            ..Default::default()
269        };
270
271        id
272    }
273
274    pub fn deep_clone_node(&mut self, node_id: NodeId) -> NodeId {
275        self.doc.deep_clone_node(node_id)
276    }
277
278    /// The independently owned contents fragment of an HTML template.
279    pub fn template_contents(&mut self, node_id: NodeId) -> NodeId {
280        if let Some(contents) = self.doc.nodes[node_id]
281            .element_data()
282            .and_then(|element| element.template_contents)
283        {
284            return contents;
285        }
286        let contents = self.create_template_contents();
287        self.doc.nodes[node_id]
288            .element_data_mut()
289            .expect("template contents require an element")
290            .template_contents = Some(contents);
291        contents
292    }
293
294    // Node mutation methods
295
296    pub fn set_node_text(&mut self, node_id: NodeId, value: &str) {
297        if matches!(
298            self.doc.nodes[node_id].markup.as_deref(),
299            Some(crate::node::MarkupNode::Doctype { .. })
300        ) {
301            return;
302        }
303        if !self.doc.live_ranges.is_empty()
304            && let Some(length) = self
305                .doc
306                .range_character_data(node_id)
307                .map(|text| text.encode_utf16().count())
308        {
309            self.doc
310                .range_replace_data(node_id, 0, length, value.encode_utf16().count());
311        }
312        self.set_node_text_without_range_update(node_id, value);
313    }
314
315    /// Replace CharacterData without resetting endpoints outside the replaced span.
316    pub fn replace_character_data(
317        &mut self,
318        node_id: NodeId,
319        offset: usize,
320        count: usize,
321        value: &str,
322    ) -> Result<(), &'static str> {
323        let old = self
324            .doc
325            .range_character_data(node_id)
326            .ok_or("InvalidNodeTypeError")?;
327        let length = old.encode_utf16().count();
328        if offset > length {
329            return Err("IndexSizeError");
330        }
331        let count = count.min(length - offset);
332        let mut replacement = crate::range::utf16_slice(old, 0, offset);
333        replacement.push_str(value);
334        replacement.push_str(&crate::range::utf16_slice(old, offset + count, length));
335        self.doc
336            .range_replace_data(node_id, offset, count, value.encode_utf16().count());
337        self.set_node_text_without_range_update(node_id, &replacement);
338        Ok(())
339    }
340
341    pub fn split_text(&mut self, node_id: NodeId, offset: usize) -> Result<NodeId, &'static str> {
342        let node = self.doc.get_node(node_id).ok_or("InvalidNodeTypeError")?;
343        let NodeData::Text(text) = &node.data else {
344            return Err("InvalidNodeTypeError");
345        };
346        let length = text.content.encode_utf16().count();
347        if offset > length {
348            return Err("IndexSizeError");
349        }
350        let before = crate::range::utf16_slice(&text.content, 0, offset);
351        let after = crate::range::utf16_slice(&text.content, offset, length);
352        let position = node
353            .parent
354            .and_then(|parent| Some((parent, self.doc.get_node(parent)?.index_of_child(node_id)?)));
355        let new_id = self.create_text_node(&after);
356        if position.is_some() {
357            self.insert_nodes_after(node_id, &[new_id]);
358        }
359        self.doc.range_split_text(node_id, new_id, offset, position);
360        self.set_node_text_without_range_update(node_id, &before);
361        Ok(new_id)
362    }
363
364    fn set_node_text_without_range_update(&mut self, node_id: NodeId, value: &str) {
365        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
366        if node_is_in_document && self.doc.is_recording_mutations() {
367            let old_value = match &self.doc.nodes[node_id].data {
368                NodeData::Comment { contents } => Some(contents.clone()),
369                NodeData::Text(text) => Some(text.content.clone()),
370                _ => None,
371            };
372            if let Some(old_value) = old_value.filter(|old| old != value) {
373                self.doc.record_mutation(crate::DomMutation::CharacterData {
374                    target: node_id,
375                    old_value,
376                });
377            }
378        }
379        let node = &mut self.doc.nodes[node_id];
380
381        // A comment is CharacterData too: `comment.data = "x"` and
382        // `comment.nodeValue = "x"` both land here, and until this arm existed
383        // they fell through to the `_ => return` below and vanished. The
384        // contents were already on the node and simply never written.
385        //
386        // Deliberately not the Text arm's damage handling. A comment generates
387        // no layout box, so `insert_damage(ALL_DAMAGE)` and
388        // `mark_ancestors_dirty` would schedule a relayout for a change that
389        // cannot affect a pixel, once per write. Nothing rendered depends on
390        // this string, so setting it is the whole operation.
391        if let NodeData::Comment { ref mut contents } = node.data {
392            if contents != value {
393                contents.clear();
394                contents.push_str(value);
395            }
396            return;
397        }
398
399        let text = match node.data {
400            NodeData::Text(ref mut text) => text,
401            // TODO: otherwise this is basically element.textContent which is a bit different - need to parse as html
402            _ => return,
403        };
404
405        let changed = text.content != value;
406        if changed {
407            self.mutations_occurred |= node_is_in_document;
408            text.content.clear();
409            text.content.push_str(value);
410            node.insert_damage(ALL_DAMAGE);
411            // Mark ancestors dirty so the style traversal visits this subtree.
412            // Without this, the traversal may skip nodes with pending damage.
413            node.mark_ancestors_dirty();
414            let parent_id = node.parent;
415
416            // Also insert damage on the parent element, since text content changes
417            // affect the parent's layout (text may wrap differently, change size, etc.)
418            if let Some(parent_id) = parent_id {
419                let parent = &mut self.doc.nodes[parent_id];
420                parent.insert_damage(ALL_DAMAGE);
421            }
422
423            self.maybe_record_node(parent_id);
424        }
425    }
426
427    pub fn append_text_to_node(
428        &mut self,
429        node_id: NodeId,
430        text: &str,
431    ) -> Result<(), AppendTextErr> {
432        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
433        let node = &mut self.doc.nodes[node_id];
434        node.insert_damage(ALL_DAMAGE);
435        node.mark_ancestors_dirty();
436        let parent = node.parent;
437        match node.text_data_mut() {
438            Some(data) => {
439                data.content += text;
440                self.mutations_occurred |= node_is_in_document;
441                // A streaming parser appends a <style>'s text in several
442                // chunks, possibly across mutator sessions. Each chunk must
443                // reprocess the sheet, or it keeps only the first chunk.
444                if let Some(parent) = parent
445                    && self.doc.nodes[parent]
446                        .data
447                        .downcast_element()
448                        .is_some_and(|element| element.name.local.as_ref() == "style")
449                {
450                    self.style_nodes.insert(parent);
451                }
452                Ok(())
453            }
454            None => Err(AppendTextErr::NotTextNode),
455        }
456    }
457
458    pub fn add_attrs_if_missing(&mut self, node_id: NodeId, attrs: Vec<Attribute>) {
459        let node = &mut self.doc.nodes[node_id];
460        node.insert_damage(ALL_DAMAGE);
461        let element_data = node.element_data_mut().expect("Not an element");
462
463        let existing_names = element_data
464            .attrs
465            .iter()
466            .map(|e| e.name.clone())
467            .collect::<HashSet<_>>();
468
469        for attr in attrs
470            .into_iter()
471            .filter(|attr| !existing_names.contains(&attr.name))
472        {
473            self.set_attribute(node_id, attr.name, &attr.value);
474        }
475    }
476
477    pub fn set_attribute(&mut self, node_id: NodeId, name: QualName, value: &str) {
478        self.doc.note_stash_subtree(node_id);
479        self.doc.note_stash_attribute(node_id, &name, value);
480        #[cfg(feature = "shadow-dom")]
481        if name.local == local_name!("slot") || name.local == local_name!("name") {
482            self.doc.note_shadow_tree_change(node_id);
483        }
484        self.doc
485            .record_script_custom_element_attribute(node_id, &name, Some(value));
486        self.doc
487            .record_inline_handler_attribute(node_id, &name, Some(value));
488        self.doc.record_details_open(node_id, &name, true);
489        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
490        if node_is_in_document && self.doc.is_recording_mutations() {
491            // Recorded even when the value does not change: a browser reports
492            // every `setAttribute`, and observers that re-sync on any write
493            // rely on it.
494            if let Some(element) = self.doc.nodes[node_id].element_data() {
495                let old_value = element.attr(name.local.clone()).map(str::to_owned);
496                self.doc.record_mutation(crate::DomMutation::Attributes {
497                    target: node_id,
498                    name: name.local.to_string(),
499                    old_value,
500                });
501            }
502        }
503        if node_is_in_document {
504            self.doc.snapshot_attribute(node_id, &name.local);
505
506            // Damage is asserted only where an attribute can change what the
507            // element renders without changing a computed value.
508            //
509            // For everything else Stylo calls `compute_layout_damage` with the
510            // old and new values during the restyle the hint above asks for,
511            // and that answer is the accurate one. Asserting `ALL_DAMAGE`
512            // first can only OR it back up to everything, which is what made a
513            // colour-only class toggle reconstruct a box: 842us against 295us,
514            // and four nodes recomputed where the correct answer is none.
515            //
516            // The exceptions are real. `<use href>` names a sprite symbol and
517            // no computed value moves when it changes, so the cached SVG has to
518            // be rebuilt by damage or not at all
519            // (`setting_a_use_href_later_rebuilds_the_cached_svg`). Replaced
520            // elements are the same story for `src`, `width` and `height`.
521            let renders_from_attributes = self.doc.nodes[node_id]
522                .data
523                .downcast_element()
524                .is_some_and(|el| {
525                    el.name.ns == ns!(svg)
526                        || crate::layout::replaced::is_replaced_element(&el.name.local)
527                });
528
529            let node = &mut self.doc.nodes[node_id];
530            node.set_dirty_descendants();
531            if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut()) {
532                data.hint |= RestyleHint::restyle_subtree();
533                if renders_from_attributes {
534                    data.damage.insert(ALL_DAMAGE);
535                }
536            }
537
538            // The parent is restyled only when a selector says it depends on
539            // its children.
540            //
541            // It used to be restyled unconditionally, which meant a class
542            // toggle on one row restyled every sibling of that row: on a
543            // 40-row list, a colour-only change cost 773us of style against
544            // 15us for a frame that changed nothing, while layout recomputed
545            // four nodes. Style was half of the whole resolve, for one
546            // element's colour.
547            //
548            // The flags say exactly when the wide hint is needed, because
549            // `apply_selector_flags` deposits them on the parent while matching:
550            // `:empty` and `:only-child` on the parent, `:nth-child` and the
551            // sibling combinators on the siblings, `:has()` through the
552            // relative-selector directions. A parent carrying none of them has
553            // no rule whose match can change because a child's attribute did.
554            let parent = node.parent;
555            if let Some(parent_id) = parent {
556                let parent = &self.doc.nodes[parent_id];
557                let flags = parent.selector_flags().get();
558                let child_dependent = ElementSelectorFlags::HAS_SLOW_SELECTOR
559                    | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
560                    | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR
561                    | ElementSelectorFlags::HAS_EMPTY_SELECTOR
562                    | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR
563                    | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
564                    | ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING;
565
566                if flags.intersects(child_dependent) {
567                    let parent = &mut self.doc.nodes[parent_id];
568                    if let Some(mut data) = parent
569                        .stylo_element_data_opt_mut()
570                        .and_then(|s| s.get_mut())
571                    {
572                        data.hint |= RestyleHint::restyle_subtree();
573                    }
574                }
575            }
576
577            // Mark ancestors dirty so the style traversal visits this subtree.
578            // Without this, the traversal may skip nodes with pending RestyleHint/damage
579            // because it uses dirty_descendants flags to determine which subtrees to visit.
580            self.doc.nodes[node_id].mark_ancestors_dirty();
581        }
582
583        if name.ns == ns!() && name.local == local_name!("id") {
584            if let Some(owner) = self.doc.nodes[node_id].owner_document {
585                let old_id = self.doc.nodes[node_id]
586                    .element_data()
587                    .and_then(|element| element.id.as_ref())
588                    .map(ToString::to_string);
589                if let Some(old_id) = old_id {
590                    self.doc
591                        .remove_from_document_id_map(owner, &old_id, node_id);
592                }
593                self.doc.add_to_document_id_map(owner, value, node_id);
594            }
595            if node_is_in_document {
596                if let Some(old_id) = self.doc.nodes[node_id]
597                    .element_data()
598                    .map(|element| element.id.clone())
599                {
600                    if let Some(old_id) = old_id {
601                        self.doc.remove_from_id_map(&old_id, node_id);
602                    }
603                    self.doc.add_to_id_map(value, node_id);
604                }
605            }
606        }
607
608        let node = &mut self.doc.nodes[node_id];
609
610        let NodeData::Element(ref mut element) = node.data else {
611            return;
612        };
613
614        self.mutations_occurred |= node_is_in_document;
615        // If element is a CustomWidget, then Ccall attribute_changed on it
616        #[cfg(feature = "custom-widget")]
617        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
618            let old_value = element.attrs.get(&name).as_ref().map(|attr| &*attr.value);
619            widget_data
620                .widget
621                .attribute_changed(&name.local, old_value, Some(value));
622        }
623
624        // If element is a CustomElement, defer an attribute_changed notification
625        // (it needs mutable document access, so it can't run inline here).
626        #[cfg(feature = "shadow-dom")]
627        if element.custom_element_data().is_some() {
628            let old_value = element
629                .attrs
630                .get(&name)
631                .as_ref()
632                .map(|attr| attr.value.to_string());
633            self.custom_element_attr_changes.push((
634                node_id,
635                name.clone(),
636                old_value,
637                Some(value.to_string()),
638            ));
639        }
640
641        element.attrs.set(name.clone(), value);
642        if name.ns != ns!() {
643            return;
644        }
645
646        // Focusability is cached on the element and comes from these
647        // attributes, so it has to follow a change to one of them: a widget
648        // that hands the focus around its own children - a menu, a grid -
649        // sets their tabindex after creating them.
650        if name.local == local_name!("tabindex")
651            || name.local == local_name!("href")
652            || name.local == local_name!("disabled")
653        {
654            element.flush_is_focussable();
655        }
656
657        let tag = &element.name.local;
658        let attr = &name.local;
659
660        if *attr == local_name!("id") {
661            element.id = Some(Atom::from(value))
662        }
663
664        if *attr == local_name!("value") {
665            if let Some(input_data) = element.text_input_data_mut() {
666                // Update text input value
667                input_data.set_text(
668                    &mut self.doc.font_ctx.lock().unwrap(),
669                    &mut self.doc.layout_ctx,
670                    value,
671                );
672            }
673            return;
674        }
675
676        if *attr == local_name!("style") {
677            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
678            node.mark_style_attr_updated();
679            return;
680        }
681
682        if *attr == local_name!("disabled") && element.can_be_disabled() {
683            node.disable();
684            return;
685        }
686
687        // If node if not in the document, then don't apply any special behaviours
688        // and simply set the attribute value
689        if !node.flags.is_in_document() {
690            return;
691        }
692
693        if (tag, attr) == tag_and_attr!("input", "checked") {
694            set_input_checked_state(element, value.to_string());
695        } else if (tag, attr) == tag_and_attr!("img", "src") {
696            self.load_image(node_id);
697        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
698            self.load_custom_paint_src(node_id);
699        } else if (tag, attr) == tag_and_attr!("link", "href") {
700            self.load_linked_stylesheet(node_id);
701        } else if (tag, attr) == tag_and_attr!("iframe", "src")
702            || (tag, attr) == tag_and_attr!("iframe", "srcdoc")
703        {
704            self.load_iframe(node_id);
705        } else if (tag, attr) == tag_and_attr!("option", "selected") {
706            // `selected` is an HTML boolean attribute: present means selected,
707            // whatever the value reads. The same trap `checked` fell into, where
708            // `selected="false"` selected the option.
709            //
710            // Selectedness lives on the owning select once that has been
711            // constructed, and construction is idempotent, so writing the
712            // attribute alone would land nowhere anything reads. Before
713            // construction the attribute is the only carrier and seeds the
714            // state on the next resolve, which is why this is allowed to do
715            // nothing at all.
716            self.set_option_selected_state(node_id, true);
717        }
718    }
719
720    /// Push an option's selectedness into the owning select's live state, if
721    /// that state exists yet.
722    fn set_option_selected_state(&mut self, option_id: NodeId, selected: bool) {
723        let Some(select_id) = self.doc.option_owner_select(option_id) else {
724            return;
725        };
726        let Some(index) = self
727            .doc
728            .select_options(select_id)
729            .iter()
730            .position(|id| *id == option_id)
731        else {
732            return;
733        };
734        let changed = if selected {
735            self.doc.set_select_selected_index(select_id, index)
736        } else {
737            self.doc
738                .get_node_mut(select_id)
739                .and_then(|node| node.data.downcast_element_mut())
740                .and_then(|el| el.select_data_mut())
741                .is_some_and(|data| data.set_selected(index, false))
742        };
743        if changed {
744            // `option:checked` is matched from this state, so the restyle has
745            // to be asked for here or the change is invisible to CSS.
746            self.doc.snapshot_node(option_id);
747            self.doc.snapshot_node(select_id);
748        }
749    }
750
751    pub fn clear_attribute(&mut self, node_id: NodeId, name: QualName) {
752        self.doc.note_stash_subtree(node_id);
753        #[cfg(feature = "shadow-dom")]
754        if name.local == local_name!("slot") || name.local == local_name!("name") {
755            self.doc.note_shadow_tree_change(node_id);
756        }
757        self.doc
758            .record_script_custom_element_attribute(node_id, &name, None);
759        self.doc
760            .record_inline_handler_attribute(node_id, &name, None);
761        self.doc.record_details_open(node_id, &name, false);
762        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
763        if node_is_in_document && self.doc.is_recording_mutations() {
764            // Removing an attribute that is not there changes nothing, and a
765            // browser reports nothing for it.
766            let old_value = self.doc.nodes[node_id]
767                .element_data()
768                .and_then(|element| element.attr(name.local.clone()))
769                .map(str::to_owned);
770            if old_value.is_some() {
771                self.doc.record_mutation(crate::DomMutation::Attributes {
772                    target: node_id,
773                    name: name.local.to_string(),
774                    old_value,
775                });
776            }
777        }
778        if node_is_in_document {
779            let removes_existing_attribute = self.doc.nodes[node_id]
780                .element_data()
781                .is_some_and(|element| element.attrs.iter().any(|attr| attr.name == name));
782            self.doc.snapshot_attribute(node_id, &name.local);
783            let parent_id = self.doc.nodes[node_id].parent;
784            {
785                let node = &mut self.doc.nodes[node_id];
786                node.set_dirty_descendants();
787                if let Some(mut data) = node.stylo_element_data_opt_mut().and_then(|s| s.get_mut())
788                {
789                    data.hint |= RestyleHint::restyle_subtree();
790                    data.damage.insert(ALL_DAMAGE);
791                }
792                // Make pending restyle hints reachable by traversal.
793                node.mark_ancestors_dirty();
794            }
795            if removes_existing_attribute {
796                self.restyle_parent_for_attribute_change(parent_id);
797            }
798        }
799
800        if name.ns == ns!() && name.local == local_name!("id") {
801            if let Some(old_id) = self.doc.nodes[node_id]
802                .element_data()
803                .and_then(|element| element.id.clone())
804            {
805                if let Some(owner) = self.doc.nodes[node_id].owner_document {
806                    self.doc
807                        .remove_from_document_id_map(owner, &old_id, node_id);
808                }
809                if node_is_in_document {
810                    self.doc.remove_from_id_map(&old_id, node_id);
811                }
812            }
813        }
814
815        let node = &mut self.doc.nodes[node_id];
816
817        let Some(element) = node.element_data_mut() else {
818            return;
819        };
820
821        let removed_attr = element.attrs.remove(&name);
822        let had_attr = removed_attr.is_some();
823        if !had_attr {
824            return;
825        }
826        self.mutations_occurred |= node_is_in_document;
827
828        // If element is a CustomWidget, then call attribute_changed on it
829        #[cfg(feature = "custom-widget")]
830        if let SpecialElementData::CustomWidget(widget_data) = &mut element.special_data {
831            let old_value = removed_attr.as_ref().map(|attr| &*attr.value);
832            widget_data
833                .widget
834                .attribute_changed(&name.local, old_value, None);
835        }
836
837        // If element is a CustomElement, defer an attribute_changed notification.
838        #[cfg(feature = "shadow-dom")]
839        if element.custom_element_data().is_some() {
840            let old_value = removed_attr.as_ref().map(|attr| attr.value.to_string());
841            self.custom_element_attr_changes
842                .push((node_id, name.clone(), old_value, None));
843        }
844
845        if name.ns != ns!() {
846            return;
847        }
848        if name.local == local_name!("id") {
849            element.id = None;
850        }
851
852        // As in `set_attribute`: taking one of these away can make the element
853        // unfocusable again.
854        if name.local == local_name!("tabindex")
855            || name.local == local_name!("href")
856            || name.local == local_name!("disabled")
857        {
858            element.flush_is_focussable();
859        }
860
861        // Update text input value
862        if name.local == local_name!("value") {
863            if let Some(input_data) = element.text_input_data_mut() {
864                input_data.set_text(
865                    &mut self.doc.font_ctx.lock().unwrap(),
866                    &mut self.doc.layout_ctx,
867                    "",
868                );
869            }
870        }
871
872        let tag = &element.name.local;
873        let attr = &name.local;
874
875        if *attr == local_name!("disabled") && element.can_be_disabled() {
876            node.enable();
877            return;
878        }
879
880        if *attr == local_name!("style") {
881            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
882            node.mark_style_attr_updated();
883        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
884            self.recompute_is_animating = true;
885        } else if (tag, attr) == tag_and_attr!("link", "href") {
886            self.unload_stylesheet(node_id);
887        } else if (tag, attr) == tag_and_attr!("iframe", "srcdoc") && node_is_in_document {
888            // Fall back to loading from the `src` attribute (if any)
889            self.load_iframe(node_id);
890        }
891    }
892
893    pub fn set_style_property(&mut self, node_id: NodeId, name: &str, value: &str) {
894        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
895        self.doc.set_style_property(node_id, name, value);
896        self.mutations_occurred |= node_is_in_document;
897    }
898
899    pub fn remove_style_property(&mut self, node_id: NodeId, name: &str) {
900        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
901        self.doc.remove_style_property(node_id, name);
902        self.mutations_occurred |= node_is_in_document;
903    }
904
905    pub fn set_sub_document(&mut self, node_id: NodeId, sub_document: Box<dyn Document>) {
906        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
907        self.doc.set_sub_document(node_id, sub_document);
908        self.mutations_occurred |= node_is_in_document;
909    }
910
911    pub fn remove_sub_document(&mut self, node_id: NodeId) {
912        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
913        self.doc.remove_sub_document(node_id);
914        self.mutations_occurred |= node_is_in_document;
915    }
916
917    #[cfg(feature = "custom-widget")]
918    pub fn set_custom_widget(&mut self, node_id: NodeId, widget: Box<dyn crate::Widget>) {
919        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
920        self.doc.set_custom_widget(node_id, widget);
921        self.mutations_occurred |= node_is_in_document;
922    }
923
924    #[cfg(feature = "custom-widget")]
925    pub fn remove_custom_widget(&mut self, node_id: NodeId) {
926        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
927        self.doc.remove_custom_widget(node_id);
928        self.mutations_occurred |= node_is_in_document;
929    }
930
931    /// Attach a shadow root to the given host element, returning the shadow
932    /// root's node id.
933    #[cfg(feature = "shadow-dom")]
934    pub fn attach_shadow(&mut self, host_id: NodeId, mode: crate::node::ShadowRootMode) -> NodeId {
935        self.doc.attach_shadow(host_id, mode)
936    }
937
938    #[cfg(feature = "shadow-dom")]
939    pub fn try_attach_declarative_shadow(
940        &mut self,
941        host_id: NodeId,
942        template_id: NodeId,
943        mode: crate::node::ShadowRootMode,
944        clonable: bool,
945        serializable: bool,
946        delegates_focus: bool,
947    ) -> bool {
948        self.doc.try_attach_declarative_shadow(
949            host_id,
950            template_id,
951            mode,
952            clonable,
953            serializable,
954            delegates_focus,
955        )
956    }
957
958    #[cfg(feature = "shadow-dom")]
959    pub fn declarative_shadow_for_template(&self, template_id: NodeId) -> Option<NodeId> {
960        self.doc
961            .declarative_template_shadows
962            .get(&template_id)
963            .copied()
964    }
965
966    #[cfg(feature = "shadow-dom")]
967    pub fn forget_declarative_template(&mut self, template_id: NodeId) {
968        self.doc.declarative_template_shadows.remove(&template_id);
969    }
970
971    /// Attach a custom element controller to the given node and run its
972    /// `connected` lifecycle callback (attaching a shadow root if needed).
973    #[cfg(feature = "shadow-dom")]
974    pub fn set_custom_element(
975        &mut self,
976        node_id: NodeId,
977        controller: Box<dyn crate::node::CustomElement>,
978    ) {
979        self.doc.set_custom_element(node_id, controller);
980        self.upgrade_custom_element(node_id);
981    }
982
983    /// Remove the custom element controller from the given node, running its
984    /// `disconnected` callback first.
985    #[cfg(feature = "shadow-dom")]
986    pub fn remove_custom_element(&mut self, node_id: NodeId) {
987        self.disconnect_custom_element(node_id);
988        let _ = self.doc.take_custom_element(node_id);
989    }
990
991    /// Upgrade an element into a custom element: instantiate a controller from
992    /// the registry (if the node does not already have one), attach a shadow
993    /// root, and run the `connected` lifecycle callback. No-op if the element is
994    /// already upgraded or has no matching definition / controller.
995    #[cfg(feature = "shadow-dom")]
996    pub(crate) fn upgrade_custom_element(&mut self, node_id: NodeId) {
997        use crate::node::{CustomElementData, ShadowRootMode, SpecialElementData};
998
999        let Some(node) = self.doc.get_node(node_id) else {
1000            return;
1001        };
1002        let Some(element) = node.element_data() else {
1003            return;
1004        };
1005
1006        // Determine whether a controller is already attached, and if not, look
1007        // up a matching registry definition to instantiate one.
1008        let already_has_controller =
1009            matches!(element.special_data, SpecialElementData::CustomElement(_));
1010
1011        let mode = if already_has_controller {
1012            // Already attached (e.g. via set_custom_element). Default mode.
1013            ShadowRootMode::Open
1014        } else {
1015            let tag = element.name.local.clone();
1016            let Some(definition) = self.doc.custom_element_registry.get(&tag) else {
1017                return;
1018            };
1019            let mode = definition.mode;
1020            let controller = (definition.factory)();
1021            self.doc.nodes[node_id]
1022                .element_data_mut()
1023                .unwrap()
1024                .special_data =
1025                SpecialElementData::CustomElement(CustomElementData::new(controller));
1026            self.doc.custom_element_nodes.insert(node_id);
1027            mode
1028        };
1029
1030        // Bail out if already upgraded.
1031        let is_upgraded = self.doc.nodes[node_id]
1032            .element_data()
1033            .and_then(|el| el.custom_element_data())
1034            .map(|data| data.upgraded)
1035            .unwrap_or(true);
1036        if is_upgraded {
1037            return;
1038        }
1039
1040        // Ensure a shadow root is attached.
1041        let shadow_root_id = self.doc.attach_shadow(node_id, mode);
1042
1043        // Take the controller out so we can pass `&mut self` (the mutator) to it.
1044        let Some(mut controller) = self.take_controller(node_id) else {
1045            return;
1046        };
1047
1048        {
1049            let mut ctx = crate::node::CustomElementCtx {
1050                mutator: self,
1051                host_id: node_id,
1052                shadow_root_id,
1053            };
1054            controller.connected(&mut ctx);
1055        }
1056
1057        self.restore_controller(node_id, controller, true);
1058    }
1059
1060    /// Run the `disconnected` callback for a custom element node.
1061    #[cfg(feature = "shadow-dom")]
1062    pub(crate) fn disconnect_custom_element(&mut self, node_id: NodeId) {
1063        let Some(shadow_root_id) = self
1064            .doc
1065            .get_node(node_id)
1066            .and_then(|node| node.shadow_root_id())
1067        else {
1068            // No shadow root: still run disconnected if a controller exists.
1069            if let Some(mut controller) = self.take_controller(node_id) {
1070                // Use the host id as a stand-in shadow root id; controllers
1071                // should guard against missing shadow trees.
1072                {
1073                    let mut ctx = crate::node::CustomElementCtx {
1074                        mutator: self,
1075                        host_id: node_id,
1076                        shadow_root_id: node_id,
1077                    };
1078                    controller.disconnected(&mut ctx);
1079                }
1080                self.restore_controller(node_id, controller, false);
1081            }
1082            return;
1083        };
1084
1085        if let Some(mut controller) = self.take_controller(node_id) {
1086            {
1087                let mut ctx = crate::node::CustomElementCtx {
1088                    mutator: self,
1089                    host_id: node_id,
1090                    shadow_root_id,
1091                };
1092                controller.disconnected(&mut ctx);
1093            }
1094            self.restore_controller(node_id, controller, false);
1095        }
1096    }
1097
1098    /// Take the custom element controller out of a node, leaving the
1099    /// `CustomElementData` in place (with `controller == None`).
1100    #[cfg(feature = "shadow-dom")]
1101    fn take_controller(&mut self, node_id: NodeId) -> Option<Box<dyn crate::node::CustomElement>> {
1102        self.doc
1103            .nodes
1104            .get_mut(node_id)?
1105            .element_data_mut()?
1106            .custom_element_data_mut()?
1107            .controller
1108            .take()
1109    }
1110
1111    /// Put a controller back into a node's `CustomElementData`, optionally
1112    /// marking it as upgraded.
1113    #[cfg(feature = "shadow-dom")]
1114    fn restore_controller(
1115        &mut self,
1116        node_id: NodeId,
1117        controller: Box<dyn crate::node::CustomElement>,
1118        upgraded: bool,
1119    ) {
1120        if let Some(data) = self
1121            .doc
1122            .nodes
1123            .get_mut(node_id)
1124            .and_then(|node| node.element_data_mut())
1125            .and_then(|el| el.custom_element_data_mut())
1126        {
1127            data.controller = Some(controller);
1128            if upgraded {
1129                data.upgraded = true;
1130            }
1131        }
1132    }
1133
1134    /// Sever the cached box-tree edge before a DOM child is detached or freed.
1135    ///
1136    /// The layout tree is not always the DOM tree: inline content can sit
1137    /// below an anonymous block, and fixed content can be hoisted. Hidden
1138    /// subtrees deliberately retain their layout caches, so damage on the DOM
1139    /// parent alone cannot make a stale cached child safe before rounding and
1140    /// painting traverse it. Invalidating the actual layout parent at mutation
1141    /// time prevents either pass from indexing a SlotMap key that was freed.
1142    fn invalidate_layout_parent_edge(&mut self, node_id: NodeId) {
1143        let Some(layout_parent_id) = self
1144            .doc
1145            .nodes
1146            .get(node_id)
1147            .and_then(|node| node.layout_parent.get())
1148        else {
1149            return;
1150        };
1151        if let Some(layout_parent) = self.doc.nodes.get_mut(layout_parent_id) {
1152            layout_parent.layout_children.get_mut().take();
1153            layout_parent.paint_children.get_mut().take();
1154            layout_parent.insert_damage(ALL_DAMAGE);
1155        }
1156        if let Some(node) = self.doc.nodes.get(node_id) {
1157            node.layout_parent.set(None);
1158        }
1159    }
1160
1161    /// Zero the layout of a node and everything under it.
1162    fn clear_layout_of_subtree(doc: &mut BaseDocument, node_id: NodeId) {
1163        let mut stack = vec![node_id];
1164        while let Some(id) = stack.pop() {
1165            let Some(node) = doc.nodes.get_mut(id) else {
1166                continue;
1167            };
1168            // The accessors panic on node kinds that have none, so ask the data
1169            // first rather than every node in the subtree: a text node has no
1170            // layout of its own and a removal walk hits plenty of them.
1171            if node.data.downcast_element().is_some() {
1172                *node.unrounded_layout_mut() = taffy::Layout::with_order(0);
1173                *node.final_layout_mut() = taffy::Layout::with_order(0);
1174                node.cache_mut().clear();
1175            }
1176            stack.extend(node.children.iter().copied());
1177        }
1178    }
1179
1180    /// Remove the node from its parent but don't drop it.
1181    /// Record `node_id` leaving its parent, for a `MutationObserver`. Called
1182    /// before the link is cut, while its siblings can still be read.
1183    fn record_removal(&mut self, node_id: NodeId) {
1184        self.doc.range_remove_node(node_id);
1185        if !self.doc.is_recording_mutations() || !self.doc.nodes[node_id].flags.is_in_document() {
1186            return;
1187        }
1188        if let Some((parent_id, previous_sibling, next_sibling)) = self.doc.sibling_context(node_id)
1189        {
1190            self.doc.record_mutation(crate::DomMutation::ChildList {
1191                target: parent_id,
1192                added: Vec::new(),
1193                removed: vec![node_id],
1194                previous_sibling,
1195                next_sibling,
1196            });
1197        }
1198    }
1199
1200    pub fn remove_node(&mut self, node_id: NodeId) {
1201        self.record_removal(node_id);
1202        self.doc.detach_script_custom_element_subtree(node_id);
1203        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
1204        // Process the subtree *before* severing the parent link so that
1205        // interaction state referencing removed nodes can retarget to the
1206        // nearest surviving ancestor.
1207        self.process_removed_subtree(node_id);
1208
1209        // A detached node keeps its box otherwise, and a box is all layout and
1210        // paint need: the application's boot splash was removed by its
1211        // framework the moment the workspace was ready, kept a 1318x880 layout
1212        // for the rest of the session, and painted its own background over
1213        // whichever panel it landed on. The page looked blank. The node is
1214        // deliberately not dropped, so JS wrappers stay valid, but nothing
1215        // outside the document should occupy space in it.
1216        Self::clear_layout_of_subtree(self.doc, node_id);
1217        self.invalidate_layout_parent_edge(node_id);
1218
1219        let node = &mut self.doc.nodes[node_id];
1220
1221        // Update child_idx values
1222        if let Some(parent_id) = node.parent.take() {
1223            self.mutations_occurred |= node_is_in_document;
1224            let parent = &mut self.doc.nodes[parent_id];
1225            parent.insert_damage(ALL_DAMAGE);
1226            // Mark ancestors dirty so the style traversal visits this subtree.
1227            parent.mark_ancestors_dirty();
1228            parent.children.retain(|id| *id != node_id);
1229            self.maybe_record_node(parent_id);
1230        }
1231    }
1232
1233    pub fn remove_and_drop_node(&mut self, node_id: NodeId) -> Option<Node> {
1234        self.remove_and_drop_node_with(node_id, &mut |_| {})
1235    }
1236
1237    /// Like [`Self::remove_and_drop_node`], but calls `on_drop` with the id of
1238    /// every dropped node (the node itself and all of its descendants).
1239    pub fn remove_and_drop_node_with(
1240        &mut self,
1241        node_id: NodeId,
1242        on_drop: &mut dyn FnMut(NodeId),
1243    ) -> Option<Node> {
1244        self.record_removal(node_id);
1245        self.doc.detach_script_custom_element_subtree(node_id);
1246        let node_is_in_document = self.doc.nodes[node_id].flags.is_in_document();
1247        self.process_removed_subtree(node_id);
1248        self.invalidate_layout_parent_edge(node_id);
1249
1250        let node = self.doc.drop_node_ignoring_parent_with(node_id, on_drop);
1251        self.mutations_occurred |= node_is_in_document;
1252
1253        // Update child_idx values
1254        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
1255            let parent = &mut self.doc.nodes[parent_id];
1256            parent.insert_damage(ALL_DAMAGE);
1257            let parent_is_in_doc = parent.flags.is_in_document();
1258
1259            // TODO: make this fine grained / conditional based on ElementSelectorFlags
1260            if parent_is_in_doc {
1261                if let Some(mut data) = parent
1262                    .stylo_element_data_opt_mut()
1263                    .and_then(|s| s.get_mut())
1264                {
1265                    data.hint |= RestyleHint::restyle_subtree();
1266                }
1267                // Mark ancestors dirty so the style traversal visits this subtree.
1268                parent.mark_ancestors_dirty();
1269            }
1270
1271            parent.children.retain(|id| *id != node_id);
1272            self.maybe_record_node(parent_id);
1273        }
1274
1275        node
1276    }
1277
1278    pub fn remove_and_drop_all_children(&mut self, node_id: NodeId) {
1279        if !self.doc.live_ranges.is_empty() {
1280            let children = self.doc.nodes[node_id].children.clone();
1281            for child in children.into_iter().rev() {
1282                self.doc.range_remove_node(child);
1283            }
1284        }
1285        if self.doc.is_recording_mutations()
1286            && self.doc.nodes[node_id].flags.is_in_document()
1287            && !self.doc.nodes[node_id].children.is_empty()
1288        {
1289            let removed = self.doc.nodes[node_id].children.to_vec();
1290            self.doc.record_mutation(crate::DomMutation::ChildList {
1291                target: node_id,
1292                added: Vec::new(),
1293                removed,
1294                previous_sibling: None,
1295                next_sibling: None,
1296            });
1297        }
1298        let parent = &mut self.doc.nodes[node_id];
1299        let parent_is_in_doc = parent.flags.is_in_document();
1300
1301        // TODO: make this fine grained / conditional based on ElementSelectorFlags
1302        if parent_is_in_doc {
1303            if let Some(mut data) = parent
1304                .stylo_element_data_opt_mut()
1305                .and_then(|s| s.get_mut())
1306            {
1307                data.hint |= RestyleHint::restyle_subtree();
1308            }
1309            // Mark ancestors dirty so the style traversal visits this subtree.
1310            parent.mark_ancestors_dirty();
1311        }
1312
1313        let children = mem::take(&mut parent.children);
1314        self.mutations_occurred |= parent_is_in_doc && !children.is_empty();
1315        for child_id in children {
1316            self.doc.detach_script_custom_element_subtree(child_id);
1317            self.process_removed_subtree(child_id);
1318            self.invalidate_layout_parent_edge(child_id);
1319            let _ = self.doc.drop_node_ignoring_parent(child_id);
1320        }
1321        self.maybe_record_node(node_id);
1322    }
1323
1324    // Tree mutation methods
1325    pub fn remove_node_if_unparented(&mut self, node_id: NodeId) {
1326        self.remove_node_if_unparented_with(node_id, &mut |_| {});
1327    }
1328
1329    /// Like [`Self::remove_node_if_unparented`], but calls `on_drop` with the id of
1330    /// every dropped node (the node itself and all of its descendants).
1331    pub fn remove_node_if_unparented_with(
1332        &mut self,
1333        node_id: NodeId,
1334        on_drop: &mut dyn FnMut(NodeId),
1335    ) {
1336        if let Some(node) = self.doc.get_node(node_id) {
1337            if node.parent.is_none() {
1338                self.remove_and_drop_node_with(node_id, on_drop);
1339            }
1340        }
1341    }
1342
1343    /// Remove all of the children from old_parent_id and append them to new_parent_id
1344    pub fn append_children(&mut self, parent_id: NodeId, child_ids: &[NodeId]) {
1345        self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
1346            parent.children.extend_from_slice(child_ids);
1347        });
1348    }
1349
1350    pub fn insert_nodes_before(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1351        let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
1352        self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
1353            let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
1354            parent
1355                .children
1356                .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
1357        });
1358    }
1359
1360    fn add_children_to_parent(
1361        &mut self,
1362        parent_id: NodeId,
1363        child_ids: &[NodeId],
1364        insert_children_fn: &dyn Fn(&mut Node, &[NodeId]),
1365    ) {
1366        // A fragment inserts its children, not itself. Flattened here rather
1367        // than in each caller because this is the one place `appendChild` and
1368        // `insertBefore` both pass through, so they cannot disagree about it.
1369        //
1370        // The fragment is emptied as it is expanded, which is what the spec
1371        // requires: after insertion a fragment has no children, and leaving
1372        // them behind would give every child two parents.
1373        let child_ids: Vec<NodeId> = if child_ids
1374            .iter()
1375            .any(|id| matches!(self.doc.nodes[*id].data, NodeData::DocumentFragment))
1376        {
1377            let mut flattened = Vec::with_capacity(child_ids.len());
1378            for id in child_ids.iter().copied() {
1379                if matches!(self.doc.nodes[id].data, NodeData::DocumentFragment) {
1380                    if !self.doc.live_ranges.is_empty() {
1381                        let children = self.doc.nodes[id].children.clone();
1382                        for child in children.into_iter().rev() {
1383                            self.doc.range_remove_node(child);
1384                        }
1385                    }
1386                    let moved = std::mem::take(&mut self.doc.nodes[id].children);
1387                    flattened.extend(moved);
1388                } else {
1389                    flattened.push(id);
1390                }
1391            }
1392            flattened
1393        } else {
1394            child_ids.to_vec()
1395        };
1396        let child_ids = child_ids.as_slice();
1397
1398        let new_parent_is_in_document = self.doc.nodes[parent_id].flags.is_in_document();
1399        self.mutations_occurred |= new_parent_is_in_document && !child_ids.is_empty();
1400        // Detach the children from their old parents *before* inserting them into
1401        // the new parent (matching DOM `insertBefore` semantics). If a child is
1402        // being moved within the same parent then detaching it after insertion
1403        // would remove both the old and the newly-inserted entries from the
1404        // parent's child list, and anchor indices would be computed against a
1405        // child list that still contains the moved nodes.
1406        for child_id in child_ids.iter().copied() {
1407            self.record_removal(child_id);
1408            self.doc.detach_script_custom_element_subtree(child_id);
1409            self.invalidate_layout_parent_edge(child_id);
1410            let child = &mut self.doc.nodes[child_id];
1411            let child_was_in_doc = child.flags.is_in_document();
1412            self.mutations_occurred |= child_was_in_doc;
1413            let Some(old_parent_id) = child.parent.take() else {
1414                continue;
1415            };
1416
1417            let old_parent = &mut self.doc.nodes[old_parent_id];
1418            old_parent.insert_damage(ALL_DAMAGE);
1419
1420            // TODO: make this fine grained / conditional based on ElementSelectorFlags
1421            if child_was_in_doc {
1422                if let Some(mut data) = old_parent
1423                    .stylo_element_data_opt_mut()
1424                    .and_then(|s| s.get_mut())
1425                {
1426                    data.hint |= RestyleHint::restyle_subtree();
1427                }
1428                // Mark ancestors dirty so the style traversal visits this subtree.
1429                old_parent.mark_ancestors_dirty();
1430            }
1431
1432            old_parent.children.retain(|id| *id != child_id);
1433            self.maybe_record_node(old_parent_id);
1434        }
1435
1436        let new_parent = &mut self.doc.nodes[parent_id];
1437        new_parent.insert_damage(ALL_DAMAGE);
1438
1439        // TODO: make this fine grained / conditional based on ElementSelectorFlags
1440        if new_parent_is_in_document {
1441            if let Some(mut data) = new_parent
1442                .stylo_element_data_opt_mut()
1443                .and_then(|s| s.get_mut())
1444            {
1445                data.hint |= RestyleHint::restyle_subtree();
1446            }
1447            // Mark ancestors dirty so the style traversal visits this subtree.
1448            new_parent.mark_ancestors_dirty();
1449        }
1450
1451        insert_children_fn(new_parent, child_ids);
1452        if !self.doc.live_ranges.is_empty()
1453            && let Some(first) = child_ids.first()
1454            && let Some(index) = self.doc.nodes[parent_id].index_of_child(*first)
1455        {
1456            self.doc
1457                .range_insert_nodes(parent_id, index, child_ids.len());
1458        }
1459
1460        for child_id in child_ids.iter().copied() {
1461            let child = &mut self.doc.nodes[child_id];
1462            let child_was_in_doc = child.flags.is_in_document();
1463            child.parent = Some(parent_id);
1464            self.doc
1465                .attach_script_custom_element_subtree(child_id, parent_id);
1466
1467            if new_parent_is_in_document && !child_was_in_doc {
1468                self.process_added_subtree(child_id);
1469            } else if !new_parent_is_in_document && child_was_in_doc {
1470                self.process_removed_subtree(child_id);
1471            }
1472        }
1473
1474        if new_parent_is_in_document && self.doc.is_recording_mutations() {
1475            if let (Some(first), Some(last)) = (child_ids.first(), child_ids.last()) {
1476                let previous_sibling = self.doc.sibling_context(*first).and_then(|c| c.1);
1477                let next_sibling = self.doc.sibling_context(*last).and_then(|c| c.2);
1478                self.doc.record_mutation(crate::DomMutation::ChildList {
1479                    target: parent_id,
1480                    added: child_ids.to_vec(),
1481                    removed: Vec::new(),
1482                    previous_sibling,
1483                    next_sibling,
1484                });
1485            }
1486        }
1487
1488        self.doc.note_stash_subtree(parent_id);
1489        self.maybe_record_node(parent_id);
1490    }
1491
1492    // Tree mutation methods (that defer to other methods)
1493    pub fn insert_nodes_after(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1494        match self.next_sibling_id(anchor_node_id) {
1495            Some(id) => self.insert_nodes_before(id, new_node_ids),
1496            None => {
1497                let parent_id = self.parent_id(anchor_node_id).unwrap();
1498                self.append_children(parent_id, new_node_ids)
1499            }
1500        }
1501    }
1502
1503    pub fn reparent_children(&mut self, old_parent_id: NodeId, new_parent_id: NodeId) {
1504        if self.doc.is_recording_mutations()
1505            && self.doc.nodes[old_parent_id].flags.is_in_document()
1506            && !self.doc.nodes[old_parent_id].children.is_empty()
1507        {
1508            let removed = self.doc.nodes[old_parent_id].children.to_vec();
1509            self.doc.record_mutation(crate::DomMutation::ChildList {
1510                target: old_parent_id,
1511                added: Vec::new(),
1512                removed,
1513                previous_sibling: None,
1514                next_sibling: None,
1515            });
1516        }
1517        if !self.doc.live_ranges.is_empty() {
1518            let children = self.doc.nodes[old_parent_id].children.clone();
1519            for child in children.into_iter().rev() {
1520                self.doc.range_remove_node(child);
1521            }
1522        }
1523        let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
1524        self.maybe_record_node(old_parent_id);
1525        self.append_children(new_parent_id, &child_ids);
1526    }
1527
1528    pub fn replace_node_with(&mut self, anchor_node_id: NodeId, new_node_ids: &[NodeId]) {
1529        self.insert_nodes_before(anchor_node_id, new_node_ids);
1530        self.remove_node(anchor_node_id);
1531    }
1532}
1533
1534impl<'doc> DocumentMutator<'doc> {
1535    pub fn flush(&mut self) {
1536        if self.recompute_is_animating {
1537            self.doc.has_canvas = self.doc.compute_has_canvas();
1538        }
1539
1540        if let Some(id) = self.title_node {
1541            let title = self.doc.nodes[id].text_content();
1542            self.doc.shell_provider.set_window_title(title);
1543        }
1544
1545        // Add/Update inline stylesheets (<style> elements)
1546        for id in self.style_nodes.drain() {
1547            self.doc.process_style_element(id);
1548        }
1549
1550        for id in self.form_nodes.drain() {
1551            self.doc.reset_form_owner(id);
1552        }
1553
1554        #[cfg(feature = "autofocus")]
1555        if let Some(node_id) = self.node_to_autofocus.take() {
1556            if self.doc.get_node(node_id).is_some() {
1557                self.doc.set_focus_to(node_id);
1558            }
1559        }
1560
1561        #[cfg(feature = "shadow-dom")]
1562        self.dispatch_custom_element_attr_changes();
1563    }
1564
1565    /// Dispatch all deferred custom-element `attribute_changed` callbacks.
1566    #[cfg(feature = "shadow-dom")]
1567    fn dispatch_custom_element_attr_changes(&mut self) {
1568        if self.custom_element_attr_changes.is_empty() {
1569            return;
1570        }
1571        let changes = mem::take(&mut self.custom_element_attr_changes);
1572        for (node_id, name, old_value, new_value) in changes {
1573            // Skip if the registered definition observes a restricted set that
1574            // excludes this attribute. Manually-attached controllers (no
1575            // definition) observe all attributes.
1576            let tag = self
1577                .doc
1578                .get_node(node_id)
1579                .and_then(|node| node.element_data())
1580                .map(|el| el.name.local.clone());
1581            let observed = tag
1582                .as_ref()
1583                .and_then(|tag| self.doc.custom_element_registry.get(tag))
1584                .map(|def| def.observes(&name.local))
1585                .unwrap_or(true);
1586            if !observed {
1587                continue;
1588            }
1589
1590            let Some(shadow_root_id) = self
1591                .doc
1592                .get_node(node_id)
1593                .and_then(|node| node.shadow_root_id())
1594            else {
1595                continue;
1596            };
1597            let Some(mut controller) = self.take_controller(node_id) else {
1598                continue;
1599            };
1600            {
1601                let mut ctx = crate::node::CustomElementCtx {
1602                    mutator: self,
1603                    host_id: node_id,
1604                    shadow_root_id,
1605                };
1606                controller.attribute_changed(
1607                    &mut ctx,
1608                    &name.local,
1609                    old_value.as_deref(),
1610                    new_value.as_deref(),
1611                );
1612            }
1613            self.restore_controller(node_id, controller, false);
1614        }
1615    }
1616
1617    pub fn set_inner_html(&mut self, node_id: NodeId, html: &str) {
1618        self.set_html_replacing(node_id, html, false);
1619    }
1620
1621    /// Replace descendants and honor declarative shadow roots.
1622    pub fn set_html_unsafe(&mut self, node_id: NodeId, html: &str) {
1623        self.set_html_replacing(node_id, html, true);
1624    }
1625
1626    fn set_html_replacing(
1627        &mut self,
1628        node_id: NodeId,
1629        html: &str,
1630        allow_declarative_shadow_roots: bool,
1631    ) {
1632        let target = self.doc.nodes[node_id]
1633            .element_data()
1634            .and_then(|element| element.template_contents)
1635            .unwrap_or(node_id);
1636        self.remove_and_drop_all_children(target);
1637        #[cfg(feature = "shadow-dom")]
1638        if let Some(host_id) = self.doc.nodes[node_id]
1639            .shadow_root_data()
1640            .map(|root| root.host)
1641        {
1642            // html5ever's fragment context must be an element. Parsing into a
1643            // detached host-shaped element also keeps scripts inert.
1644            // Do not promote a top-level template onto that throwaway. The real
1645            // host already has a shadow root, so the template stays a template
1646            // and moves into this shadow root with the other children.
1647            let name = self.doc.nodes[host_id].element_data().unwrap().name.clone();
1648            let context_id = self.create_element(name, Vec::new());
1649            if allow_declarative_shadow_roots {
1650                self.doc
1651                    .html_parser_provider
1652                    .clone()
1653                    .parse_fragment(self, context_id, html, true);
1654            } else {
1655                self.doc
1656                    .html_parser_provider
1657                    .clone()
1658                    .parse_inner_html(self, context_id, html);
1659            }
1660            let children = self.child_ids(context_id);
1661            self.append_children(node_id, &children);
1662            self.remove_and_drop_node(context_id);
1663            self.doc.note_shadow_tree_change(node_id);
1664            return;
1665        }
1666        if allow_declarative_shadow_roots {
1667            self.doc
1668                .html_parser_provider
1669                .clone()
1670                .parse_fragment(self, node_id, html, true);
1671            // html5ever does not attach a template whose adjusted current node
1672            // is the fragment context (its open-element stack has length 1).
1673            // Promote that one direct child after the children have moved onto
1674            // the real target. Nested hosts are attached during the parse.
1675            #[cfg(feature = "shadow-dom")]
1676            self.promote_direct_declarative_shadows(target);
1677        } else {
1678            self.doc
1679                .html_parser_provider
1680                .clone()
1681                .parse_inner_html(self, node_id, html);
1682        }
1683    }
1684
1685    /// Turn the first direct `<template shadowrootmode>` child into a shadow root.
1686    ///
1687    /// Later siblings stay templates, because the host then already has a root.
1688    #[cfg(feature = "shadow-dom")]
1689    fn promote_direct_declarative_shadows(&mut self, target: NodeId) {
1690        let can_host = self
1691            .doc
1692            .nodes
1693            .get(target)
1694            .and_then(|node| node.element_data())
1695            .is_some_and(|element| {
1696                BaseDocument::element_can_be_shadow_host(element) && element.shadow_root.is_none()
1697            });
1698        if !can_host {
1699            return;
1700        }
1701        let children = self.child_ids(target);
1702        for child_id in children {
1703            let info = self.doc.nodes[child_id]
1704                .element_data()
1705                .and_then(Self::declarative_template_flags);
1706            let Some((mode, clonable, serializable, delegates_focus, contents)) = info else {
1707                continue;
1708            };
1709            let root_id = self.doc.attach_shadow(target, mode);
1710            if let Some(root) = self.doc.nodes[root_id].shadow_root_data_mut() {
1711                root.declarative = true;
1712                root.clonable = clonable;
1713                root.serializable = serializable;
1714                root.delegates_focus = delegates_focus;
1715            }
1716            if let Some(contents) = contents {
1717                let inner = self.child_ids(contents);
1718                if !inner.is_empty() {
1719                    self.append_children(root_id, &inner);
1720                }
1721            }
1722            self.remove_and_drop_node(child_id);
1723            self.doc.note_shadow_tree_change(target);
1724            break;
1725        }
1726    }
1727
1728    #[cfg(feature = "shadow-dom")]
1729    fn declarative_template_flags(
1730        element: &ElementData,
1731    ) -> Option<(
1732        crate::node::ShadowRootMode,
1733        bool,
1734        bool,
1735        bool,
1736        Option<NodeId>,
1737    )> {
1738        if element.name.ns != ns!(html) || element.name.local != local_name!("template") {
1739            return None;
1740        }
1741        let raw = element.attr(local_name!("shadowrootmode"))?;
1742        let mode = if raw.eq_ignore_ascii_case("open") {
1743            crate::node::ShadowRootMode::Open
1744        } else if raw.eq_ignore_ascii_case("closed") {
1745            crate::node::ShadowRootMode::Closed
1746        } else {
1747            return None;
1748        };
1749        Some((
1750            mode,
1751            element.has_attr(local_name!("shadowrootclonable")),
1752            element.has_attr(local_name!("shadowrootserializable")),
1753            element.has_attr(local_name!("shadowrootdelegatesfocus")),
1754            element.template_contents,
1755        ))
1756    }
1757
1758    fn flush_eager_ops(&mut self) {
1759        let mut ops = mem::take(&mut self.eager_op_queue);
1760        for op in ops.drain(0..) {
1761            match op {
1762                SpecialOp::LoadImage(node_id) => self.load_image(node_id),
1763                SpecialOp::LoadIframe(node_id) => self.load_iframe(node_id),
1764                SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
1765                SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
1766                SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
1767                SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
1768                SpecialOp::UnloadSubDocument(node_id) => self.remove_sub_document(node_id),
1769                #[cfg(feature = "custom-widget")]
1770                SpecialOp::UnloadCustomWidget(node_id) => self.remove_custom_widget(node_id),
1771                #[cfg(feature = "shadow-dom")]
1772                SpecialOp::UpgradeCustomElement(node_id) => self.upgrade_custom_element(node_id),
1773                #[cfg(feature = "shadow-dom")]
1774                SpecialOp::DisconnectCustomElement(node_id) => {
1775                    self.disconnect_custom_element(node_id)
1776                }
1777            }
1778        }
1779
1780        // Queue is empty, but put Vec back anyway so allocation can be reused.
1781        self.eager_op_queue = ops;
1782    }
1783
1784    fn process_added_subtree(&mut self, node_id: NodeId) {
1785        self.doc
1786            .iter_shadow_including_subtree_mut(node_id, |node_id, doc| {
1787                let node = &mut doc.nodes[node_id];
1788                node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
1789                node.insert_damage(ALL_DAMAGE);
1790
1791                // If the node has an "id" attribute, store it in the ID map.
1792                if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1793                    doc.add_to_id_map(&id_attr, node_id);
1794                }
1795
1796                let node = &mut doc.nodes[node_id];
1797                let NodeData::Element(ref mut element) = node.data else {
1798                    return;
1799                };
1800
1801                // Custom post-processing by element tag name
1802                let tag = element.name.local.as_ref();
1803                match tag {
1804                    "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1805                    "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
1806                    "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
1807                    "iframe" => self.eager_op_queue.push(SpecialOp::LoadIframe(node_id)),
1808                    "canvas" => self
1809                        .eager_op_queue
1810                        .push(SpecialOp::LoadCustomPaintSource(node_id)),
1811                    "style" => {
1812                        self.style_nodes.insert(node_id);
1813                    }
1814                    "button" | "fieldset" | "input" | "select" | "textarea" | "object"
1815                    | "output" => {
1816                        self.eager_op_queue
1817                            .push(SpecialOp::ProcessButtonInput(node_id));
1818                        self.form_nodes.insert(node_id);
1819                    }
1820                    _ => {}
1821                }
1822
1823                // If the element's tag name matches a registered custom element
1824                // definition (and it hasn't already been upgraded), queue it for
1825                // upgrade.
1826                #[cfg(feature = "shadow-dom")]
1827                {
1828                    let needs_upgrade = doc.custom_element_registry.contains(&element.name.local)
1829                        && element.custom_element_data().is_none();
1830                    if needs_upgrade {
1831                        self.eager_op_queue
1832                            .push(SpecialOp::UpgradeCustomElement(node_id));
1833                    }
1834                }
1835
1836                // `autofocus` is a boolean attribute: present is true, whatever
1837                // the value, and absent is the only false. Requiring the literal
1838                // string "true" meant the one spelling almost nothing uses, since
1839                // markup writes `<input autofocus>` and the parser stores that as
1840                // the empty string. Every framework agrees: Solid's boolean
1841                // attribute setter is `setAttribute(name, "")`.
1842                //
1843                // So a field marked autofocus in markup never took focus, and
1844                // blitz-script papered over its own path by writing "true" from
1845                // the property setter, which left the parsed path broken.
1846                #[cfg(feature = "autofocus")]
1847                if node.is_focussable() {
1848                    if let NodeData::Element(ref element) = node.data {
1849                        if element.attr(local_name!("autofocus")).is_some() {
1850                            self.node_to_autofocus = Some(node_id);
1851                        }
1852                    }
1853                }
1854            });
1855
1856        self.flush_eager_ops();
1857    }
1858
1859    fn process_removed_subtree(&mut self, node_id: NodeId) {
1860        let mut removed = FxHashSet::default();
1861        let mut parents = FxHashSet::default();
1862        self.doc
1863            .iter_shadow_including_subtree_mut(node_id, |node_id, doc| {
1864                removed.insert(node_id);
1865                let node = &doc.nodes[node_id];
1866                parents.extend(
1867                    [node.parent, node.layout_parent.get()]
1868                        .into_iter()
1869                        .flatten(),
1870                );
1871                doc.nodes[node_id]
1872                    .flags
1873                    .set(NodeFlags::IS_IN_DOCUMENT, false);
1874
1875                // Clear any interaction state that references this node, running
1876                // the usual teardown steps (unhover/unactive the surviving
1877                // ancestor chain, IME disable on blur of a focused input).
1878                doc.clear_interaction_state_for_removed_node(node_id);
1879
1880                let node = &mut doc.nodes[node_id];
1881
1882                // Same for focus and for the node the last press landed on.
1883                //
1884                // These two were missed, and they are the two most likely to point
1885                // at a node that is being removed: dismissing a panel is a click on
1886                // a control *inside* it, so that control is both the focused node
1887                // and the mousedown node at the moment its subtree goes away.
1888                //
1889                // A stale id here is not inert. The next click calls `set_focus_to`,
1890                // which blurs the old node by indexing it, and indexing a dropped
1891                // id panics inside the event handler. The window then stops
1892                // responding to clicks until something forces a full rebuild.
1893                //
1894                // The upstream fix carried a second failure mode, the blur landing
1895                // on whatever node had taken the recycled slot. That one cannot
1896                // happen here: `NodeId` is versioned, so a dropped id resolves to
1897                // nothing rather than aliasing its successor.
1898                if doc.focus_node_id == Some(node_id) {
1899                    doc.focus_node_id = None;
1900                }
1901                if doc.mousedown_node_id == Some(node_id) {
1902                    doc.mousedown_node_id = None;
1903                }
1904
1905                // Clear the text selection if one of its endpoints references this node.
1906                // This prevents stale selection endpoint references.
1907                if doc.text_selection.anchor.node_or_parent == Some(node_id)
1908                    || doc.text_selection.focus.node_or_parent == Some(node_id)
1909                {
1910                    doc.text_selection.clear();
1911                }
1912
1913                // Remove any snapshot for this node to prevent stale snapshot references
1914                // during style invalidation.
1915                if node.has_snapshot() {
1916                    let opaque_id = style::dom::TNode::opaque(&&*node);
1917                    doc.snapshots.remove(&opaque_id);
1918                    node.set_has_snapshot(false);
1919                }
1920
1921                // If the node has an "id" attribute remove it from the ID map.
1922                if let Some(id_attr) = node.attr(local_name!("id")).map(ToString::to_string) {
1923                    doc.remove_from_id_map(&id_attr, node_id);
1924                }
1925
1926                let node = &mut doc.nodes[node_id];
1927                let NodeData::Element(ref mut element) = node.data else {
1928                    return;
1929                };
1930
1931                match &element.special_data {
1932                    SpecialElementData::SubDocument(_) => {
1933                        self.eager_op_queue
1934                            .push(SpecialOp::UnloadSubDocument(node_id));
1935                    }
1936                    #[cfg(feature = "custom-widget")]
1937                    SpecialElementData::CustomWidget(_) => {
1938                        self.eager_op_queue
1939                            .push(SpecialOp::UnloadCustomWidget(node_id));
1940                    }
1941                    #[cfg(feature = "shadow-dom")]
1942                    SpecialElementData::CustomElement(_) => {
1943                        self.eager_op_queue
1944                            .push(SpecialOp::DisconnectCustomElement(node_id));
1945                    }
1946                    SpecialElementData::Stylesheet(_) => self
1947                        .eager_op_queue
1948                        .push(SpecialOp::UnloadStylesheet(node_id)),
1949                    SpecialElementData::Image(_) => {}
1950                    SpecialElementData::Canvas(_) => {
1951                        self.recompute_is_animating = true;
1952                    }
1953                    SpecialElementData::TableRoot(_) => {}
1954                    SpecialElementData::TextInput(_) => {}
1955                    SpecialElementData::CheckboxInput(_) => {}
1956                    SpecialElementData::Select(_) => {}
1957                    #[cfg(feature = "file-input")]
1958                    SpecialElementData::FileInput(_) => {}
1959                    SpecialElementData::None => {}
1960                }
1961            });
1962
1963        // Detachment must remove published paint edges even while script
1964        // still owns wrappers. The later drop also invalidates references to
1965        // pseudo-elements, anonymous blocks and template contents it frees.
1966        self.doc
1967            .invalidate_node_references(&removed, parents, FxHashSet::default());
1968        #[cfg(feature = "autofocus")]
1969        if self
1970            .node_to_autofocus
1971            .is_some_and(|id| removed.contains(&id))
1972        {
1973            self.node_to_autofocus = None;
1974        }
1975
1976        self.flush_eager_ops();
1977    }
1978
1979    fn maybe_record_node(&mut self, node_id: impl Into<Option<NodeId>>) {
1980        let Some(node_id) = node_id.into() else {
1981            return;
1982        };
1983        if self.doc.nodes[node_id].owner_document != Some(self.doc.root_node().id) {
1984            return;
1985        }
1986
1987        #[cfg(feature = "shadow-dom")]
1988        self.doc.note_shadow_tree_change(node_id);
1989
1990        let Some(element) = self.doc.nodes[node_id].data.downcast_element() else {
1991            return;
1992        };
1993
1994        match element.name.local.as_ref() {
1995            "title" if element.name.ns == ns!(html) => self.title_node = Some(node_id),
1996            "style" => {
1997                self.style_nodes.insert(node_id);
1998            }
1999            _ => {}
2000        }
2001    }
2002
2003    fn load_linked_stylesheet(&mut self, target_id: NodeId) {
2004        let node = &self.doc.nodes[target_id];
2005
2006        let mut is_in_head = false;
2007        let mut parent_id = node.parent;
2008        while let Some(id) = parent_id
2009            && !is_in_head
2010        {
2011            let parent = &self.doc.nodes[id];
2012            is_in_head |= parent.data.is_element_with_tag_name(&local_name!("head"));
2013            parent_id = parent.parent;
2014        }
2015
2016        let rel_attr = node.attr(local_name!("rel"));
2017        let href_attr = node.attr(local_name!("href"));
2018
2019        let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
2020            return;
2021        };
2022        if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
2023            return;
2024        }
2025
2026        let url = self.doc.resolve_url(href);
2027        let handler = ResourceHandler::new(
2028            self.doc.tx.clone(),
2029            self.doc.id(),
2030            Some(node.id),
2031            self.doc.shell_provider.clone(),
2032            StylesheetHandler {
2033                source_url: url.clone(),
2034                guard: self.doc.guard.clone(),
2035                net_provider: self.doc.net_provider.clone(),
2036                abort_signal: self.doc.abort_signal.clone(),
2037            },
2038        );
2039
2040        if is_in_head && !self.doc.net_provider.is_noop() {
2041            self.doc
2042                .pending_critical_resources
2043                .insert(handler.request_id());
2044        }
2045
2046        self.doc.net_provider.fetch(
2047            self.doc.id(),
2048            self.doc
2049                .build_request(url)
2050                .destination(RequestDestination::Style),
2051            Box::new(handler),
2052        );
2053    }
2054
2055    fn unload_stylesheet(&mut self, node_id: NodeId) {
2056        #[cfg(feature = "shadow-dom")]
2057        let shadow_root = self
2058            .doc
2059            .shadow_sheet_roots
2060            .remove(&node_id)
2061            .or_else(|| self.doc.containing_shadow_root(node_id));
2062        let node = &mut self.doc.nodes[node_id];
2063        let Some(element) = node.element_data_mut() else {
2064            return;
2065        };
2066        let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
2067            return;
2068        };
2069
2070        self.doc.nodes_to_stylesheet.remove(&node_id);
2071        #[cfg(feature = "shadow-dom")]
2072        if let Some(root_id) = shadow_root {
2073            self.doc.invalidate_shadow_styles(root_id);
2074            return;
2075        }
2076
2077        if !self.doc.document_sheet_nodes.remove(&node_id) {
2078            return;
2079        }
2080        let guard = self.doc.guard.read();
2081        self.doc.stylist.remove_stylesheet(stylesheet, &guard);
2082        self.doc
2083            .stylist
2084            .force_stylesheet_origins_dirty(OriginSet::all());
2085    }
2086
2087    fn load_image(&mut self, target_id: NodeId) {
2088        let node = &self.doc.nodes[target_id];
2089        if let Some(raw_src) = node.attr(local_name!("src")) {
2090            if !raw_src.is_empty() {
2091                let src = self.doc.resolve_url(raw_src);
2092                let src_string = src.as_str();
2093
2094                // Check cache first
2095                if let Some(cached_image) = self.doc.image_cache.get(src_string) {
2096                    #[cfg(feature = "tracing")]
2097                    tracing::info!("Loading image {src_string} from cache");
2098                    let node = &mut self.doc.nodes[target_id];
2099                    node.element_data_mut().unwrap().special_data =
2100                        SpecialElementData::Image(Box::new(cached_image.clone()));
2101                    node.cache_mut().clear();
2102                    node.insert_damage(ALL_DAMAGE);
2103                    return;
2104                }
2105
2106                // Check if there's already a pending request for this URL
2107                if let Some(waiting_list) = self.doc.pending_images.get_mut(src_string) {
2108                    #[cfg(feature = "tracing")]
2109                    tracing::info!("Image {src_string} already pending, queueing node {target_id}");
2110                    waiting_list.push((target_id, ImageType::Image));
2111                    return;
2112                }
2113
2114                // Start fetch and track as pending
2115                #[cfg(feature = "tracing")]
2116                tracing::info!("Fetching image {src_string}");
2117                self.doc
2118                    .pending_images
2119                    .insert(src_string.to_string(), vec![(target_id, ImageType::Image)]);
2120
2121                self.doc.net_provider.fetch(
2122                    self.doc.id(),
2123                    self.doc
2124                        .build_request(src)
2125                        .destination(RequestDestination::Image),
2126                    ResourceHandler::boxed(
2127                        self.doc.tx.clone(),
2128                        self.doc.id(),
2129                        None, // Don't pass node_id, we'll handle it via pending_images
2130                        self.doc.shell_provider.clone(),
2131                        ImageHandler::new(ImageType::Image),
2132                    ),
2133                );
2134            }
2135        }
2136    }
2137
2138    fn load_iframe(&mut self, target_id: NodeId) {
2139        if self.doc.subdocument_depth >= crate::iframe::MAX_SUBDOCUMENT_DEPTH {
2140            #[cfg(feature = "tracing")]
2141            tracing::warn!(
2142                "Not loading iframe: max sub-document nesting depth ({}) reached",
2143                crate::iframe::MAX_SUBDOCUMENT_DEPTH
2144            );
2145            return;
2146        }
2147
2148        let node = &self.doc.nodes[target_id];
2149        let Some(element) = node.element_data() else {
2150            return;
2151        };
2152
2153        // `srcdoc` takes precedence over `src`
2154        if let Some(srcdoc) = element.attr(local_name!("srcdoc")) {
2155            let srcdoc = srcdoc.to_string();
2156            self.doc.load_iframe_srcdoc(target_id, &srcdoc);
2157            return;
2158        }
2159
2160        let Some(raw_src) = element.attr(local_name!("src")) else {
2161            return;
2162        };
2163        if raw_src.is_empty() {
2164            return;
2165        }
2166        let Some(url) = self.doc.url.resolve_relative(raw_src) else {
2167            #[cfg(feature = "tracing")]
2168            tracing::warn!("Not loading iframe: could not resolve url {raw_src}");
2169            return;
2170        };
2171        self.doc.start_iframe_load(target_id, url);
2172    }
2173
2174    fn load_custom_paint_src(&mut self, target_id: NodeId) {
2175        let node = &mut self.doc.nodes[target_id];
2176        if let Some(raw_src) = node.attr(local_name!("src")) {
2177            if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
2178                self.recompute_is_animating = true;
2179                let canvas_data = SpecialElementData::Canvas(CanvasData {
2180                    custom_paint_source_id,
2181                });
2182                node.element_data_mut().unwrap().special_data = canvas_data;
2183            }
2184        }
2185    }
2186
2187    fn process_button_input(&mut self, target_id: NodeId) {
2188        let node = &self.doc.nodes[target_id];
2189        let Some(data) = node.element_data() else {
2190            return;
2191        };
2192
2193        let tagname = data.name.local.as_ref();
2194        let type_attr = data.attr(local_name!("type"));
2195        let value = data.attr(local_name!("value"));
2196
2197        // Add content of "value" attribute as a text node child if:
2198        //   - Tag name is
2199        if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
2200            (tagname, type_attr, value)
2201        {
2202            let value = value.to_string();
2203            let id = self.create_text_node(&value);
2204            self.append_children(target_id, &[id]);
2205            // Needed only when the file-input arm below is compiled in.
2206            #[cfg_attr(not(feature = "file-input"), allow(clippy::needless_return))]
2207            return;
2208        }
2209        #[cfg(feature = "file-input")]
2210        if let ("input", Some("file")) = (tagname, type_attr) {
2211            let button_id = self.create_element(
2212                qual_name!("button", html),
2213                vec![
2214                    Attribute {
2215                        name: qual_name!("type", html),
2216                        value: "button".into(),
2217                    },
2218                    Attribute {
2219                        name: qual_name!("tabindex", html),
2220                        value: "-1".into(),
2221                    },
2222                ],
2223            );
2224            let label_id = self.create_element(qual_name!("label", html), vec![]);
2225            let text_id = self.create_text_node("No File Selected");
2226            let button_text_id = self.create_text_node("Browse");
2227            self.append_children(target_id, &[button_id, label_id]);
2228            self.append_children(label_id, &[text_id]);
2229            self.append_children(button_id, &[button_text_id]);
2230        }
2231    }
2232}
2233
2234/// Set 'checked' state on an input based on given attributevalue
2235fn set_input_checked_state(element: &mut ElementData, value: String) {
2236    let Ok(checked) = value.parse() else {
2237        return;
2238    };
2239    match element.special_data {
2240        SpecialElementData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
2241        // If we have just constructed the element, set the node attribute,
2242        // and NodeSpecificData will be created from that later
2243        // this simulates the checked attribute being set in html,
2244        // and the element's checked property being set from that
2245        SpecialElementData::None => element.attrs.push(Attribute {
2246            name: qual_name!("checked", html),
2247            value: checked.to_string().into(),
2248        }),
2249        _ => {}
2250    }
2251}
2252
2253/// Type that allows mutable access to the viewport
2254/// And syncs it back to stylist on drop.
2255pub struct ViewportMut<'doc> {
2256    doc: &'doc mut BaseDocument,
2257    initial_viewport: Viewport,
2258}
2259impl ViewportMut<'_> {
2260    pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
2261        let initial_viewport = doc.viewport.clone();
2262        ViewportMut {
2263            doc,
2264            initial_viewport,
2265        }
2266    }
2267}
2268impl Deref for ViewportMut<'_> {
2269    type Target = Viewport;
2270
2271    fn deref(&self) -> &Self::Target {
2272        &self.doc.viewport
2273    }
2274}
2275impl DerefMut for ViewportMut<'_> {
2276    fn deref_mut(&mut self) -> &mut Self::Target {
2277        &mut self.doc.viewport
2278    }
2279}
2280impl Drop for ViewportMut<'_> {
2281    fn drop(&mut self) {
2282        if self.doc.viewport == self.initial_viewport {
2283            return;
2284        }
2285
2286        self.doc.set_stylist_device(make_device(
2287            &self.doc.viewport,
2288            self.doc.media_type.clone(),
2289            self.doc.font_ctx.clone(),
2290        ));
2291        self.doc.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
2292
2293        let scale_has_changed =
2294            self.doc.viewport().scale_f64() != self.initial_viewport.scale_f64();
2295        if scale_has_changed {
2296            self.doc.invalidate_inline_contexts();
2297            self.doc.shell_provider.request_redraw();
2298        }
2299    }
2300}
2301
2302/// A qualified name in the HTML namespace.
2303fn html_tag(name: &str) -> markup5ever::QualName {
2304    markup5ever::QualName::new(None, markup5ever::ns!(html), name.into())
2305}
2306
2307#[cfg(test)]
2308mod test {
2309    use style::media_queries::MediaType;
2310    use style_dom::ElementState;
2311
2312    use std::sync::{
2313        Arc,
2314        atomic::{AtomicUsize, Ordering},
2315    };
2316
2317    use blitz_traits::shell::{ColorScheme, ShellProvider, Viewport};
2318
2319    use crate::{
2320        Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, NodeId, qual_name,
2321    };
2322
2323    #[test]
2324    fn media_type_defaults_to_screen() {
2325        let mut document = BaseDocument::new(DocumentConfig::default());
2326        assert_eq!(*document.media_type(), MediaType::screen());
2327        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
2328    }
2329
2330    #[test]
2331    fn media_type_honors_config() {
2332        let mut document = BaseDocument::new(DocumentConfig {
2333            media_type: Some(MediaType::print()),
2334            ..Default::default()
2335        });
2336        assert_eq!(*document.media_type(), MediaType::print());
2337        assert_eq!(document.stylist_device().media_type(), MediaType::print());
2338    }
2339
2340    #[test]
2341    fn set_media_type_updates_stylist_device() {
2342        let mut document = BaseDocument::new(DocumentConfig::default());
2343        assert_eq!(document.stylist_device().media_type(), MediaType::screen());
2344
2345        document.set_media_type(MediaType::print());
2346        assert_eq!(*document.media_type(), MediaType::print());
2347        assert_eq!(document.stylist_device().media_type(), MediaType::print());
2348    }
2349
2350    #[test]
2351    fn removing_a_node_forgets_it_as_focused_and_pressed() {
2352        // Dismissing a panel is a click on a control inside it, so at that
2353        // moment the control is both the focused node and the mousedown node,
2354        // and then its subtree goes away. Removal used to clear hover, active
2355        // and the selection endpoints but leave these two, and the next click
2356        // indexed a dropped id and panicked inside the event handler.
2357        let mut document = BaseDocument::new(DocumentConfig::default());
2358        let button = document.create_node(NodeData::Element(Box::new(ElementData::new(
2359            qual_name!("button"),
2360            Vec::new(),
2361        ))));
2362        let root = document.root_node().id;
2363
2364        let mut mutator = document.mutate();
2365        mutator.append_children(root, &[button]);
2366        drop(mutator);
2367
2368        document.set_focus_to(button);
2369        document.set_mousedown_node_id(Some(button));
2370        assert_eq!(document.get_focussed_node_id(), Some(button));
2371        assert_eq!(document.mousedown_node_id, Some(button));
2372
2373        let mut mutator = document.mutate();
2374        mutator.remove_node(button);
2375        drop(mutator);
2376
2377        assert_eq!(
2378            document.get_focussed_node_id(),
2379            None,
2380            "a removed node must not stay focused"
2381        );
2382        assert_eq!(
2383            document.mousedown_node_id, None,
2384            "a removed node must not stay the pressed node"
2385        );
2386    }
2387
2388    #[test]
2389    fn dropping_a_child_clears_a_hidden_retained_layout_edge() {
2390        let mut document = BaseDocument::new(DocumentConfig {
2391            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2392            ..Default::default()
2393        });
2394        let root = document.root_node().id;
2395        let (parent, child) = {
2396            let mut mutator = document.mutate();
2397            let parent = mutator.create_element(qual_name!("div"), vec![]);
2398            let child = mutator.create_element(qual_name!("button"), vec![]);
2399            mutator.set_style_property(parent, "width", "200px");
2400            mutator.set_style_property(parent, "height", "100px");
2401            mutator.append_children(parent, &[child]);
2402            mutator.append_children(root, &[parent]);
2403            (parent, child)
2404        };
2405
2406        document.resolve(0.0);
2407        {
2408            let mut mutator = document.mutate();
2409            mutator.set_style_property(parent, "display", "none");
2410        }
2411        document.resolve(0.0);
2412        assert!(
2413            document.nodes[parent]
2414                .layout_children
2415                .borrow()
2416                .as_ref()
2417                .is_some_and(|children| children.contains(&child)),
2418            "the hidden subtree should retain the layout edge that makes this regression possible"
2419        );
2420
2421        document.mutate().remove_and_drop_node(child);
2422        assert!(document.get_node(child).is_none(), "the child was freed");
2423        assert!(
2424            document.nodes[parent].layout_children.borrow().is_none(),
2425            "the surviving layout parent must not retain the freed key"
2426        );
2427
2428        // This used to panic in Taffy's rounding pass after indexing `child`.
2429        document.resolve(0.0);
2430    }
2431
2432    #[test]
2433    fn mutator_remove_disabled() {
2434        let mut document = BaseDocument::new(DocumentConfig::default());
2435        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2436            qual_name!("button"),
2437            vec![Attribute {
2438                name: qual_name!("disabled"),
2439                value: "".into(),
2440            }],
2441        ))));
2442
2443        let node = document.get_node(id).unwrap();
2444        assert!(
2445            node.element_state().contains(ElementState::DISABLED),
2446            "form node is disabled"
2447        );
2448        assert!(
2449            !node.element_state().contains(ElementState::ENABLED),
2450            "form node is not enabled yet"
2451        );
2452
2453        let mut mutator = document.mutate();
2454        mutator.clear_attribute(id, qual_name!("disabled"));
2455        drop(mutator);
2456
2457        let node = document.get_node(id).unwrap();
2458        assert!(
2459            !node.element_state().contains(ElementState::DISABLED),
2460            "form node is no longer disabled"
2461        );
2462        assert!(
2463            node.element_state().contains(ElementState::ENABLED),
2464            "form node is enabled"
2465        );
2466    }
2467
2468    #[test]
2469    fn mutator_set_disabled() {
2470        let mut document = BaseDocument::new(DocumentConfig::default());
2471        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2472            qual_name!("button"),
2473            vec![],
2474        ))));
2475
2476        let node = document.get_node(id).unwrap();
2477        assert!(
2478            !node.element_state().contains(ElementState::DISABLED),
2479            "form node is not disabled"
2480        );
2481        assert!(
2482            node.element_state().contains(ElementState::ENABLED),
2483            "form node is enabled"
2484        );
2485
2486        let mut mutator = document.mutate();
2487        mutator.set_attribute(id, qual_name!("disabled"), "");
2488        drop(mutator);
2489
2490        let node = document.get_node(id).unwrap();
2491
2492        assert!(
2493            node.element_state().contains(ElementState::DISABLED),
2494            "form node is disabled"
2495        );
2496        assert!(
2497            !node.element_state().contains(ElementState::ENABLED),
2498            "form node is no longer enabled enabled"
2499        );
2500    }
2501
2502    #[test]
2503    fn mutator_set_disabled_invalid_node() {
2504        let mut document = BaseDocument::new(DocumentConfig::default());
2505        let id = document.create_node(NodeData::Element(Box::new(ElementData::new(
2506            qual_name!("a"),
2507            vec![],
2508        ))));
2509
2510        let node = document.get_node(id).unwrap();
2511        assert!(
2512            !node.element_state().contains(ElementState::DISABLED),
2513            "form node is not disabled"
2514        );
2515        assert!(
2516            !node.element_state().contains(ElementState::ENABLED),
2517            "form node is enabled"
2518        );
2519
2520        let mut mutator = document.mutate();
2521        mutator.set_attribute(id, qual_name!("disabled"), "");
2522        drop(mutator);
2523
2524        let node = document.get_node(id).unwrap();
2525        assert!(
2526            !node.element_state().contains(ElementState::DISABLED),
2527            "form node is not disabled"
2528        );
2529        assert!(
2530            !node.element_state().contains(ElementState::ENABLED),
2531            "form node is enabled"
2532        );
2533    }
2534
2535    #[test]
2536    fn mutator_id_attribute_updates_id_map() {
2537        let mut document = BaseDocument::new(DocumentConfig::default());
2538        let root_id = document.root_node().id;
2539
2540        let node_id = {
2541            let mut mutator = document.mutate();
2542            let node_id = mutator.create_element(
2543                qual_name!("div"),
2544                vec![Attribute {
2545                    name: qual_name!("id"),
2546                    value: "old".into(),
2547                }],
2548            );
2549            mutator.append_children(root_id, &[node_id]);
2550            node_id
2551        };
2552        assert_eq!(document.get_element_by_id("old"), Some(node_id));
2553
2554        {
2555            let mut mutator = document.mutate();
2556            mutator.set_attribute(node_id, qual_name!("id"), "new");
2557        }
2558        assert_eq!(document.get_element_by_id("new"), Some(node_id));
2559        assert_eq!(document.get_element_by_id("old"), None);
2560
2561        {
2562            let mut mutator = document.mutate();
2563            mutator.clear_attribute(node_id, qual_name!("id"));
2564        }
2565        assert_eq!(document.get_element_by_id("new"), None);
2566    }
2567
2568    #[test]
2569    fn get_element_by_id_duplicate_ids_first_in_tree_order_wins() {
2570        let mut document = BaseDocument::new(DocumentConfig::default());
2571        let root_id = document.root_node().id;
2572
2573        let (first_id, second_id) = {
2574            let mut mutator = document.mutate();
2575            let first_id = mutator.create_element(qual_name!("div"), vec![]);
2576            let second_id = mutator.create_element(qual_name!("div"), vec![]);
2577            mutator.append_children(root_id, &[first_id, second_id]);
2578            // Assign the id to the later node first so that insertion order
2579            // differs from tree order
2580            mutator.set_attribute(second_id, qual_name!("id"), "dup");
2581            mutator.set_attribute(first_id, qual_name!("id"), "dup");
2582            (first_id, second_id)
2583        };
2584        assert_eq!(document.get_element_by_id("dup"), Some(first_id));
2585
2586        {
2587            let mut mutator = document.mutate();
2588            mutator.remove_node(first_id);
2589        }
2590        assert_eq!(document.get_element_by_id("dup"), Some(second_id));
2591    }
2592
2593    #[derive(Default)]
2594    struct RedrawShell {
2595        redraw_requests: AtomicUsize,
2596    }
2597
2598    impl ShellProvider for RedrawShell {
2599        fn request_redraw(&self) {
2600            self.redraw_requests.fetch_add(1, Ordering::Relaxed);
2601        }
2602    }
2603
2604    #[test]
2605    fn mutator_requests_redraw_only_after_mutation() {
2606        let shell = Arc::new(RedrawShell::default());
2607        let mut document = BaseDocument::new(DocumentConfig {
2608            shell_provider: Some(shell.clone()),
2609            ..Default::default()
2610        });
2611        let root_id = document.root_node().id;
2612
2613        {
2614            let mut mutator = document.mutate();
2615            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2616            let child_id = mutator.create_element(qual_name!("span"), vec![]);
2617            mutator.append_children(parent_id, &[child_id]);
2618            mutator.remove_and_drop_all_children(parent_id);
2619            mutator.set_attribute(parent_id, qual_name!("id"), "detached");
2620        }
2621        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 0);
2622
2623        {
2624            let mutator = document.mutate();
2625            assert_eq!(mutator.child_ids(root_id).len(), 0);
2626        }
2627
2628        {
2629            let mut mutator = document.mutate();
2630            let node_id = mutator.create_element(qual_name!("div"), vec![]);
2631            mutator.append_children(root_id, &[node_id]);
2632            mutator.set_attribute(node_id, qual_name!("id"), "in-document");
2633        }
2634        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2635
2636        {
2637            let mut mutator = document.mutate();
2638            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2639            let child_id = mutator.create_element(qual_name!("span"), vec![]);
2640            mutator.append_children(root_id, &[parent_id]);
2641            mutator.append_children(parent_id, &[child_id]);
2642            mutator.remove_and_drop_all_children(parent_id);
2643        }
2644        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2645
2646        {
2647            let mut mutator = document.mutate();
2648            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2649            let child_id = mutator.create_element(qual_name!("span"), vec![]);
2650            let detached_target_id = mutator.create_element(qual_name!("div"), vec![]);
2651            mutator.append_children(root_id, &[parent_id]);
2652            mutator.append_children(parent_id, &[child_id]);
2653            assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2654            mutator.append_children(detached_target_id, &[child_id]);
2655        }
2656        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2657    }
2658
2659    #[test]
2660    fn moving_subtree_out_of_document_clears_in_document_flag() {
2661        let shell = Arc::new(RedrawShell::default());
2662        let mut document = BaseDocument::new(DocumentConfig {
2663            shell_provider: Some(shell.clone()),
2664            ..Default::default()
2665        });
2666        let root_id = document.root_node().id;
2667        let (child_id, grandchild_id, detached_parent_id) = {
2668            let mut mutator = document.mutate();
2669            let in_document_parent_id = mutator.create_element(qual_name!("div"), vec![]);
2670            let child_id = mutator.create_element(qual_name!("div"), vec![]);
2671            let grandchild_id = mutator.create_element(qual_name!("span"), vec![]);
2672            let detached_parent_id = mutator.create_element(qual_name!("section"), vec![]);
2673            mutator.append_children(root_id, &[in_document_parent_id]);
2674            mutator.append_children(in_document_parent_id, &[child_id]);
2675            mutator.append_children(child_id, &[grandchild_id]);
2676            (child_id, grandchild_id, detached_parent_id)
2677        };
2678        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 1);
2679        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2680        assert!(
2681            document
2682                .get_node(grandchild_id)
2683                .unwrap()
2684                .flags
2685                .is_in_document()
2686        );
2687
2688        {
2689            let mut mutator = document.mutate();
2690            mutator.append_children(detached_parent_id, &[child_id]);
2691        }
2692        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2693        assert!(!document.get_node(child_id).unwrap().flags.is_in_document());
2694        assert!(
2695            !document
2696                .get_node(grandchild_id)
2697                .unwrap()
2698                .flags
2699                .is_in_document()
2700        );
2701
2702        {
2703            let mut mutator = document.mutate();
2704            mutator.set_attribute(child_id, qual_name!("id"), "detached");
2705        }
2706        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 2);
2707
2708        {
2709            let mut mutator = document.mutate();
2710            mutator.append_children(root_id, &[child_id]);
2711        }
2712        assert_eq!(shell.redraw_requests.load(Ordering::Relaxed), 3);
2713        assert!(document.get_node(child_id).unwrap().flags.is_in_document());
2714        assert!(
2715            document
2716                .get_node(grandchild_id)
2717                .unwrap()
2718                .flags
2719                .is_in_document()
2720        );
2721    }
2722
2723    /// A `calc()` does not reach taffy as a value. `stylo_taffy` hands it over
2724    /// as a raw pointer into the node's `ComputedValues`, and layout
2725    /// dereferences that pointer on every resolve, so the cached taffy style
2726    /// must never outlive the arc it was built from.
2727    ///
2728    /// A restyle that lands no relayout damage still replaces those computed
2729    /// values. Colour is the cheapest example and it is the real one: a slow
2730    /// command's response restyled the project header two seconds after boot,
2731    /// the header's absolutely positioned chip carries
2732    /// `max-width: calc(100% - 24px)`, and 0.6.x experimental died there in
2733    /// three different ways depending on what had taken the freed allocation.
2734    #[test]
2735    fn a_paint_only_restyle_refreshes_the_calc_the_taffy_style_points_at() {
2736        use style::servo_arc::Arc as ServoArc;
2737
2738        let mut document = BaseDocument::new(DocumentConfig {
2739            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2740            ..Default::default()
2741        });
2742        let root_id = document.root_node().id;
2743
2744        let (header_id, chip_id) = {
2745            let mut mutator = document.mutate();
2746            let header_id = mutator.create_element(qual_name!("div"), vec![]);
2747            let chip_id = mutator.create_element(qual_name!("span"), vec![]);
2748            mutator.set_style_property(header_id, "position", "relative");
2749            mutator.set_style_property(header_id, "width", "800px");
2750            mutator.set_style_property(header_id, "height", "60px");
2751            mutator.set_style_property(chip_id, "position", "absolute");
2752            mutator.set_style_property(chip_id, "max-width", "calc(100% - 24px)");
2753            mutator.set_style_property(chip_id, "color", "rgb(1, 2, 3)");
2754            mutator.append_children(header_id, &[chip_id]);
2755            mutator.append_children(root_id, &[header_id]);
2756            (header_id, chip_id)
2757        };
2758
2759        document.resolve(0.0);
2760
2761        // Restyled through inheritance, not directly: the chip's own mutation
2762        // damage would force a rebuild and hide the hazard. Recolouring the
2763        // parent recomputes the child's values — a new arc — while the child's
2764        // own damage stays repaint-only, which is exactly the gap the gate left
2765        // open.
2766        {
2767            let mut mutator = document.mutate();
2768            mutator.set_style_property(header_id, "color", "rgb(4, 5, 6)");
2769        }
2770        document.resolve(0.0);
2771
2772        let node = document.get_node(chip_id).unwrap();
2773        let stylo_data = node.stylo_element_data_opt().and_then(|data| data.get());
2774        let primary = stylo_data
2775            .as_ref()
2776            .and_then(|data| data.styles.get_primary())
2777            .expect("the chip is styled");
2778        let source = node
2779            .style_source_opt()
2780            .expect("a styled node records the computed values its taffy style was built from");
2781
2782        assert!(
2783            ServoArc::ptr_eq(primary, source),
2784            "the cached taffy style still points into computed values that a restyle replaced, \
2785             so every calc() in it is a dangling pointer",
2786        );
2787    }
2788
2789    #[test]
2790    fn style_property_updates_nested_layout() {
2791        let mut document = BaseDocument::new(DocumentConfig {
2792            viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
2793            ..Default::default()
2794        });
2795        let root_id = document.root_node().id;
2796
2797        let mover_id = {
2798            let mut mutator = document.mutate();
2799            let parent_id = mutator.create_element(qual_name!("div"), vec![]);
2800            let mover_id = mutator.create_element(qual_name!("div"), vec![]);
2801            mutator.set_style_property(parent_id, "position", "relative");
2802            mutator.set_style_property(parent_id, "width", "800px");
2803            mutator.set_style_property(parent_id, "height", "600px");
2804            mutator.set_style_property(mover_id, "position", "absolute");
2805            mutator.set_style_property(mover_id, "left", "0px");
2806            mutator.set_style_property(mover_id, "top", "0px");
2807            mutator.append_children(parent_id, &[mover_id]);
2808            mutator.append_children(root_id, &[parent_id]);
2809            mover_id
2810        };
2811
2812        document.resolve(0.0);
2813        assert_eq!(
2814            document
2815                .get_node(mover_id)
2816                .unwrap()
2817                .final_layout()
2818                .location
2819                .x,
2820            0.0
2821        );
2822
2823        {
2824            let mut mutator = document.mutate();
2825            mutator.set_style_property(mover_id, "left", "120px");
2826        }
2827
2828        document.resolve(0.0);
2829        assert_eq!(
2830            document
2831                .get_node(mover_id)
2832                .unwrap()
2833                .final_layout()
2834                .location
2835                .x,
2836            120.0
2837        );
2838    }
2839
2840    /// `<html><body><div>text<!--comment--></div></body></html>`, laid out
2841    /// once, returning the text and comment ids.
2842    fn doc_with_a_comment() -> (BaseDocument, NodeId, NodeId, NodeId) {
2843        let mut doc = BaseDocument::new(DocumentConfig {
2844            viewport: Some(Viewport::new(400, 300, 1.0, ColorScheme::Light)),
2845            ..Default::default()
2846        });
2847        let root_id = doc.root_node().id;
2848
2849        let mut mutr = doc.mutate();
2850        let html = mutr.create_element(qual_name!("html"), vec![]);
2851        let body = mutr.create_element(qual_name!("body"), vec![]);
2852        let container = mutr.create_element(qual_name!("div"), vec![]);
2853        let text = mutr.create_text_node("text");
2854        let comment = mutr.create_comment_node("comment");
2855        mutr.append_children(container, &[text, comment]);
2856        mutr.append_children(body, &[container]);
2857        mutr.append_children(html, &[body]);
2858        mutr.append_children(root_id, &[html]);
2859        drop(mutr);
2860
2861        doc.resolve(0.0);
2862        (doc, container, text, comment)
2863    }
2864
2865    /// A comment is CharacterData: `comment.data = "x"` has to land somewhere.
2866    /// Before this arm existed it fell through and vanished, so a getter that
2867    /// returned the contents would have disagreed with every write.
2868    #[test]
2869    fn setting_a_comments_data_writes_the_contents() {
2870        let (mut doc, _container, _text, comment) = doc_with_a_comment();
2871
2872        doc.mutate().set_node_text(comment, "rewritten");
2873
2874        let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2875            panic!("expected a comment node");
2876        };
2877        assert_eq!(contents, "rewritten");
2878    }
2879
2880    /// A comment generates no box, so writing its data must not schedule a
2881    /// relayout. Without this the obvious implementation (copy the Text arm)
2882    /// costs a full resolve per write, and nothing observable would say so.
2883    ///
2884    /// The text-node write at the end is the control: it proves the assertion
2885    /// above is capable of failing.
2886    #[test]
2887    fn setting_a_comments_data_does_not_dirty_layout() {
2888        let (mut doc, container, text, comment) = doc_with_a_comment();
2889
2890        let container_damage_before = doc.get_node(container).unwrap().damage();
2891        let comment_damage_before = doc.get_node(comment).unwrap().damage();
2892
2893        doc.mutate().set_node_text(comment, "rewritten");
2894
2895        assert_eq!(
2896            doc.get_node(comment).unwrap().damage(),
2897            comment_damage_before,
2898            "writing a comment's data damaged the comment"
2899        );
2900        assert_eq!(
2901            doc.get_node(container).unwrap().damage(),
2902            container_damage_before,
2903            "writing a comment's data damaged its parent, scheduling a relayout \
2904             for a change that cannot affect a pixel"
2905        );
2906
2907        doc.mutate().set_node_text(text, "rewritten");
2908        assert_ne!(
2909            doc.get_node(container).unwrap().damage(),
2910            container_damage_before,
2911            "a text write should damage the parent, so the assertions above can fail"
2912        );
2913    }
2914
2915    /// Writing the same contents back is not a change, and must stay as inert
2916    /// as a write of different contents.
2917    #[test]
2918    fn rewriting_a_comment_with_its_own_contents_is_inert() {
2919        let (mut doc, container, _text, comment) = doc_with_a_comment();
2920        let container_damage_before = doc.get_node(container).unwrap().damage();
2921
2922        doc.mutate().set_node_text(comment, "comment");
2923
2924        let NodeData::Comment { contents } = &doc.get_node(comment).unwrap().data else {
2925            panic!("expected a comment node");
2926        };
2927        assert_eq!(contents, "comment");
2928        assert_eq!(
2929            doc.get_node(container).unwrap().damage(),
2930            container_damage_before
2931        );
2932    }
2933}