From 43da933247eeb0b023bdd136984a4d5d6d481888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=BClker?= Date: Thu, 14 Aug 2025 15:34:02 +0200 Subject: [PATCH] script: Implement `CSS.registerProperty` (#38682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.lock | 1 + components/layout/Cargo.toml | 1 + components/layout/layout_impl.rs | 100 +++++++++++++++++- components/script/dom/css.rs | 32 +++++- components/script_bindings/webidls/CSS.webidl | 18 ++-- components/shared/layout/lib.rs | 24 ++++- .../animation-important-001.html.ini | 15 +-- ...-units-computational-independence.html.ini | 18 ---- .../css/css-values/attr-security.html.ini | 22 +++- 9 files changed, 185 insertions(+), 46 deletions(-) delete mode 100644 tests/wpt/meta/css/css-conditional/container-queries/container-units-computational-independence.html.ini diff --git a/Cargo.lock b/Cargo.lock index 6cf45c8954f..18ef9d2bdad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4736,6 +4736,7 @@ dependencies = [ "base", "bitflags 2.9.1", "compositing_traits", + "cssparser", "data-url", "embedder_traits", "euclid", diff --git a/components/layout/Cargo.toml b/components/layout/Cargo.toml index 3270d5ffb26..139f2ce3dba 100644 --- a/components/layout/Cargo.toml +++ b/components/layout/Cargo.toml @@ -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 } diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index 2aadbf67690..a6194ac61d5 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -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() } + + /// + 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 { diff --git a/components/script/dom/css.rs b/components/script/dom/css.rs index 7a4623b23f1..28b5dee0b57 100644 --- a/components/script/dom/css.rs +++ b/components/script/dom/css.rs @@ -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 for CSS { fn PaintWorklet(win: &Window) -> DomRoot { win.paint_worklet() } + + /// + 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(()) + } } diff --git a/components/script_bindings/webidls/CSS.webidl b/components/script_bindings/webidls/CSS.webidl index a797b926e60..31d19027dc6 100644 --- a/components/script_bindings/webidls/CSS.webidl +++ b/components/script_bindings/webidls/CSS.webidl @@ -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); +}; diff --git a/components/shared/layout/lib.rs b/components/shared/layout/lib.rs index d32ce8eef55..8f2aaa8a38b 100644 --- a/components/shared/layout/lib.rs +++ b/components/shared/layout/lib.rs @@ -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, + 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; } @@ -299,6 +317,10 @@ pub trait Layout { point: LayoutPoint, flags: ElementsFromPointFlags, ) -> Vec; + 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` diff --git a/tests/wpt/meta/css/css-animations/animation-important-001.html.ini b/tests/wpt/meta/css/css-animations/animation-important-001.html.ini index 3398c40ec07..ae068187e21 100644 --- a/tests/wpt/meta/css/css-animations/animation-important-001.html.ini +++ b/tests/wpt/meta/css/css-animations/animation-important-001.html.ini @@ -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 - diff --git a/tests/wpt/meta/css/css-conditional/container-queries/container-units-computational-independence.html.ini b/tests/wpt/meta/css/css-conditional/container-queries/container-units-computational-independence.html.ini deleted file mode 100644 index c515493b253..00000000000 --- a/tests/wpt/meta/css/css-conditional/container-queries/container-units-computational-independence.html.ini +++ /dev/null @@ -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 diff --git a/tests/wpt/meta/css/css-values/attr-security.html.ini b/tests/wpt/meta/css/css-values/attr-security.html.ini index de13155f29d..01fee7bb07c 100644 --- a/tests/wpt/meta/css/css-values/attr-security.html.ini +++ b/tests/wpt/meta/css/css-values/attr-security.html.ini @@ -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())' 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