Skip to main content

blitz_dom/
mutator.rs

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