From d461e96f40030c5d21efcbe9fb2c35c9fa591d48 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 28 Aug 2025 10:29:57 +0100 Subject: [PATCH] LibWeb/CSS: Make :heading() pseudo-class take integers not AN+B Corresponds to https://github.com/w3c/csswg-drafts/commit/8eb3787e34fff15b12b110e2492aa414d82ef965 --- .../LibWeb/CSS/Parser/SelectorParsing.cpp | 43 ++++++ Libraries/LibWeb/CSS/PseudoClasses.json | 2 +- Libraries/LibWeb/CSS/Selector.cpp | 6 +- Libraries/LibWeb/CSS/Selector.h | 3 + Libraries/LibWeb/CSS/SelectorEngine.cpp | 11 +- Libraries/LibWeb/Dump.cpp | 6 + .../LibWeb/GenerateCSSPseudoClass.cpp | 3 + .../wpt-import/css/selectors/heading.txt | 132 +----------------- .../css/selectors/parsing/parse-heading.txt | 25 ++-- .../wpt-import/css/selectors/heading.html | 9 -- 10 files changed, 79 insertions(+), 161 deletions(-) diff --git a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp index 2883b4c38fb..673e3247711 100644 --- a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp @@ -899,6 +899,49 @@ Parser::ParseErrorOr Parser::parse_pseudo_simple_selec .languages = move(languages) } }; } + case PseudoClassMetadata::ParameterType::LevelList: { + // https://drafts.csswg.org/selectors-5/#heading-functional-pseudo + // :heading() = :heading( # ) + // where is a with its type flag set to "integer". + Vector levels; + auto function_token_stream = TokenStream(pseudo_function.value); + auto level_lists = parse_a_comma_separated_list_of_component_values(function_token_stream); + + for (auto const& level_tokens : level_lists) { + TokenStream level_token_stream { level_tokens }; + level_token_stream.discard_whitespace(); + auto& maybe_integer = level_token_stream.consume_a_token(); + level_token_stream.discard_whitespace(); + + if (!maybe_integer.is(Token::Type::Number) || !maybe_integer.token().number().is_integer()) { + ErrorReporter::the().report(InvalidPseudoClassOrElementError { + .name = MUST(String::formatted(":{}", pseudo_function.name)), + .value_string = pseudo_class_token.to_string(), + .description = "Failed to parse argument as a : Not an literal."_string, + }); + return ParseError::SyntaxError; + } + + if (level_token_stream.has_next_token()) { + ErrorReporter::the().report(InvalidPseudoClassOrElementError { + .name = MUST(String::formatted(":{}", pseudo_function.name)), + .value_string = pseudo_class_token.to_string(), + .description = "Failed to parse argument as a : Has trailing tokens."_string, + }); + return ParseError::SyntaxError; + } + + levels.append(maybe_integer.token().number().integer_value()); + } + + return Selector::SimpleSelector { + .type = Selector::SimpleSelector::Type::PseudoClass, + .value = Selector::SimpleSelector::PseudoClassSelector { + .type = pseudo_class, + .levels = move(levels), + } + }; + } case PseudoClassMetadata::ParameterType::RelativeSelectorList: case PseudoClassMetadata::ParameterType::SelectorList: { auto function_token_stream = TokenStream(pseudo_function.value); diff --git a/Libraries/LibWeb/CSS/PseudoClasses.json b/Libraries/LibWeb/CSS/PseudoClasses.json index 4048796dddb..d4e2d8cac9b 100644 --- a/Libraries/LibWeb/CSS/PseudoClasses.json +++ b/Libraries/LibWeb/CSS/PseudoClasses.json @@ -51,7 +51,7 @@ "argument": "" }, "heading": { - "argument": "#?" + "argument": "#?" }, "high-value": { "argument": "" diff --git a/Libraries/LibWeb/CSS/Selector.cpp b/Libraries/LibWeb/CSS/Selector.cpp index 4f2a99d2a30..d4d4eb354f8 100644 --- a/Libraries/LibWeb/CSS/Selector.cpp +++ b/Libraries/LibWeb/CSS/Selector.cpp @@ -435,7 +435,7 @@ String Selector::SimpleSelector::serialize() const // For pseudo-classes with both a function and identifier form, see if they have arguments. switch (pseudo_class.type) { case PseudoClass::Heading: - return !pseudo_class.an_plus_b_patterns.is_empty(); + return !pseudo_class.levels.is_empty(); case PseudoClass::Host: return !pseudo_class.argument_selector_list.is_empty(); default: @@ -484,6 +484,10 @@ String Selector::SimpleSelector::serialize() const // The serialization of a comma-separated list of each argument’s serialization as a string, preserving relative order. s.join(", "sv, pseudo_class.languages); break; + case PseudoClassMetadata::ParameterType::LevelList: + // AD-HOC: not in the spec. + s.join(", "sv, pseudo_class.levels); + break; } s.append(')'); } diff --git a/Libraries/LibWeb/CSS/Selector.h b/Libraries/LibWeb/CSS/Selector.h index 1f6860f77c4..8a908f2b0d6 100644 --- a/Libraries/LibWeb/CSS/Selector.h +++ b/Libraries/LibWeb/CSS/Selector.h @@ -109,6 +109,9 @@ public: FlyString string_value; }; Optional ident {}; + + // Used by :heading() + Vector levels {}; }; struct Name { diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index ad768939dee..f6978eb6c8b 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -1037,7 +1037,7 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla // The :heading pseudo-class must match all h1, h2, h3, h4, h5, and h6 elements. // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-heading-functional - // The :heading(An+B#) pseudo-class must match all h1, h2, h3, h4, h5, and h6 elements that have a heading level among An+B. [CSSSYNTAX] [CSSVALUES] + // The :heading(integer#) pseudo-class must match all h1, h2, h3, h4, h5, and h6 elements that have a heading level of integer. [CSSSYNTAX] [CSSVALUES] // NB: We combine the "is this an h* element?" and "what is it's level?" checks together here. if (!element.is_html_element()) @@ -1061,15 +1061,10 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla if (!heading_level.has_value()) return false; - if (pseudo_class.an_plus_b_patterns.is_empty()) + if (pseudo_class.levels.is_empty()) return true; - for (auto const& an_plus_b_pattern : pseudo_class.an_plus_b_patterns) { - if (an_plus_b_pattern.matches(heading_level.value())) - return true; - } - - return false; + return pseudo_class.levels.contains_slow(heading_level.value()); } } diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index b9ebcd7f69f..ce9bcca9165 100644 --- a/Libraries/LibWeb/Dump.cpp +++ b/Libraries/LibWeb/Dump.cpp @@ -598,6 +598,12 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector, int in builder.append(')'); break; } + case CSS::PseudoClassMetadata::ParameterType::LevelList: { + builder.append('('); + builder.join(',', pseudo_class.levels); + builder.append(')'); + break; + } } } diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp index 1bf225a9d3c..53ba1e031d7 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoClass.cpp @@ -77,6 +77,7 @@ struct PseudoClassMetadata { ForgivingRelativeSelectorList, Ident, LanguageRanges, + LevelList, RelativeSelectorList, SelectorList, } parameter_type; @@ -183,6 +184,8 @@ PseudoClassMetadata pseudo_class_metadata(PseudoClass pseudo_class) parameter_type = "Ident"_string; } else if (argument_string == ""sv) { parameter_type = "LanguageRanges"_string; + } else if (argument_string == "#"sv) { + parameter_type = "LevelList"_string; } else if (argument_string == ""sv) { parameter_type = "RelativeSelectorList"_string; } else if (argument_string == ""sv) { diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/selectors/heading.txt b/Tests/LibWeb/Text/expected/wpt-import/css/selectors/heading.txt index ac3cfcad8d7..524054c835f 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/selectors/heading.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/selectors/heading.txt @@ -1,8 +1,8 @@ Harness status: OK -Found 308 tests +Found 182 tests -308 Pass +182 Pass Pass

