Meta: Handle juxtaposition in CSS value parsing code generation

This commit is contained in:
Callum Law 2026-05-06 14:22:44 +12:00 committed by Sam Atkins
parent 30e58a48c6
commit 099c3fa590
4 changed files with 97 additions and 2 deletions

View file

@ -19,6 +19,10 @@ class ComponentValueGrammarNode(GrammarNode):
class CombinatorType(Enum): 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 # https://drafts.csswg.org/css-values-4/#comb-one
# A bar (|) separates two or more alternatives: exactly one of them must occur. # A bar (|) separates two or more alternatives: exactly one of them must occur.
ALTERNATIVES = "Alternatives" ALTERNATIVES = "Alternatives"

View file

@ -26,17 +26,28 @@ class Parser:
return value return value
def parse_alternatives(self) -> GrammarNode: def parse_alternatives(self) -> GrammarNode:
children = [self.parse_component_value()] children = [self.parse_juxtaposition()]
while self.peek().is_token_type(TokenType.SINGLE_BAR): while self.peek().is_token_type(TokenType.SINGLE_BAR):
self.consume() self.consume()
children.append(self.parse_component_value()) children.append(self.parse_juxtaposition())
if len(children) == 1: if len(children) == 1:
return children[0] return children[0]
return CombinatorGrammarNode(CombinatorType.ALTERNATIVES, children) 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: def parse_component_value(self) -> GrammarNode:
# https://drafts.csswg.org/css-values-4/#component-multipliers # https://drafts.csswg.org/css-values-4/#component-multipliers
# FIXME: Support component multipliers # FIXME: Support component multipliers
@ -67,6 +78,12 @@ class Parser:
return component_value 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: def peek(self, offset: int = 0) -> Token:
index = min(self.index + offset, len(self.tokens) - 1) index = min(self.index + offset, len(self.tokens) - 1)
return self.tokens[index] return self.tokens[index]

View file

@ -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<StyleValue const> {{
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( def generate_css_parser_expression_for_combinator_grammar_node(
out: TextIO, cpp_name: str, grammar_node: CombinatorGrammarNode out: TextIO, cpp_name: str, grammar_node: CombinatorGrammarNode
) -> None: ) -> 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: if grammar_node.combinator_type == CombinatorType.ALTERNATIVES:
generate_css_parser_expression_for_alternatives(out, cpp_name, grammar_node.children) generate_css_parser_expression_for_alternatives(out, cpp_name, grammar_node.children)
return return

View file

@ -119,6 +119,51 @@ class TestCSSGrammarParser(unittest.TestCase):
""", """,
) )
def test_parse_juxtaposition(self) -> None:
syntax = parse_value_definition_grammar("<foo> <bar> 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("<foo> <bar> | <baz>")
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("[ <foo> | <bar> ] <baz>")
self.assertEqual(
syntax.dump(),
"""Combinator(Juxtaposition):
Group:
Combinator(Alternatives):
ComponentValue
Type: foo
ComponentValue
Type: bar
ComponentValue
Type: baz
""",
)
def test_parse_group(self) -> None: def test_parse_group(self) -> None:
syntax = parse_value_definition_grammar("[ <length> ]") syntax = parse_value_definition_grammar("[ <length> ]")
self.assertEqual( self.assertEqual(