Meta: Handle groups in CSS value parsing code generation

We don't yet support required groups so for the moment these just fall
back to generating the parsing code of their child
This commit is contained in:
Callum Law 2026-05-05 16:06:10 +12:00 committed by Sam Atkins
parent cb14d574de
commit 6361dcaae5
7 changed files with 91 additions and 2 deletions

View file

@ -1,8 +1,7 @@
{
"<font-weight-absolute>": {
"spec": "https://drafts.csswg.org/css-fonts-4/#font-weight-absolute-values",
"grammar": "normal | bold | <number [1,1000]>",
"__comment": "FIXME: The spec defines this wrapped in a group i.e. '[ normal | bold | <number [1,1000]> ]' but our CSS grammar parser doesn't currently know how to handle that"
"grammar": "[ normal | bold | <number [1,1000]> ]"
},
"<symbol>": {
"spec": "https://drafts.csswg.org/css-counter-styles-3/#typedef-symbol",

View file

@ -37,3 +37,11 @@ class CombinatorGrammarNode(GrammarNode):
output += child.dump(indent + 2)
return output
@dataclass(frozen=True)
class GroupGrammarNode(GrammarNode):
child: GrammarNode
def dump(self, indent: int = 0) -> str:
return f"{'': >{indent}}Group:\n" + self.child.dump(indent + 2)

View file

@ -2,6 +2,7 @@ from Utils.CSSGrammar.Parser.grammar_node import CombinatorGrammarNode
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.token import Token
from Utils.CSSGrammar.Parser.token import TokenType
from Utils.CSSGrammar.Parser.tokenizer import Tokenizer
@ -39,6 +40,17 @@ 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):
self.consume()
group = self.parse_alternatives()
if not self.peek().is_token_type(TokenType.CLOSE_SQUARE_BRACKET):
raise SyntaxError("CSSGrammar::Parser: Expected ']'")
# FIXME: Support required groups (e.g. [ <foo>? ]!)
self.consume()
return GroupGrammarNode(group)
if not self.peek().is_token_type(TokenType.COMPONENT_VALUE):
raise SyntaxError("CSSGrammar::Parser: Expected a component value")

View file

@ -8,6 +8,8 @@ from Utils.CSSGrammar.Parser.component_values import ComponentValue
class TokenType(Enum):
END_OF_FILE = "end-of-file"
SINGLE_BAR = "single-bar"
OPEN_SQUARE_BRACKET = "open-square-bracket"
CLOSE_SQUARE_BRACKET = "close-square-bracket"
COMPONENT_VALUE = "component-value"

View file

@ -45,6 +45,12 @@ class Tokenizer:
if peeked == "|":
self.lexer.consume()
return Token.create(TokenType.SINGLE_BAR)
if peeked == "[":
self.lexer.consume()
return Token.create(TokenType.OPEN_SQUARE_BRACKET)
if peeked == "]":
self.lexer.consume()
return Token.create(TokenType.CLOSE_SQUARE_BRACKET)
if peeked == "<":
return self.consume_a_non_terminal_token()

View file

@ -8,6 +8,7 @@ from Utils.CSSGrammar.Parser.grammar_node import CombinatorGrammarNode
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.parser import parse_value_definition_grammar
from Utils.utils import snake_casify
from Utils.utils import title_casify
@ -108,10 +109,19 @@ def generate_css_parser_expression_for_combinator_grammar_node(
raise TypeError(f"Unhandled combinator type: {grammar_node.combinator_type}")
def generate_css_parser_expression_for_group_grammar_node(
out: TextIO, cpp_name: str, grammar_node: GroupGrammarNode
) -> None:
generate_css_parser_expression_for_grammar_node(out, cpp_name, grammar_node.child)
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)
return
if isinstance(grammar_node, GroupGrammarNode):
generate_css_parser_expression_for_group_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

@ -119,6 +119,47 @@ class TestCSSGrammarParser(unittest.TestCase):
""",
)
def test_parse_group(self) -> None:
syntax = parse_value_definition_grammar("[ <length> ]")
self.assertEqual(
syntax.dump(),
"""Group:
ComponentValue
Type: length [-,]
""",
)
def test_parse_grouped_alternatives(self) -> None:
syntax = parse_value_definition_grammar("[ auto | none | <length> ]")
self.assertEqual(
syntax.dump(),
"""Group:
Combinator(Alternatives):
ComponentValue
Keyword: auto
ComponentValue
Keyword: none
ComponentValue
Type: length [-,]
""",
)
def test_parse_groups_as_alternative(self) -> None:
syntax = parse_value_definition_grammar("[ auto | none ] | <length>")
self.assertEqual(
syntax.dump(),
"""Combinator(Alternatives):
Group:
Combinator(Alternatives):
ComponentValue
Keyword: auto
ComponentValue
Keyword: none
ComponentValue
Type: length [-,]
""",
)
def test_parse_ignores_whitespace_around_tokens(self) -> None:
syntax = parse_value_definition_grammar(" <foo>\t|\n<bar> ")
self.assertEqual(
@ -143,6 +184,17 @@ class TestCSSGrammarParser(unittest.TestCase):
with self.assertRaises(SyntaxError):
parse_value_definition_grammar("<foo> |")
def test_reject_invalid_group(self) -> None:
for value in (
"[]",
"[ <foo>",
"<foo> ]",
"[ <foo> | ]",
):
with self.subTest(value=value):
with self.assertRaises(SyntaxError):
parse_value_definition_grammar(value)
def test_reject_invalid_type_reference(self) -> None:
for value in (
"<>",