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