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