LibWeb: Implement functional pseudo-element parsing

"Functional" as in "it's a function token" and not "it works", because
the behaviour for these is unimplemented. :^)

This is modeled after the pseudo-class parsing, but with some changes
based on things I don't like about that implementation. I've
implemented the `<pt-name-selector>` parameter used by view-transitions
for now, but nothing else.
This commit is contained in:
Sam Atkins 2025-03-24 13:56:24 +00:00 committed by Andreas Kling
parent 5cf04a33ad
commit 88e11eea2d
Notes: github-actions[bot] 2025-03-25 07:56:12 +00:00
8 changed files with 237 additions and 43 deletions

View file

@ -127,13 +127,15 @@ This generated `PsuedoElement.h` and `PseudoElement.cpp`.
Each entry has the following properties: Each entry has the following properties:
| Field | Required | Default | Description | | Field | Required | Default | Description |
|----------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `spec` | No | Nothing | Link to the spec definition, for reference. Not used in generated code. | | `alias-for` | No | Nothing | Use to specify that this should be treated as an alias for the named pseudo-element. |
| `alias-for` | No | Nothing | Use to specify that this should be treated as an alias for the named pseudo-element. | | `function-syntax` | No | Nothing | Syntax for the function arguments if this is a function-type pseudo-element. Copied directly from the spec. |
| `is-generated` | No | `false` | Whether this is a [generated pseudo-element.](https://drafts.csswg.org/css-pseudo-4/#generated-content) | | `is-generated` | No | `false` | Whether this is a [generated pseudo-element.](https://drafts.csswg.org/css-pseudo-4/#generated-content) |
| `is-allowed-in-has` | No | `false` | Whether this is a [`:has`-allowed pseudo-element.](https://drafts.csswg.org/selectors/#has-allowed-pseudo-element) | | `is-allowed-in-has` | No | `false` | Whether this is a [`:has`-allowed pseudo-element.](https://drafts.csswg.org/selectors/#has-allowed-pseudo-element) |
| `property-whitelist` | No | Nothing | Some pseudo-elements only permit certain properties. If so, name them in an array here. Some special values are allowed here for categories of properties - see below. | | `property-whitelist` | No | Nothing | Some pseudo-elements only permit certain properties. If so, name them in an array here. Some special values are allowed here for categories of properties - see below. |
| `spec` | No | Nothing | Link to the spec definition, for reference. Not used in generated code. |
| `type` | No | `"identifier"` | What type of pseudo-element is this. Either "identifier", "function", or "both". |
The generated code provides: The generated code provides:
- A `PseudoElement` enum listing every pseudo-element name - A `PseudoElement` enum listing every pseudo-element name

View file

@ -778,7 +778,7 @@ Optional<String> KeyframeEffect::pseudo_element() const
{ {
if (!m_target_pseudo_selector.has_value()) if (!m_target_pseudo_selector.has_value())
return {}; return {};
return MUST(String::formatted("::{}", m_target_pseudo_selector->name())); return m_target_pseudo_selector->serialize();
} }
// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudoelement // https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudoelement

View file

@ -401,46 +401,104 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
if (peek_token_ends_selector()) if (peek_token_ends_selector())
return ParseError::SyntaxError; return ParseError::SyntaxError;
bool is_pseudo = false; // Note that we already consumed one colon before we entered this function.
// FIXME: Don't do that.
bool is_pseudo_element = false;
if (tokens.next_token().is(Token::Type::Colon)) { if (tokens.next_token().is(Token::Type::Colon)) {
is_pseudo = true; is_pseudo_element = true;
tokens.discard_a_token(); tokens.discard_a_token();
if (peek_token_ends_selector()) if (peek_token_ends_selector())
return ParseError::SyntaxError; return ParseError::SyntaxError;
} }
if (is_pseudo) { if (is_pseudo_element) {
auto const& name_token = tokens.consume_a_token(); auto const& name_token = tokens.consume_a_token();
if (!name_token.is(Token::Type::Ident)) { bool is_function = false;
dbgln_if(CSS_PARSER_DEBUG, "Expected an ident for pseudo-element, got: '{}'", name_token.to_debug_string()); FlyString pseudo_name;
if (name_token.is(Token::Type::Ident)) {
pseudo_name = name_token.token().ident();
} else if (name_token.is_function()) {
pseudo_name = name_token.function().name;
is_function = true;
} else {
dbgln_if(CSS_PARSER_DEBUG, "Expected an ident or function token for pseudo-element, got: '{}'", name_token.to_debug_string());
return ParseError::SyntaxError; return ParseError::SyntaxError;
} }
auto pseudo_name = name_token.token().ident(); bool is_aliased_pseudo = false;
auto pseudo_element = pseudo_element_from_string(pseudo_name);
if (auto pseudo_element = pseudo_element_from_string(pseudo_name); pseudo_element.has_value()) { if (!pseudo_element.has_value()) {
// :has() is fussy about pseudo-elements inside it pseudo_element = aliased_pseudo_element_from_string(pseudo_name);
if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(*pseudo_element)) { is_aliased_pseudo = pseudo_element.has_value();
return ParseError::SyntaxError;
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoElement,
.value = Selector::PseudoElementSelector { pseudo_element.release_value() }
};
} }
// Aliased pseudo-elements behave like their target pseudo-element, but serialize as themselves. So store their if (pseudo_element.has_value()) {
// name like we do for unknown -webkit pseudos below. auto metadata = pseudo_element_metadata(*pseudo_element);
if (auto pseudo_element = aliased_pseudo_element_from_string(pseudo_name); pseudo_element.has_value()) {
// :has() is fussy about pseudo-elements inside it // :has() is fussy about pseudo-elements inside it
if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(*pseudo_element)) { if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(*pseudo_element)) {
return ParseError::SyntaxError; return ParseError::SyntaxError;
} }
Selector::PseudoElementSelector::Value value = Empty {};
if (is_function) {
if (!metadata.is_valid_as_function) {
dbgln_if(CSS_PARSER_DEBUG, "Pseudo-element '::{}()' is not valid as a function.", pseudo_name);
return ParseError::SyntaxError;
}
// Parse arguments
TokenStream function_tokens { name_token.function().value };
function_tokens.discard_whitespace();
switch (metadata.parameter_type) {
case PseudoElementMetadata::ParameterType::None:
if (function_tokens.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "Pseudo-element '::{}()' invalid: Should have no arguments.", pseudo_name);
return ParseError::SyntaxError;
}
break;
case PseudoElementMetadata::ParameterType::PTNameSelector: {
// <pt-name-selector> = '*' | <custom-ident>
// https://drafts.csswg.org/css-view-transitions-1/#typedef-pt-name-selector
if (function_tokens.next_token().is_delim('*')) {
function_tokens.discard_a_token(); // *
value = Selector::PseudoElementSelector::PTNameSelector { .is_universal = true };
} else if (auto custom_ident = parse_custom_ident(function_tokens, {}); custom_ident.has_value()) {
value = Selector::PseudoElementSelector::PTNameSelector { .value = custom_ident.release_value() };
} else {
dbgln_if(CSS_PARSER_DEBUG, "Invalid <pt-name-selector> in :{}() - expected `*` or `<custom-ident>`, got `{}`", pseudo_name, function_tokens.next_token().to_debug_string());
return ParseError::SyntaxError;
}
function_tokens.discard_whitespace();
if (function_tokens.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "Invalid <pt-name-selector> in :{}() - trailing tokens", pseudo_name);
return ParseError::SyntaxError;
}
break;
}
}
} else {
if (!metadata.is_valid_as_identifier) {
dbgln_if(CSS_PARSER_DEBUG, "Pseudo-element '::{}' is not valid as an identifier.", pseudo_name);
return ParseError::SyntaxError;
}
}
// Aliased pseudo-elements behave like their target pseudo-element, but serialize as themselves. So store their
// name like we do for unknown -webkit pseudos below.
if (is_aliased_pseudo) {
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoElement,
.value = Selector::PseudoElementSelector { pseudo_element.release_value(), pseudo_name.to_string().to_ascii_lowercase(), move(value) }
};
}
return Selector::SimpleSelector { return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoElement, .type = Selector::SimpleSelector::Type::PseudoElement,
.value = Selector::PseudoElementSelector { pseudo_element.release_value(), pseudo_name.to_string().to_ascii_lowercase() } .value = Selector::PseudoElementSelector { pseudo_element.release_value(), move(value) }
}; };
} }
@ -449,7 +507,7 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
// and that are not functional notations must be treated as valid at parse time. (That is, ::-webkit-asdf is // and that are not functional notations must be treated as valid at parse time. (That is, ::-webkit-asdf is
// valid at parse time, but ::-webkit-jkl() is not.) If theyre not otherwise recognized and supported, they // valid at parse time, but ::-webkit-jkl() is not.) If theyre not otherwise recognized and supported, they
// must be treated as matching nothing, and are unknown -webkit- pseudo-elements. // must be treated as matching nothing, and are unknown -webkit- pseudo-elements.
if (pseudo_name.starts_with_bytes("-webkit-"sv, CaseSensitivity::CaseInsensitive)) { if (!is_function && pseudo_name.starts_with_bytes("-webkit-"sv, CaseSensitivity::CaseInsensitive)) {
// :has() only allows a limited set of pseudo-elements inside it, which doesn't include unknown ones. // :has() only allows a limited set of pseudo-elements inside it, which doesn't include unknown ones.
if (m_pseudo_class_context.contains_slow(PseudoClass::Has)) if (m_pseudo_class_context.contains_slow(PseudoClass::Has))
return ParseError::SyntaxError; return ParseError::SyntaxError;

View file

@ -111,5 +111,28 @@
}, },
"track": { "track": {
"spec": "https://drafts.csswg.org/css-forms-1/#selectordef-track" "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-track"
},
"view-transition": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition"
},
"view-transition-group": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-group",
"type": "function",
"function-syntax": "<pt-name-selector>"
},
"view-transition-image-pair": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-image-pair",
"type": "function",
"function-syntax": "<pt-name-selector>"
},
"view-transition-new": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-new",
"type": "function",
"function-syntax": "<pt-name-selector>"
},
"view-transition-old": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-old",
"type": "function",
"function-syntax": "<pt-name-selector>"
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org> * Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -285,6 +285,31 @@ u32 Selector::specificity() const
return *m_specificity; return *m_specificity;
} }
String Selector::PseudoElementSelector::serialize() const
{
StringBuilder builder;
builder.append("::"sv);
if (!m_name.is_empty()) {
builder.append(m_name);
} else {
builder.append(pseudo_element_name(m_type));
}
m_value.visit(
[&builder](PTNameSelector const& pt_name_selector) {
builder.append('(');
if (pt_name_selector.is_universal)
builder.append('*');
else
builder.append(pt_name_selector.value);
builder.append(')');
},
[](Empty const&) {});
return builder.to_string_without_validation();
}
// https://www.w3.org/TR/cssom/#serialize-a-simple-selector // https://www.w3.org/TR/cssom/#serialize-a-simple-selector
String Selector::SimpleSelector::serialize() const String Selector::SimpleSelector::serialize() const
{ {
@ -519,8 +544,7 @@ String Selector::serialize() const
// 4. If this is the last part of the chain of the selector and there is a pseudo-element, // 4. If this is the last part of the chain of the selector and there is a pseudo-element,
// append "::" followed by the name of the pseudo-element, to s. // append "::" followed by the name of the pseudo-element, to s.
if (compound_selector.simple_selectors.last().type == Selector::SimpleSelector::Type::PseudoElement) { if (compound_selector.simple_selectors.last().type == Selector::SimpleSelector::Type::PseudoElement) {
s.append("::"sv); s.append(compound_selector.simple_selectors.last().pseudo_element().serialize());
s.append(compound_selector.simple_selectors.last().pseudo_element().name());
} }
} }
} }

