LibWeb: Limit <ident> to <custom-ident> in @property/syntax

The definition of syntax in the "css-properties-values-api" spec (which
is used for the `@property/syntax` descriptor) is slightly different
from the definition of `<syntax>` in the "css-values" spec (which we
implement) in that it limits literal idents to exclusively
`<custom-ident>`s (i.e. not CSS-wide keywords or "default").

`<custom-ident>`s are also case-sensitive so that behavior is
implemented for syntax matching here as well
This commit is contained in:
Callum Law 2026-03-09 18:08:07 +13:00 committed by Sam Atkins
parent cfc2e64b4b
commit 6afe2ff27b
8 changed files with 51 additions and 31 deletions

View file

@ -83,7 +83,7 @@ WebIDL::ExceptionOr<void> register_property(JS::VM& vm, PropertyDefinition defin
// 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);
auto maybe_syntax = parse_as_syntax(syntax_component_values, Parser::LimitSingleComponentIdentToCustomIdent::Yes);
if (!maybe_syntax) {
return WebIDL::SyntaxError::create(realm, "Invalid syntax definition"_utf16);
}

View file

@ -793,7 +793,7 @@ GC::Ptr<CSSPropertyRule> Parser::convert_to_property_rule(AtRule const& rule)
parsing_params = CSS::Parser::ParsingParams { realm() };
auto syntax_component_values = parse_component_values_list(parsing_params, syntax_maybe.value());
auto maybe_syntax = parse_as_syntax(syntax_component_values);
auto maybe_syntax = parse_as_syntax(syntax_component_values, LimitSingleComponentIdentToCustomIdent::Yes);
// If the provided string is not a valid syntax string (if it returns failure when consume
// a syntax definition is called on it), the descriptor is invalid and must be ignored.

View file

@ -59,9 +59,10 @@ void TypeSyntaxNode::dump(StringBuilder& builder, int indent) const
builder.appendff("{: >{}}Type: {}\n", "", indent, m_type_name);
}
IdentSyntaxNode::IdentSyntaxNode(FlyString ident)
IdentSyntaxNode::IdentSyntaxNode(FlyString ident, CaseSensitivity case_sensitivity)
: SyntaxNode(NodeType::Ident)
, m_ident(move(ident))
, m_case_sensitivity(case_sensitivity)
{
}

View file

@ -62,20 +62,22 @@ private:
// 'foo'
class IdentSyntaxNode final : public SyntaxNode {
public:
static NonnullOwnPtr<IdentSyntaxNode> create(FlyString ident)
static NonnullOwnPtr<IdentSyntaxNode> create(FlyString ident, CaseSensitivity case_sensitivity)
{
return adopt_own(*new IdentSyntaxNode(move(ident)));
return adopt_own(*new IdentSyntaxNode(move(ident), case_sensitivity));
}
virtual ~IdentSyntaxNode() override;
FlyString const& ident() const { return m_ident; }
CaseSensitivity case_sensitivity() const { return m_case_sensitivity; }
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent) const override;
private:
IdentSyntaxNode(FlyString);
IdentSyntaxNode(FlyString, CaseSensitivity);
FlyString m_ident;
CaseSensitivity m_case_sensitivity;
};
// '<foo>'

View file

