Meta: Handle bracketed range notations in CSS value grammar
This commit is contained in:
parent
521beb5a96
commit
0d75d66f2e
4 changed files with 230 additions and 3 deletions
|
|
@ -1,11 +1,58 @@
|
|||
from dataclasses import dataclass
|
||||
from math import inf
|
||||
from typing import Union
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NumericTypeRangeRestriction:
|
||||
minimum: float
|
||||
maximum: float
|
||||
|
||||
def dump(self) -> str:
|
||||
return f"[{bound_value_to_string(self.minimum)},{bound_value_to_string(self.maximum)}]"
|
||||
|
||||
|
||||
def bound_value_to_string(value: float) -> str:
|
||||
if value == -inf:
|
||||
return "-∞"
|
||||
if value == inf:
|
||||
return "∞"
|
||||
return str(value)
|
||||
|
||||
|
||||
def is_dimension_type(type_name: str) -> bool:
|
||||
# NB: Keep this up to date with the list of dimensions in Units.json
|
||||
return type_name in (
|
||||
"angle",
|
||||
"decibel",
|
||||
"flex",
|
||||
"frequency",
|
||||
"length",
|
||||
"resolution",
|
||||
"time",
|
||||
)
|
||||
|
||||
|
||||
def is_dimension_percentage_mix_type(type_name: str) -> bool:
|
||||
# https://drafts.csswg.org/css-values-4/#mixed-percentages
|
||||
return type_name in (
|
||||
"angle-percentage",
|
||||
"frequency-percentage",
|
||||
"length-percentage",
|
||||
"time-percentage",
|
||||
)
|
||||
|
||||
|
||||
def is_numeric_type(type_name: str) -> bool:
|
||||
# https://drafts.csswg.org/css-values-4/#numeric-data-types
|
||||
return type_name in ("integer", "number", "percentage") or is_dimension_type(type_name)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Type:
|
||||
name: str
|
||||
custom_ident_blacklist: list[str] | None
|
||||
numeric_type_accepted_range: NumericTypeRangeRestriction | None
|
||||
|
||||
def dump(self, indent: int) -> str:
|
||||
output = f"{'': >{indent}}Type: {self.name}"
|
||||
|
|
@ -13,6 +60,9 @@ class Type:
|
|||
if self.custom_ident_blacklist:
|
||||
output += f" ![{', '.join(self.custom_ident_blacklist)}]"
|
||||
|
||||
if self.numeric_type_accepted_range:
|
||||
output += f" {self.numeric_type_accepted_range.dump()}"
|
||||
|
||||
return output + "\n"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
from math import inf
|
||||
|
||||
from Utils.CSSGrammar.Parser.component_values import Keyword
|
||||
from Utils.CSSGrammar.Parser.component_values import NumericTypeRangeRestriction
|
||||
from Utils.CSSGrammar.Parser.component_values import Type
|
||||
from Utils.CSSGrammar.Parser.component_values import is_dimension_percentage_mix_type
|
||||
from Utils.CSSGrammar.Parser.component_values import is_dimension_type
|
||||
from Utils.CSSGrammar.Parser.component_values import is_numeric_type
|
||||
from Utils.CSSGrammar.Parser.token import Token
|
||||
from Utils.CSSGrammar.Parser.token import TokenType
|
||||
from Utils.lexer import Lexer
|
||||
|
|
@ -74,6 +80,67 @@ class Tokenizer:
|
|||
if not self.lexer.consume_specific_char(","):
|
||||
raise SyntaxError("Expected ',' in custom-ident blacklist")
|
||||
|
||||
# https://drafts.csswg.org/css-values-4/#css-bracketed-range-notation
|
||||
def consume_bracketed_range_notation(self, type_name: str) -> NumericTypeRangeRestriction:
|
||||
self.discard_whitespace()
|
||||
|
||||
# If no range is indicated, either by using the bracketed range notation or in the property description, then
|
||||
# [-∞,∞] is assumed.
|
||||
if not self.lexer.consume_specific_char("["):
|
||||
return NumericTypeRangeRestriction(-inf, inf)
|
||||
|
||||
self.discard_whitespace()
|
||||
minimum = self.consume_bracketed_range_bound(type_name)
|
||||
|
||||
self.discard_whitespace()
|
||||
if not self.lexer.consume_specific_char(","):
|
||||
raise SyntaxError("Expected ',' in bracketed range notation")
|
||||
|
||||
self.discard_whitespace()
|
||||
maximum = self.consume_bracketed_range_bound(type_name)
|
||||
|
||||
self.discard_whitespace()
|
||||
if not self.lexer.consume_specific_char("]"):
|
||||
raise SyntaxError("Expected ']' to close bracketed range notation")
|
||||
|
||||
return NumericTypeRangeRestriction(minimum, maximum)
|
||||
|
||||
def consume_bracketed_range_bound(self, type_name: str) -> float:
|
||||
# Values of -∞ or ∞ must be written without units, even if the value type uses units.
|
||||
if self.lexer.consume_specific_string("-∞"):
|
||||
return -inf
|
||||
|
||||
if self.lexer.consume_specific_string("∞"):
|
||||
return inf
|
||||
|
||||
# FIXME: Do we need to allow non-integer values?
|
||||
bound_value = self.consume_decimal_integer()
|
||||
|
||||
if bound_value != 0 and (is_dimension_percentage_mix_type(type_name) or type_name == "length"):
|
||||
raise SyntaxError("Types with units not resolvable at parse time only support zero and infinite bounds")
|
||||
|
||||
# FIXME: Validate and store the unit, for now we drop it and assume it was the relevant canonical unit.
|
||||
unit = self.lexer.consume_while(is_identifier_character)
|
||||
|
||||
if unit and not is_dimension_type(type_name) and not is_dimension_percentage_mix_type(type_name):
|
||||
raise SyntaxError("Unexpected unit for unitless bound value")
|
||||
|
||||
if not unit and bound_value != 0 and is_dimension_type(type_name):
|
||||
raise SyntaxError("Expected unit for non-zero, non-infinite bound value")
|
||||
|
||||
return float(bound_value)
|
||||
|
||||
def consume_decimal_integer(self) -> int:
|
||||
sign = 1
|
||||
if self.lexer.consume_specific_char("-"):
|
||||
sign = -1
|
||||
|
||||
digits = self.lexer.consume_while(lambda ch: ch.isdigit())
|
||||
if not digits:
|
||||
raise SyntaxError("Expected decimal integer")
|
||||
|
||||
return sign * int(digits)
|
||||
|
||||
def consume_a_non_terminal_token(self) -> Token:
|
||||
assert self.lexer.consume_specific_char("<")
|
||||
|
||||
|
|
@ -86,11 +153,14 @@ class Tokenizer:
|
|||
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]>)
|
||||
numeric_type_accepted_range = None
|
||||
if is_numeric_type(name) or is_dimension_percentage_mix_type(name):
|
||||
numeric_type_accepted_range = self.consume_bracketed_range_notation(name)
|
||||
|
||||
if not self.lexer.consume_specific_char(">"):
|
||||
raise SyntaxError("CSSGrammar::Tokenizer: Expected '>'")
|
||||
|
||||
return Token.create_component_value(Type(name, custom_ident_blacklist))
|
||||
return Token.create_component_value(Type(name, custom_ident_blacklist, numeric_type_accepted_range))
|
||||
|
||||
def consume_a_keyword_token(self) -> Token:
|
||||
value = self.consume_an_identifier()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from math import inf
|
||||
from typing import TextIO
|
||||
|
||||
from Utils.CSSGrammar.Parser.component_values import Keyword
|
||||
from Utils.CSSGrammar.Parser.component_values import Type
|
||||
from Utils.CSSGrammar.Parser.component_values import is_dimension_percentage_mix_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
|
||||
|
|
@ -11,6 +13,14 @@ from Utils.utils import snake_casify
|
|||
from Utils.utils import title_casify
|
||||
|
||||
|
||||
def bound_value_to_code(value: float, type_name: str) -> str:
|
||||
if value == -inf:
|
||||
return "AK::NumericLimits<i32>::min()" if type_name == "integer" else "AK::NumericLimits<float>::lowest()"
|
||||
if value == inf:
|
||||
return "AK::NumericLimits<i32>::max()" if type_name == "integer" else "AK::NumericLimits<float>::max()"
|
||||
return str(value)
|
||||
|
||||
|
||||
def generate_css_parser_expression_for_type_component_value(out: TextIO, cpp_name: str, type: Type) -> None:
|
||||
type_name = snake_casify(type.name)
|
||||
|
||||
|
|
@ -24,6 +34,17 @@ def generate_css_parser_expression_for_type_component_value(out: TextIO, cpp_nam
|
|||
|
||||
additional_arguments += "}"
|
||||
|
||||
if type.numeric_type_accepted_range is not None:
|
||||
minimum = bound_value_to_code(type.numeric_type_accepted_range.minimum, type_name)
|
||||
maximum = bound_value_to_code(type.numeric_type_accepted_range.maximum, type_name)
|
||||
accepted_range = f", {{ {minimum}, {maximum} }}"
|
||||
|
||||
additional_arguments += accepted_range
|
||||
|
||||
# NB: Pass the accepted range twice for dimension-percentage mixes, once for the dimension and once for the percentage.
|
||||
if is_dimension_percentage_mix_type(type.name):
|
||||
additional_arguments += accepted_range
|
||||
|
||||
out.write(f"auto {cpp_name} = parse_{type_name}_value(tokens{additional_arguments});\n")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,39 @@ class TestCSSGrammarParser(unittest.TestCase):
|
|||
""",
|
||||
)
|
||||
|
||||
def test_parse_unitless_numeric_type_bracketed_range_notation(self) -> None:
|
||||
for value, expected in (
|
||||
("<number [1,∞]>", "number [1.0,∞]"),
|
||||
("<integer [1,∞]>", "integer [1.0,∞]"),
|
||||
("<percentage [1,100]>", "percentage [1.0,100.0]"),
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
syntax = parse_value_definition_grammar(value)
|
||||
self.assertEqual(
|
||||
syntax.dump(),
|
||||
f"""ComponentValue
|
||||
Type: {expected}
|
||||
""",
|
||||
)
|
||||
|
||||
def test_parse_type_percentage_bracketed_range_notation(self) -> None:
|
||||
syntax = parse_value_definition_grammar("<length-percentage [0,∞]>")
|
||||
self.assertEqual(
|
||||
syntax.dump(),
|
||||
"""ComponentValue
|
||||
Type: length-percentage [0.0,∞]
|
||||
""",
|
||||
)
|
||||
|
||||
def test_parse_numeric_type_without_bracketed_range_notation_defaults_to_infinite_range(self) -> None:
|
||||
syntax = parse_value_definition_grammar("<number>")
|
||||
self.assertEqual(
|
||||
syntax.dump(),
|
||||
"""ComponentValue
|
||||
Type: number [-∞,∞]
|
||||
""",
|
||||
)
|
||||
|
||||
def test_parse_keyword(self) -> None:
|
||||
syntax = parse_value_definition_grammar("auto")
|
||||
self.assertEqual(
|
||||
|
|
@ -65,7 +98,7 @@ class TestCSSGrammarParser(unittest.TestCase):
|
|||
ComponentValue
|
||||
Keyword: none
|
||||
ComponentValue
|
||||
Type: length
|
||||
Type: length [-∞,∞]
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
@ -122,6 +155,59 @@ class TestCSSGrammarParser(unittest.TestCase):
|
|||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar(value)
|
||||
|
||||
def test_reject_invalid_bracketed_range_notation(self) -> None:
|
||||
for value in (
|
||||
"<number [0 ∞]>",
|
||||
"<number [0,∞>",
|
||||
"<number [,∞]>",
|
||||
"<number [1,]>",
|
||||
"<number [1]>",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar(value)
|
||||
|
||||
def test_reject_bracketed_range_notation_on_non_numeric_data_types(self) -> None:
|
||||
for value in (
|
||||
"<color [0,∞]>",
|
||||
"<custom-ident [0,∞]>",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar(value)
|
||||
|
||||
def test_reject_units_on_unitless_bracketed_range_bounds(self) -> None:
|
||||
for value in (
|
||||
"<number [1px,∞]>",
|
||||
"<integer [1px,∞]>",
|
||||
"<percentage [1px,100px]>",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar(value)
|
||||
|
||||
def test_reject_units_on_infinite_bracketed_range_bounds(self) -> None:
|
||||
for value in (
|
||||
"<length [0,∞px]>",
|
||||
"<length [-∞px,0]>",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar(value)
|
||||
|
||||
def test_reject_non_zero_non_infinite_bounds_on_types_with_units_not_resolvable_at_parse_time(self) -> None:
|
||||
for value in (
|
||||
"<length [1px,10px]>",
|
||||
"<angle-percentage [1deg,360deg]>",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar(value)
|
||||
|
||||
def test_unit_required_for_non_zero_non_infinite_bounds(self) -> None:
|
||||
with self.assertRaises(SyntaxError):
|
||||
parse_value_definition_grammar("<angle [-90,90]>")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Reference in a new issue