View file

@ -25,15 +25,24 @@ class Selector : public RefCounted<Selector> {
public: public:
class PseudoElementSelector { class PseudoElementSelector {
public: public:
explicit PseudoElementSelector(PseudoElement type) struct PTNameSelector {
bool is_universal { false };
FlyString value {};
};
using Value = Variant<Empty, PTNameSelector>;
explicit PseudoElementSelector(PseudoElement type, Value value = {})
: m_type(type) : m_type(type)
, m_value(move(value))
{ {
VERIFY(is_known_pseudo_element_type(type)); VERIFY(is_known_pseudo_element_type(type));
} }
PseudoElementSelector(PseudoElement type, String name) PseudoElementSelector(PseudoElement type, String name, Value value = {})
: m_type(type) : m_type(type)
, m_name(move(name)) , m_name(move(name))
, m_value(move(value))
{ {
} }
@ -44,19 +53,16 @@ public:
return to_underlying(type) < to_underlying(PseudoElement::KnownPseudoElementCount); return to_underlying(type) < to_underlying(PseudoElement::KnownPseudoElementCount);
} }
StringView name() const String serialize() const;
{
if (!m_name.is_empty())
return m_name;
return pseudo_element_name(m_type);
}
PseudoElement type() const { return m_type; } PseudoElement type() const { return m_type; }
PTNameSelector const& pt_name_selector() const { return m_value.get<PTNameSelector>(); }
private: private:
PseudoElement m_type; PseudoElement m_type;
String m_name; String m_name;
Variant<Empty, PTNameSelector> m_value;
}; };
struct SimpleSelector { struct SimpleSelector {

View file

@ -586,7 +586,19 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector, int in
} }
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoElement) { if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoElement) {
builder.appendff(" pseudo_element={}", simple_selector.pseudo_element().name()); auto const& pseudo_element = simple_selector.pseudo_element();
builder.appendff(" pseudo_element={}", CSS::pseudo_element_name(pseudo_element.type()));
auto pseudo_element_metadata = CSS::pseudo_element_metadata(pseudo_element.type());
switch (pseudo_element_metadata.parameter_type) {
case CSS::PseudoElementMetadata::ParameterType::None:
break;
case CSS::PseudoElementMetadata::ParameterType::PTNameSelector: {
auto const& [is_universal, value] = pseudo_element.pt_name_selector();
builder.appendff("(is_universal={}, value='{}')", is_universal, value);
break;
}
}
} }
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::Attribute) { if (simple_selector.type == CSS::Selector::SimpleSelector::Type::Attribute) {

View file

@ -88,6 +88,16 @@ StringView pseudo_element_name(PseudoElement);
bool is_has_allowed_pseudo_element(PseudoElement); bool is_has_allowed_pseudo_element(PseudoElement);
bool pseudo_element_supports_property(PseudoElement, PropertyID); bool pseudo_element_supports_property(PseudoElement, PropertyID);
struct PseudoElementMetadata {
enum class ParameterType {
None,
PTNameSelector,
} parameter_type;
bool is_valid_as_function;
bool is_valid_as_identifier;
};
PseudoElementMetadata pseudo_element_metadata(PseudoElement);
enum class GeneratedPseudoElement : @generated_pseudo_element_underlying_type@ { enum class GeneratedPseudoElement : @generated_pseudo_element_underlying_type@ {
)~~~"); )~~~");
pseudo_elements_data.for_each_member([&](auto& name, JsonValue const& value) { pseudo_elements_data.for_each_member([&](auto& name, JsonValue const& value) {
@ -463,6 +473,65 @@ bool pseudo_element_supports_property(PseudoElement pseudo_element, PropertyID p
} }
} }
PseudoElementMetadata pseudo_element_metadata(PseudoElement pseudo_element)
{
switch (pseudo_element) {
)~~~");
pseudo_elements_data.for_each_member([&](auto& name, JsonValue const& value) {
auto& pseudo_element = value.as_object();
if (pseudo_element.has("alias-for"sv))
return;
bool is_valid_as_function = false;
bool is_valid_as_identifier = false;
auto const& type = pseudo_element.get_string("type"sv);
if (type == "function"sv) {
is_valid_as_function = true;
} else if (type == "both"sv) {
is_valid_as_function = true;
is_valid_as_identifier = true;
} else {
is_valid_as_identifier = true;
}
String parameter_type = "None"_string;
if (is_valid_as_function) {
auto const& function_syntax = pseudo_element.get_string("function-syntax"sv).value();
if (function_syntax == "<pt-name-selector>"sv) {
parameter_type = "PTNameSelector"_string;
} else {
warnln("Unrecognized pseudo-element parameter type: `{}`", function_syntax);
VERIFY_NOT_REACHED();
}
} else if (pseudo_element.has("function-syntax"sv)) {
warnln("Pseudo-element `::{}` has `function-syntax` but is not a function type.", name);
VERIFY_NOT_REACHED();
}
auto member_generator = generator.fork();
member_generator.set("name:titlecase", title_casify(name));
member_generator.set("parameter_type", parameter_type);
member_generator.set("is_valid_as_function", is_valid_as_function ? "true"_string : "false"_string);
member_generator.set("is_valid_as_identifier", is_valid_as_identifier ? "true"_string : "false"_string);
member_generator.append(R"~~~(
case PseudoElement::@name:titlecase@:
return {
.parameter_type = PseudoElementMetadata::ParameterType::@parameter_type@,
.is_valid_as_function = @is_valid_as_function@,
.is_valid_as_identifier = @is_valid_as_identifier@,
};
)~~~");
});
generator.append(R"~~~(
case PseudoElement::KnownPseudoElementCount:
case PseudoElement::UnknownWebKit:
break;
}
VERIFY_NOT_REACHED();
}
Optional<GeneratedPseudoElement> to_generated_pseudo_element(PseudoElement pseudo_element) Optional<GeneratedPseudoElement> to_generated_pseudo_element(PseudoElement pseudo_element)
{ {
switch (pseudo_element) { switch (pseudo_element) {