Skip to main content

script/dom/element/attributes/
storage.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Ref;
6use std::ops::Deref;
7
8use html5ever::{LocalName, Namespace, Prefix, ns};
9use js::context::JSContext;
10use script_bindings::cell::DomRefCell;
11use script_bindings::root::{Dom, DomRoot};
12use script_bindings::str::DOMString;
13use style::attr::{AttrIdentifier, AttrValue};
14use style::values::GenericAtomIdent;
15
16use crate::dom::attr::Attr;
17use crate::dom::bindings::root::{LayoutDom, ToLayout};
18use crate::dom::element::Element;
19use crate::dom::node::node::NodeTraits;
20
21/// The empty namespace, so that [`AttrName::namespace`] can hand out a reference
22/// for unqualified attributes without storing one per attribute.
23static EMPTY_NAMESPACE: Namespace = ns!();
24
25/// The name of a content attribute.
26#[derive(Clone, MallocSizeOf)]
27pub(crate) enum AttrName {
28    /// Common case: `name == local_name`, empty namespace, no prefix.
29    Unqualified(LocalName),
30    /// Namespaced and/or prefixed.
31    Qualified(Box<AttrIdentifier>),
32}
33
34impl AttrName {
35    /// Build a name from the four parts of an [`AttrIdentifier`], picking the
36    /// compact representation when they allow it.
37    pub(crate) fn new(
38        local_name: LocalName,
39        name: LocalName,
40        namespace: Namespace,
41        prefix: Option<Prefix>,
42    ) -> Self {
43        if prefix.is_none() && name == local_name && namespace == ns!() {
44            AttrName::Unqualified(local_name)
45        } else {
46            AttrName::Qualified(Box::new(AttrIdentifier {
47                local_name: GenericAtomIdent(local_name),
48                name: GenericAtomIdent(name),
49                namespace: GenericAtomIdent(namespace),
50                prefix: prefix.map(GenericAtomIdent),
51            }))
52        }
53    }
54
55    #[inline]
56    pub(crate) fn local_name(&self) -> &LocalName {
57        match self {
58            AttrName::Unqualified(local_name) => local_name,
59            AttrName::Qualified(identifier) => &identifier.local_name.0,
60        }
61    }
62
63    #[inline]
64    pub(crate) fn name(&self) -> &LocalName {
65        match self {
66            AttrName::Unqualified(local_name) => local_name,
67            AttrName::Qualified(identifier) => &identifier.name.0,
68        }
69    }
70
71    #[inline]
72    pub(crate) fn namespace(&self) -> &Namespace {
73        match self {
74            AttrName::Unqualified(_) => &EMPTY_NAMESPACE,
75            AttrName::Qualified(identifier) => &identifier.namespace.0,
76        }
77    }
78
79    #[inline]
80    pub(crate) fn prefix(&self) -> Option<&Prefix> {
81        match self {
82            AttrName::Unqualified(_) => None,
83            AttrName::Qualified(identifier) => Some(&identifier.prefix.as_ref()?.0),
84        }
85    }
86
87    /// Decompose into the parts `Attr::new` takes.
88    pub(crate) fn into_parts(self) -> (LocalName, LocalName, Namespace, Option<Prefix>) {
89        match self {
90            AttrName::Unqualified(local_name) => (local_name.clone(), local_name, ns!(), None),
91            AttrName::Qualified(identifier) => {
92                let AttrIdentifier {
93                    local_name,
94                    name,
95                    namespace,
96                    prefix,
97                } = *identifier;
98                (local_name.0, name.0, namespace.0, prefix.map(|p| p.0))
99            },
100        }
101    }
102
103    /// Materialize a stylo [`AttrIdentifier`].
104    pub(crate) fn as_identifier(&self) -> AttrIdentifier {
105        match self {
106            AttrName::Unqualified(local_name) => AttrIdentifier {
107                local_name: GenericAtomIdent(local_name.clone()),
108                name: GenericAtomIdent(local_name.clone()),
109                namespace: GenericAtomIdent(ns!()),
110                prefix: None,
111            },
112            AttrName::Qualified(identifier) => (**identifier).clone(),
113        }
114    }
115}
116
117/// Lightweight attribute storage that avoids allocating a full DOM `Attr` node.
118#[derive(MallocSizeOf)]
119pub(crate) struct ContentAttributeData {
120    pub identifier: AttrName,
121    pub value: AttrValue,
122}
123
124impl ContentAttributeData {
125    #[inline]
126    pub(crate) fn local_name(&self) -> &LocalName {
127        self.identifier.local_name()
128    }
129
130    #[inline]
131    pub(crate) fn name(&self) -> &LocalName {
132        self.identifier.name()
133    }
134
135    #[inline]
136    pub(crate) fn namespace(&self) -> &Namespace {
137        self.identifier.namespace()
138    }
139
140    #[inline]
141    pub(crate) fn prefix(&self) -> Option<&Prefix> {
142        self.identifier.prefix()
143    }
144
145    #[inline]
146    pub(crate) fn value(&self) -> &AttrValue {
147        &self.value
148    }
149}
150
151/// A reference to an attribute value, abstracting over direct and RefCell-borrowed access.
152pub(crate) enum AttrValueRef<'a> {
153    /// Direct reference to a value (from [`ContentAttributeData`]).
154    Direct(&'a AttrValue),
155    /// Borrowed from a [`DomRefCell`] (from [`Attr`]).
156    Borrowed(Ref<'a, AttrValue>),
157}
158
159impl Deref for AttrValueRef<'_> {
160    type Target = AttrValue;
161
162    fn deref(&self) -> &AttrValue {
163        match self {
164            AttrValueRef::Direct(value) => value,
165            AttrValueRef::Borrowed(value) => value,
166        }
167    }
168}
169
170impl AsRef<str> for AttrValueRef<'_> {
171    fn as_ref(&self) -> &str {
172        self.deref()
173    }
174}
175
176/// A reference to attribute data, either from a lightweight [`ContentAttributeData`]
177/// or from a full [`Attr`] DOM node. Provides the same accessor interface regardless
178/// of storage form.
179#[derive(Clone, Copy)]
180pub(crate) enum AttrRef<'a> {
181    /// Lightweight data (no DOM node allocated).
182    Raw(&'a ContentAttributeData),
183    /// Full Attr DOM node.
184    Dom(&'a Attr),
185}
186
187impl<'a> AttrRef<'a> {
188    #[inline]
189    pub(crate) fn local_name(&self) -> &'a LocalName {
190        match self {
191            AttrRef::Raw(data) => data.local_name(),
192            AttrRef::Dom(attr) => attr.local_name(),
193        }
194    }
195
196    #[inline]
197    pub(crate) fn name(&self) -> &'a LocalName {
198        match self {
199            AttrRef::Raw(data) => data.name(),
200            AttrRef::Dom(attr) => attr.name(),
201        }
202    }
203
204    #[inline]
205    pub(crate) fn namespace(&self) -> &'a Namespace {
206        match self {
207            AttrRef::Raw(data) => data.namespace(),
208            AttrRef::Dom(attr) => attr.namespace(),
209        }
210    }
211
212    #[inline]
213    pub(crate) fn prefix(&self) -> Option<&'a Prefix> {
214        match self {
215            AttrRef::Raw(data) => data.prefix(),
216            AttrRef::Dom(attr) => attr.prefix(),
217        }
218    }
219
220    #[inline]
221    pub(crate) fn value(&self) -> AttrValueRef<'a> {
222        match self {
223            AttrRef::Raw(data) => AttrValueRef::Direct(data.value()),
224            AttrRef::Dom(attr) => AttrValueRef::Borrowed(attr.value()),
225        }
226    }
227
228    /// Returns the underlying `&Attr` if this is a `Dom` reference.
229    /// Returns `None` for `Raw` data (no DOM node exists).
230    #[inline]
231    pub(crate) fn as_attr(&self) -> Option<&'a Attr> {
232        match self {
233            AttrRef::Dom(attr) => Some(attr),
234            AttrRef::Raw(_) => None,
235        }
236    }
237
238    /// Returns the attribute value as a `DOMString`, equivalent to `Attr::Value()`.
239    pub(crate) fn to_dom_string(self) -> DOMString {
240        DOMString::from(&**self.value())
241    }
242
243    /// Returns a summary for devtools.
244    #[cfg(feature = "devtools")]
245    pub(crate) fn summarize(&self) -> devtools_traits::AttrInfo {
246        devtools_traits::AttrInfo {
247            namespace: (**self.namespace()).to_owned(),
248            name: (**self.name()).to_owned(),
249            value: (**self.value()).to_owned(),
250        }
251    }
252
253    /// Materializes the stylo `AttrIdentifier` for this attribute.
254    pub(crate) fn as_identifier(&self) -> AttrIdentifier {
255        match self {
256            AttrRef::Raw(data) => data.identifier.as_identifier(),
257            AttrRef::Dom(attr) => attr.identifier().clone(),
258        }
259    }
260}
261
262/// A single attribute entry, either lightweight raw data or a full DOM Attr node.
263#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
264#[derive(MallocSizeOf)]
265pub(crate) enum AttributeEntry {
266    /// Lightweight data — no Attr DOM node allocated.
267    Raw(ContentAttributeData),
268    /// Full Attr DOM node.
269    Dom(Dom<Attr>),
270}
271
272impl AttributeEntry {
273    /// Get an `AttrRef` for this entry.
274    #[inline]
275    pub(crate) fn as_ref(&self) -> AttrRef<'_> {
276        match self {
277            AttributeEntry::Raw(data) => AttrRef::Raw(data),
278            AttributeEntry::Dom(attr) => AttrRef::Dom(attr),
279        }
280    }
281
282    /// Get the value of this attribute for layout.
283    #[expect(unsafe_code)]
284    #[inline]
285    pub(crate) fn value_for_layout(&self) -> &AttrValue {
286        match self {
287            AttributeEntry::Raw(data) => &data.value,
288            AttributeEntry::Dom(attr) => unsafe {
289                let value: LayoutDom<'_, _> = attr.to_layout();
290                value
291            }
292            .value(),
293        }
294    }
295
296    /// Get the local name of this attribute for layout.
297    #[expect(unsafe_code)]
298    #[inline]
299    pub(crate) fn local_name_for_layout(&self) -> &LocalName {
300        match self {
301            AttributeEntry::Raw(data) => data.local_name(),
302            AttributeEntry::Dom(attr) => unsafe {
303                let value: LayoutDom<'_, _> = attr.to_layout();
304                value
305            }
306            .local_name(),
307        }
308    }
309
310    /// Get the namespace of this attribute for layout.
311    #[expect(unsafe_code)]
312    #[inline]
313    pub(crate) fn namespace_for_layout(&self) -> &Namespace {
314        match self {
315            AttributeEntry::Raw(data) => data.namespace(),
316            AttributeEntry::Dom(attr) => unsafe {
317                let value: LayoutDom<'_, _> = attr.to_layout();
318                value
319            }
320            .namespace(),
321        }
322    }
323}
324
325/// Storage for an element's attributes. Contains an internal `DomRefCell` so that
326/// `ensure_dom` can split its borrow around `Attr::new()` allocation, preventing
327/// GC-during-allocation panics from double-borrowing.
328#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
329#[derive(Default, MallocSizeOf)]
330pub(crate) struct AttributeStorage(DomRefCell<Vec<AttributeEntry>>);
331
332/// A borrowed view of attribute storage that provides convenient access
333/// to attributes as `AttrRef` items.
334pub(crate) struct AttributesBorrow<'a>(Ref<'a, Vec<AttributeEntry>>);
335
336impl<'a> AttributesBorrow<'a> {
337    /// Iterate over attributes as `AttrRef`.
338    #[inline]
339    pub(crate) fn iter(&self) -> impl Iterator<Item = AttrRef<'_>> + '_ {
340        self.0.iter().map(AttributeEntry::as_ref)
341    }
342
343    #[inline]
344    pub(crate) fn len(&self) -> usize {
345        self.0.len()
346    }
347
348    #[inline]
349    pub(crate) fn is_empty(&self) -> bool {
350        self.0.is_empty()
351    }
352
353    #[inline]
354    pub(crate) fn first(&self) -> Option<AttrRef<'_>> {
355        self.0.first().map(AttributeEntry::as_ref)
356    }
357
358    #[inline]
359    pub(crate) fn get(&self, index: usize) -> Option<AttrRef<'_>> {
360        self.0.get(index).map(AttributeEntry::as_ref)
361    }
362}
363
364impl AttributeStorage {
365    /// Borrow the attributes for read access with convenient `AttrRef` iteration.
366    #[inline]
367    pub(crate) fn borrow(&self) -> AttributesBorrow<'_> {
368        AttributesBorrow(self.0.borrow())
369    }
370
371    /// Borrow the underlying entries for layout access (unsafe).
372    #[expect(unsafe_code)]
373    #[inline]
374    pub(crate) unsafe fn borrow_for_layout(&self) -> &Vec<AttributeEntry> {
375        unsafe { self.0.borrow_for_layout() }
376    }
377
378    /// Reserve room for at least `additional` more attributes.
379    #[inline]
380    pub(crate) fn reserve_exact(&self, additional: usize) {
381        self.0.borrow_mut().reserve_exact(additional);
382    }
383
384    /// Push raw attribute data.
385    pub(crate) fn push_raw(&self, data: ContentAttributeData) {
386        self.0.borrow_mut().push(AttributeEntry::Raw(data));
387    }
388
389    /// Push a Dom Attr node.
390    pub(crate) fn push_dom(&self, attr: &Attr) {
391        self.0
392            .borrow_mut()
393            .push(AttributeEntry::Dom(Dom::from_ref(attr)));
394    }
395
396    /// Remove an attribute by index, returning the entry.
397    pub(crate) fn remove(&self, index: usize) -> AttributeEntry {
398        self.0.borrow_mut().remove(index)
399    }
400
401    /// Set an attribute entry by index.
402    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
403    pub(crate) fn set(&self, index: usize, entry: AttributeEntry) {
404        self.0.borrow_mut()[index] = entry;
405    }
406
407    /// Ensure entry at index is a Dom Attr node, materializing if needed.
408    /// Returns a `DomRoot<Attr>`.
409    ///
410    /// This method carefully splits the mutable borrow around the `Attr::new()`
411    /// allocation so that GC tracing can safely borrow the storage.
412    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
413    pub(crate) fn ensure_dom(
414        &self,
415        cx: &mut JSContext,
416        index: usize,
417        element: &Element,
418    ) -> DomRoot<Attr> {
419        // Fast path: already materialized.
420        if let AttributeEntry::Dom(attr) = &self.0.borrow()[index] {
421            return DomRoot::from_ref(&**attr);
422        }
423
424        // Extract the raw data, dropping the mutable borrow before allocating.
425        let data = {
426            let mut entries = self.0.borrow_mut();
427            let placeholder = AttributeEntry::Raw(ContentAttributeData {
428                identifier: AttrName::Unqualified(html5ever::local_name!("")),
429                value: AttrValue::String(String::new()),
430            });
431            let old = std::mem::replace(&mut entries[index], placeholder);
432            match old {
433                AttributeEntry::Raw(data) => data,
434                _ => unreachable!(),
435            }
436        };
437
438        let doc = element.owner_document();
439        let (local_name, name, namespace, prefix) = data.identifier.into_parts();
440        let attr = Attr::new(
441            cx,
442            &doc,
443            local_name,
444            data.value,
445            name,
446            namespace,
447            prefix,
448            Some(element),
449        );
450
451        self.0.borrow_mut()[index] = AttributeEntry::Dom(Dom::from_ref(&*attr));
452        attr
453    }
454}
455
456// Safety: Only Dom entries contain GC-traced Dom<Attr> pointers.
457// Raw entries have no pointers to trace.
458#[expect(unsafe_code)]
459unsafe impl crate::dom::bindings::trace::JSTraceable for AttributeStorage {
460    unsafe fn trace(&self, trc: *mut js::jsapi::JSTracer) {
461        for entry in self.0.borrow().iter() {
462            if let AttributeEntry::Dom(attr) = entry {
463                unsafe { js::rust::Trace::trace(attr, trc) };
464            }
465        }
466    }
467}