From 099c3fa590f748e2abee3fedb790f35b072df072 Mon Sep 17 00:00:00 2001 From: Callum Law Date: Wed, 6 May 2026 14:22:44 +1200 Subject: [PATCH] Meta: Handle juxtaposition in CSS value parsing code generation --- Meta/Utils/CSSGrammar/Parser/grammar_node.py | 4 ++ Meta/Utils/CSSGrammar/Parser/parser.py | 21 ++++++++- Meta/Utils/CSSGrammar/generator.py | 29 +++++++++++++ Tests/Meta/test-css-grammar-parser.py | 45 ++++++++++++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/Meta/Utils/CSSGrammar/Parser/grammar_node.py b/Meta/Utils/CSSGrammar/Parser/grammar_node.py index 3801e3aee9..eaf415ebba 100644 --- a/Meta/Utils/CSSGrammar/Parser/grammar_node.py +++ b/Meta/Utils/CSSGrammar/Parser/grammar_node.py @@ -19,6 +19,10 @@ class ComponentValueGrammarNode(GrammarNode): class CombinatorType(Enum): + # https://drafts.csswg.org/css-values-4/#component-combinators + # Juxtaposing components means that all of them must occur, in the given order. + JUXTAPOSITION = "Juxtaposition" + # https://drafts.csswg.org/css-values-4/#comb-one # A bar (|) separates two or more alternatives: exactly one of them must occur. ALTERNATIVES = "Alternatives" diff --git a/Meta/Utils/CSSGrammar/Parser/parser.py b/Meta/Utils/CSSGrammar/Parser/parser.py index ec2637a141..b8bfe5f8fa 100644 --- a/Meta/Utils/CSSGrammar/Parser/parser.py +++ b/Meta/Utils/CSSGrammar/Parser/parser.py @@ -26,17 +26,28 @@ class Parser: return value def parse_alternatives(self) -> GrammarNode: - children = [self.parse_component_value()] + children = [self.parse_juxtaposition()] while self.peek().is_token_type(TokenType.SINGLE_BAR): self.consume() - children.append(self.parse_component_value()) + children.append(self.parse_juxtaposition()) if len(children) == 1: return children[0] return CombinatorGrammarNode(CombinatorType.ALTERNATIVES, children) + def parse_juxtaposition(self) -> GrammarNode: + children = [self.parse_component_value()] + + while self.next_token_starts_component_value(): + children.append(self.parse_component_value()) + + if len(children) == 1: + return children[0] + + return CombinatorGrammarNode(CombinatorType.JUXTAPOSITION, children) + def parse_component_value(self) -> GrammarNode: # https://drafts.csswg.org/css-values-4/#component-multipliers # FIXME: Support component multipliers @@ -67,6 +78,12 @@ class Parser: return component_value + def next_token_starts_component_value(self) -> bool: + return self.peek().token_type in ( + TokenType.OPEN_SQUARE_BRACKET, + TokenType.COMPONENT_VALUE, + ) + def peek(self, offset: int = 0) -> Token: index = min(self.index + offset, len(self.tokens) - 1) return self.tokens[index] diff --git a/Meta/Utils/CSSGrammar/generator.py b/Meta/Utils/CSSGrammar/generator.py index b0c66c0ef2..2d3c0561e8 100644 --- a/Meta/Utils/CSSGrammar/generator.py +++ b/Meta/Utils/CSSGrammar/generator.py @@ -100,9 +100,38 @@ auto {cpp_name} = parse_{cpp_name}_alternatives(); """) +def generate_css_parser_expression_for_juxtaposition(out: TextIO, cpp_name: str, children: list[GrammarNode]) -> None: + out.write(f"""auto const parse_{cpp_name}_juxtaposition = [&]() -> RefPtr {{ +auto {cpp_name}_transaction = tokens.begin_transaction(); +StyleValueVector {cpp_name}_values; +{cpp_name}_values.ensure_capacity({len(children)}); + +""") + + for i, component in enumerate(children): + component_name = f"{cpp_name}_component_{i}" + generate_css_parser_expression_for_grammar_node(out, component_name, component) + out.write(f"""if (!{component_name}) + return nullptr; + +{cpp_name}_values.append({component_name}.release_nonnull()); +""") + + out.write(f"""{cpp_name}_transaction.commit(); +return StyleValueList::create(move({cpp_name}_values), StyleValueList::Separator::Space, StyleValueList::Collapsible::No); +}}; + +auto {cpp_name} = parse_{cpp_name}_juxtaposition(); +""") + + def generate_css_parser_expression_for_combinator_grammar_node( out: TextIO, cpp_name: str, grammar_node: CombinatorGrammarNode ) -> None: + if grammar_node.combinator_type == CombinatorType.JUXTAPOSITION: + generate_css_parser_expression_for_juxtaposition(out, cpp_name, grammar_node.children) + return + if grammar_node.combinator_type == CombinatorType.ALTERNATIVES: generate_css_parser_expression_for_alternatives(out, cpp_name, grammar_node.children) return diff --git a/Tests/Meta/test-css-grammar-parser.py b/Tests/Meta/test-css-grammar-parser.py index 09a3141a2d..40774ec54d 100644 --- a/Tests/Meta/test-css-grammar-parser.py +++ b/Tests/Meta/test-css-grammar-parser.py @@ -119,6 +119,51 @@ class TestCSSGrammarParser(unittest.TestCase): """, ) + def test_parse_juxtaposition(self) -> None: + syntax = parse_value_definition_grammar(" baz") + self.assertEqual( + syntax.dump(), + """Combinator(Juxtaposition): + ComponentValue + Type: foo + ComponentValue + Type: bar + ComponentValue + Keyword: baz +""", + ) + + def test_parse_juxtaposition_has_higher_precedence_than_alternatives(self) -> None: + syntax = parse_value_definition_grammar(" | ") + self.assertEqual( + syntax.dump(), + """Combinator(Alternatives): + Combinator(Juxtaposition): + ComponentValue + Type: foo + ComponentValue + Type: bar + ComponentValue + Type: baz +""", + ) + + def test_parse_grouped_alternatives_in_juxtaposition(self) -> None: + syntax = parse_value_definition_grammar("[ | ] ") + self.assertEqual( + syntax.dump(), + """Combinator(Juxtaposition): + Group: + Combinator(Alternatives): + ComponentValue + Type: foo + ComponentValue + Type: bar + ComponentValue + Type: baz +""", + ) + def test_parse_group(self) -> None: syntax = parse_value_definition_grammar("[ ]") self.assertEqual(