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