script: Implement CSS.registerProperty (#38682)

The implementation is mostly equivalent to
https://searchfox.org/mozilla-central/source/servo/ports/geckolib/glue.rs#9480.

Testing: New web platform tests start to pass

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2025-08-14 15:34:02 +02:00 committed by GitHub
parent 3a0e8fefde
commit 43da933247
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 185 additions and 46 deletions

1
Cargo.lock generated
View file

@ -4736,6 +4736,7 @@ dependencies = [
"base",
"bitflags 2.9.1",
"compositing_traits",
"cssparser",
"data-url",
"embedder_traits",
"euclid",

View file

@ -22,6 +22,7 @@ atomic_refcell = { workspace = true }
base = { workspace = true }
bitflags = { workspace = true }
compositing_traits = { workspace = true }
cssparser = { workspace = true }
data-url = { workspace = true }
embedder_traits = { workspace = true }
euclid = { workspace = true }

View file

@ -17,6 +17,7 @@ use base::id::{PipelineId, WebViewId};
use bitflags::bitflags;
use compositing_traits::CrossProcessCompositorApi;
use compositing_traits::display_list::ScrollType;
use cssparser::ParserInput;
use embedder_traits::{Theme, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect};
use euclid::{Point2D, Scale, Size2D};
@ -27,9 +28,9 @@ use fxhash::FxHashMap;
use ipc_channel::ipc::IpcSender;
use layout_api::wrapper_traits::LayoutNode;
use layout_api::{
IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, OffsetParentResponse, QueryMsg,
ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, ReflowResult,
TrustedNodeAddress,
IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, OffsetParentResponse,
PropertyRegistration, QueryMsg, ReflowGoal, ReflowPhasesRun, ReflowRequest,
ReflowRequestRestyle, ReflowResult, RegisterPropertyError, TrustedNodeAddress,
};
use log::{debug, error, warn};
use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps};
@ -50,6 +51,7 @@ use style::animation::DocumentAnimationSet;
use style::context::{
QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters, SharedStyleContext,
};
use style::custom_properties::{SpecifiedValue, parse_name};
use style::dom::{OpaqueNode, ShowSubtreeDataAndPrimaryValues, TElement, TNode};
use style::error_reporting::RustLogReporter;
use style::font_metrics::FontMetrics;
@ -58,6 +60,11 @@ use style::invalidation::element::restyle_hints::RestyleHint;
use style::media_queries::{Device, MediaList, MediaType};
use style::properties::style_structs::Font;
use style::properties::{ComputedValues, PropertyId};
use style::properties_and_values::registry::{
PropertyRegistration as StyloPropertyRegistration, PropertyRegistrationData,
};
use style::properties_and_values::rule::{Inherits, PropertyRegistrationError, PropertyRuleName};
use style::properties_and_values::syntax::Descriptor;
use style::queries::values::PrefersColorScheme;
use style::selector_parser::{PseudoElement, RestyleDamage, SnapshotMap};
use style::servo::media_queries::FontMetricsProvider;
@ -72,6 +79,7 @@ use style::traversal_flags::TraversalFlags;
use style::values::computed::font::GenericFontFamily;
use style::values::computed::{CSSPixelLength, FontSize, Length, NonNegativeLength};
use style::values::specified::font::{KeywordInfo, QueryFontMetricsFlags};
use style::values::{Parser, SourceLocation};
use style::{Zero, driver};
use style_traits::{CSSPixel, SpeculativePainter};
use stylo_atoms::Atom;
@ -510,6 +518,92 @@ impl Layout for LayoutThread {
fn needs_new_display_list(&self) -> bool {
self.need_new_display_list.get()
}
/// <https://drafts.css-houdini.org/css-properties-values-api-1/#the-registerproperty-function>
fn register_custom_property(
&mut self,
property_registration: PropertyRegistration,
) -> Result<(), RegisterPropertyError> {
// Step 2. If name is not a custom property name string, throw a SyntaxError and exit this algorithm.
// If property set already contains an entry with name as its property name
// (compared codepoint-wise), throw an InvalidModificationError and exit this algorithm.
let Ok(name) = parse_name(&property_registration.name) else {
return Err(RegisterPropertyError::InvalidName);
};
let name = Atom::from(name);
if self
.stylist
.custom_property_script_registry()
.get(&name)
.is_some()
{
return Err(RegisterPropertyError::AlreadyRegistered);
}
// Step 3. Attempt to consume a syntax definition from syntax. If it returns failure,
// throw a SyntaxError. Otherwise, let syntax definition be the returned syntax definition.
let syntax = Descriptor::from_str(&property_registration.syntax, false)
.map_err(|_| RegisterPropertyError::InvalidSyntax)?;
// Step 4 - Parse and validate initial value
let initial_value = match property_registration.initial_value {
Some(value) => {
let mut input = ParserInput::new(&value);
let parsed = Parser::new(&mut input)
.parse_entirely(|input| {
input.skip_whitespace();
SpecifiedValue::parse(input, &property_registration.url_data)
.map(servo_arc::Arc::new)
})
.ok();
if parsed.is_none() {
return Err(RegisterPropertyError::InvalidInitialValue);
}
parsed
},
None => None,
};
StyloPropertyRegistration::validate_initial_value(&syntax, initial_value.as_deref())
.map_err(|error| match error {
PropertyRegistrationError::InitialValueNotComputationallyIndependent => {
RegisterPropertyError::InitialValueNotComputationallyIndependent
},
PropertyRegistrationError::InvalidInitialValue => {
RegisterPropertyError::InvalidInitialValue
},
PropertyRegistrationError::NoInitialValue => RegisterPropertyError::NoInitialValue,
})?;
// Step 5. Set inherit flag to the value of inherits.
let inherits = if property_registration.inherits {
Inherits::True
} else {
Inherits::False
};
// Step 6. Let registered property be a struct with a property name of name, a syntax of
// syntax definition, an initial value of parsed initial value, and an inherit flag of inherit flag.
// Append registered property to property set.
let property_registration = StyloPropertyRegistration {
name: PropertyRuleName(name),
data: PropertyRegistrationData {
syntax,
initial_value,
inherits,
},
url_data: property_registration.url_data,
source_location: SourceLocation { line: 0, column: 0 },
};
self.stylist
.custom_property_script_registry_mut()
.register(property_registration);
self.stylist.rebuild_initial_values_for_custom_properties();
Ok(())
}
}
impl LayoutThread {

View file

@ -4,6 +4,8 @@
use cssparser::{Parser, ParserInput, serialize_identifier};
use dom_struct::dom_struct;
use layout_api::{PropertyRegistration, RegisterPropertyError};
use script_bindings::codegen::GenericBindings::CSSBinding::PropertyDefinition;
use style::context::QuirksMode;
use style::parser::ParserContext;
use style::stylesheets::supports_rule::{Declaration, parse_condition_or_declaration};
@ -12,7 +14,7 @@ use style_traits::ParsingMode;
use crate::dom::bindings::codegen::Bindings::CSSBinding::CSSMethods;
use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::Reflector;
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
@ -81,4 +83,32 @@ impl CSSMethods<crate::DomTypeHolder> for CSS {
fn PaintWorklet(win: &Window) -> DomRoot<Worklet> {
win.paint_worklet()
}
/// <https://drafts.css-houdini.org/css-properties-values-api/#the-registerproperty-function>
fn RegisterProperty(window: &Window, property_definition: &PropertyDefinition) -> Fallible<()> {
let property_registration = PropertyRegistration {
name: property_definition.name.str().to_owned(),
inherits: property_definition.inherits,
url_data: UrlExtraData(window.get_url().get_arc()),
initial_value: property_definition
.initialValue
.as_ref()
.map(|value| value.str().to_owned()),
syntax: property_definition.syntax.str().to_owned(),
};
window
.layout_mut()
.register_custom_property(property_registration)
.map_err(|error| match error {
RegisterPropertyError::InvalidName |
RegisterPropertyError::InvalidSyntax |
RegisterPropertyError::InvalidInitialValue |
RegisterPropertyError::NoInitialValue |
RegisterPropertyError::InitialValueNotComputationallyIndependent => Error::Syntax,
RegisterPropertyError::AlreadyRegistered => Error::InvalidModification,
})?;
Ok(())
}
}

View file

@ -24,13 +24,13 @@ partial namespace CSS {
};
// https://drafts.css-houdini.org/css-properties-values-api/#the-registerproperty-function
// dictionary PropertyDefinition {
// required DOMString name;
// DOMString syntax = "*";
// required boolean inherits;
// DOMString initialValue;
// };
dictionary PropertyDefinition {
required DOMString name;
DOMString syntax = "*";
required boolean inherits;
DOMString initialValue;
};
// partial namespace CSS {
// undefined registerProperty(PropertyDefinition definition);
// };
partial namespace CSS {
[Throws] undefined registerProperty(PropertyDefinition definition);
};

View file

@ -54,7 +54,7 @@ use style::media_queries::Device;
use style::properties::PropertyId;
use style::properties::style_structs::Font;
use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
use style::stylesheets::Stylesheet;
use style::stylesheets::{Stylesheet, UrlExtraData};
use style_traits::CSSPixel;
use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D};
use webrender_api::{ExternalScrollId, ImageKey};
@ -206,6 +206,24 @@ pub struct LayoutConfig {
pub theme: Theme,
}
pub struct PropertyRegistration {
pub name: String,
pub syntax: String,
pub initial_value: Option<String>,
pub inherits: bool,
pub url_data: UrlExtraData,
}
#[derive(Debug)]
pub enum RegisterPropertyError {
InvalidName,
AlreadyRegistered,
InvalidSyntax,
InvalidInitialValue,
InitialValueNotComputationallyIndependent,
NoInitialValue,
}
pub trait LayoutFactory: Send + Sync {
fn create(&self, config: LayoutConfig) -> Box<dyn Layout>;
}
@ -299,6 +317,10 @@ pub trait Layout {
point: LayoutPoint,
flags: ElementsFromPointFlags,
) -> Vec<ElementsFromPointResult>;
fn register_custom_property(
&mut self,
property_registration: PropertyRegistration,
) -> Result<(), RegisterPropertyError>;
}
/// This trait is part of `layout_api` because it depends on both `script_traits`