:heading Pass

:heading(1) Pass

:heading(2) @@ -14,17 +14,8 @@ Pass

:heading(7) Pass

:heading(8) Pass

:heading(9) Pass

:heading(0) -Pass

:heading(-1) Pass

:heading(0, 1, 2) Pass

:heading(6, 7) -Pass

:heading(n) -Pass

:heading(2n) -Pass

:heading(2n+1) -Pass

:heading(2n+2) -Pass

:heading(-n+3) -Pass

:heading(2n, 3n) -Pass

:heading(even) -Pass

:heading(odd) Pass

:heading Pass

:heading(1) Pass

:heading(2) @@ -36,17 +27,8 @@ Pass

:heading(7) Pass

:heading(8) Pass

:heading(9) Pass

:heading(0) -Pass

:heading(-1) Pass

:heading(0, 1, 2) Pass

:heading(6, 7) -Pass

:heading(n) -Pass

:heading(2n) -Pass

:heading(2n+1) -Pass

:heading(2n+2) -Pass

:heading(-n+3) -Pass

:heading(2n, 3n) -Pass

:heading(even) -Pass

:heading(odd) Pass

:heading Pass

:heading(1) Pass

:heading(2) @@ -58,17 +40,8 @@ Pass

:heading(7) Pass

:heading(8) Pass

