From 90c0decd952ec93f03edb639e4f8c83ff9db2b1a Mon Sep 17 00:00:00 2001 From: norbiros Date: Fri, 18 Jul 2025 14:17:42 +0200 Subject: [PATCH] LibWeb/CSS: Add `CSS.registerProperty` JS method This adds an *almost* complete implementation of `CSS.registerProperty` method enabling further progress on the `@property` feature. --- Libraries/LibWeb/CSS/CSS.cpp | 84 +++++++++++++++++++ Libraries/LibWeb/CSS/CSS.h | 9 ++ Libraries/LibWeb/CSS/CSS.idl | 10 +++ Libraries/LibWeb/CSS/Parser/Helpers.cpp | 5 ++ Libraries/LibWeb/CSS/Parser/Parser.h | 1 + ...ce-objects-default-property-attributes.txt | 5 ++ .../css/css-values/attr-security.txt | 45 ++++++++-- 7 files changed, 154 insertions(+), 5 deletions(-) diff --git a/Libraries/LibWeb/CSS/CSS.cpp b/Libraries/LibWeb/CSS/CSS.cpp index ded5789db4c..cc55083f188 100644 --- a/Libraries/LibWeb/CSS/CSS.cpp +++ b/Libraries/LibWeb/CSS/CSS.cpp @@ -8,9 +8,12 @@ #include #include #include +#include +#include #include #include #include +#include namespace Web::CSS { @@ -59,4 +62,85 @@ WebIDL::ExceptionOr supports(JS::VM& vm, StringView condition_text) return false; } +// https://www.w3.org/TR/css-properties-values-api-1/#the-registerproperty-function +WebIDL::ExceptionOr register_property(JS::VM& vm, PropertyDefinition definition) +{ + // 1. Let property set be the value of the current global object’s associated Document’s [[registeredPropertySet]] slot. + auto& realm = *vm.current_realm(); + auto& window = static_cast(realm.global_object()); + auto& document = window.associated_document(); + + // 2. If name is not a custom property name string, throw a SyntaxError and exit this algorithm. + if (!is_a_custom_property_name_string(definition.name)) + return WebIDL::SyntaxError::create(realm, "Invalid property name"_string); + + // If property set already contains an entry with name as its property name (compared codepoint-wise), + // throw an InvalidModificationError and exit this algorithm. + if (document.registered_custom_properties().contains(definition.name)) + return WebIDL::InvalidModificationError::create(realm, "Property already registered"_string); + + auto parsing_params = CSS::Parser::ParsingParams { document }; + + // 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. + auto syntax_component_values = parse_component_values_list(parsing_params, definition.syntax); + auto maybe_syntax = parse_as_syntax(syntax_component_values); + if (!maybe_syntax) { + return WebIDL::SyntaxError::create(realm, "Invalid syntax definition"_string); + } + + RefPtr initial_value_maybe; + + // 4. If syntax definition is the universal syntax definition, and initialValue is not present, + if (maybe_syntax->type() == Parser::SyntaxNode::NodeType::Universal) { + if (!definition.initial_value.has_value()) { + // let parsed initial value be empty. + // This must be treated identically to the "default" initial value of custom properties, as defined in [css-variables]. + initial_value_maybe = nullptr; + } else { + // Otherwise, if syntax definition is the universal syntax definition, + // parse initialValue as a + initial_value_maybe = parse_css_value(parsing_params, definition.initial_value.value(), PropertyID::Custom); + // If this fails, throw a SyntaxError and exit this algorithm. + // Otherwise, let parsed initial value be the parsed result. + if (!initial_value_maybe) { + return WebIDL::SyntaxError::create(realm, "Invalid initial value"_string); + } + } + } else if (!definition.initial_value.has_value()) { + // Otherwise, if initialValue is not present, throw a SyntaxError and exit this algorithm. + return WebIDL::SyntaxError::create(realm, "Initial value must be provided for non-universal syntax"_string); + } else { + // Otherwise, parse initialValue according to syntax definition. + auto initial_value_component_values = parse_component_values_list(CSS::Parser::ParsingParams {}, definition.initial_value.value()); + + initial_value_maybe = Parser::parse_with_a_syntax( + Parser::ParsingParams { realm }, + initial_value_component_values, + *maybe_syntax); + + // If this fails, throw a SyntaxError and exit this algorithm. + if (!initial_value_maybe || initial_value_maybe->is_guaranteed_invalid()) { + return WebIDL::SyntaxError::create(realm, "Invalid initial value"_string); + } + // Otherwise, let parsed initial value be the parsed result. + // NB: Already done + + // FIXME: If parsed initial value is not computationally independent, throw a SyntaxError and exit this algorithm. + } + + // 5. Set inherit flag to the value of inherits. + // NB: Combined with 6. + + // 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. + auto registered_property = CSSPropertyRule::create(realm, definition.name, definition.syntax, definition.inherits, initial_value_maybe); + // Append registered property to property set. + document.registered_custom_properties().set( + registered_property->name(), + registered_property); + + return {}; +} + } diff --git a/Libraries/LibWeb/CSS/CSS.h b/Libraries/LibWeb/CSS/CSS.h index eb84f7b1def..1ba8fddeb8f 100644 --- a/Libraries/LibWeb/CSS/CSS.h +++ b/Libraries/LibWeb/CSS/CSS.h @@ -15,9 +15,18 @@ // https://www.w3.org/TR/cssom-1/#namespacedef-css namespace Web::CSS { +struct PropertyDefinition { + String name; + String syntax; + bool inherits; + Optional initial_value; +}; + WebIDL::ExceptionOr escape(JS::VM&, StringView identifier); bool supports(JS::VM&, StringView property, StringView value); WebIDL::ExceptionOr supports(JS::VM&, StringView condition_text); +WebIDL::ExceptionOr register_property(JS::VM&, PropertyDefinition definition); + } diff --git a/Libraries/LibWeb/CSS/CSS.idl b/Libraries/LibWeb/CSS/CSS.idl index 77e87a523f4..5ea87c44cdf 100644 --- a/Libraries/LibWeb/CSS/CSS.idl +++ b/Libraries/LibWeb/CSS/CSS.idl @@ -1,3 +1,10 @@ +dictionary PropertyDefinition { + required CSSOMString name; + CSSOMString syntax = "*"; + required boolean inherits; + CSSOMString initialValue; +}; + // https://www.w3.org/TR/cssom-1/#namespacedef-css [Exposed=Window] namespace CSS { @@ -5,4 +12,7 @@ namespace CSS { boolean supports(CSSOMString property, CSSOMString value); boolean supports(CSSOMString conditionText); + + // https://www.w3.org/TR/css-properties-values-api-1/#dom-css-registerproperty + undefined registerProperty(PropertyDefinition definition); }; diff --git a/Libraries/LibWeb/CSS/Parser/Helpers.cpp b/Libraries/LibWeb/CSS/Parser/Helpers.cpp index 92292570e0b..f0da23ced35 100644 --- a/Libraries/LibWeb/CSS/Parser/Helpers.cpp +++ b/Libraries/LibWeb/CSS/Parser/Helpers.cpp @@ -133,4 +133,9 @@ RefPtr parse_css_supports(CSS::Parser::ParsingParams const& conte return CSS::Parser::Parser::create(context, string).parse_as_supports(); } +Vector parse_component_values_list(CSS::Parser::ParsingParams const& parsing_params, StringView string) +{ + return CSS::Parser::Parser::create(parsing_params, string).parse_as_list_of_component_values(); +} + } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index b50d4b37128..eb84d3c7785 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -577,6 +577,7 @@ CSS::CSSRule* parse_css_rule(CSS::Parser::ParsingParams const&, StringView); RefPtr parse_media_query(CSS::Parser::ParsingParams const&, StringView); Vector> parse_media_query_list(CSS::Parser::ParsingParams const&, StringView); RefPtr parse_css_supports(CSS::Parser::ParsingParams const&, StringView); +Vector parse_component_values_list(CSS::Parser::ParsingParams const&, StringView); GC::Ref internal_css_realm(); } diff --git a/Tests/LibWeb/Text/expected/namespace-objects-default-property-attributes.txt b/Tests/LibWeb/Text/expected/namespace-objects-default-property-attributes.txt index 259052893ef..25334009e74 100644 --- a/Tests/LibWeb/Text/expected/namespace-objects-default-property-attributes.txt +++ b/Tests/LibWeb/Text/expected/namespace-objects-default-property-attributes.txt @@ -1,4 +1,9 @@ == CSS property descriptors +registerProperty writable: true +registerProperty configurable: true +registerProperty enumerable: true +registerProperty value before: function registerProperty() { [native code] } +registerProperty value after: replaced supports writable: true supports configurable: true supports enumerable: true diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/attr-security.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/attr-security.txt index d357c568101..ce8c64b2da4 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/attr-security.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/attr-security.txt @@ -1,9 +1,9 @@ -Harness status: Error +Harness status: OK -Found 22 tests +Found 43 tests -15 Pass -7 Fail +28 Pass +15 Fail Pass '--x: image-set(attr(data-foo))' with data-foo="https://does-not-exist.test/404.png" Pass 'background-image: image-set(attr(data-foo))' with data-foo="https://does-not-exist.test/404.png" Fail 'background-image: image-set("https://does-not-exist.test/404.png")' with data-foo="https://does-not-exist.test/404.png" @@ -25,4 +25,39 @@ Pass 'background-image: image-set(var(--y, attr(data-foo)))' with data-foo="http Pass '--x: image-set(var(--some-string))' with data-foo="https://does-not-exist.test/404.png" Pass 'background-image: image-set(var(--some-string))' with data-foo="https://does-not-exist.test/404.png" Pass '--x: image-set(var(--some-string-list))' with data-foo="https://does-not-exist.test/404.png" -Pass 'background-image: image-set(var(--some-string-list))' with data-foo="https://does-not-exist.test/404.png" \ No newline at end of file +Pass 'background-image: image-set(var(--some-string-list))' with data-foo="https://does-not-exist.test/404.png" +Fail '--registered-url: attr(data-foo type())' with data-foo="https://does-not-exist.test/404.png" +Fail '--registered-color: attr(data-foo type())' with data-foo="blue" +Pass '--x: image-set(var(--some-other-url))' with data-foo="https://does-not-exist.test/404.png" +Pass 'background-image: image-set(var(--some-other-url))' with data-foo="https://does-not-exist.test/404.png" +Fail 'background-image: attr(data-foo type(*))' with data-foo="url(https://does-not-exist.test/404.png), linear-gradient(black, white)" +Pass 'background-image: image-set(var(--image-set-valid))' with data-foo="image/jpeg" +Pass 'background-image: image-set(var(--image-set-invalid))' with data-foo="https://does-not-exist.test/404.png" +Fail '--x: image-set(if(style(--true): attr(data-foo);))' with data-foo="https://does-not-exist.test/404.png" +Pass 'background-image: image-set(if(style(--true): attr(data-foo);))' with data-foo="https://does-not-exist.test/404.png" +Fail 'background-image: image-set( + if(style(--true): url(https://does-not-exist-2.test/404.png); + else: attr(data-foo);))' with data-foo="https://does-not-exist-2.test/404.png" +Pass 'background-image: image-set( + if(style(--some-string): url(https://does-not-exist.test/404.png);))' with data-foo="https://does-not-exist.test/404.png" +Pass 'background-image: image-set( + if(style(--condition-val: attr(data-foo type(*))): url(https://does-not-exist.test/404.png);))' with data-foo="3" +Pass 'background-image: image-set( + if(style(--condition-val: attr(data-foo type(*))): url(https://does-not-exist.test/404.png); + style(--true): url(https://does-not-exist.test/404.png); + else: url(https://does-not-exist.test/404.png);))' with data-foo="1" +Fail 'background-image: image-set(if(style(--true): url(https://does-not-exist.test/404.png); + style(--condition-val): url(https://does-not-exist.test/404.png); + else: url(https://does-not-exist.test/404.png);))' with data-foo="attr(data-foo type(*))" +Pass 'background-image: 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" +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" +Fail '--x: image-set(if(style(--condition-val >= attr(data-foo type(*))): url(https://does-not-exist.test/404.png);))' with data-foo="3" +Pass 'background-image: image-set( + if(style(--condition-val >= attr(data-foo type(*))): url(https://does-not-exist.test/404.png);))' with data-foo="3" +Pass 'background-image: image-set( + if(style(--condition-val < attr(data-foo type(*))): url(https://does-not-exist.test/404.png);))' with data-foo="3" +Pass 'background-image: image-set( + if(style(--str < attr(data-foo type(*))): url(https://does-not-exist.test/404.png);))' with data-foo="3" +Pass 'background-image: image-set( + if(style(--condition-val < attr(data-foo type(*))): url(https://does-not-exist.test/404.png);))' with data-foo="text" \ No newline at end of file