Skip to main content

script/dom/element/attributes/
accessors.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::sync::LazyLock;
6
7use html5ever::{LocalName, Namespace, local_name, ns};
8use js::context::JSContext;
9use servo_arc::Arc as ServoArc;
10use style::attr::AttrValue;
11use stylo_atoms::Atom;
12
13use crate::dom::bindings::codegen::UnionTypes::{TrustedHTMLOrString, TrustedScriptURLOrUSVString};
14use crate::dom::bindings::str::{DOMString, USVString};
15use crate::dom::element::Element;
16use crate::dom::element::attributes::storage::AttrRef;
17use crate::dom::element::storage::{AttrName, AttributesBorrow, ContentAttributeData};
18use crate::dom::node::NodeTraits;
19
20static EMPTY_CONTENTATTRIBUTE_DATA: LazyLock<ContentAttributeData> =
21    LazyLock::new(|| ContentAttributeData {
22        identifier: AttrName::new(local_name!(""), local_name!(""), ns!(), None),
23        value: AttrValue::Atom(atom!("")),
24    });
25
26pub(crate) struct AttrStrRefInner<'a> {
27    attributes_borrow: AttributesBorrow<'a>,
28    position: usize,
29}
30
31/// A reference to an attribute value as `&str`. Keeps the borrow alive.
32// We need to support `get_attribute_string_ref().unwrap_or_default()` and return an empty `&str` for this.
33// To support this, we always return `Some(AttrStrRef(Some(AttrStrRefInner)))` if we find an attribute and `None` otherwise.
34// We only create a `AttrStrRef(None)` with the default constructor. For `as_attr_ref`, we can then return a special empty `ContentAttribute`
35// packages in a `AttrRef`.
36#[derive(Default)]
37pub(crate) struct AttrStrRef<'a>(Option<AttrStrRefInner<'a>>);
38
39impl<'a> AttrStrRef<'a> {
40    /// Create a new [`AttrStrRef`] from localname. Returns None if the attribute was not found.
41    pub(in crate::dom::element) fn maybe_new(
42        attrs: AttributesBorrow<'a>,
43        namespace: &Namespace,
44        local_name: &LocalName,
45    ) -> Option<AttrStrRef<'a>> {
46        let position = attrs.iter().position(|attribute| {
47            attribute.local_name() == local_name && attribute.namespace() == namespace
48        });
49        position.map(|position| {
50            AttrStrRef(Some(AttrStrRefInner {
51                attributes_borrow: attrs,
52                position,
53            }))
54        })
55    }
56
57    /// Return the `AttrRef` from the `AttrStrRef`.
58    pub(crate) fn as_attr_ref<'b>(&'b self) -> AttrRef<'b> {
59        if let Some(inner) = &self.0 {
60            inner.attributes_borrow.get(inner.position).unwrap()
61        } else {
62            AttrRef::Raw(&EMPTY_CONTENTATTRIBUTE_DATA)
63        }
64    }
65
66    pub(crate) fn is_empty(&self) -> bool {
67        if let Some(inner) = self.0.as_ref() {
68            inner
69                .attributes_borrow
70                .get(inner.position)
71                .unwrap()
72                .value()
73                .is_empty()
74        } else {
75            true
76        }
77    }
78
79    pub(crate) fn eq_ignore_ascii_case(&self, other: &str) -> bool {
80        if let Some(inner) = self.0.as_ref() {
81            let attr_ref = inner.attributes_borrow.get(inner.position).unwrap();
82            let value = attr_ref.value();
83            value.eq_ignore_ascii_case(other)
84        } else {
85            other.is_empty()
86        }
87    }
88}
89
90impl Element {
91    /// Callers should convert the `LocalName` to ASCII lowercase before calling.
92    /// Consider using `get_attribute_string_ref`.
93    /// <https://dom.spec.whatwg.org/#concept-element-attributes-get-by-name>
94    pub(crate) fn get_attribute_string_value(&self, local_name: &LocalName) -> Option<String> {
95        // Step 1. If element is in the HTML namespace and its node document is an HTML document,
96        // then set qualifiedName to qualifiedName in ASCII lowercase.
97        debug_assert_eq!(
98            *local_name,
99            local_name.to_ascii_lowercase(),
100            "All namespace-less attribute accesses should use a lowercase ASCII name"
101        );
102
103        self.get_attribute_string_value_with_namespace(&ns!(), local_name)
104    }
105
106    /// This returns an attribute reference that can be seen as a `&str`. This keeps the borrow on attributes alive.
107    /// Callers should convert the `LocalName` to ASCII lowercase before calling.
108    pub(crate) fn get_attribute_string_ref<'a>(
109        &'a self,
110        local_name: &LocalName,
111    ) -> Option<AttrStrRef<'a>> {
112        debug_assert_eq!(
113            *local_name,
114            local_name.to_ascii_lowercase(),
115            "All namespace-less attribute accesses should use a lowercase ASCII name"
116        );
117        self.attribute_str_ref(&ns!(), local_name)
118    }
119
120    pub(crate) fn get_attribute_string_value_with_namespace(
121        &self,
122        namespace: &Namespace,
123        local_name: &LocalName,
124    ) -> Option<String> {
125        self.with_attribute(namespace, local_name, |attribute| {
126            String::from(&**attribute.value())
127        })
128    }
129
130    pub(crate) fn get_int_attribute(&self, local_name: &LocalName, default: i32) -> i32 {
131        self.with_attribute(&ns!(), local_name, |attribute| {
132            if let AttrValue::Int(_, value) = *attribute.value() {
133                value
134            } else {
135                unreachable!("Expected an AttrValue::Int: implement parse_plain_attribute")
136            }
137        })
138        .unwrap_or(default)
139    }
140
141    pub(crate) fn set_atomic_attribute(
142        &self,
143        cx: &mut JSContext,
144        local_name: &LocalName,
145        value: DOMString,
146    ) {
147        self.set_attribute(cx, local_name, AttrValue::from_atomic(value.into()));
148    }
149
150    pub(crate) fn set_bool_attribute(
151        &self,
152        cx: &mut JSContext,
153        local_name: &LocalName,
154        value: bool,
155    ) {
156        if self.has_attribute(local_name) == value {
157            return;
158        }
159        if value {
160            self.set_string_attribute(cx, local_name, DOMString::new());
161        } else {
162            self.remove_attribute(cx, &ns!(), local_name);
163        }
164    }
165
166    pub(crate) fn get_url_attribute(&self, local_name: &LocalName) -> USVString {
167        let Some(value) = self.get_attribute_string_ref(local_name) else {
168            return Default::default();
169        };
170        self.owner_document()
171            .encoding_parse_a_url(&value.as_attr_ref().value())
172            .map(|parsed| USVString(parsed.into_string()))
173            .unwrap_or_else(|_| USVString(value.as_attr_ref().value().to_string()))
174    }
175
176    pub(crate) fn set_url_attribute(
177        &self,
178        cx: &mut JSContext,
179        local_name: &LocalName,
180        value: USVString,
181    ) {
182        self.set_attribute(cx, local_name, AttrValue::String(value.into()));
183    }
184
185    pub(crate) fn get_trusted_type_url_attribute(
186        &self,
187        local_name: &LocalName,
188    ) -> TrustedScriptURLOrUSVString {
189        let Some(value) = self.get_attribute_string_ref(local_name) else {
190            return TrustedScriptURLOrUSVString::USVString(USVString::default());
191        };
192        let value_ref = value.as_attr_ref();
193        self.owner_document()
194            .encoding_parse_a_url(&value_ref.value())
195            .map(|parsed| TrustedScriptURLOrUSVString::USVString(USVString(parsed.into_string())))
196            .unwrap_or_else(|_| {
197                TrustedScriptURLOrUSVString::USVString(USVString(value_ref.value().to_string()))
198            })
199    }
200
201    pub(crate) fn get_trusted_html_attribute(&self, local_name: &LocalName) -> TrustedHTMLOrString {
202        TrustedHTMLOrString::String(self.get_string_attribute(local_name))
203    }
204
205    pub(crate) fn get_string_attribute(&self, local_name: &LocalName) -> DOMString {
206        self.get_attribute_string_value(local_name)
207            .map(|value| value.into())
208            .unwrap_or_default()
209    }
210
211    pub(crate) fn set_string_attribute(
212        &self,
213        cx: &mut JSContext,
214        local_name: &LocalName,
215        value: DOMString,
216    ) {
217        self.set_attribute(cx, local_name, value.str().to_string().into());
218    }
219
220    /// Used for string attribute reflections where absence of the attribute returns `null`,
221    /// e.g. `element.ariaLabel` returning `null` when the `aria-label` attribute is absent.
222    pub(crate) fn get_nullable_string_attribute(
223        &self,
224        local_name: &LocalName,
225    ) -> Option<DOMString> {
226        if self.has_attribute(local_name) {
227            Some(self.get_string_attribute(local_name))
228        } else {
229            None
230        }
231    }
232
233    /// Used for string attribute reflections where setting `null`/`undefined` removes the
234    /// attribute, e.g. `element.ariaLabel = null` removing the `aria-label` attribute.
235    pub(crate) fn set_nullable_string_attribute(
236        &self,
237        cx: &mut JSContext,
238        local_name: &LocalName,
239        value: Option<DOMString>,
240    ) {
241        match value {
242            Some(val) => {
243                self.set_string_attribute(cx, local_name, val);
244            },
245            None => {
246                self.remove_attribute(cx, &ns!(), local_name);
247            },
248        }
249    }
250
251    pub(crate) fn set_nullable_tokenlist_attribute(
252        &self,
253        cx: &mut JSContext,
254        local_name: &LocalName,
255        value: Option<DOMString>,
256    ) {
257        match value {
258            Some(string_value) => {
259                self.set_tokenlist_attribute(cx, local_name, string_value);
260            },
261            None => {
262                self.remove_attribute(cx, &ns!(), local_name);
263            },
264        }
265    }
266
267    /// Returns true if any attribute in the tokenlist fulfill `f`. Equivalent to
268    /// `get_tokenlist_attribute(name).iter().any(f)`.
269    pub(crate) fn any_tokenlist_attribute(
270        &self,
271        local_name: &LocalName,
272        f: impl Fn(&Atom) -> bool,
273    ) -> bool {
274        self.with_attribute(&ns!(), local_name, |attribute| {
275            attribute.value().as_tokens().iter().any(f)
276        })
277        .unwrap_or(false)
278    }
279
280    pub(crate) fn get_tokenlist_attribute(&self, local_name: &LocalName) -> Vec<Atom> {
281        self.with_attribute(&ns!(), local_name, |attribute| {
282            attribute.value().as_tokens().to_vec()
283        })
284        .unwrap_or_default()
285    }
286
287    pub(crate) fn set_tokenlist_attribute(
288        &self,
289        cx: &mut JSContext,
290        local_name: &LocalName,
291        value: DOMString,
292    ) {
293        self.set_attribute(
294            cx,
295            local_name,
296            AttrValue::from_serialized_tokenlist(value.into()),
297        );
298    }
299
300    pub(crate) fn get_uint_attribute(&self, local_name: &LocalName, default: u32) -> u32 {
301        self.with_attribute(&ns!(), local_name, |attribute| {
302            if let AttrValue::UInt(_, value) = *attribute.value() {
303                value
304            } else {
305                unreachable!("Expected an AttrValue::Int: implement parse_plain_attribute")
306            }
307        })
308        .unwrap_or(default)
309    }
310
311    /// Ensure that for styles, we clone the already-parsed property declaration block.
312    /// This does two things:
313    /// 1. It uses the same fast-path as CSSStyleDeclaration
314    /// 2. It also avoids the CSP checks when cloning (it shouldn't run any when cloning
315    ///    existing valid attributes)
316    fn compute_attribute_value_with_style_fast_path(&self, attr: AttrRef<'_>) -> AttrValue {
317        if *attr.local_name() == local_name!("style") {
318            let document = self.owner_document();
319
320            if let AttrValue::Declaration {
321                block,
322                lock,
323                serialization,
324            } = &*attr.value()
325            {
326                // Even though the property declaration block inside this AttrValue will
327                // be replaced, the serialization will be exactly the same, so preserve
328                // that instead of re-serializing.
329                let cloned_block = block.read_with(&lock.read()).clone();
330                return AttrValue::Declaration {
331                    block: ServoArc::new(lock.wrap(cloned_block)),
332                    lock: lock.clone(),
333                    serialization: serialization.clone(),
334                };
335            }
336
337            if let Some(ref pdb) = *self.style_attribute().borrow() {
338                let shared_lock = document.style_shared_author_lock();
339                let new_pdb = pdb.read_with(&shared_lock.read()).clone();
340                return AttrValue::Declaration {
341                    block: ServoArc::new(shared_lock.wrap(new_pdb)),
342                    lock: shared_lock.clone(),
343                    // The style attribute was not set via a declaration, so try to
344                    // preserve any serialization that existed before instead of
345                    // re-serializing.
346                    serialization: (**attr.value()).to_owned().into(),
347                };
348            }
349        }
350
351        attr.value().clone()
352    }
353
354    /// <https://dom.spec.whatwg.org/#concept-node-clone>
355    pub(crate) fn copy_all_attributes_to_other_element(
356        &self,
357        cx: &mut JSContext,
358        target_element: &Element,
359    ) {
360        // Step 2.5. For each attribute of node’s attribute list:
361        let attribute_count = self.attrs().borrow().len();
362        target_element.attrs().reserve_exact(attribute_count);
363        for attr in self.attrs().borrow().iter() {
364            // Step 2.5.1. Let copyAttribute be the result of cloning a single node given attribute, document, and null.
365            let new_value = self.compute_attribute_value_with_style_fast_path(attr);
366            // Step 2.5.2. Append copyAttribute to copy.
367            target_element.push_new_attribute(
368                cx,
369                attr.local_name().clone(),
370                new_value,
371                attr.name().clone(),
372                attr.namespace().clone(),
373                attr.prefix().cloned(),
374            );
375        }
376    }
377}