View file

@ -1,17 +1,6 @@
[animation-important-001.html]
expected: ERROR
[Interpolated value is observable]
[Standard property animations appearing via setKeyframes do not override important declarations]
expected: FAIL
[Important rules override animations (::before)]
[Custom property animations appearing via setKeyframes do not override important declarations]
expected: FAIL
[Non-overriden interpolations are observable]
expected: FAIL
[Important rules do not override animations on :visited as seen from JS]
expected: FAIL
[Important rules override animations]
expected: FAIL

View file

@ -1,18 +0,0 @@
[container-units-computational-independence.html]
[Container relative unit cqw is not computationally independent]
expected: FAIL
[Container relative unit cqh is not computationally independent]
expected: FAIL
[Container relative unit cqi is not computationally independent]
expected: FAIL
[Container relative unit cqb is not computationally independent]
expected: FAIL
[Container relative unit cqmin is not computationally independent]
expected: FAIL
[Container relative unit cqmax is not computationally independent]
expected: FAIL

View file

@ -1,5 +1,4 @@
[attr-security.html]
expected: ERROR
[CSS Values and Units Test: attr() security limitations]
expected: FAIL
@ -74,3 +73,24 @@
['--x: image-set(var(--some-string-list))' with data-foo="https://does-not-exist.test/404.png"]
expected: FAIL
['--registered-color: attr(data-foo type(<color>))' with data-foo="blue"]
expected: FAIL
['--x: image-set(var(--some-other-url))' with data-foo="https://does-not-exist.test/404.png"]
expected: FAIL
['--x: image-set(if(style(--true): attr(data-foo);))' with data-foo="https://does-not-exist.test/404.png"]
expected: FAIL
['background-image: image-set(\n if(style(--true): url(https://does-not-exist-2.test/404.png);\n else: attr(data-foo);))' with data-foo="https://does-not-exist-2.test/404.png"]
expected: FAIL
['background-image: image-set(if(style(--true): url(https://does-not-exist.test/404.png);\n style(--condition-val): url(https://does-not-exist.test/404.png);\n else: url(https://does-not-exist.test/404.png);))' with data-foo="attr(data-foo type(*))"]
expected: FAIL
['--x: image-set(if(style(--condition-val: if(style(--true): attr(data-foo type(*));)): url(https://does-not-exist.test/404.png);))' with data-foo="3"]
expected: FAIL
['--x: image-set(if(style(--condition-val >= attr(data-foo type(*))): url(https://does-not-exist.test/404.png);))' with data-foo="3"]
expected: FAIL