From 30e58a48c68d94c03186fc7929e0e7115a722e4f Mon Sep 17 00:00:00 2001 From: Callum Law Date: Tue, 5 May 2026 22:32:41 +1200 Subject: [PATCH] Meta: Handle optionals in CSS value parsing code generation --- ...generate_libweb_css_value_types_parsing.py | 1 + Meta/Utils/CSSGrammar/Parser/grammar_node.py | 8 +++++ Meta/Utils/CSSGrammar/Parser/parser.py | 19 ++++++++--- Meta/Utils/CSSGrammar/Parser/token.py | 1 + Meta/Utils/CSSGrammar/Parser/tokenizer.py | 3 ++ Meta/Utils/CSSGrammar/generator.py | 17 ++++++++++ Tests/Meta/test-css-grammar-parser.py | 34 +++++++++++++++++++ 7 files changed, 79 insertions(+), 4 deletions(-) diff --git a/Meta/Generators/generate_libweb_css_value_types_parsing.py b/Meta/Generators/generate_libweb_css_value_types_parsing.py index e2eec0deaa..098bdfc1ae 100644 --- a/Meta/Generators/generate_libweb_css_value_types_parsing.py +++ b/Meta/Generators/generate_libweb_css_value_types_parsing.py @@ -95,6 +95,7 @@ def generate_implementation_file(out: TextIO, value_type_data: dict[str, Any]) - #include #include +#include #include namespace Web::CSS::Parser { diff --git a/Meta/Utils/CSSGrammar/Parser/grammar_node.py b/Meta/Utils/CSSGrammar/Parser/grammar_node.py index 1286493da4..3801e3aee9 100644 --- a/Meta/Utils/CSSGrammar/Parser/grammar_node.py +++ b/Meta/Utils/CSSGrammar/Parser/grammar_node.py @@ -45,3 +45,11 @@ class GroupGrammarNode(GrammarNode): def dump(self, indent: int = 0) -> str: return f"{'': >{indent}}Group:\n" + self.child.dump(indent + 2) + + +@dataclass(frozen=True) +class OptionalGrammarNode(GrammarNode): + child: GrammarNode + + def dump(self, indent: int = 0) -> str: + return f"{'': >{indent}}Optional:\n" + self.child.dump(indent + 2) diff --git a/Meta/Utils/CSSGrammar/Parser/parser.py b/Meta/Utils/CSSGrammar/Parser/parser.py index caf5c8239e..ec2637a141 100644 --- a/Meta/Utils/CSSGrammar/Parser/parser.py +++ b/Meta/Utils/CSSGrammar/Parser/parser.py @@ -3,6 +3,7 @@ from Utils.CSSGrammar.Parser.grammar_node import CombinatorType from Utils.CSSGrammar.Parser.grammar_node import ComponentValueGrammarNode from Utils.CSSGrammar.Parser.grammar_node import GrammarNode from Utils.CSSGrammar.Parser.grammar_node import GroupGrammarNode +from Utils.CSSGrammar.Parser.grammar_node import OptionalGrammarNode from Utils.CSSGrammar.Parser.token import Token from Utils.CSSGrammar.Parser.token import TokenType from Utils.CSSGrammar.Parser.tokenizer import Tokenizer @@ -40,7 +41,10 @@ class Parser: # https://drafts.csswg.org/css-values-4/#component-multipliers # FIXME: Support component multipliers - if self.peek().is_token_type(TokenType.OPEN_SQUARE_BRACKET): + peeked = self.peek() + component_value = None + + if peeked.is_token_type(TokenType.OPEN_SQUARE_BRACKET): self.consume() group = self.parse_alternatives() @@ -49,12 +53,19 @@ class Parser: # FIXME: Support required groups (e.g. [ ? ]!) self.consume() - return GroupGrammarNode(group) + component_value = GroupGrammarNode(group) - if not self.peek().is_token_type(TokenType.COMPONENT_VALUE): + if peeked.is_token_type(TokenType.COMPONENT_VALUE): + component_value = ComponentValueGrammarNode(self.consume().component_value()) + + if component_value is None: raise SyntaxError("CSSGrammar::Parser: Expected a component value") - return ComponentValueGrammarNode(self.consume().component_value()) + if self.peek().is_token_type(TokenType.QUESTION_MARK): + self.consume() + component_value = OptionalGrammarNode(component_value) + + return component_value def peek(self, offset: int = 0) -> Token: index = min(self.index + offset, len(self.tokens) - 1) diff --git a/Meta/Utils/CSSGrammar/Parser/token.py b/Meta/Utils/CSSGrammar/Parser/token.py index 45d93e6529..edf98156f6 100644 --- a/Meta/Utils/CSSGrammar/Parser/token.py +++ b/Meta/Utils/CSSGrammar/Parser/token.py @@ -10,6 +10,7 @@ class TokenType(Enum): SINGLE_BAR = "single-bar" OPEN_SQUARE_BRACKET = "open-square-bracket" CLOSE_SQUARE_BRACKET = "close-square-bracket" + QUESTION_MARK = "question-mark" COMPONENT_VALUE = "component-value" diff --git a/Meta/Utils/CSSGrammar/Parser/tokenizer.py b/Meta/Utils/CSSGrammar/Parser/tokenizer.py index 58b20d5ba0..7a4053ebb4 100644 --- a/Meta/Utils/CSSGrammar/Parser/tokenizer.py +++ b/Meta/Utils/CSSGrammar/Parser/tokenizer.py @@ -51,6 +51,9 @@ class Tokenizer: if peeked == "]": self.lexer.consume() return Token.create(TokenType.CLOSE_SQUARE_BRACKET) + if peeked == "?": + self.lexer.consume() + return Token.create(TokenType.QUESTION_MARK) if peeked == "<": return self.consume_a_non_terminal_token() diff --git a/Meta/Utils/CSSGrammar/generator.py b/Meta/Utils/CSSGrammar/generator.py index a861388336..b0c66c0ef2 100644 --- a/Meta/Utils/CSSGrammar/generator.py +++ b/Meta/Utils/CSSGrammar/generator.py @@ -9,6 +9,7 @@ from Utils.CSSGrammar.Parser.grammar_node import CombinatorType from Utils.CSSGrammar.Parser.grammar_node import ComponentValueGrammarNode from Utils.CSSGrammar.Parser.grammar_node import GrammarNode from Utils.CSSGrammar.Parser.grammar_node import GroupGrammarNode +from Utils.CSSGrammar.Parser.grammar_node import OptionalGrammarNode from Utils.CSSGrammar.Parser.parser import parse_value_definition_grammar from Utils.utils import snake_casify from Utils.utils import title_casify @@ -115,6 +116,19 @@ def generate_css_parser_expression_for_group_grammar_node( generate_css_parser_expression_for_grammar_node(out, cpp_name, grammar_node.child) +def generate_css_parser_expression_for_optional_grammar_node( + out: TextIO, cpp_name: str, grammar_node: OptionalGrammarNode +) -> None: + out.write(f"""RefPtr {cpp_name} = EmptyOptionalStyleValue::create(); +""") + + generate_css_parser_expression_for_grammar_node(out, f"maybe_{cpp_name}", grammar_node.child) + + out.write(f"""if (maybe_{cpp_name}) + {cpp_name} = maybe_{cpp_name}; +""") + + def generate_css_parser_expression_for_grammar_node(out: TextIO, cpp_name: str, grammar_node: GrammarNode) -> None: if isinstance(grammar_node, ComponentValueGrammarNode): generate_css_parser_expression_for_component_value_grammar_node(out, cpp_name, grammar_node) @@ -122,6 +136,9 @@ def generate_css_parser_expression_for_grammar_node(out: TextIO, cpp_name: str, if isinstance(grammar_node, GroupGrammarNode): generate_css_parser_expression_for_group_grammar_node(out, cpp_name, grammar_node) return + if isinstance(grammar_node, OptionalGrammarNode): + generate_css_parser_expression_for_optional_grammar_node(out, cpp_name, grammar_node) + return if isinstance(grammar_node, CombinatorGrammarNode): generate_css_parser_expression_for_combinator_grammar_node(out, cpp_name, grammar_node) return diff --git a/Tests/Meta/test-css-grammar-parser.py b/Tests/Meta/test-css-grammar-parser.py index 817c30efca..09a3141a2d 100644 --- a/Tests/Meta/test-css-grammar-parser.py +++ b/Tests/Meta/test-css-grammar-parser.py @@ -160,6 +160,30 @@ class TestCSSGrammarParser(unittest.TestCase): """, ) + def test_parse_optional_type_reference(self) -> None: + syntax = parse_value_definition_grammar("?") + self.assertEqual( + syntax.dump(), + """Optional: + ComponentValue + Type: foo +""", + ) + + def test_parse_optional_group(self) -> None: + syntax = parse_value_definition_grammar("[ auto | none ]?") + self.assertEqual( + syntax.dump(), + """Optional: + Group: + Combinator(Alternatives): + ComponentValue + Keyword: auto + ComponentValue + Keyword: none +""", + ) + def test_parse_ignores_whitespace_around_tokens(self) -> None: syntax = parse_value_definition_grammar(" \t|\n ") self.assertEqual( @@ -184,6 +208,16 @@ class TestCSSGrammarParser(unittest.TestCase): with self.assertRaises(SyntaxError): parse_value_definition_grammar(" |") + def test_reject_invalid_optional(self) -> None: + for value in ( + "?", + "??", + "[ ]??", + ): + with self.subTest(value=value): + with self.assertRaises(SyntaxError): + parse_value_definition_grammar(value) + def test_reject_invalid_group(self) -> None: for value in ( "[]",