diff --git a/Documentation/CSSGeneratedFiles.md b/Documentation/CSSGeneratedFiles.md index 4607083a2eb..7128f044834 100644 --- a/Documentation/CSSGeneratedFiles.md +++ b/Documentation/CSSGeneratedFiles.md @@ -127,13 +127,15 @@ 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. | -| `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. | +| Field | Required | Default | Description | +|----------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `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 diff --git a/Libraries/LibWeb/Animations/KeyframeEffect.cpp b/Libraries/LibWeb/Animations/KeyframeEffect.cpp index e3b7b7d5ab9..306cc0f4d6f 100644 --- a/Libraries/LibWeb/Animations/KeyframeEffect.cpp +++ b/Libraries/LibWeb/Animations/KeyframeEffect.cpp @@ -778,7 +778,7 @@ Optional 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 diff --git a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp index 02bd813e04b..fcbbe94db00 100644 --- a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp @@ -401,46 +401,104 @@ Parser::ParseErrorOr 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(); - - 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() } - }; + 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(); } - // 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()) { + if (pseudo_element.has_value()) { + auto metadata = pseudo_element_metadata(*pseudo_element); + // :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; } + 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: { + // = '*' | + // 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 in :{}() - expected `*` or ``, 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 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 { .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 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; diff --git a/Libraries/LibWeb/CSS/PseudoElements.json b/Libraries/LibWeb/CSS/PseudoElements.json index f2847166fb6..ba7d3a75997 100644 --- a/Libraries/LibWeb/CSS/PseudoElements.json +++ b/Libraries/LibWeb/CSS/PseudoElements.json @@ -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": "" + }, + "view-transition-image-pair": { + "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-image-pair", + "type": "function", + "function-syntax": "" + }, + "view-transition-new": { + "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-new", + "type": "function", + "function-syntax": "" + }, + "view-transition-old": { + "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-old", + "type": "function", + "function-syntax": "" } } diff --git a/Libraries/LibWeb/CSS/Selector.cpp b/Libraries/LibWeb/CSS/Selector.cpp index 2e095ef77da..4521db02cfb 100644 --- a/Libraries/LibWeb/CSS/Selector.cpp +++ b/Libraries/LibWeb/CSS/Selector.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2018-2020, Andreas Kling - * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2021-2025, Sam Atkins * * 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()); } } } diff --git a/Libraries/LibWeb/CSS/Selector.h b/Libraries/LibWeb/CSS/Selector.h index 8679aa33331..ac206fc07eb 100644 --- a/Libraries/LibWeb/CSS/Selector.h +++ b/Libraries/LibWeb/CSS/Selector.h @@ -25,15 +25,24 @@ class Selector : public RefCounted { public: class PseudoElementSelector { public: - explicit PseudoElementSelector(PseudoElement type) + struct PTNameSelector { + bool is_universal { false }; + FlyString value {}; + }; + + using Value = Variant; + + 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(); } + private: PseudoElement m_type; String m_name; + Variant m_value; }; struct SimpleSelector { diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index 54793304b92..76aad860870 100644 --- a/Libraries/LibWeb/Dump.cpp +++ b/Libraries/LibWeb/Dump.cpp @@ -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) { diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp index 2a1659ea498..c6432d4d201 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp @@ -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 == ""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 to_generated_pseudo_element(PseudoElement pseudo_element) { switch (pseudo_element) {