Skip to main content

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 js::context::JSContext;
13use script_bindings::cell::{DomRefCell, Ref};
14use style::attr::{AttrIdentifier, AttrValue};
15use style::values::GenericAtomIdent;
16
17use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
18use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrTrustedScriptOrTrustedScriptURLOrString as TrustedTypeOrString;
19use crate::dom::bindings::error::Fallible;
20use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
21use crate::dom::bindings::str::DOMString;
22use crate::dom::document::Document;
23use crate::dom::element::Element;
24use crate::dom::node::{Node, NodeTraits};
25use crate::dom::trustedtypes::trustedtypepolicyfactory::TrustedTypePolicyFactory;
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        cx: &mut js::context::JSContext,
65        document: &Document,
66        local_name: LocalName,
67        value: AttrValue,
68        name: LocalName,
69        namespace: Namespace,
70        prefix: Option<Prefix>,
71        owner: Option<&Element>,
72    ) -> DomRoot<Attr> {
73        Node::reflect_node(
74            cx,
75            Box::new(Attr::new_inherited(
76                document, local_name, value, name, namespace, prefix, owner,
77            )),
78            document,
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, cx: &mut JSContext, value: DOMString) -> 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                cx,
121                owner.namespace(),
122                owner.local_name(),
123                self.local_name(),
124                Some(self.namespace()),
125                TrustedTypeOrString::String(value),
126                &owner.owner_global(),
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(cx, self, value);
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, cx: &mut JSContext, 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_with_namespace(cx, 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
235#[expect(unsafe_code)]
236impl<'dom> LayoutDom<'dom, Attr> {
237    #[inline]
238    pub(crate) fn value(self) -> &'dom AttrValue {
239        unsafe { self.unsafe_get().value.borrow_for_layout() }
240    }
241
242    #[inline]
243    pub(crate) fn local_name(self) -> &'dom LocalName {
244        &self.unsafe_get().identifier.local_name.0
245    }
246
247    #[inline]
248    pub(crate) fn namespace(self) -> &'dom Namespace {
249        &self.unsafe_get().identifier.namespace.0
250    }
251}
252
253/// A helper function to check if attribute is relevant.
254pub(crate) fn is_relevant_attribute(namespace: &Namespace, local_name: &LocalName) -> bool {
255    // <https://svgwg.org/svg2-draft/linking.html#XLinkHrefAttribute>
256    namespace == &ns!() || (namespace == &ns!(xlink) && local_name == &local_name!("href"))
257}
258
259/// A help function to check if an attribute is a boolean attribute.
260pub(crate) fn is_boolean_attribute(name: &str) -> bool {
261    // The full list of attributes can be found in [1]. All attributes marked as "Boolean
262    // attribute" in the "Value" column are boolean attributes. Note that "hidden" is effectively
263    // treated as a boolean attribute, according to WPT test "test_global_boolean_attributes" in
264    // webdriver/tests/classic/get_element_attribute/get.py
265    //
266    // [1] <https://html.spec.whatwg.org/multipage/#attributes-3>
267    static BOOLEAN_ATTRIBUTES: LazyLock<[&str; 30]> = LazyLock::new(|| {
268        [
269            "allowfullscreen",
270            "alpha",
271            "async",
272            "autofocus",
273            "autoplay",
274            "checked",
275            "controls",
276            "default",
277            "defer",
278            "disabled",
279            "formnovalidate",
280            "hidden",
281            "inert",
282            "ismap",
283            "itemscope",
284            "loop",
285            "multiple",
286            "muted",
287            "nomodule",
288            "novalidate",
289            "open",
290            "playsinline",
291            "readonly",
292            "required",
293            "reversed",
294            "selected",
295            "shadowrootclonable",
296            "shadowrootcustomelementregistry",
297            "shadowrootdelegatesfocus",
298            "shadowrootserializable",
299        ]
300    });
301
302    BOOLEAN_ATTRIBUTES
303        .iter()
304        .any(|&boolean_attr| boolean_attr.eq_ignore_ascii_case(name))
305}