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