Files
servo/components/script/dom/attr.rs
Martin Robinson 401d327b96 script: Clean up attribute access a little bit in Element (#43064)
- Use modern Rust conveniences such as `unwrap_or_default`
 - Unabbreviate `attr`
- Unify the lowercase ASCII name assertion and make it a debug assertion
 - Use `unreachable!` instead of panic
- Expose two attribute getters that follow the behavior of two
specification concepts and what we expect internally in Servo:
- One that takes a namespace, but does not require lowercase attribute
names. ([specification
concept](https://dom.spec.whatwg.org/#concept-element-attributes-get-by-namespace>))
- One that does not take a namespace, but does require lowercase
attribute names. ([specification
concept](https://dom.spec.whatwg.org/#concept-element-attributes-get-by-name))
Testing: This should not really change behavior so should be covered by
existing tests.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2026-03-06 20:02:37 +00:00

312 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::borrow::ToOwned;
use std::mem;
use std::sync::LazyLock;
use devtools_traits::AttrInfo;
use dom_struct::dom_struct;
use html5ever::{LocalName, Namespace, Prefix, local_name, ns};
use style::attr::{AttrIdentifier, AttrValue};
use style::values::GenericAtomIdent;
use crate::dom::bindings::cell::{DomRefCell, Ref};
use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrTrustedScriptOrTrustedScriptURLOrString as TrustedTypeOrString;
use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::document::Document;
use crate::dom::element::Element;
use crate::dom::node::{Node, NodeTraits};
use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory;
use crate::script_runtime::CanGc;
// https://dom.spec.whatwg.org/#interface-attr
#[dom_struct]
pub(crate) struct Attr {
node_: Node,
#[no_trace]
identifier: AttrIdentifier,
#[no_trace]
value: DomRefCell<AttrValue>,
/// the element that owns this attribute.
owner: MutNullableDom<Element>,
}
impl Attr {
fn new_inherited(
document: &Document,
local_name: LocalName,
value: AttrValue,
name: LocalName,
namespace: Namespace,
prefix: Option<Prefix>,
owner: Option<&Element>,
) -> Attr {
Attr {
node_: Node::new_inherited(document),
identifier: AttrIdentifier {
local_name: GenericAtomIdent(local_name),
name: GenericAtomIdent(name),
namespace: GenericAtomIdent(namespace),
prefix: prefix.map(GenericAtomIdent),
},
value: DomRefCell::new(value),
owner: MutNullableDom::new(owner),
}
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn new(
document: &Document,
local_name: LocalName,
value: AttrValue,
name: LocalName,
namespace: Namespace,
prefix: Option<Prefix>,
owner: Option<&Element>,
can_gc: CanGc,
) -> DomRoot<Attr> {
Node::reflect_node(
Box::new(Attr::new_inherited(
document, local_name, value, name, namespace, prefix, owner,
)),
document,
can_gc,
)
}
#[inline]
pub(crate) fn name(&self) -> &LocalName {
&self.identifier.name.0
}
#[inline]
pub(crate) fn namespace(&self) -> &Namespace {
&self.identifier.namespace.0
}
#[inline]
pub(crate) fn prefix(&self) -> Option<&Prefix> {
Some(&self.identifier.prefix.as_ref()?.0)
}
}
impl AttrMethods<crate::DomTypeHolder> for Attr {
/// <https://dom.spec.whatwg.org/#dom-attr-localname>
fn LocalName(&self) -> DOMString {
// FIXME(ajeffrey): convert directly from LocalName to DOMString
DOMString::from(&**self.local_name())
}
/// <https://dom.spec.whatwg.org/#dom-attr-value>
fn Value(&self) -> DOMString {
// FIXME(ajeffrey): convert directly from AttrValue to DOMString
DOMString::from(&**self.value())
}
/// <https://dom.spec.whatwg.org/#set-an-existing-attribute-value>
fn SetValue(&self, value: DOMString, can_gc: CanGc) -> Fallible<()> {
// Step 2. Otherwise:
if let Some(owner) = self.owner() {
// Step 2.1. Let element be attributes element.
// Step 2.2. Let verifiedValue be the result of calling
// get trusted type compliant attribute value with attributes local name,
// attributes namespace, element, and value. [TRUSTED-TYPES]
let value = TrustedTypePolicyFactory::get_trusted_types_compliant_attribute_value(
owner.namespace(),
owner.local_name(),
self.local_name(),
Some(self.namespace()),
TrustedTypeOrString::String(value),
&owner.owner_global(),
can_gc,
)?;
if let Some(owner) = self.owner() {
// Step 2.4. Change attribute to verifiedValue.
let value = owner.parse_attribute(self.namespace(), self.local_name(), value);
owner.change_attribute(self, value, can_gc);
} else {
// Step 2.3. If attributes element is null, then set attributes value to verifiedValue, and return.
self.set_value(value);
}
} else {
// Step 1. If attributes element is null, then set attributes value to value.
self.set_value(value);
}
Ok(())
}
/// <https://dom.spec.whatwg.org/#dom-attr-name>
fn Name(&self) -> DOMString {
// FIXME(ajeffrey): convert directly from LocalName to DOMString
DOMString::from(&**self.name())
}
/// <https://dom.spec.whatwg.org/#dom-attr-namespaceuri>
fn GetNamespaceURI(&self) -> Option<DOMString> {
match *self.namespace() {
ns!() => None,
ref url => Some(DOMString::from(&**url)),
}
}
/// <https://dom.spec.whatwg.org/#dom-attr-prefix>
fn GetPrefix(&self) -> Option<DOMString> {
// FIXME(ajeffrey): convert directly from LocalName to DOMString
self.prefix().map(|p| DOMString::from(&**p))
}
/// <https://dom.spec.whatwg.org/#dom-attr-ownerelement>
fn GetOwnerElement(&self) -> Option<DomRoot<Element>> {
self.owner()
}
/// <https://dom.spec.whatwg.org/#dom-attr-specified>
fn Specified(&self) -> bool {
true // Always returns true
}
}
impl Attr {
/// Used to swap the attribute's value without triggering mutation events
pub(crate) fn swap_value(&self, value: &mut AttrValue) {
mem::swap(&mut *self.value.borrow_mut(), value);
}
pub(crate) fn identifier(&self) -> &AttrIdentifier {
&self.identifier
}
pub(crate) fn value(&self) -> Ref<'_, AttrValue> {
self.value.borrow()
}
fn set_value(&self, value: DOMString) {
*self.value.borrow_mut() = AttrValue::String(value.into());
}
pub(crate) fn local_name(&self) -> &LocalName {
&self.identifier.local_name
}
/// Sets the owner element. Should be called after the attribute is added
/// or removed from its older parent.
pub(crate) fn set_owner(&self, owner: Option<&Element>) {
let ns = self.namespace();
match (self.owner(), owner) {
(Some(old), None) => {
// Already gone from the list of attributes of old owner.
assert!(
old.get_attribute_with_namespace(ns, &self.identifier.local_name)
.as_deref() !=
Some(self)
)
},
(Some(old), Some(new)) => assert_eq!(&*old, new),
_ => {},
}
self.owner.set(owner);
}
pub(crate) fn owner(&self) -> Option<DomRoot<Element>> {
self.owner.get()
}
pub(crate) fn summarize(&self) -> AttrInfo {
AttrInfo {
namespace: (**self.namespace()).to_owned(),
name: (**self.name()).to_owned(),
value: (**self.value()).to_owned(),
}
}
pub(crate) fn qualified_name(&self) -> DOMString {
match self.prefix() {
Some(ref prefix) => DOMString::from(format!("{}:{}", prefix, &**self.local_name())),
None => DOMString::from(&**self.local_name()),
}
}
}
pub(crate) trait AttrHelpersForLayout<'dom> {
fn value(self) -> &'dom AttrValue;
fn local_name(self) -> &'dom LocalName;
fn namespace(self) -> &'dom Namespace;
}
#[expect(unsafe_code)]
impl<'dom> AttrHelpersForLayout<'dom> for LayoutDom<'dom, Attr> {
#[inline]
fn value(self) -> &'dom AttrValue {
unsafe { self.unsafe_get().value.borrow_for_layout() }
}
#[inline]
fn local_name(self) -> &'dom LocalName {
&self.unsafe_get().identifier.local_name.0
}
#[inline]
fn namespace(self) -> &'dom Namespace {
&self.unsafe_get().identifier.namespace.0
}
}
/// A helper function to check if attribute is relevant.
pub(crate) fn is_relevant_attribute(namespace: &Namespace, local_name: &LocalName) -> bool {
// <https://svgwg.org/svg2-draft/linking.html#XLinkHrefAttribute>
namespace == &ns!() || (namespace == &ns!(xlink) && local_name == &local_name!("href"))
}
/// A help function to check if an attribute is a boolean attribute.
pub(crate) fn is_boolean_attribute(name: &str) -> bool {
// The full list of attributes can be found in [1]. All attributes marked as "Boolean
// attribute" in the "Value" column are boolean attributes. Note that "hidden" is effectively
// treated as a boolean attribute, according to WPT test "test_global_boolean_attributes" in
// webdriver/tests/classic/get_element_attribute/get.py
//
// [1] <https://html.spec.whatwg.org/multipage/#attributes-3>
static BOOLEAN_ATTRIBUTES: LazyLock<[&str; 30]> = LazyLock::new(|| {
[
"allowfullscreen",
"alpha",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootcustomelementregistry",
"shadowrootdelegatesfocus",
"shadowrootserializable",
]
});
BOOLEAN_ATTRIBUTES
.iter()
.any(|&boolean_attr| boolean_attr.eq_ignore_ascii_case(name))
}