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