:heading(9) Pass

:heading(0) -Pass

:heading(-1) Pass

:heading(0, 1, 2) Pass

:heading(6, 7) -Pass

:heading(n) -Pass

:heading(2n) -Pass

:heading(2n+1) -Pass

:heading(2n+2) -Pass

:heading(-n+3) -Pass

:heading(2n, 3n) -Pass

:heading(even) -Pass

:heading(odd) Pass

:heading Pass

:heading(1) Pass

:heading(2) @@ -80,17 +53,8 @@ Pass

:heading(7) Pass

:heading(8) Pass

:heading(9) Pass

:heading(0) -Pass

:heading(-1) Pass

:heading(0, 1, 2) Pass

:heading(6, 7) -Pass

:heading(n) -Pass

:heading(2n) -Pass

:heading(2n+1) -Pass

:heading(2n+2) -Pass

:heading(-n+3) -Pass

:heading(2n, 3n) -Pass

:heading(even) -Pass

:heading(odd) Pass
:heading Pass
:heading(1) Pass
:heading(2) @@ -102,17 +66,8 @@ Pass
:heading(7) Pass
:heading(8) Pass
:heading(9) Pass
:heading(0) -Pass
:heading(-1) Pass
:heading(0, 1, 2) Pass
:heading(6, 7) -Pass
:heading(n) -Pass
:heading(2n) -Pass
:heading(2n+1) -Pass
:heading(2n+2) -Pass
:heading(-n+3) -Pass
:heading(2n, 3n) -Pass
:heading(even) -Pass
:heading(odd) Pass
:heading Pass
:heading(1) Pass
:heading(2) @@ -124,17 +79,8 @@ Pass
:heading(7) Pass
:heading(8) Pass
:heading(9) Pass
:heading(0) -Pass
:heading(-1) Pass
:heading(0, 1, 2) Pass
:heading(6, 7) -Pass
:heading(n) -Pass
:heading(2n) -Pass
:heading(2n+1) -Pass
:heading(2n+2) -Pass
:heading(-n+3) -Pass
:heading(2n, 3n) -Pass
:heading(even) -Pass
:heading(odd) Pass :heading Pass :heading(1) Pass :heading(2) @@ -146,17 +92,8 @@ Pass :heading(7) Pass :heading(8) Pass :heading(9) Pass :heading(0) -Pass :heading(-1) Pass :heading(0, 1, 2) Pass :heading(6, 7) -Pass :heading(n) -Pass :heading(2n) -Pass :heading(2n+1) -Pass :heading(2n+2) -Pass :heading(-n+3) -Pass :heading(2n, 3n) -Pass :heading(even) -Pass :heading(odd) Pass :heading Pass :heading(1) Pass :heading(2) @@ -168,17 +105,8 @@ Pass :heading(7) Pass :heading(8) Pass :heading(9) Pass :heading(0) -Pass :heading(-1) Pass :heading(0, 1, 2) Pass :heading(6, 7) -Pass :heading(n) -Pass :heading(2n) -Pass :heading(2n+1) -Pass :heading(2n+2) -Pass :heading(-n+3) -Pass :heading(2n, 3n) -Pass :heading(even) -Pass :heading(odd) Pass :heading Pass :heading(1) Pass :heading(2) @@ -190,17 +118,8 @@ Pass :heading(7) Pass :heading(8) Pass :heading(9) Pass :heading(0) -Pass :heading(-1) Pass :heading(0, 1, 2) Pass :heading(6, 7) -Pass :heading(n) -Pass :heading(2n) -Pass :heading(2n+1) -Pass :heading(2n+2) -Pass :heading(-n+3) -Pass :heading(2n, 3n) -Pass :heading(even) -Pass :heading(odd) Pass :heading Pass :heading(1) Pass :heading(2) @@ -212,17 +131,8 @@ Pass :heading(7) Pass :heading(8) Pass :heading(9) Pass :heading(0) -Pass :heading(-1) Pass :heading(0, 1, 2) Pass :heading(6, 7) -Pass :heading(n) -Pass :heading(2n) -Pass :heading(2n+1) -Pass :heading(2n+2) -Pass :heading(-n+3) -Pass :heading(2n, 3n) -Pass :heading(even) -Pass :heading(odd) Pass

