mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-05-18 17:12:54 +00:00
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:
parent
5cf04a33ad
commit
88e11eea2d
Notes:
github-actions[bot]
2025-03-25 07:56:12 +00:00
Author: https://github.com/AtkinsSJ
Commit: 88e11eea2d
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4063
8 changed files with 237 additions and 43 deletions
|
@ -128,12 +128,14 @@ This generated `PsuedoElement.h` and `PseudoElement.cpp`.
|
|||
Each entry has the following properties:
|
||||
|
||||
| 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. |
|
||||
| `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-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. |
|
||||
| `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:
|
||||
- A `PseudoElement` enum listing every pseudo-element name
|
||||
|
|
|
@ -778,7 +778,7 @@ Optional<String> KeyframeEffect::pseudo_element() const
|
|||
{
|
||||
if (!m_target_pseudo_selector.has_value())
|
||||
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
|
||||
|
|
|
@ -401,46 +401,104 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
|
|||
if (peek_token_ends_selector())
|
||||
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)) {
|
||||
is_pseudo = true;
|
||||
is_pseudo_element = true;
|
||||
tokens.discard_a_token();
|
||||
if (peek_token_ends_selector())
|
||||
return ParseError::SyntaxError;
|
||||
}
|
||||
|
||||
if (is_pseudo) {
|
||||
if (is_pseudo_element) {
|
||||
auto const& name_token = tokens.consume_a_token();
|
||||
if (!name_token.is(Token::Type::Ident)) {
|
||||
dbgln_if(CSS_PARSER_DEBUG, "Expected an ident for pseudo-element, got: '{}'", name_token.to_debug_string());
|
||||
bool is_function = false;
|
||||
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;
|
||||
}
|
||||
|
||||
auto pseudo_name = name_token.token().ident();
|
||||
bool is_aliased_pseudo = false;
|
||||
auto pseudo_element = pseudo_element_from_string(pseudo_name);
|
||||
if (!pseudo_element.has_value()) {
|
||||
pseudo_element = aliased_pseudo_element_from_string(pseudo_name);
|
||||
is_aliased_pseudo = pseudo_element.has_value();
|
||||
}
|
||||
|
||||
if (pseudo_element.has_value()) {
|
||||
auto metadata = pseudo_element_metadata(*pseudo_element);
|
||||
|
||||
if (auto pseudo_element = pseudo_element_from_string(pseudo_name); pseudo_element.has_value()) {
|
||||
// :has() is fussy about pseudo-elements inside it
|
||||
if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(*pseudo_element)) {
|
||||
return ParseError::SyntaxError;
|
||||
}
|
||||
|
||||
return Selector::SimpleSelector {
|
||||
.type = Selector::SimpleSelector::Type::PseudoElement,
|
||||
.value = Selector::PseudoElementSelector { pseudo_element.release_value() }
|
||||
};
|
||||
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 (auto pseudo_element = aliased_pseudo_element_from_string(pseudo_name); pseudo_element.has_value()) {
|
||||
// :has() is fussy about pseudo-elements inside it
|
||||
if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(*pseudo_element)) {
|
||||
return ParseError::SyntaxError;
|
||||
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 {
|
||||
.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
|
||||
// valid at parse time, but ::-webkit-jkl() is not.) If they’re not otherwise recognized and supported, they
|
||||
// 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.
|
||||
if (m_pseudo_class_context.contains_slow(PseudoClass::Has))
|
||||
return ParseError::SyntaxError;
|
||||
|
|
|
@ -111,5 +111,28 @@
|
|||
},
|
||||
"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>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* 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
|
||||
*/
|
||||
|
@ -285,6 +285,31 @@ u32 Selector::specificity() const
|
|||
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
|
||||
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,
|
||||
// append "::" followed by the name of the pseudo-element, to s.
|
||||
if (compound_selector.simple_selectors.last().type == Selector::SimpleSelector::Type::PseudoElement) {
|
||||
s.append("::"sv);
|
||||
s.append(compound_selector.simple_selectors.last().pseudo_element().name());
|
||||
s.append(compound_selector.simple_selectors.last().pseudo_element().serialize());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,15 +25,24 @@ class Selector : public RefCounted<Selector> {
|
|||
public:
|
||||
class PseudoElementSelector {
|
||||
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_value(move(value))
|
||||
{
|
||||
VERIFY(is_known_pseudo_element_type(type));
|
||||
}
|
||||
|
||||
PseudoElementSelector(PseudoElement type, String name)
|
||||
PseudoElementSelector(PseudoElement type, String name, Value value = {})
|
||||
: m_type(type)
|
||||
, m_name(move(name))
|
||||
, m_value(move(value))
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -44,19 +53,16 @@ public:
|
|||
return to_underlying(type) < to_underlying(PseudoElement::KnownPseudoElementCount);
|
||||
}
|
||||
|
||||
StringView name() const
|
||||
{
|
||||
if (!m_name.is_empty())
|
||||
return m_name;
|
||||
|
||||
return pseudo_element_name(m_type);
|
||||
}
|
||||
String serialize() const;
|
||||
|
||||
PseudoElement type() const { return m_type; }
|
||||
|
||||
PTNameSelector const& pt_name_selector() const { return m_value.get<PTNameSelector>(); }
|
||||
|
||||
private:
|
||||
PseudoElement m_type;
|
||||
String m_name;
|
||||
Variant<Empty, PTNameSelector> m_value;
|
||||
};
|
||||
|
||||
struct SimpleSelector {
|
||||
|
|
|
@ -586,7 +586,19 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector, int in
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -88,6 +88,16 @@ StringView pseudo_element_name(PseudoElement);
|
|||
bool is_has_allowed_pseudo_element(PseudoElement);
|
||||
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@ {
|
||||
)~~~");
|
||||
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)
|
||||
{
|
||||
switch (pseudo_element) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue