Meta: Implement code generation for CSS custom ident value blacklists

This commit is contained in:
Callum Law 2026-04-24 23:47:47 +12:00 committed by Sam Atkins
parent 09418e8c77
commit dd9f0b8838
4 changed files with 70 additions and 5 deletions

View file

@ -5,9 +5,15 @@ from typing import Union
@dataclass(frozen=True)
class Type:
name: str
custom_ident_blacklist: list[str] | None
def dump(self, indent: int) -> str:
return f"{'': >{indent}}Type: {self.name}\n"
output = f"{'': >{indent}}Type: {self.name}"
if self.custom_ident_blacklist:
output += f" ![{', '.join(self.custom_ident_blacklist)}]"
return output + "\n"
# https://drafts.csswg.org/css-values-4/#component-types

View file

@ -43,6 +43,33 @@ class Tokenizer:
raise SyntaxError("CSSGrammar::Tokenizer: Unexpected character")
def consume_custom_ident_blacklist(self) -> list[str]:
# NB: This notation isn't yet included in the spec but we use it internally and the CSSWG has resolved to add it in
# https://github.com/w3c/csswg-drafts/issues/11924
self.discard_whitespace()
if not self.lexer.consume_specific_string("!["):
return []
blacklist = []
while True:
self.discard_whitespace()
ident = self.consume_an_identifier()
if not ident:
raise SyntaxError("Expected identifier in custom-ident blacklist")
blacklist.append(ident)
self.discard_whitespace()
if self.lexer.consume_specific_char("]"):
return blacklist
if not self.lexer.consume_specific_char(","):
raise SyntaxError("Expected ',' in custom-ident blacklist")
def consume_a_non_terminal_token(self) -> Token:
assert self.lexer.consume_specific_char("<")
@ -51,9 +78,12 @@ class Tokenizer:
if not name:
raise SyntaxError("CSSGrammar::Tokenizer: Expected a type name")
# FIXME: Support custom-ident blacklist notation (i.e. <custom-ident ![foo, bar]>)
custom_ident_blacklist = None
if name == "custom-ident":
custom_ident_blacklist = self.consume_custom_ident_blacklist()
# FIXME: Support numeric data type bracketed range notations (i.e. <integer [0,10]>)
if not self.lexer.consume_specific_char(">"):
raise SyntaxError("CSSGrammar::Tokenizer: Expected '>'")
return Token.create_component_value(Type(name))
return Token.create_component_value(Type(name, custom_ident_blacklist))

View file

@ -11,7 +11,18 @@ from Utils.utils import snake_casify
def generate_css_parser_expression_for_type_component_value(out: TextIO, cpp_name: str, type: Type) -> None:
type_name = snake_casify(type.name)
out.write(f"auto {cpp_name} = parse_{type_name}_value(tokens);\n")
additional_arguments = ""
if type.custom_ident_blacklist is not None:
additional_arguments = ", ReadonlySpan<StringView> { "
if len(type.custom_ident_blacklist) > 0:
disallowed_idents = "".join(f'"{disallowed_ident}"sv, ' for disallowed_ident in type.custom_ident_blacklist)
additional_arguments += f"Array<StringView, {len(type.custom_ident_blacklist)}> {{{disallowed_idents}}}"
additional_arguments += "}"
out.write(f"auto {cpp_name} = parse_{type_name}_value(tokens{additional_arguments});\n")
def generate_css_parser_expression_for_component_value_grammar_node(

View file

@ -28,6 +28,15 @@ class TestCSSGrammarParser(unittest.TestCase):
""",
)
def test_parse_custom_ident_blacklist(self) -> None:
syntax = parse_value_definition_grammar("<custom-ident ![foo, bar-baz]>")
self.assertEqual(
syntax.dump(),
"""ComponentValue
Type: custom-ident ![foo, bar-baz]
""",
)
def test_parse_alternatives(self) -> None:
syntax = parse_value_definition_grammar("<foo> | <bar> | <baz>")
self.assertEqual(
@ -67,7 +76,16 @@ class TestCSSGrammarParser(unittest.TestCase):
parse_value_definition_grammar("<foo> |")
def test_reject_invalid_type_reference(self) -> None:
for value in ("<>", "<foo", "<foo/auto>"):
for value in (
"<>",
"<foo",
"<foo/auto>",
"<length ![foo]>",
"<custom-ident ![]>",
"<custom-ident ![foo bar]>",
"<custom-ident ![foo,]>",
"<custom-ident ![foo>",
):
with self.subTest(value=value):
with self.assertRaises(SyntaxError):
parse_value_definition_grammar(value)