:heading Pass

:heading(1) Pass

:heading(2) @@ -234,17 +144,8 @@ Pass

:heading(7) Pass

:heading(8) Pass

:heading(9) Pass

:heading(0) -Pass

:heading(-1) Pass

:heading(0, 1, 2) Pass

:heading(6, 7) -Pass

:heading(n) -Pass

:heading(2n) -Pass

:heading(2n+1) -Pass

:heading(2n+2) -Pass

:heading(-n+3) -Pass

:heading(2n, 3n) -Pass

:heading(even) -Pass

:heading(odd) Pass

in section :heading Pass

in section :heading(1) Pass

in section :heading(2) @@ -256,17 +157,8 @@ Pass

in section :heading(7) Pass

in section :heading(8) Pass

in section :heading(9) Pass

in section :heading(0) -Pass

in section :heading(-1) Pass

in section :heading(0, 1, 2) Pass

in section :heading(6, 7) -Pass

in section :heading(n) -Pass

in section :heading(2n) -Pass

in section :heading(2n+1) -Pass

in section :heading(2n+2) -Pass

in section :heading(-n+3) -Pass

in section :heading(2n, 3n) -Pass

in section :heading(even) -Pass

in section :heading(odd) Pass
:heading Pass
:heading(1) Pass
:heading(2) @@ -278,17 +170,8 @@ Pass
:heading(7) Pass
:heading(8) Pass
:heading(9) Pass
:heading(0) -Pass
:heading(-1) Pass
:heading(0, 1, 2) Pass
:heading(6, 7) -Pass
:heading(n) -Pass
:heading(2n) -Pass
:heading(2n+1) -Pass
:heading(2n+2) -Pass
:heading(-n+3) -Pass
:heading(2n, 3n) -Pass
:heading(even) -Pass
:heading(odd) Pass

:heading Pass

:heading(1) Pass

:heading(2) @@ -300,14 +183,5 @@ Pass

:heading(7) Pass

:heading(8) Pass

:heading(9) Pass

:heading(0) -Pass

:heading(-1) Pass

:heading(0, 1, 2) -Pass

