script/dom/
attr.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::borrow::ToOwned;
6use std::mem;
7use std::sync::LazyLock;
8
9use devtools_traits::AttrInfo;
10use dom_struct::dom_struct;
11use html5ever::{LocalName, Namespace, Prefix, local_name, ns};
12use style::attr::{AttrIdentifier, AttrValue};
13use style::values::GenericAtomIdent;
14
15use crate::dom::bindings::cell::{DomRefCell, Ref};
16use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
17use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrTrustedScriptOrTrustedScriptURLOrString as TrustedTypeOrString;
18use crate::dom::bindings::error::Fallible;
19use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
20use crate::dom::bindings::str::DOMString;
21use crate::dom::document::Document;
22use crate::dom::element::Element;
23use crate::dom::node::{Node, NodeTraits};
24use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory;
25use crate::script_runtime::CanGc;
26
27// https://dom.spec.whatwg.org/#interface-attr
28#[dom_struct]
29pub(crate) struct Attr {
30    node_: Node,
31    #[no_trace]
32    identifier: AttrIdentifier,
33    #[no_trace]
34    value: DomRefCell<AttrValue>,
35
36    /// the element that owns this attribute.
37    owner: MutNullableDom<Element>,
38}
39
40impl Attr {
41    fn new_inherited(
42        document: &Document,
43        local_name: LocalName,
44        value: AttrValue,
45        name: LocalName,
46        namespace: Namespace,
47        prefix: Option<Prefix>,
48        owner: Option<&Element>,
49    ) -> Attr {
50        Attr {
51            node_: Node::new_inherited(document),
52            identifier: AttrIdentifier {
53                local_name: GenericAtomIdent(local_name),
54                name: GenericAtomIdent(name),
55                namespace: GenericAtomIdent(namespace),
56                prefix: prefix.map(GenericAtomIdent),
57            },
58            value: DomRefCell::new(value),
59            owner: MutNullableDom::new(owner),
60        }
61    }
62    #[expect(clippy::too_many_arguments)]
63    pub(crate) fn new(
64        document: &Document,
65        local_name: LocalName,
66        value: AttrValue,
67        name: LocalName,
68        namespace: Namespace,
69        prefix: Option<Prefix>,
70        owner: Option<&Element>,
71        can_gc: CanGc,
72    ) -> DomRoot<Attr> {
73        Node::reflect_node(
74            Box::new(Attr::new_inherited(
75                document, local_name, value, name, namespace, prefix, owner,
76            )),
77            document,
78            can_gc,
79        )
80    }
81
82    #[inline]
83    pub(crate) fn name(&self) -> &LocalName {
84        &self.identifier.name.0
85    }
86
87    #[inline]
88    pub(crate) fn namespace(&self) -> &Namespace {
89        &self.identifier.namespace.0
90    }
91
92    #[inline]
93    pub(crate) fn prefix(&self) -> Option<&Prefix> {
94        Some(&self.identifier.prefix.as_ref()?.0)
95    }
96}
97
98impl AttrMethods<crate::DomTypeHolder> for Attr {
99    /// <https://dom.spec.whatwg.org/#dom-attr-localname>
100    fn LocalName(&self) -> DOMString {
101        // FIXME(ajeffrey): convert directly from LocalName to DOMString
102        DOMString::from(&**self.local_name())
103    }
104
105    /// <https://dom.spec.whatwg.org/#dom-attr-value>
106    fn Value(&self) -> DOMString {
107        // FIXME(ajeffrey): convert directly from AttrValue to DOMString
108        DOMString::from(&**self.value())
109    }
110
111    /// <https://dom.spec.whatwg.org/#set-an-existing-attribute-value>
112    fn SetValue(&self, value: DOMString, can_gc: CanGc) -> Fallible<()> {
113        // Step 2. Otherwise:
114        if let Some(owner) = self.owner() {
115            // Step 2.1. Let element be attribute’s element.
116            // Step 2.2. Let verifiedValue be the result of calling
117            // get trusted type compliant attribute value with attribute’s local name,
118            // attribute’s namespace, element, and value. [TRUSTED-TYPES]
119            let value = TrustedTypePolicyFactory::get_trusted_types_compliant_attribute_value(
120                owner.namespace(),
121                owner.local_name(),
122                self.local_name(),
123                Some(self.namespace()),
124                TrustedTypeOrString::String(value),
125                &owner.owner_global(),
126                can_gc,
127            )?;
128            if let Some(owner) = self.owner() {
129                // Step 2.4. Change attribute to verifiedValue.
130                let value = owner.parse_attribute(self.namespace(), self.local_name(), value);
131                owner.change_attribute(self, value, can_gc);
132            } else {
133                // Step 2.3. If attribute’s element is null, then set attribute’s value to verifiedValue, and return.
134                self.set_value(value);
135            }
136        } else {
137            // Step 1. If attribute’s element is null, then set attribute’s value to value.
138            self.set_value(value);
139        }
140        Ok(())
141    }
142
143    /// <https://dom.spec.whatwg.org/#dom-attr-name>
144    fn Name(&self) -> DOMString {
145        // FIXME(ajeffrey): convert directly from LocalName to DOMString
146        DOMString::from(&**self.name())
147    }
148
149    /// <https://dom.spec.whatwg.org/#dom-attr-namespaceuri>
150    fn GetNamespaceURI(&self) -> Option<DOMString> {
151        match *self.namespace() {
152            ns!() => None,
153            ref url => Some(DOMString::from(&**url)),
154        }
155    }
156
157    /// <https://dom.spec.whatwg.org/#dom-attr-prefix>
158    fn GetPrefix(&self) -> Option<DOMString> {
159        // FIXME(ajeffrey): convert directly from LocalName to DOMString
160        self.prefix().map(|p| DOMString::from(&**p))
161    }
162
163    /// <https://dom.spec.whatwg.org/#dom-attr-ownerelement>
164    fn GetOwnerElement(&self) -> Option<DomRoot<Element>> {
165        self.owner()
166    }
167
168    /// <https://dom.spec.whatwg.org/#dom-attr-specified>
169    fn Specified(&self) -> bool {
170        true // Always returns true
171    }
172}
173
174impl Attr {
175    /// Used to swap the attribute's value without triggering mutation events
176    pub(crate) fn swap_value(&self, value: &mut AttrValue) {
177        mem::swap(&mut *self.value.borrow_mut(), value);
178    }
179
180    pub(crate) fn identifier(&self) -> &AttrIdentifier {
181        &self.identifier
182    }
183
184    pub(crate) fn value(&self) -> Ref<'_, AttrValue> {
185        self.value.borrow()
186    }
187
188    fn set_value(&self, value: DOMString) {
189        *self.value.borrow_mut() = AttrValue::String(value.into());
190    }
191
192    pub(crate) fn local_name(&self) -> &LocalName {
193        &self.identifier.local_name
194    }
195
196    /// Sets the owner element. Should be called after the attribute is added
197    /// or removed from its older parent.
198    pub(crate) fn set_owner(&self, owner: Option<&Element>) {
199        let ns = self.namespace();
200        match (self.owner(), owner) {
201            (Some(old), None) => {
202                // Already gone from the list of attributes of old owner.
203                assert!(
204                    old.get_attribute(ns, &self.identifier.local_name)
205                        .as_deref() !=
206                        Some(self)
207                )
208            },
209            (Some(old), Some(new)) => assert_eq!(&*old, new),
210            _ => {},
211        }
212        self.owner.set(owner);
213    }
214
215    pub(crate) fn owner(&self) -> Option<DomRoot<Element>> {
216        self.owner.get()
217    }
218
219    pub(crate) fn summarize(&self) -> AttrInfo {
220        AttrInfo {
221            namespace: (**self.namespace()).to_owned(),
222            name: (**self.name()).to_owned(),
223            value: (**self.value()).to_owned(),
224        }
225    }
226
227    pub(crate) fn qualified_name(&self) -> DOMString {
228        match self.prefix() {
229            Some(ref prefix) => DOMString::from(format!("{}:{}", prefix, &**self.local_name())),
230            None => DOMString::from(&**self.local_name()),
231        }
232    }
233}
234
235pub(crate) trait AttrHelpersForLayout<'dom> {
236    fn value(self) -> &'dom AttrValue;
237    fn local_name(self) -> &'dom LocalName;
238    fn namespace(self) -> &'dom Namespace;
239}
240
241#[expect(unsafe_code)]
242impl<'dom> AttrHelpersForLayout<'dom> for LayoutDom<'dom, Attr> {
243    #[inline]
244    fn value(self) -> &'dom AttrValue {
245        unsafe { self.unsafe_get().value.borrow_for_layout() }
246    }
247
248    #[inline]
249    fn local_name(self) -> &'dom LocalName {
250        &self.unsafe_get().identifier.local_name.0
251    }
252
253    #[inline]
254    fn namespace(self) -> &'dom Namespace {
255        &self.unsafe_get().identifier.namespace.0
256    }
257}
258
259/// A helper function to check if attribute is relevant.
260pub(crate) fn is_relevant_attribute(namespace: &Namespace, local_name: &LocalName) -> bool {
261    // <https://svgwg.org/svg2-draft/linking.html#XLinkHrefAttribute>
262    namespace == &ns!() || (namespace == &ns!(xlink) && local_name == &local_name!("href"))
263}
264
265/// A help function to check if an attribute is a boolean attribute.
266pub(crate) fn is_boolean_attribute(name: &str) -> bool {
267    // The full list of attributes can be found in [1]. All attributes marked as "Boolean
268    // attribute" in the "Value" column are boolean attributes. Note that "hidden" is effectively
269    // treated as a boolean attribute, according to WPT test "test_global_boolean_attributes" in
270    // webdriver/tests/classic/get_element_attribute/get.py
271    //
272    // [1] <https://html.spec.whatwg.org/multipage/#attributes-3>
273    static BOOLEAN_ATTRIBUTES: LazyLock<[&str; 30]> = LazyLock::new(|| {
274        [
275            "allowfullscreen",
276            "alpha",
277            "async",
278            "autofocus",
279            "autoplay",
280            "checked",
281            "controls",
282            "default",
283            "defer",
284            "disabled",
285            "formnovalidate",
286            "hidden",
287            "inert",
288            "ismap",
289            "itemscope",
290            "loop",
291            "multiple",
292            "muted",
293            "nomodule",
294            "novalidate",
295            "open",
296            "playsinline",
297            "readonly",
298            "required",
299            "reversed",
300            "selected",
301            "shadowrootclonable",
302            "shadowrootcustomelementregistry",
303            "shadowrootdelegatesfocus",
304            "shadowrootserializable",
305        ]
306    });
307
308    BOOLEAN_ATTRIBUTES
309        .iter()
310        .any(|&boolean_attr| boolean_attr.eq_ignore_ascii_case(name))
311}