diff --git a/tests/test_tinycss2.py b/tests/test_tinycss2.py index fb1130a..d393f91 100644 --- a/tests/test_tinycss2.py +++ b/tests/test_tinycss2.py @@ -703,3 +703,37 @@ def test_escape_in_function_name(): function, = parse_component_value_list('\\dddf()') assert function.type == 'function' assert function.name == '\udddf' + + +def test_serialize_dimension_scientific_notation(): + # A dimension whose unit begins with 'e'/'E' followed by an optional sign + # and a digit must not be serialized in a way that merges back into a + # scientific-notation number-token, and the unit's case must be preserved. + # e.g. the dimension "1" + unit "e3" must not serialize to "1e3" (== 1000), + # and the dimension "1" + unit "E" must not become unit "e". + cases = [ + (r'1\65 ', 1, 'e'), # unit 'e' + (r'1\45 ', 1, 'E'), # unit 'E': case must survive round-trip + (r'1\65 3', 1, 'e3'), # must not merge into number 1e3 == 1000 + (r'1\45 3', 1, 'E3'), + (r'1\65 05', 1, 'e05'), # must not merge into number 1e05 == 100000 + (r'1\65 -3', 1, 'e-3'), # must not merge into number 1e-3 == 0.001 + (r'1\45 -3', 1, 'E-3'), + ] + for source, int_value, unit in cases: + token, = parse_component_value_list(source) + assert token.type == 'dimension' + assert token.int_value == int_value + assert token.unit == unit + reparsed, = parse_component_value_list(token.serialize()) + assert reparsed.type == 'dimension', ( + f'{source!r} serialized to {token.serialize()!r} which re-parsed ' + f'as a {reparsed.type}-token') + assert reparsed.value == token.value + assert reparsed.int_value == int_value + assert reparsed.unit == unit + + # Units that cannot form scientific notation stay unescaped (no over-escape). + assert parse_component_value_list('1em')[0].serialize() == '1em' + assert parse_component_value_list('1ex')[0].serialize() == '1ex' + assert parse_component_value_list('5EX')[0].serialize() == '5EX' diff --git a/tinycss2/ast.py b/tinycss2/ast.py index 2831abc..0e34430 100644 --- a/tinycss2/ast.py +++ b/tinycss2/ast.py @@ -566,10 +566,18 @@ def __init__(self, line, column, value, int_value, representation, unit): def _serialize_to(self, write): write(self.representation) - # Disambiguate with scientific notation + # Disambiguate with scientific notation. + # A unit starting with 'e' or 'E' followed by an optional sign and a + # digit would otherwise merge with the number into a single + # scientific-notation number-token when re-parsed + # (e.g. '1' + 'e3' -> '1e3', or '1' + 'E-3' -> '1E-3'). + # Escaping the leading letter prevents this; the escape must use the + # letter's own code point so that its case is preserved. unit = self.unit - if unit in ('e', 'E') or unit.startswith(('e-', 'E-')): - write('\\65 ') + first = unit[0] + if first in ('e', 'E') and ( + len(unit) == 1 or unit[1] in '+-0123456789'): + write('\\%X ' % ord(first)) write(serialize_name(unit[1:])) else: write(serialize_identifier(unit))