Meta: Implement CSS value type parsing code generation
To do this we parse the grammar supplied in `ValueTypes.json` and generate the appropriate parsing function in `CSS::Parser::Parser` Only a small subset of the CSS grammar (i.e. types `<foo>` and alternatives `<foo> | <bar>`) is implemented so far, it will be expanded in later commits.
This commit is contained in:
parent
8849435d6f
commit
09418e8c77
10 changed files with 370 additions and 3 deletions
|
|
@ -14,6 +14,7 @@ from typing import TextIO
|
|||
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from Utils.CSSGrammar.generator import generate_css_parser_expression_for_grammar
|
||||
from Utils.utils import snake_casify
|
||||
|
||||
|
||||
|
|
@ -106,9 +107,9 @@ namespace Web::CSS::Parser {
|
|||
RefPtr<StyleValue const> Parser::parse_{name_snake_case}_value(TokenStream<ComponentValue>& tokens)
|
||||
{{
|
||||
// {value_type_name} = {grammar}
|
||||
(void)tokens;
|
||||
// FIXME: Generate parser code for this value type.
|
||||
return nullptr;
|
||||
""")
|
||||
generate_css_parser_expression_for_grammar(out, name_snake_case, grammar)
|
||||
out.write(f""" return {name_snake_case};
|
||||
}}
|
||||
""")
|
||||
|
||||
|
|
|
|||
14
Meta/Utils/CSSGrammar/Parser/component_values.py
Normal file
14
Meta/Utils/CSSGrammar/Parser/component_values.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Type:
|
||||
name: str
|
||||
|
||||
def dump(self, indent: int) -> str:
|
||||
return f"{'': >{indent}}Type: {self.name}\n"
|
||||
|
||||
|
||||
# https://drafts.csswg.org/css-values-4/#component-types
|
||||
ComponentValue = Union[Type]
|
||||
39
Meta/Utils/CSSGrammar/Parser/grammar_node.py
Normal file
39
Meta/Utils/CSSGrammar/Parser/grammar_node.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from Utils.CSSGrammar.Parser.component_values import ComponentValue
|
||||
|
||||
|
||||
class GrammarNode:
|
||||
def dump(self, indent: int = 0) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# https://drafts.csswg.org/css-values-4/#component-types
|
||||
@dataclass(frozen=True)
|
||||
class ComponentValueGrammarNode(GrammarNode):
|
||||
component_value: ComponentValue
|
||||
|
||||
def dump(self, indent: int = 0) -> str:
|
||||
return f"{'': >{indent}}ComponentValue\n" + self.component_value.dump(indent + 2)
|
||||
|
||||
|
||||
class CombinatorType(Enum):
|
||||
# https://drafts.csswg.org/css-values-4/#comb-one
|
||||
# A bar (|) separates two or more alternatives: exactly one of them must occur.
|
||||
ALTERNATIVES = "Alternatives"
|
||||
|
||||
|
||||
# https://drafts.csswg.org/css-values-4/#component-combinators
|
||||
@dataclass(frozen=True)
|
||||
class CombinatorGrammarNode(GrammarNode):
|
||||
combinator_type: CombinatorType
|
||||
children: list[GrammarNode]
|
||||
|
||||
def dump(self, indent: int = 0) -> str:
|
||||
output = f"{'': >{indent}}Combinator({self.combinator_type.value}):\n"
|
||||
|
||||
for child in self.children:
|
||||
output += child.dump(indent + 2)
|
||||
|
||||
return output
|
||||
61
Meta/Utils/CSSGrammar/Parser/parser.py
Normal file
61
Meta/Utils/CSSGrammar/Parser/parser.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
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.token import Token
|
||||
from Utils.CSSGrammar.Parser.token import TokenType
|
||||
from Utils.CSSGrammar.Parser.tokenizer import Tokenizer
|
||||
|
||||
|
||||
class Parser:
|
||||
def __init__(self, tokens: list[Token]) -> None:
|
||||
self.tokens = tokens
|
||||
self.index = 0
|
||||
|
||||
@classmethod
|
||||
def parse_value_definition_grammar(cls, input: str) -> GrammarNode:
|
||||
parser = cls(Tokenizer.tokenize(input))
|
||||
|
||||
value = parser.parse_alternatives()
|
||||
|
||||
if not parser.peek().is_token_type(TokenType.END_OF_FILE):
|
||||
raise SyntaxError("CSSGrammar::Parser: Unexpected trailing input")
|
||||
|
||||
return value
|
||||
|
||||
def parse_alternatives(self) -> GrammarNode:
|
||||
children = [self.parse_component_value()]
|
||||
|
||||
while self.peek().is_token_type(TokenType.SINGLE_BAR):
|
||||
self.consume()
|
||||
children.append(self.parse_component_value())
|
||||
|
||||
if len(children) == 1:
|
||||
return children[0]
|
||||
|
||||
return CombinatorGrammarNode(CombinatorType.ALTERNATIVES, children)
|
||||
|
||||
def parse_component_value(self) -> GrammarNode:
|
||||
# https://drafts.csswg.org/css-values-4/#component-multipliers
|
||||
# FIXME: Support component multipliers
|
||||
|
||||
if not self.peek().is_token_type(TokenType.COMPONENT_VALUE):
|
||||
raise SyntaxError("CSSGrammar::Parser: Expected a component value")
|
||||
|
||||
return ComponentValueGrammarNode(self.consume().component_value())
|
||||
|
||||
def peek(self, offset: int = 0) -> Token:
|
||||
index = min(self.index + offset, len(self.tokens) - 1)
|
||||
return self.tokens[index]
|
||||
|
||||
def consume(self) -> Token:
|
||||
token = self.peek()
|
||||
|
||||
if not token.is_token_type(TokenType.END_OF_FILE):
|
||||
self.index += 1
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def parse_value_definition_grammar(input: str) -> GrammarNode:
|
||||
return Parser.parse_value_definition_grammar(input)
|
||||
33
Meta/Utils/CSSGrammar/Parser/token.py
Normal file
33
Meta/Utils/CSSGrammar/Parser/token.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Self
|
||||
|
||||
from Utils.CSSGrammar.Parser.component_values import ComponentValue
|
||||
|
||||
|
||||
class TokenType(Enum):
|
||||
END_OF_FILE = "end-of-file"
|
||||
SINGLE_BAR = "single-bar"
|
||||
COMPONENT_VALUE = "component-value"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Token:
|
||||
token_type: TokenType
|
||||
value: ComponentValue | None
|
||||
|
||||
@classmethod
|
||||
def create(cls, token_type: TokenType) -> Self:
|
||||
return cls(token_type, None)
|
||||
|
||||
@classmethod
|
||||
def create_component_value(cls, component_value: ComponentValue) -> Self:
|
||||
return cls(TokenType.COMPONENT_VALUE, component_value)
|
||||
|
||||
def is_token_type(self, token_type: TokenType) -> bool:
|
||||
return self.token_type == token_type
|
||||
|
||||
def component_value(self) -> ComponentValue:
|
||||
assert self.token_type == TokenType.COMPONENT_VALUE and isinstance(self.value, ComponentValue)
|
||||
|
||||
return self.value
|
||||
59
Meta/Utils/CSSGrammar/Parser/tokenizer.py
Normal file
59
Meta/Utils/CSSGrammar/Parser/tokenizer.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from Utils.CSSGrammar.Parser.component_values import Type
|
||||
from Utils.CSSGrammar.Parser.token import Token
|
||||
from Utils.CSSGrammar.Parser.token import TokenType
|
||||
from Utils.lexer import Lexer
|
||||
|
||||
|
||||
def is_identifier_character(ch: str) -> bool:
|
||||
return ch.isascii() and (ch.isalnum() or ch == "-")
|
||||
|
||||
|
||||
class Tokenizer:
|
||||
def __init__(self, input: str) -> None:
|
||||
self.lexer = Lexer(input)
|
||||
|
||||
@classmethod
|
||||
def tokenize(cls, input: str) -> list[Token]:
|
||||
return cls(input).tokenize_impl()
|
||||
|
||||
def tokenize_impl(self) -> list[Token]:
|
||||
tokens = []
|
||||
|
||||
while True:
|
||||
self.discard_whitespace()
|
||||
if self.lexer.is_eof():
|
||||
tokens.append(Token.create(TokenType.END_OF_FILE))
|
||||
return tokens
|
||||
|
||||
tokens.append(self.consume_a_token())
|
||||
|
||||
def discard_whitespace(self) -> None:
|
||||
self.lexer.ignore_while(lambda ch: ch.isspace() and ch.isascii())
|
||||
|
||||
def consume_an_identifier(self) -> str:
|
||||
return self.lexer.consume_while(is_identifier_character)
|
||||
|
||||
def consume_a_token(self) -> Token:
|
||||
match self.lexer.peek():
|
||||
case "|":
|
||||
self.lexer.consume()
|
||||
return Token.create(TokenType.SINGLE_BAR)
|
||||
case "<":
|
||||
return self.consume_a_non_terminal_token()
|
||||
|
||||
raise SyntaxError("CSSGrammar::Tokenizer: Unexpected character")
|
||||
|
||||
def consume_a_non_terminal_token(self) -> Token:
|
||||
assert self.lexer.consume_specific_char("<")
|
||||
|
||||
name = self.consume_an_identifier()
|
||||
|
||||
if not name:
|
||||
raise SyntaxError("CSSGrammar::Tokenizer: Expected a type name")
|
||||
|
||||
# FIXME: Support custom-ident blacklist notation (i.e. <custom-ident ![foo, bar]>)
|
||||
# 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))
|
||||
72
Meta/Utils/CSSGrammar/generator.py
Normal file
72
Meta/Utils/CSSGrammar/generator.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from typing import TextIO
|
||||
|
||||
from Utils.CSSGrammar.Parser.component_values import Type
|
||||
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.parser import parse_value_definition_grammar
|
||||
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")
|
||||
|
||||
|
||||
def generate_css_parser_expression_for_component_value_grammar_node(
|
||||
out: TextIO, cpp_name: str, grammar_node: ComponentValueGrammarNode
|
||||
) -> None:
|
||||
match grammar_node.component_value:
|
||||
case Type() as type_component_value:
|
||||
generate_css_parser_expression_for_type_component_value(out, cpp_name, type_component_value)
|
||||
return
|
||||
|
||||
raise TypeError(f"Unhandled component value type: {type(grammar_node.component_value).__name__}")
|
||||
|
||||
|
||||
def generate_css_parser_expression_for_alternatives(
|
||||
out: TextIO, cpp_name: str, alternatives: list[GrammarNode]
|
||||
) -> None:
|
||||
out.write(f"auto const parse_{cpp_name}_alternatives = [&]() -> RefPtr<StyleValue const> {{\n")
|
||||
|
||||
for i, alternative in enumerate(alternatives):
|
||||
alternative_name = f"{cpp_name}_alternative_{i}"
|
||||
generate_css_parser_expression_for_grammar_node(out, alternative_name, alternative)
|
||||
out.write(f"""if ({alternative_name})
|
||||
return {alternative_name};
|
||||
|
||||
""")
|
||||
|
||||
out.write(f"""return nullptr;
|
||||
}};
|
||||
|
||||
auto {cpp_name} = parse_{cpp_name}_alternatives();
|
||||
""")
|
||||
|
||||
|
||||
def generate_css_parser_expression_for_combinator_grammar_node(
|
||||
out: TextIO, cpp_name: str, grammar_node: CombinatorGrammarNode
|
||||
) -> None:
|
||||
match grammar_node.combinator_type:
|
||||
case CombinatorType.ALTERNATIVES:
|
||||
generate_css_parser_expression_for_alternatives(out, cpp_name, grammar_node.children)
|
||||
return
|
||||
|
||||
raise TypeError(f"Unhandled combinator type: {grammar_node.combinator_type}")
|
||||
|
||||
|
||||
def generate_css_parser_expression_for_grammar_node(out: TextIO, cpp_name: str, grammar_node: GrammarNode) -> None:
|
||||
match grammar_node:
|
||||
case ComponentValueGrammarNode():
|
||||
generate_css_parser_expression_for_component_value_grammar_node(out, cpp_name, grammar_node)
|
||||
return
|
||||
case CombinatorGrammarNode():
|
||||
generate_css_parser_expression_for_combinator_grammar_node(out, cpp_name, grammar_node)
|
||||
return
|
||||
|
||||
raise TypeError(f"Unhandled grammar node type: {type(grammar_node).__name__}")
|
||||
|
||||
|
||||
def generate_css_parser_expression_for_grammar(out: TextIO, cpp_name: str, grammar: str) -> None:
|
||||
generate_css_parser_expression_for_grammar_node(out, cpp_name, parse_value_definition_grammar(grammar))
|
||||
|
|
@ -17,6 +17,7 @@ add_subdirectory(LibUnicode)
|
|||
add_subdirectory(LibURL)
|
||||
add_subdirectory(LibWasm)
|
||||
add_subdirectory(LibXML)
|
||||
add_subdirectory(Meta)
|
||||
|
||||
if (ENABLE_GUI_TARGETS)
|
||||
add_subdirectory(LibGfx)
|
||||
|
|
|
|||
10
Tests/Meta/CMakeLists.txt
Normal file
10
Tests/Meta/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
if (NOT WIN32)
|
||||
add_custom_target(test-css-grammar-parser ALL DEPENDS "${CMAKE_BINARY_DIR}/bin/test-css-grammar-parser.py")
|
||||
add_custom_command(
|
||||
OUTPUT "${CMAKE_BINARY_DIR}/bin/test-css-grammar-parser.py"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy "${ladybird_SOURCE_DIR}/Tests/Meta/test-css-grammar-parser.py" "${CMAKE_BINARY_DIR}/bin/test-css-grammar-parser.py"
|
||||
DEPENDS "${ladybird_SOURCE_DIR}/Tests/Meta/test-css-grammar-parser.py"
|
||||
)
|
||||
add_test(NAME test-css-grammar-parser COMMAND "${Python3_EXECUTABLE}" "${CMAKE_BINARY_DIR}/bin/test-css-grammar-parser.py")
|
||||
set_tests_properties(test-css-grammar-parser PROPERTIES ENVIRONMENT LADYBIRD_SOURCE_DIR=${LADYBIRD_SOURCE_DIR})
|
||||
endif()
|
||||
77
Tests/Meta/test-css-grammar-parser.py
Normal file
77
Tests/Meta/test-css-grammar-parser.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(os.environ["LADYBIRD_SOURCE_DIR"]) / "Meta"))
|
||||
|
||||
from Utils.CSSGrammar.Parser.parser import parse_value_definition_grammar
|
||||
|
||||
|
||||
class TestCSSGrammarParser(unittest.TestCase):
|
||||
def test_parse_type_reference(self) -> None:
|
||||
syntax = parse_value_definition_grammar("<foo>")
|
||||
self.assertEqual(
|
||||
syntax.dump(),
|
||||
"""ComponentValue
|
||||
Type: foo
|
||||
""",
|
||||
)
|
||||
|
||||
def test_parse_hyphenated_type_reference(self) -> None:
|
||||
syntax = parse_value_definition_grammar("<foo-bar>")
|
||||
self.assertEqual(
|
||||
syntax.dump(),
|
||||
"""ComponentValue
|
||||
Type: foo-bar
|
||||
""",
|
||||
)
|
||||
|
||||
def test_parse_alternatives(self) -> None:
|
||||
syntax = parse_value_definition_grammar("<foo> | <bar> | <baz>")
|
||||
self.assertEqual(
|
||||
syntax.dump(),
|
||||
"""Combinator(Alternatives):
|
||||
ComponentValue
|
||||
Type: foo
|
||||
ComponentValue
|
||||
Type: bar
|
||||
ComponentValue
|
||||
Type: baz
|
||||
""",
|
||||
)
|
||||
|
||||
def test_parse_ignores_whitespace_around_tokens(self) -> None:
|
||||
syntax = parse_value_definition_grammar(" <foo>\t|\n<bar> ")
|
||||
self.assertEqual(
|
||||
syntax.dump(),
|
||||
"""Combinator(Alternatives):
|
||||
ComponentValue
|
||||
Type: foo
|
||||
ComponentValue
|
||||
Type: bar
|
||||
""",
|
||||
)
|
||||
|
||||
def test_reject_empty_input(self) -> None:
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar("")
|
||||
|
||||
def test_reject_standalone_bar(self) -> None:
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar("|")
|
||||
|
||||
def test_reject_trailing_bar(self) -> None:
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar("<foo> |")
|
||||
|
||||
def test_reject_invalid_type_reference(self) -> None:
|
||||
for value in ("<>", "<foo", "<foo/auto>"):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar(value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue