from __future__ import annotations import string import pytest from rich import cells from rich.cells import ( CellSpan, _is_single_cell_widths, cell_len, chop_cells, get_character_cell_size, split_graphemes, split_text, ) @pytest.mark.parametrize( "character,size", [ ("\0", 0), ("\u200d", 0), ("a", 1), ("💩", 2), (chr(917999 + 1), 0), ], ) def test_get_character_cell_size(character: str, size: int) -> None: """Test single character cell size.""" assert get_character_cell_size(character) == size def test_cell_len_long_string(): # Long strings don't use cached cell length implementation assert cells.cell_len("abc" * 200) == 3 * 200 # Boundary case assert cells.cell_len("a" * 512) == 512 def test_cell_len_short_string(): # Short strings use cached cell length implementation assert cells.cell_len("abc" * 100) == 3 * 100 # Boundary case assert cells.cell_len("a" * 511) == 511 def test_set_cell_size(): assert cells.set_cell_size("foo", 0) == "" assert cells.set_cell_size("f", 0) == "" assert cells.set_cell_size("", 0) == "" assert cells.set_cell_size("😽😽", 0) == "" assert cells.set_cell_size("foo", 2) == "fo" assert cells.set_cell_size("foo", 3) == "foo" assert cells.set_cell_size("foo", 4) == "foo " assert cells.set_cell_size("😽😽", 4) == "😽😽" assert cells.set_cell_size("😽😽", 3) == "😽 " assert cells.set_cell_size("😽😽", 2) == "😽" assert cells.set_cell_size("😽😽", 1) == " " assert cells.set_cell_size("😽😽", 5) == "😽😽 " def test_set_cell_size_infinite(): for size in range(38): assert ( cells.cell_len( cells.set_cell_size( "เป็นเกมที่ต้องมีความอดทนมากที่สุดตั้งเเต่เคยเล่นมา", size ) ) == size ) FM = "👩\u200d🔧" @pytest.mark.parametrize( "text,offset,left,right", [ # Edge cases ("", -1, "", ""), ("x", -1, "", "x"), ("x", 1, "x", ""), ("x", 2, "x", ""), ("", 0, "", ""), ("", 1, "", ""), ("a", 0, "", "a"), ("a", 1, "a", ""), # Check simple double width character ("💩", 0, "", "💩"), ("💩", 1, " ", " "), # Split in the middle of a double wide results in spaces ("💩", 2, "💩", ""), ("💩x", 1, " ", " x"), ("💩x", 2, "💩", "x"), ("💩x", 3, "💩x", ""), # Check same for multi-codepoint emoji (FM, 0, "", FM), (FM, 1, " ", " "), # Split in the middle of a double wide results in spaces (FM, 2, FM, ""), (FM + "x", 1, " ", " x"), (FM + "x", 2, FM, "x"), (FM + "x", 3, FM + "x", ""), # Edge cases ("xxxxxxxxxxxxxxx💩💩", 10, "xxxxxxxxxx", "xxxxx💩💩"), ("xxxxxxxxxxxxxxx💩💩", 15, "xxxxxxxxxxxxxxx", "💩💩"), ("xxxxxxxxxxxxxxx💩💩", 16, "xxxxxxxxxxxxxxx ", " 💩"), ("💩💩", 3, "💩 ", " "), ("💩💩xxxxxxxxxx", 2, "💩", "💩xxxxxxxxxx"), ("💩💩xxxxxxxxxx", 3, "💩 ", " xxxxxxxxxx"), ("💩💩xxxxxxxxxx", 4, "💩💩", "xxxxxxxxxx"), ], ) def test_split_text(text: str, offset: int, left: str, right: str) -> None: """Check that split_text works on grapheme boundaries""" assert split_text(text, offset) == (left, right) def test_chop_cells(): """Simple example of splitting cells into lines of width 3.""" text = "abcdefghijk" assert chop_cells(text, 3) == ["abc", "def", "ghi", "jk"] def test_chop_cells_double_width_boundary(): """The available width lies within a double-width character.""" text = "ありがとう" assert chop_cells(text, 3) == ["あ", "り", "が", "と", "う"] def test_chop_cells_mixed_width(): """Mixed single and double-width characters.""" text = "あ1り234が5と6う78" assert chop_cells(text, 3) == ["あ1", "り2", "34", "が5", "と6", "う7", "8"] @pytest.mark.parametrize( "text,expected", [ ("", []), ("\x1b", []), ("\x1b\x1b", []), ("\x1b\x1b\x1b", []), ("\x1b\x1b\x1b\x1b", []), ], ) def test_chop_cells_zero_width(text: str, expected: list[str]) -> None: """Test zer width characters being chopped.""" assert chop_cells(text, 3) == expected def test_is_single_cell_widths() -> None: # Check _is_single_cell_widths reports correctly for character in string.printable: if ord(character) >= 32: assert _is_single_cell_widths(character) BOX = "┌─┬┐│ ││├─┼┤│ ││├─┼┤├─┼┤│ ││└─┴┘" for character in BOX: assert _is_single_cell_widths(character) for character in "💩😽": assert not _is_single_cell_widths(character) for character in "わさび": assert not _is_single_cell_widths(character) @pytest.mark.parametrize( "text,expected_spans,expected_cell_length", [ ("", [], 0), ("a", [(0, 1, 1)], 1), ("ab", [(0, 1, 1), (1, 2, 1)], 2), ("💩", [(0, 1, 2)], 2), ("わさび", [(0, 1, 2), (1, 2, 2), (2, 3, 2)], 6), ( "👩\u200d🔧", [(0, 3, 2)], 2, ), # 3 code points for female mechanic: female, joiner, spanner ("a👩\u200d🔧", [(0, 1, 1), (1, 4, 2)], 3), ("a👩\u200d🔧b", [(0, 1, 1), (1, 4, 2), (4, 5, 1)], 4), ("⬇", [(0, 1, 1)], 1), ("⬇️", [(0, 2, 2)], 2), # Variation selector, makes it double width ("♻", [(0, 1, 1)], 1), ("♻️", [(0, 2, 2)], 2), ("♻♻️", [(0, 1, 1), (1, 3, 2)], 3), ("\x1b", [(0, 1, 0)], 0), # One escape should have zero width ("\x1b\x1b", [(0, 2, 0)], 0), # Two escapes should have zero width ( "\ufe0f", [(0, 1, 0)], 0, ), # Variation selector 16, without anything to change should have zero width ( "\ufe0f\ufe0f", [(0, 2, 0)], 0, ), # 2 X variation selector 16, without anything to change should have zero width ( "\u200d", [(0, 1, 0)], 0, ), # A zero width joiner with nothing prior should have zero width ( "\u200d\u200d", [(0, 2, 0)], 0, ), # Two ZWJs should have zero width ( "\x1b\ufe0f", [(0, 2, 0)], 0, ), # VS16 after escape (zero-width, doesn't set last_measured_character) should have zero width ( "\u200d\ufe0f", [(0, 2, 0)], 0, ), # VS16 after ZWJ (zero-width, doesn't set last_measured_character) should have zero width ], ) def test_split_graphemes( text: str, expected_spans: list[CellSpan], expected_cell_length: int ): spans, cell_length = split_graphemes(text) print(spans) assert cell_len(text) == expected_cell_length assert spans == expected_spans assert cell_length == expected_cell_length def test_nerd_font(): """Regression test for https://github.com/Textualize/rich/issues/3943""" # Not allocated by unicode, but used by nerd fonts assert cell_len("\U000f024d") == 1 def test_zwj(): """Test special case of zero width joiners""" assert cell_len("") == 0 assert cell_len("\u200d") == 0 assert cell_len("1\u200d") == 1 # This sequence should really produce 2, but it aligns with with wcwidth # What gets written to the terminal is anybody's guess, I've seen multiple variations assert cell_len("1\u200d2") == 1 def test_non_printable(): """Non printable characters should report a width of 0.""" for ordinal in range(31): character = chr(ordinal) assert cell_len(character) == 0