:heading(6, 7) -Pass

:heading(n) -Pass

:heading(2n) -Pass

:heading(2n+1) -Pass

:heading(2n+2) -Pass

:heading(-n+3) -Pass

:heading(2n, 3n) -Pass

:heading(even) -Pass

:heading(odd) \ No newline at end of file +Pass

:heading(6, 7) \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/selectors/parsing/parse-heading.txt b/Tests/LibWeb/Text/expected/wpt-import/css/selectors/parsing/parse-heading.txt index 6baa7303344..51cc9791663 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/selectors/parsing/parse-heading.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/selectors/parsing/parse-heading.txt @@ -2,8 +2,7 @@ Harness status: OK Found 28 tests -17 Pass -11 Fail +28 Pass Pass ":heading" should be a valid selector Pass ":heading(2)" should be a valid selector Pass ":heading(99999)" should be a valid selector @@ -17,17 +16,17 @@ Pass "h1:heading(2)" should be a valid selector Pass ":heading()" should be an invalid selector Pass ":heading(1.0)" should be an invalid selector Pass ":heading(1.4)" should be an invalid selector -Fail ":heading(n)" should be an invalid selector -Fail ":heading(odd)" should be an invalid selector -Fail ":heading(even)" should be an invalid selector -Fail ":heading(2n)" should be an invalid selector -Fail ":heading(2n+1)" should be an invalid selector -Fail ":heading(2n+2)" should be an invalid selector -Fail ":heading(-n+3)" should be an invalid selector -Fail ":heading(2n, 3n)" should be an invalid selector -Fail ":heading(2, 3n)" should be an invalid selector -Fail ":heading(2 of .foo)" should be an invalid selector -Fail ":heading(2n of .foo)" should be an invalid selector +Pass ":heading(n)" should be an invalid selector +Pass ":heading(odd)" should be an invalid selector +Pass ":heading(even)" should be an invalid selector +Pass ":heading(2n)" should be an invalid selector +Pass ":heading(2n+1)" should be an invalid selector +Pass ":heading(2n+2)" should be an invalid selector +Pass ":heading(-n+3)" should be an invalid selector +Pass ":heading(2n, 3n)" should be an invalid selector +Pass ":heading(2, 3n)" should be an invalid selector +Pass ":heading(2 of .foo)" should be an invalid selector +Pass ":heading(2n of .foo)" should be an invalid selector Pass ":heading(calc(1))" should be an invalid selector Pass ":heading(max(1, 2))" should be an invalid selector Pass ":heading(min(1, 2)" should be an invalid selector diff --git a/Tests/LibWeb/Text/input/wpt-import/css/selectors/heading.html b/Tests/LibWeb/Text/input/wpt-import/css/selectors/heading.html index 5165474b2ff..d303a5bc21e 100644 --- a/Tests/LibWeb/Text/input/wpt-import/css/selectors/heading.html +++ b/Tests/LibWeb/Text/input/wpt-import/css/selectors/heading.html @@ -33,17 +33,8 @@ const tests = [ {args: ['8'], match: []}, {args: ['9'], match: []}, {args: ['0'], match: []}, - {args: ['-1'], match: []}, {args: ['0', '1', '2'], match: [1, 2]}, {args: ['6', '7'], match: [6]}, - {args: ['n'], match: [1, 2, 3, 4, 5, 6]}, - {args: ['2n'], match: [2, 4, 6]}, - {args: ['2n+1'], match: [1, 3, 5]}, - {args: ['2n+2'], match: [2, 4, 6]}, - {args: ['-n+3'], match: [1, 2, 3]}, - {args: ['2n', '3n'], match: [2, 3, 4, 6]}, - {args: ['even'], match: [2, 4, 6]}, - {args: ['odd'], match: [1, 3, 5]}, ]; for (const el of els) { const testName = el.outerHTML + (el.parentNode === document.body ? '' : ' in ' + el.parentNode.localName);