@ -20,7 +20,7 @@
namespace Web::CSS::Parser {
static OwnPtr<SyntaxNode> parse_syntax_single_component(TokenStream<ComponentValue>& tokens)
static OwnPtr<SyntaxNode> parse_syntax_single_component(TokenStream<ComponentValue>& tokens, LimitSingleComponentIdentToCustomIdent limit_single_component_ident_to_custom_ident)
{
// <syntax-single-component> = '<' <syntax-type-name> '>' | <ident>
// <syntax-type-name> = angle | color | custom-ident | image | integer
@ -34,8 +34,14 @@ static OwnPtr<SyntaxNode> parse_syntax_single_component(TokenStream<ComponentVal
// <ident>
if (tokens.next_token().is(Token::Type::Ident)) {
auto ident = tokens.consume_a_token().token().ident();
// AD-HOC: Some users (i.e. the @property syntax descriptor) only allow custom idents here,
// https://github.com/w3c/csswg-drafts/issues/13614
if (limit_single_component_ident_to_custom_ident == LimitSingleComponentIdentToCustomIdent::Yes && !is_valid_custom_ident(ident, {}))
return {};
transaction.commit();
return IdentSyntaxNode::create(move(ident));
return IdentSyntaxNode::create(ident, limit_single_component_ident_to_custom_ident == LimitSingleComponentIdentToCustomIdent::Yes ? CaseSensitivity::CaseSensitive : CaseSensitivity::CaseInsensitive);
}
// '<' <syntax-type-name> '>'
@ -82,7 +88,7 @@ static Optional<char> parse_syntax_multiplier(TokenStream<ComponentValue>& token
return {};
}
static OwnPtr<SyntaxNode> parse_syntax_component(TokenStream<ComponentValue>& tokens)
static OwnPtr<SyntaxNode> parse_syntax_component(TokenStream<ComponentValue>& tokens, LimitSingleComponentIdentToCustomIdent limit_single_component_ident_to_custom_ident)
{
// <syntax-component> = <syntax-single-component> <syntax-multiplier>?
// | '<' transform-list '>'
@ -105,7 +111,7 @@ static OwnPtr<SyntaxNode> parse_syntax_component(TokenStream<ComponentValue>& to
}
// <syntax-single-component> <syntax-multiplier>?
auto syntax_single_component = parse_syntax_single_component(tokens);
auto syntax_single_component = parse_syntax_single_component(tokens, limit_single_component_ident_to_custom_ident);
if (!syntax_single_component)
return nullptr;
@ -143,7 +149,7 @@ static Optional<char> parse_syntax_combinator(TokenStream<ComponentValue>& token
}
// https://drafts.csswg.org/css-values-5/#typedef-syntax
OwnPtr<SyntaxNode> parse_as_syntax(Vector<ComponentValue> const& component_values)
OwnPtr<SyntaxNode> parse_as_syntax(Vector<ComponentValue> const& component_values, LimitSingleComponentIdentToCustomIdent limit_single_component_ident_to_custom_ident)
{
// <syntax> = '*' | <syntax-component> [ <syntax-combinator> <syntax-component> ]* | <syntax-string>
// <syntax-component> = <syntax-single-component> <syntax-multiplier>?
@ -182,11 +188,11 @@ OwnPtr<SyntaxNode> parse_as_syntax(Vector<ComponentValue> const& component_value
return nullptr;
auto child_component_values = Parser::create(ParsingParams {}, string).parse_as_list_of_component_values();
return parse_as_syntax(child_component_values);
return parse_as_syntax(child_component_values, limit_single_component_ident_to_custom_ident);
}
// <syntax-component> [ <syntax-combinator> <syntax-component> ]*
auto first = parse_syntax_component(tokens);
auto first = parse_syntax_component(tokens, limit_single_component_ident_to_custom_ident);
if (!first)
return nullptr;
Vector<NonnullOwnPtr<SyntaxNode>> syntax_components;
@ -196,7 +202,7 @@ OwnPtr<SyntaxNode> parse_as_syntax(Vector<ComponentValue> const& component_value
while (tokens.has_next_token()) {
auto combinator = parse_syntax_combinator(tokens);
tokens.discard_whitespace();
auto component = parse_syntax_component(tokens);
auto component = parse_syntax_component(tokens, limit_single_component_ident_to_custom_ident);
tokens.discard_whitespace();
if (!combinator.has_value() || !component) {
dbgln("Failed parsing syntax portion, combinator = `{}`, component = `{}`", combinator, component);
@ -234,7 +240,14 @@ RefPtr<StyleValue const> Parser::parse_according_to_syntax_node(TokenStream<Comp
case SyntaxNode::NodeType::Ident: {
auto const& ident_node = as<IdentSyntaxNode>(syntax_node);
tokens.discard_whitespace();
if (tokens.consume_a_token().is_ident(ident_node.ident())) {
auto token = tokens.consume_a_token();
if (!token.is(Token::Type::Ident))
return nullptr;
auto ident = token.token().ident();
if (ident_node.case_sensitivity() == CaseSensitivity::CaseSensitive ? ident == ident_node.ident() : ident.equals_ignoring_ascii_case(ident_node.ident())) {
transaction.commit();
if (auto keyword = keyword_from_string(ident_node.ident()); keyword.has_value())
return KeywordStyleValue::create(keyword.release_value());

View file

@ -14,7 +14,11 @@
namespace Web::CSS::Parser {
WEB_API OwnPtr<SyntaxNode> parse_as_syntax(Vector<ComponentValue> const&);
enum class LimitSingleComponentIdentToCustomIdent : u8 {
No,
Yes,
};
WEB_API OwnPtr<SyntaxNode> parse_as_syntax(Vector<ComponentValue> const&, LimitSingleComponentIdentToCustomIdent = LimitSingleComponentIdentToCustomIdent::No);
NonnullRefPtr<StyleValue const> parse_with_a_syntax(ParsingParams const&, Vector<ComponentValue> const& input, SyntaxNode const& syntax);

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 106 tests
100 Pass
6 Fail
102 Pass
4 Fail
Pass Attribute 'syntax' returns expected value for ["<color>"]
Pass Attribute 'syntax' returns expected value for ["<color> | none"]
Pass Attribute 'syntax' returns expected value for ["<color># | <image> | none"]
@ -18,8 +18,8 @@ Pass Attribute 'syntax' returns expected value for ["red"]
Pass Attribute 'syntax' makes the @property rule invalid for ["rgb(255, 0, 0)"]
Pass Attribute 'syntax' makes the @property rule invalid for [<color>]
Pass Attribute 'syntax' makes the @property rule invalid for [foo | bar]
Fail Attribute 'syntax' makes the @property rule invalid for ["default"]
Fail Attribute 'syntax' makes the @property rule invalid for ["Default"]
Pass Attribute 'syntax' makes the @property rule invalid for ["default"]
Pass Attribute 'syntax' makes the @property rule invalid for ["Default"]
Pass Attribute 'syntax' makes the @property rule invalid for ["initial"]
Pass Attribute 'syntax' makes the @property rule invalid for ["Initial"]
Pass Attribute 'syntax' makes the @property rule invalid for ["inherit"]

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 239 tests
217 Pass
22 Fail
226 Pass
13 Fail
Pass syntax:'*', initialValue:'a' is valid
Pass syntax:' * ', initialValue:'b' is valid
Pass syntax:'<length>', initialValue:'2px' is valid
@ -173,12 +173,12 @@ Pass syntax:'inherit', initialValue:'inherit' is invalid
Pass syntax:'unset', initialValue:'unset' is invalid
Pass syntax:'revert', initialValue:'revert' is invalid
Pass syntax:'revert-layer', initialValue:'revert-layer' is invalid
Fail syntax:'default', initialValue:'default' is invalid
Fail syntax:'<length>|initial', initialValue:'10px' is invalid
Fail syntax:'<length>|INHERIT', initialValue:'10px' is invalid
Fail syntax:'<percentage>|unsEt', initialValue:'2%' is invalid
Fail syntax:'<color>|REVert', initialValue:'red' is invalid
Fail syntax:'<integer>|deFAUlt', initialValue:'1' is invalid
Pass syntax:'default', initialValue:'default' is invalid
Pass syntax:'<length>|initial', initialValue:'10px' is invalid
Pass syntax:'<length>|INHERIT', initialValue:'10px' is invalid
Pass syntax:'<percentage>|unsEt', initialValue:'2%' is invalid
Pass syntax:'<color>|REVert', initialValue:'red' is invalid
Pass syntax:'<integer>|deFAUlt', initialValue:'1' is invalid
Fail syntax:'*', initialValue:'initial' is invalid
Fail syntax:'*', initialValue:'inherit' is invalid
Fail syntax:'*', initialValue:'unset' is invalid
@ -205,7 +205,7 @@ Fail syntax:'*', initialValue:'url(moo '')' is invalid
Pass syntax:'*', initialValue:'semi;colon' is invalid
Fail syntax:'*', initialValue:'var(invalid var ref)' is invalid
Fail syntax:'*', initialValue:'var(--foo)' is invalid
Fail syntax:'banana', initialValue:'bAnAnA' is invalid
Pass syntax:'banana', initialValue:'bAnAnA' is invalid
Pass syntax:'<length>', initialValue:'var(--moo)' is invalid
Pass syntax:'<length>', initialValue:'10' is invalid
Pass syntax:'<length>', initialValue:'10%' is invalid
@ -223,8 +223,8 @@ Pass syntax:'<percentage>', initialValue:'0' is invalid
Pass syntax:'<integer>', initialValue:'1.0' is invalid
Pass syntax:'<integer>', initialValue:'1e0' is invalid
Pass syntax:'<number>|foo', initialValue:'foo var(--foo, bla)' is invalid
Fail syntax:'Foo | bar', initialValue:'foo' is invalid
Fail syntax:'Foo | bar', initialValue:'Bar' is invalid
Pass syntax:'Foo | bar', initialValue:'foo' is invalid
Pass syntax:'Foo | bar', initialValue:'Bar' is invalid
Pass syntax:'<angle>', initialValue:'0' is invalid
Pass syntax:'<angle>', initialValue:'10%' is invalid
Pass syntax:'<time>', initialValue:'2px' is invalid