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 devtools_traits::AttrInfo;
9use html5ever::{LocalName, Namespace, Prefix, ns};
10use js::context::JSContext;
11use script_bindings::cell::DomRefCell;
12use script_bindings::root::{Dom, DomRoot};
13use script_bindings::str::DOMString;
14use style::attr::{AttrIdentifier, AttrValue};
15use style::values::GenericAtomIdent;
16
17use crate::dom::attr::Attr;
18use crate::dom::bindings::root::{LayoutDom, ToLayout};
19use crate::dom::element::Element;
20use crate::dom::node::node::NodeTraits;
21
22/// The empty namespace, so that [`AttrName::namespace`] can hand out a reference
23/// for unqualified attributes without storing one per attribute.
24static EMPTY_NAMESPACE: Namespace = ns!();
25
26/// The name of a content attribute.
27#[derive(Clone, MallocSizeOf)]
28pub(crate) enum AttrName {
29    /// Common case: `name == local_name`, empty namespace, no prefix.
30    Unqualified(LocalName),
31    /// Namespaced and/or prefixed.
32    Qualified(Box<AttrIdentifier>),
33}
34
35impl AttrName {
36    /// Build a name from the four parts of an [`AttrIdentifier`], picking the
37    /// compact representation when they allow it.
38    pub(crate) fn new(
39        local_name: LocalName,
40        name: LocalName,
41        namespace: Namespace,
42        prefix: Option<Prefix>,
43    ) -> Self {
44        if prefix.is_none() && name == local_name && namespace == ns!() {
45            AttrName::Unqualified(local_name)
46        } else {
47            AttrName::Qualified(Box::new(AttrIdentifier {
48                local_name: GenericAtomIdent(local_name),
49                name: GenericAtomIdent(name),
50                namespace: GenericAtomIdent(namespace),
51                prefix: prefix.map(GenericAtomIdent),
52            }))
53        }
54    }
55
56    #[inline]
57    pub(crate) fn local_name(&self) -> &LocalName {
58        match self {
59            AttrName::Unqualified(local_name) => local_name,
60            AttrName::Qualified(identifier) => &identifier.local_name.0,
61        }
62    }
63
64    #[inline]
65    pub(crate) fn name(&self) -> &LocalName {
66        match self {
67            AttrName::Unqualified(local_name) => local_name,
68            AttrName::Qualified(identifier) => &identifier.name.0,
69        }
70    }
71
72    #[inline]
73    pub(crate) fn namespace(&self) -> &Namespace {
74        match self {
75            AttrName::Unqualified(_) => &EMPTY_NAMESPACE,
76            AttrName::Qualified(identifier) => &identifier.namespace.0,
77        }
78    }
79
80    #[inline]
81    pub(crate) fn prefix(&self) -> Option<&Prefix> {
82        match self {
83            AttrName::Unqualified(_) => None,
84            AttrName::Qualified(identifier) => Some(&identifier.prefix.as_ref()?.0),
85        }
86    }
87
88    /// Decompose into the parts `Attr::new` takes.
89    pub(crate) fn into_parts(self) -> (LocalName, LocalName, Namespace, Option<Prefix>) {
90        match self {
91            AttrName::Unqualified(local_name) => (local_name.clone(), local_name, ns!(), None),
92            AttrName::Qualified(identifier) => {
93                let AttrIdentifier {
94                    local_name,
95                    name,
96                    namespace,
97                    prefix,
98                } = *identifier;
99                (local_name.0, name.0, namespace.0, prefix.map(|p| p.0))
100            },
101        }
102    }
103
104    /// Materialize a stylo [`AttrIdentifier`].
105    pub(crate) fn as_identifier(&self) -> AttrIdentifier {
106        match self {
107            AttrName::Unqualified(local_name) => AttrIdentifier {
108                local_name: GenericAtomIdent(local_name.clone()),
109                name: GenericAtomIdent(local_name.clone()),
110                namespace: GenericAtomIdent(ns!()),
111                prefix: None,
112            },
113            AttrName::Qualified(identifier) => (**identifier).clone(),
114        }
115    }
116}
117
118/// Lightweight attribute storage that avoids allocating a full DOM `Attr` node.
119#[derive(MallocSizeOf)]
120pub(crate) struct ContentAttributeData {
121    pub identifier: AttrName,
122    pub value: AttrValue,
123}
124
125impl ContentAttributeData {
126    #[inline]
127    pub(crate) fn local_name(&self) -> &LocalName {
128        self.identifier.local_name()
129    }
130
131    #[inline]
132    pub(crate) fn name(&self) -> &LocalName {
133        self.identifier.name()
134    }
135
136    #[inline]
137    pub(crate) fn namespace(&self) -> &Namespace {
138        self.identifier.namespace()
139    }
140
141    #[inline]
142    pub(crate) fn prefix(&self) -> Option<&Prefix> {
143        self.identifier.prefix()
144    }
145
146    #[inline]
147    pub(crate) fn value(&self) -> &AttrValue {
148        &self.value
149    }
150}
151
152/// A reference to an attribute value, abstracting over direct and RefCell-borrowed access.
153pub(crate) enum AttrValueRef<'a> {
154    /// Direct reference to a value (from [`ContentAttributeData`]).
155    Direct(&'a AttrValue),
156    /// Borrowed from a [`DomRefCell`] (from [`Attr`]).
157    Borrowed(Ref<'a, AttrValue>),
158}
159
160impl Deref for AttrValueRef<'_> {
161    type Target = AttrValue;
162
163    fn deref(&self) -> &AttrValue {
164        match self {
165            AttrValueRef::Direct(value) => value,
166            AttrValueRef::Borrowed(value) => value,
167        }
168    }
169}
170
171impl AsRef<str> for AttrValueRef<'_> {
172    fn as_ref(&self) -> &str {
173        self.deref()
174    }
175}
176
177/// A reference to attribute data, either from a lightweight [`ContentAttributeData`]
178/// or from a full [`Attr`] DOM node. Provides the same accessor interface regardless
179/// of storage form.
180#[derive(Clone, Copy)]
181pub(crate) enum AttrRef<'a> {
182    /// Lightweight data (no DOM node allocated).
183    Raw(&'a ContentAttributeData),
184    /// Full Attr DOM node.
185    Dom(&'a Attr),
186}
187
188impl<'a> AttrRef<'a> {
189    #[inline]
190    pub(crate) fn local_name(&self) -> &'a LocalName {
191        match self {
192            AttrRef::Raw(data) => data.local_name(),
193            AttrRef::Dom(attr) => attr.local_name(),
194        }
195    }
196
197    #[inline]
198    pub(crate) fn name(&self) -> &'a LocalName {
199        match self {
200            AttrRef::Raw(data) => data.name(),
201            AttrRef::Dom(attr) => attr.name(),
202        }
203    }
204
205    #[inline]
206    pub(crate) fn namespace(&self) -> &'a Namespace {
207        match self {
208            AttrRef::Raw(data) => data.namespace(),
209            AttrRef::Dom(attr) => attr.namespace(),
210        }
211    }
212
213    #[inline]
214    pub(crate) fn prefix(&self) -> Option<&'a Prefix> {
215        match self {
216            AttrRef::Raw(data) => data.prefix(),
217            AttrRef::Dom(attr) => attr.prefix(),
218        }
219    }
220
221    #[inline]
222    pub(crate) fn value(&self) -> AttrValueRef<'a> {
223        match self {
224            AttrRef::Raw(data) => AttrValueRef::Direct(data.value()),
225            AttrRef::Dom(attr) => AttrValueRef::Borrowed(attr.value()),
226        }
227    }
228
229    /// Returns the underlying `&Attr` if this is a `Dom` reference.
230    /// Returns `None` for `Raw` data (no DOM node exists).
231    #[inline]
232    pub(crate) fn as_attr(&self) -> Option<&'a Attr> {
233        match self {
234            AttrRef::Dom(attr) => Some(attr),
235            AttrRef::Raw(_) => None,
236        }
237    }
238
239    /// Returns the attribute value as a `DOMString`, equivalent to `Attr::Value()`.
240    pub(crate) fn to_dom_string(self) -> DOMString {
241        DOMString::from(&**self.value())
242    }
243
244    /// Returns a summary for devtools.
245    pub(crate) fn summarize(&self) -> AttrInfo {
246        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}