Meta: Handle optionals in CSS value parsing code generation

This commit is contained in:
Callum Law 2026-05-05 22:32:41 +12:00 committed by Sam Atkins
parent fe121d403d
commit 30e58a48c6
7 changed files with 79 additions and 4 deletions

View file

@ -95,6 +95,7 @@ def generate_implementation_file(out: TextIO, value_type_data: dict[str, Any]) -
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/CSS/StyleValues/EmptyOptionalStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
namespace Web::CSS::Parser {

View file

@ -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)

View file

@ -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. [ <foo>? ]!)
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)

View file

@ -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"

View file

@ -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()

View file

@ -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<StyleValue const> {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

View file

@ -160,6 +160,30 @@ class TestCSSGrammarParser(unittest.TestCase):
""",
)
def test_parse_optional_type_reference(self) -> None:
syntax = parse_value_definition_grammar("<foo>?")
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(" <foo>\t|\n<bar> ")
self.assertEqual(
@ -184,6 +208,16 @@ class TestCSSGrammarParser(unittest.TestCase):
with self.assertRaises(SyntaxError):
parse_value_definition_grammar("<foo> |")
def test_reject_invalid_optional(self) -> None:
for value in (
"?",
"<foo>??",
"[ <foo> ]??",
):
with self.subTest(value=value):
with self.assertRaises(SyntaxError):
parse_value_definition_grammar(value)
def test_reject_invalid_group(self) -> None:
for value in (
"[]",