/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/html/htmlslotelement.rs
564 строки
22 KB
Narfinger
script: Use NoGC for note_dirty_element (#47165)
11 авг 2026, 23:49
Не верифицирован
11 авг 2026, 23:49
b49e982
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, Ref, RefCell}; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, local_name, ns}; use js::context::{JSContext, NoGC}; use js::gc::RootedVec; use js::rust::HandleObject; use script_bindings::codegen::InheritTypes::{CharacterDataTypeId, NodeTypeId}; use crate::ScriptThread; use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::{ AssignedNodesOptions, HTMLSlotElementMethods, }; use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods}; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ ShadowRootMode, SlotAssignmentMode, }; use crate::dom::bindings::codegen::UnionTypes::ElementOrText; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; use crate::dom::document::Document; use crate::dom::element::attributes::storage::AttrRef; use crate::dom::element::{AttributeMutation, Element}; use crate::dom::html::htmlelement::HTMLElement; use crate::dom::node::virtualmethods::VirtualMethods; use crate::dom::node::{ BindContext, ForceSlottableNodeReconciliation, IsShadowTree, Node, NodeDamage, NodeTraits, UnbindContext, }; use crate::dom::{FlatTreeParent, NodeFlags}; /// <https://html.spec.whatwg.org/multipage/#the-slot-element> #[dom_struct] pub(crate) struct HTMLSlotElement { htmlelement: HTMLElement, /// <https://dom.spec.whatwg.org/#slot-assigned-nodes> assigned_nodes: RefCell<Vec<Slottable>>, /// <https://html.spec.whatwg.org/multipage/#manually-assigned-nodes> manually_assigned_nodes: RefCell<Vec<Slottable>>, /// Whether there is a queued signal change for this element /// /// Necessary to avoid triggering too many slotchange events is_in_agents_signal_slots: Cell<bool>, } impl HTMLSlotElementMethods<crate::DomTypeHolder> for HTMLSlotElement { // https://html.spec.whatwg.org/multipage/#dom-slot-name make_getter!(Name, "name"); // https://html.spec.whatwg.org/multipage/#dom-slot-name make_atomic_setter!(SetName, "name"); /// <https://html.spec.whatwg.org/multipage/#dom-slot-assignednodes> fn AssignedNodes(&self, cx: &JSContext, options: &AssignedNodesOptions) -> Vec<DomRoot<Node>> { // Step 1. If options["flatten"] is false, then return this's assigned nodes. if !options.flatten { return self .assigned_nodes .borrow() .iter() .map(|slottable| slottable.node()) .map(DomRoot::from_ref) .collect(); } // Step 2. Return the result of finding flattened slottables with this. rooted_vec!(let mut flattened_slottables); self.find_flattened_slottables(cx, &mut flattened_slottables); flattened_slottables .iter() .map(|slottable| DomRoot::from_ref(slottable.node())) .collect() } /// <https://html.spec.whatwg.org/multipage/#dom-slot-assignedelements> fn AssignedElements( &self, cx: &JSContext, options: &AssignedNodesOptions, ) -> Vec<DomRoot<Element>> { self.AssignedNodes(cx, options) .into_iter() .flat_map(|node| node.downcast::<Element>().map(DomRoot::from_ref)) .collect() } /// <https://html.spec.whatwg.org/multipage/#dom-slot-assign> fn Assign(&self, cx: &JSContext, nodes: Vec<ElementOrText>) { // Step 1. For each node of this's manually assigned nodes, set node's manual slot assignment to null. for slottable in self.manually_assigned_nodes.borrow().iter() { slottable.set_manual_slot_assignment(None); } // Step 2. Let nodesSet be a new ordered set. rooted_vec!(let mut nodes_set); // Step 3. For each node of nodes: for element_or_text in nodes.into_iter() { rooted!(&in(cx) let node = match element_or_text { ElementOrText::Element(element) => Slottable(Dom::from_ref(element.upcast())), ElementOrText::Text(text) => Slottable(Dom::from_ref(text.upcast())), }); // Step 3.1 If node's manual slot assignment refers to a slot, // then remove node from that slot's manually assigned nodes. if let Some(slot) = node.manual_slot_assignment() { let mut manually_assigned_nodes = slot.manually_assigned_nodes.borrow_mut(); if let Some(position) = manually_assigned_nodes .iter() .position(|value| *value == *node) { manually_assigned_nodes.remove(position); } } // Step 3.2 Set node's manual slot assignment to this. node.set_manual_slot_assignment(Some(self)); // Step 3.3 Append node to nodesSet. if !nodes_set.contains(&*node) { nodes_set.push(node.clone()); } } // Step 4. Set this's manually assigned nodes to nodesSet. *self.manually_assigned_nodes.borrow_mut() = nodes_set.iter().cloned().collect(); // Step 5. Run assign slottables for a tree for this's root. self.upcast::<Node>() .GetRootNode(&GetRootNodeOptions::empty()) .assign_slottables_for_a_tree(cx, ForceSlottableNodeReconciliation::Force); } } /// <https://dom.spec.whatwg.org/#concept-slotable> /// /// The contained node is assumed to be either `Element` or `Text` /// /// This field is public to make it easy to construct slottables. /// As such, it is possible to put Nodes that are not slottables /// in there. Using a [Slottable] like this will quickly lead to /// a panic. #[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] #[repr(transparent)] pub(crate) struct Slottable(pub Dom<Node>); /// Data shared between all [slottables](https://dom.spec.whatwg.org/#concept-slotable) /// /// Note that the [slottable name](https://dom.spec.whatwg.org/#slotable-name) is not /// part of this. While the spec says that all slottables have a name, only Element's /// can ever have a non-empty name, so they store it separately #[derive(Default, JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] pub struct SlottableData { /// <https://dom.spec.whatwg.org/#slotable-assigned-slot> pub(crate) assigned_slot: Option<Dom<HTMLSlotElement>>, /// <https://dom.spec.whatwg.org/#slottable-manual-slot-assignment> pub(crate) manual_slot_assignment: Option<Dom<HTMLSlotElement>>, } impl HTMLSlotElement { fn new_inherited( local_name: LocalName, prefix: Option<Prefix>, document: &Document, ) -> HTMLSlotElement { HTMLSlotElement { htmlelement: HTMLElement::new_inherited(local_name, prefix, document), assigned_nodes: Default::default(), manually_assigned_nodes: Default::default(), is_in_agents_signal_slots: Default::default(), } } pub(crate) fn new( cx: &mut JSContext, local_name: LocalName, prefix: Option<Prefix>, document: &Document, proto: Option<HandleObject>, ) -> DomRoot<HTMLSlotElement> { Node::reflect_node_with_proto( cx, Box::new(HTMLSlotElement::new_inherited(local_name, prefix, document)), document, proto, ) } pub(crate) fn has_assigned_nodes(&self) -> bool { !self.assigned_nodes.borrow().is_empty() } /// <https://dom.spec.whatwg.org/#find-flattened-slotables> fn find_flattened_slottables(&self, cx: &JSContext, result: &mut RootedVec<Slottable>) { // Step 1. Let result be an empty list. // NOTE: In our case "result" is not necessarily empty, because it contains // the results of multiple successive calls to "find_flattened_slottables". This method // only appends to it, so that doesn't matter. // Step 2. If slot’s root is not a shadow root, then return result. if !self.upcast::<Node>().is_in_a_shadow_tree() { return; }; // Step 3. Let slottables be the result of finding slottables given slot. rooted_vec!(let mut slottables); self.find_slottables(cx, &mut slottables); // Step 4. If slottables is the empty list, then append each slottable // child of slot, in tree order, to slottables. if slottables.is_empty() { for child in self.upcast::<Node>().children_unrooted(cx) { let is_slottable = matches!( child.type_id(), NodeTypeId::Element(_) | NodeTypeId::CharacterData(CharacterDataTypeId::Text(_)) ); if is_slottable { slottables.push(Slottable(Dom::from_ref(&*child))); } } } // Step 5. For each node in slottables: for slottable in slottables.iter() { // Step 5.1 If node is a slot whose root is a shadow root: match slottable.0.downcast::<HTMLSlotElement>() { Some(slot_element) if slot_element.upcast::<Node>().is_in_a_shadow_tree() => { // Step 5.1.1 Let temporaryResult be the result of finding flattened slottables given node. // Step 5.1.2 Append each slottable in temporaryResult, in order, to result. slot_element.find_flattened_slottables(cx, result); }, // Step 5.2 Otherwise, append node to result. _ => { result.push(slottable.clone()); }, }; } // Step 6. Return result. } /// <https://dom.spec.whatwg.org/#find-slotables> /// /// To avoid rooting shenanigans, this writes the returned slottables /// into the `result` argument fn find_slottables(&self, cx: &JSContext, result: &mut RootedVec<Slottable>) { // Step 1. Let result be an empty list. debug_assert!(result.is_empty()); // Step 2. Let root be slot’s root. // Step 3. If root is not a shadow root, then return result. let Some(root) = self.upcast::<Node>().containing_shadow_root() else { return; }; // Step 4. Let host be root’s host. let host = root.Host(); // Step 5. If root’s slot assignment is "manual": if root.SlotAssignment() == SlotAssignmentMode::Manual { // Step 5.1 Let result be « ». // NOTE: redundant. // Step 5.2 For each slottable slottable of slot’s manually assigned nodes, // if slottable’s parent is host, append slottable to result. for slottable in self.manually_assigned_nodes.borrow().iter() { if slottable .node() .GetParentNode() .is_some_and(|node| &*node == host.upcast::<Node>()) { result.push(slottable.clone()); } } } // Step 6. Otherwise, for each slottable child slottable of host, in tree order: else { for child in host.upcast::<Node>().children_unrooted(cx) { let is_slottable = matches!( child.type_id(), NodeTypeId::Element(_) | NodeTypeId::CharacterData(CharacterDataTypeId::Text(_)) ); if is_slottable { rooted!(&in(cx) let slottable = Slottable(Dom::from_ref(&*child))); // Step 6.1 Let foundSlot be the result of finding a slot given slottable. let found_slot = slottable.find_a_slot(cx.no_gc(), false); // Step 6.2 If foundSlot is slot, then append slottable to result. if found_slot.is_some_and(|found_slot| &*found_slot == self) { result.push(slottable.clone()); } } } } // Step 7. Return result. } /// <https://dom.spec.whatwg.org/#assign-slotables> pub(crate) fn assign_slottables(&self, cx: &JSContext) { // Step 1. Let slottables be the result of finding slottables for slot. rooted_vec!(let mut slottables); self.find_slottables(cx, &mut slottables); // Step 2. If slottables and slot’s assigned nodes are not identical, // then run signal a slot change for slot. // NOTE: If the slottables *are* identical then the rest of this algorithm // won't have any effect, so we return early. if self.assigned_nodes.borrow().iter().eq(slottables.iter()) { return; } // Clear the style and layout data for all previously assigned slottables as well as // marking them as dirty, as they need a full restyle and layout. let document = self.owner_document(); let had_assigned_nodes = !self.assigned_nodes().is_empty(); for slottable in self.assigned_nodes().iter() { document.remove_style_and_layout_data_from_subtree(cx.no_gc(), slottable.node()); slottable.node().dirty(cx.no_gc(), NodeDamage::Other); } self.signal_a_slot_change(cx); // If we don't disconnect the old slottables from this slot then they'll stay implicitily // connected, which causes problems later on. Only do this for slottables that have not // already been reassigned to a different slot. // // NOTE: This is not written in the spec, which is likely a bug. See: // <https://github.com/whatwg/dom/issues/1352>. for slottable in self.assigned_nodes().iter() { if slottable .assigned_slot() .is_some_and(|assigned_slot| &*assigned_slot == self) { slottable.set_assigned_slot(None); } } // Step 3. Set slot’s assigned nodes to slottables. *self.assigned_nodes.borrow_mut() = slottables.iter().cloned().collect(); // Step 4. For each slottable in slottables, set slottable’s assigned slot to slot. for slottable in slottables.iter() { slottable.set_assigned_slot(Some(self)); } // If the `<slot>` element did not have assigned nodes before, be sure to clear // the style and layout data on the fallback content of the element. if !had_assigned_nodes && !slottables.is_empty() { let node = self.upcast::<Node>(); for child in node.children() { document.remove_style_and_layout_data_from_subtree(cx.no_gc(), &child); } node.dirty(cx.no_gc(), NodeDamage::Other); } // Also clear the style and layout data for all currently assigned slottables // as well as marking them as dirty, as they need a full restyle and layout. for slottable in slottables.iter() { document.remove_style_and_layout_data_from_subtree(cx.no_gc(), slottable.node()); slottable.node().dirty(cx.no_gc(), NodeDamage::Other); } if let Some(selection) = self.owner_document().selection() && let FlatTreeParent::Parent(parent) = self.upcast::<Node>().parent_in_flat_tree(cx.no_gc()) && parent.get_flag(NodeFlags::OVERLAPS_DOCUMENT_SELECTION) { selection.set_visible_selection_dirty(); } } /// <https://dom.spec.whatwg.org/#signal-a-slot-change> pub(crate) fn signal_a_slot_change(&self, cx: &JSContext) { self.upcast::<Node>() .dirty(cx.no_gc(), NodeDamage::ContentOrHeritage); if self.is_in_agents_signal_slots.get() { return; } self.is_in_agents_signal_slots.set(true); let mutation_observers = ScriptThread::mutation_observers(); // Step 1. Append slot to slot’s relevant agent’s signal slots. mutation_observers.add_signal_slot(self); // Step 2. Queue a mutation observer microtask. mutation_observers.queue_mutation_observer_microtask(cx, ScriptThread::microtask_queue()); } pub(crate) fn remove_from_signal_slots(&self) { debug_assert!(self.is_in_agents_signal_slots.get()); self.is_in_agents_signal_slots.set(false); } /// Returns the slot's assigned nodes if the root's slot assignment mode /// is "named", or the manually assigned nodes otherwise pub(crate) fn assigned_nodes(&self) -> Ref<'_, [Slottable]> { Ref::map(self.assigned_nodes.borrow(), Vec::as_slice) } } impl Slottable { /// <https://dom.spec.whatwg.org/#find-a-slot> pub(crate) fn find_a_slot( &self, no_gc: &NoGC, open_flag: bool, ) -> Option<DomRoot<HTMLSlotElement>> { // Step 1. If slottable’s parent is null, then return null. let parent = self.node().get_parent_node_unrooted(no_gc)?; // Step 2. Let shadow be slottable’s parent’s shadow root. // Step 3. If shadow is null, then return null. let shadow_root = parent .downcast::<Element>() .and_then(|element| element.shadow_root_unrooted(no_gc))?; // Step 4. If the open flag is set and shadow’s mode is not "open", then return null. if open_flag && shadow_root.Mode() != ShadowRootMode::Open { return None; } // Step 5. If shadow’s slot assignment is "manual", then return the slot in shadow’s descendants whose // manually assigned nodes contains slottable, if any; otherwise null. if shadow_root.SlotAssignment() == SlotAssignmentMode::Manual { return self.assigned_slot(); } // Step 6. Return the first slot in tree order in shadow’s descendants whose // name is slottable’s name, if any; otherwise null. shadow_root.slot_for_name(&self.name()) } /// <https://dom.spec.whatwg.org/#assign-a-slot> pub(crate) fn assign_a_slot(&self, cx: &JSContext) { // Step 1. Let slot be the result of finding a slot with slottable. let slot = self.find_a_slot(cx.no_gc(), false); // Step 2. If slot is non-null, then run assign slottables for slot. if let Some(slot) = slot { slot.assign_slottables(cx); } } pub(crate) fn node(&self) -> &Node { &self.0 } pub(crate) fn assigned_slot(&self) -> Option<DomRoot<HTMLSlotElement>> { self.node().assigned_slot() } pub(crate) fn set_assigned_slot(&self, assigned_slot: Option<&HTMLSlotElement>) { self.node().set_assigned_slot(assigned_slot); } pub(crate) fn set_manual_slot_assignment( &self, manually_assigned_slot: Option<&HTMLSlotElement>, ) { self.node() .set_manual_slot_assignment(manually_assigned_slot); } pub(crate) fn manual_slot_assignment(&self) -> Option<DomRoot<HTMLSlotElement>> { self.node().manual_slot_assignment() } fn name(&self) -> DOMString { // NOTE: Only elements have non-empty names let Some(element) = self.0.downcast::<Element>() else { return DOMString::new(); }; element.get_string_attribute(&local_name!("slot")) } } impl VirtualMethods for HTMLSlotElement { fn super_type(&self) -> Option<&dyn VirtualMethods> { Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods) } /// <https://dom.spec.whatwg.org/#shadow-tree-slots> fn attribute_mutated( &self, cx: &mut JSContext, attr: AttrRef<'_>, mutation: AttributeMutation, ) { self.super_type() .unwrap() .attribute_mutated(cx, attr, mutation); if attr.local_name() == &local_name!("name") && attr.namespace() == &ns!() { if let Some(shadow_root) = self.containing_shadow_root() { // Shadow roots keep a list of slot descendants, so we need to tell it // about our name change let old_value = match mutation { AttributeMutation::Set(old, _) => old .map(|value| value.to_string().into()) .unwrap_or_default(), AttributeMutation::Removed => attr.value().to_string().into(), }; shadow_root.unregister_slot(old_value, self); shadow_root.register_slot(self); } // Changing the name might cause slot assignments to change self.upcast::<Node>() .GetRootNode(&GetRootNodeOptions::empty()) .assign_slottables_for_a_tree(cx, ForceSlottableNodeReconciliation::Skip); } } fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) { if let Some(s) = self.super_type() { s.bind_to_tree(cx, context); } let was_already_in_shadow_tree = context.is_shadow_tree == IsShadowTree::Yes; if !was_already_in_shadow_tree && let Some(shadow_root) = self.containing_shadow_root() { shadow_root.register_slot(self); } } fn unbind_from_tree(&self, cx: &mut JSContext, context: &UnbindContext) { if let Some(s) = self.super_type() { s.unbind_from_tree(cx, context); } if !self.upcast::<Node>().is_in_a_shadow_tree() && let Some(old_shadow_root) = self.containing_shadow_root() { // If we used to be in a shadow root, but aren't anymore, then unregister this slot old_shadow_root.unregister_slot(self.Name(), self); } } } impl js::gc::Rootable for Slottable {} impl js::gc::Initialize for Slottable { #[expect(unsafe_code)] unsafe fn initial() -> Option<Self> { None } }