From 46824bb4215fdb7009c99661ecbe9bb6f78a77f2 Mon Sep 17 00:00:00 2001 From: Sean Arackal Date: Thu, 17 Sep 2026 18:12:40 -0400 Subject: [PATCH 1/2] Add An+B serialization alongside parse_nth --- docs/api_reference.rst | 1 + tests/test_nth_serialization.py | 32 ++++++++++++++++++++++++++++++++ tinycss2/nth.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 tests/test_nth_serialization.py diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 077e41c..b20cffd 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -74,6 +74,7 @@ Color Level 5 .. module:: tinycss2.nth .. autofunction:: parse_nth +.. autofunction:: serialize_nth AST nodes diff --git a/tests/test_nth_serialization.py b/tests/test_nth_serialization.py new file mode 100644 index 0000000..6d39176 --- /dev/null +++ b/tests/test_nth_serialization.py @@ -0,0 +1,32 @@ +import pytest + +from tinycss2 import nth + + +@pytest.mark.parametrize(('a', 'b', 'expected'), [ + (0, 0, '0'), (0, 1, '1'), (0, -2, '-2'), + (1, 0, 'n'), (1, 3, 'n+3'), (1, -3, 'n-3'), + (-1, 0, '-n'), (-1, 3, '-n+3'), (-1, -3, '-n-3'), + (2, 0, '2n'), (2, 1, '2n+1'), (2, -4, '2n-4'), + (-2, 0, '-2n'), (-2, 1, '-2n+1'), (-2, -4, '-2n-4'), +]) +def test_serialize_nth(a, b, expected): + # CSS Syntax's canonical form, including even/odd as 2n and 2n+1. + assert nth.serialize_nth(a, b) == expected + assert nth.parse_nth(expected) == (a, b) + + +@pytest.mark.parametrize('source', [ + 'odd', 'even', '+n', '-n + 6', '3n - 2', '0n+5', 'n-0', +]) +def test_serialize_parsed_nth(source): + coefficients = nth.parse_nth(source) + serialized = nth.serialize_nth(*coefficients) + assert nth.parse_nth(serialized) == coefficients + assert nth.serialize_nth(*nth.parse_nth(serialized)) == serialized + + +@pytest.mark.parametrize('a', [-1000, -1, 0, 1, 1000]) +@pytest.mark.parametrize('b', [-1000, -1, 0, 1, 1000]) +def test_serialize_nth_round_trip(a, b): + assert nth.parse_nth(nth.serialize_nth(a, b)) == (a, b) diff --git a/tinycss2/nth.py b/tinycss2/nth.py index 225a3ac..cdd1122 100644 --- a/tinycss2/nth.py +++ b/tinycss2/nth.py @@ -74,6 +74,36 @@ def parse_nth(input): return parse_end(tokens, 1, int(match.group(1))) + +def serialize_nth(a, b): + """Serialize integer coefficients as a CSS ```` expression. + + Follow the `CSS Syntax serialization algorithm + `_. + The result can be parsed by :func:`parse_nth` to recover ``(a, b)``. + + :type a: :obj:`int` + :param a: The step coefficient. + :type b: :obj:`int` + :param b: The offset. + :returns: A :obj:`str`, for example ``'n+3'`` for ``(1, 3)``. + + """ + if a == 0: + return str(b) + if a == 1: + result = 'n' + elif a == -1: + result = '-n' + else: + result = f'{a}n' + if b > 0: + result += f'+{b}' + elif b < 0: + result += str(b) + return result + + def parse_b(tokens, a): token = _next_significant(tokens) if token is None: From f761e7f1b256f4ae719eff9e06adeec1a7e1ff9c Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 18 Sep 2026 11:36:03 +0200 Subject: [PATCH 2/2] Improve the an+b serializer and its tests --- tests/test_nth_serialization.py | 32 -------------------------------- tests/test_tinycss2.py | 18 +++++++++++++++++- tinycss2/nth.py | 30 +++++++----------------------- 3 files changed, 24 insertions(+), 56 deletions(-) delete mode 100644 tests/test_nth_serialization.py diff --git a/tests/test_nth_serialization.py b/tests/test_nth_serialization.py deleted file mode 100644 index 6d39176..0000000 --- a/tests/test_nth_serialization.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest - -from tinycss2 import nth - - -@pytest.mark.parametrize(('a', 'b', 'expected'), [ - (0, 0, '0'), (0, 1, '1'), (0, -2, '-2'), - (1, 0, 'n'), (1, 3, 'n+3'), (1, -3, 'n-3'), - (-1, 0, '-n'), (-1, 3, '-n+3'), (-1, -3, '-n-3'), - (2, 0, '2n'), (2, 1, '2n+1'), (2, -4, '2n-4'), - (-2, 0, '-2n'), (-2, 1, '-2n+1'), (-2, -4, '-2n-4'), -]) -def test_serialize_nth(a, b, expected): - # CSS Syntax's canonical form, including even/odd as 2n and 2n+1. - assert nth.serialize_nth(a, b) == expected - assert nth.parse_nth(expected) == (a, b) - - -@pytest.mark.parametrize('source', [ - 'odd', 'even', '+n', '-n + 6', '3n - 2', '0n+5', 'n-0', -]) -def test_serialize_parsed_nth(source): - coefficients = nth.parse_nth(source) - serialized = nth.serialize_nth(*coefficients) - assert nth.parse_nth(serialized) == coefficients - assert nth.serialize_nth(*nth.parse_nth(serialized)) == serialized - - -@pytest.mark.parametrize('a', [-1000, -1, 0, 1, 1000]) -@pytest.mark.parametrize('b', [-1000, -1, 0, 1, 1000]) -def test_serialize_nth_round_trip(a, b): - assert nth.parse_nth(nth.serialize_nth(a, b)) == (a, b) diff --git a/tests/test_tinycss2.py b/tests/test_tinycss2.py index fb1130a..24d3369 100644 --- a/tests/test_tinycss2.py +++ b/tests/test_tinycss2.py @@ -21,7 +21,7 @@ from tinycss2.color4 import Color # isort:skip from tinycss2.color4 import parse_color as parse_color4 # isort:skip from tinycss2.color5 import parse_color as parse_color5 # isort:skip -from tinycss2.nth import parse_nth # isort:skip +from tinycss2.nth import parse_nth, serialize_nth # isort:skip def generic(func): @@ -153,6 +153,22 @@ def test_nth(input): return parse_nth(input) +@pytest.mark.parametrize(('a', 'b', 'expected'), [ + (0, 0, '0'), (0, 1, '1'), (0, -2, '-2'), (1, 0, 'n'), (1, 3, 'n+3'), (1, -3, 'n-3'), + (-1, 0, '-n'), (-1, 3, '-n+3'), (-1, -3, '-n-3'), (2, 0, '2n'), (2, 1, '2n+1'), + (2, -4, '2n-4'), (-2, 0, '-2n'), (-2, 1, '-2n+1'), (-2, -4, '-2n-4'), +]) +def test_serialize_nth(a, b, expected): + assert serialize_nth(a, b) == expected + assert parse_nth(expected) == (a, b) + + +@json_test(filename='An+B.json') +def test_round_trip_nth(input): + if numbers := parse_nth(input): + return parse_nth(serialize_nth(*numbers)) + + @pytest.mark.parametrize('invalid', ['+', '+/**/', 'n+', 'n +', '-n-', '2n +']) def test_nth_invalid_does_not_crash(invalid): # Truncated/invalid An+B fragments must return None per parse_nth's diff --git a/tinycss2/nth.py b/tinycss2/nth.py index cdd1122..0d2650c 100644 --- a/tinycss2/nth.py +++ b/tinycss2/nth.py @@ -76,32 +76,16 @@ def parse_nth(input): def serialize_nth(a, b): - """Serialize integer coefficients as a CSS ```` expression. + """Serialize ` `_. - Follow the `CSS Syntax serialization algorithm - `_. - The result can be parsed by :func:`parse_nth` to recover ``(a, b)``. - - :type a: :obj:`int` - :param a: The step coefficient. - :type b: :obj:`int` - :param b: The offset. - :returns: A :obj:`str`, for example ``'n+3'`` for ``(1, 3)``. + :param int a: The step coefficient. + :param int b: The offset. + :returns: An ``an+b`` string. """ - if a == 0: - return str(b) - if a == 1: - result = 'n' - elif a == -1: - result = '-n' - else: - result = f'{a}n' - if b > 0: - result += f'+{b}' - elif b < 0: - result += str(b) - return result + an = 'n' if a == 1 else '-n' if a == -1 else f'{a}n' if a else '' + b = f'{b}' if not a else f'{b:+}' if b else '' + return an + b def parse_b(tokens, a):