Files
servo/components/script/dom/element/attributes.rs
Tim van der Lippe d096560b51 script: Correctly handle modifiable elements in contenteditable (#44250)
First of all, the effective command value was wrong, since there is no
relevant CSS property for the underline command. Instead, we should
directly use the text-decoration property. This then allows us to
implement reordering of modifiable elements.

We also need to "change the element to a span", which is quite annoying
to do. Instead, it mimics what would have happened by moving children
and copying attributes.

There are some regressions, but overall this is another big step towards
the right track. The regressions look related to tricky edge cases that
I am not even sure other browsers handle.

Part of #25005

Testing: WPT

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
2026-04-16 20:50:06 +00:00

229 lines
8.1 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 html5ever::{LocalName, local_name, ns};
use js::context::JSContext;
use servo_arc::Arc as ServoArc;
use style::attr::AttrValue;
use stylo_atoms::Atom;
use crate::dom::attr::Attr;
use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
use crate::dom::bindings::codegen::UnionTypes::{TrustedHTMLOrString, TrustedScriptURLOrUSVString};
use crate::dom::bindings::root::Dom;
use crate::dom::bindings::str::{DOMString, USVString};
use crate::dom::element::{AttributeMutationReason, Element};
use crate::dom::node::NodeTraits;
use crate::script_runtime::CanGc;
impl Element {
pub(crate) fn get_int_attribute(&self, local_name: &LocalName, default: i32) -> i32 {
match self.get_attribute(local_name) {
Some(ref attribute) => match *attribute.value() {
AttrValue::Int(_, value) => value,
_ => unreachable!("Expected an AttrValue::Int: implement parse_plain_attribute"),
},
None => default,
}
}
pub(crate) fn set_atomic_attribute(
&self,
local_name: &LocalName,
value: DOMString,
can_gc: CanGc,
) {
self.set_attribute(local_name, AttrValue::from_atomic(value.into()), can_gc);
}
pub(crate) fn set_bool_attribute(&self, local_name: &LocalName, value: bool, can_gc: CanGc) {
if self.has_attribute(local_name) == value {
return;
}
if value {
self.set_string_attribute(local_name, DOMString::new(), can_gc);
} else {
self.remove_attribute(&ns!(), local_name, can_gc);
}
}
pub(crate) fn get_url_attribute(&self, local_name: &LocalName) -> USVString {
let Some(attribute) = self.get_attribute(local_name) else {
return Default::default();
};
let value = &**attribute.value();
self.owner_document()
.encoding_parse_a_url(value)
.map(|parsed| USVString(parsed.into_string()))
.unwrap_or_else(|_| USVString(value.to_owned()))
}
pub(crate) fn set_url_attribute(
&self,
local_name: &LocalName,
value: USVString,
can_gc: CanGc,
) {
self.set_attribute(local_name, AttrValue::String(value.to_string()), can_gc);
}
pub(crate) fn get_trusted_type_url_attribute(
&self,
local_name: &LocalName,
) -> TrustedScriptURLOrUSVString {
let Some(attribute) = self.get_attribute(local_name) else {
return TrustedScriptURLOrUSVString::USVString(USVString::default());
};
let value = &**attribute.value();
self.owner_document()
.encoding_parse_a_url(value)
.map(|parsed| TrustedScriptURLOrUSVString::USVString(USVString(parsed.into_string())))
.unwrap_or_else(|_| TrustedScriptURLOrUSVString::USVString(USVString(value.to_owned())))
}
pub(crate) fn get_trusted_html_attribute(&self, local_name: &LocalName) -> TrustedHTMLOrString {
TrustedHTMLOrString::String(self.get_string_attribute(local_name))
}
pub(crate) fn get_string_attribute(&self, local_name: &LocalName) -> DOMString {
self.get_attribute(local_name)
.map(|attribute| attribute.Value())
.unwrap_or_default()
}
pub(crate) fn set_string_attribute(
&self,
local_name: &LocalName,
value: DOMString,
can_gc: CanGc,
) {
self.set_attribute(local_name, AttrValue::String(value.into()), can_gc);
}
/// Used for string attribute reflections where absence of the attribute returns `null`,
/// e.g. `element.ariaLabel` returning `null` when the `aria-label` attribute is absent.
pub(crate) fn get_nullable_string_attribute(
&self,
local_name: &LocalName,
) -> Option<DOMString> {
if self.has_attribute(local_name) {
Some(self.get_string_attribute(local_name))
} else {
None
}
}
/// Used for string attribute reflections where setting `null`/`undefined` removes the
/// attribute, e.g. `element.ariaLabel = null` removing the `aria-label` attribute.
pub(crate) fn set_nullable_string_attribute(
&self,
cx: &mut JSContext,
local_name: &LocalName,
value: Option<DOMString>,
) {
match value {
Some(val) => {
self.set_string_attribute(local_name, val, CanGc::from_cx(cx));
},
None => {
self.remove_attribute(&ns!(), local_name, CanGc::from_cx(cx));
},
}
}
pub(crate) fn get_tokenlist_attribute(&self, local_name: &LocalName) -> Vec<Atom> {
self.get_attribute(local_name)
.map(|attribute| attribute.value().as_tokens().to_vec())
.unwrap_or_default()
}
pub(crate) fn set_tokenlist_attribute(
&self,
local_name: &LocalName,
value: DOMString,
can_gc: CanGc,
) {
self.set_attribute(
local_name,
AttrValue::from_serialized_tokenlist(value.into()),
can_gc,
);
}
pub(crate) fn set_atomic_tokenlist_attribute(
&self,
local_name: &LocalName,
tokens: Vec<Atom>,
can_gc: CanGc,
) {
self.set_attribute(local_name, AttrValue::from_atomic_tokens(tokens), can_gc);
}
pub(crate) fn set_int_attribute(&self, local_name: &LocalName, value: i32, can_gc: CanGc) {
self.set_attribute(local_name, AttrValue::Int(value.to_string(), value), can_gc);
}
pub(crate) fn get_uint_attribute(&self, local_name: &LocalName, default: u32) -> u32 {
match self.get_attribute(local_name) {
Some(ref attribute) => match *attribute.value() {
AttrValue::UInt(_, value) => value,
_ => unreachable!("Expected an AttrValue::UInt: implement parse_plain_attribute"),
},
None => default,
}
}
pub(crate) fn set_uint_attribute(&self, local_name: &LocalName, value: u32, can_gc: CanGc) {
self.set_attribute(
local_name,
AttrValue::UInt(value.to_string(), value),
can_gc,
);
}
/// Ensure that for styles, we clone the already-parsed property declaration block.
/// This does two things:
/// 1. It uses the same fast-path as CSSStyleDeclaration
/// 2. It also avoids the CSP checks when cloning (it shouldn't run any when cloning
/// existing valid attributes)
fn compute_attribute_value_with_style_fast_path(&self, attr: &Dom<Attr>) -> AttrValue {
if *attr.local_name() == local_name!("style") {
if let Some(ref pdb) = *self.style_attribute().borrow() {
let document = self.owner_document();
let shared_lock = document.style_shared_lock();
let new_pdb = pdb.read_with(&shared_lock.read()).clone();
return AttrValue::Declaration(
(**attr.value()).to_owned(),
ServoArc::new(shared_lock.wrap(new_pdb)),
);
}
}
attr.value().clone()
}
/// <https://dom.spec.whatwg.org/#concept-node-clone>
pub(crate) fn copy_all_attributes_to_other_element(
&self,
cx: &mut JSContext,
target_element: &Element,
) {
// Step 2.5. For each attribute of nodes attribute list:
for attr in self.attrs().iter() {
// Step 2.5.1. Let copyAttribute be the result of cloning a single node given attribute, document, and null.
let new_value = self.compute_attribute_value_with_style_fast_path(attr);
// Step 2.5.2. Append copyAttribute to copy.
target_element.push_new_attribute(
attr.local_name().clone(),
new_value,
attr.name().clone(),
attr.namespace().clone(),
attr.prefix().cloned(),
AttributeMutationReason::ByCloning,
CanGc::from_cx(cx),
);
}
}
}