
This patch changes the preview style so that string splitters respect Unicode East Asian Width[^1] property. If you are not familiar to CJK languages it is not clear immediately. Let me elaborate with some examples. Traditionally, East Asian characters (including punctuation) have taken up space twice than European letters and stops when they are rendered in monospace typeset. Compare the following characters: ``` abcdefg. 글、字。 ``` The characters at the first line are half-width, and the second line are full-width. (Also note that the last character with a small circle, the East Asian period, is also full-width.) Therefore, if we want to prevent those full-width characters to exceed the maximum columns per line, we need to count their *width* rather than the number of characters. Again, the following characters: ``` 글、字。 ``` These are just 4 characters, but their total width is 8. Suppose we want to maintain up to 4 columns per line with the following text: ``` abcdefg. 글、字。 ``` How should it be then? We want it to look like: ``` abcd efg. 글、 字。 ``` However, Black currently turns it into like this: ``` abcd efg. 글、字。 ``` It's because Black currently counts the number of characters in the line instead of measuring their width. So, how could we measure the width? How can we tell if a character is full- or half-width? What if half-width characters and full-width ones are mixed in a line? That's why Unicode defined an attribute named `East_Asian_Width`. Unicode grouped every single character according to their width in fixed-width typeset. This partially addresses #1197, but only for string splitters. The other parts need to be fixed as well in future patches. This was implemented by copying rich's own approach to handling wide characters: generate a table using wcwidth, check it into source control, and use in to drive helper functions in Black's logic. This gets us the best of both worlds: accuracy and performance (and let's us update as per our stability policy too!). Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
"""Generates a width table for Unicode characters.
|
|
|
|
This script generates a width table for Unicode characters that are not
|
|
narrow (width 1). The table is written to src/black/_width_table.py (note
|
|
that although this file is generated, it is checked into Git) and is used
|
|
by the char_width() function in src/black/strings.py.
|
|
|
|
You should run this script when you upgrade wcwidth, which is expected to
|
|
happen when a new Unicode version is released. The generated table contains
|
|
the version of wcwidth and Unicode that it was generated for.
|
|
|
|
In order to run this script, you need to install the latest version of wcwidth.
|
|
You can do this by running:
|
|
|
|
pip install -U wcwidth
|
|
|
|
"""
|
|
import sys
|
|
from os.path import basename, dirname, join
|
|
from typing import Iterable, Tuple
|
|
|
|
import wcwidth
|
|
|
|
|
|
def make_width_table() -> Iterable[Tuple[int, int, int]]:
|
|
start_codepoint = -1
|
|
end_codepoint = -1
|
|
range_width = -2
|
|
for codepoint in range(0, sys.maxunicode + 1):
|
|
width = wcwidth.wcwidth(chr(codepoint))
|
|
if width <= 1:
|
|
# Ignore narrow characters along with zero-width characters so that
|
|
# they are treated as single-width. Note that treating zero-width
|
|
# characters as single-width is consistent with the heuristics built
|
|
# on top of str.isascii() in the str_width() function in strings.py.
|
|
continue
|
|
if start_codepoint < 0:
|
|
start_codepoint = codepoint
|
|
range_width = width
|
|
elif width != range_width or codepoint != end_codepoint + 1:
|
|
yield (start_codepoint, end_codepoint, range_width)
|
|
start_codepoint = codepoint
|
|
range_width = width
|
|
end_codepoint = codepoint
|
|
if start_codepoint >= 0:
|
|
yield (start_codepoint, end_codepoint, range_width)
|
|
|
|
|
|
def main() -> None:
|
|
table_path = join(dirname(__file__), "..", "src", "black", "_width_table.py")
|
|
with open(table_path, "w") as f:
|
|
f.write(
|
|
f"""# Generated by {basename(__file__)}
|
|
# wcwidth {wcwidth.__version__}
|
|
# Unicode {wcwidth.list_versions()[-1]}
|
|
import sys
|
|
from typing import List, Tuple
|
|
|
|
if sys.version_info < (3, 8):
|
|
from typing_extensions import Final
|
|
else:
|
|
from typing import Final
|
|
|
|
WIDTH_TABLE: Final[List[Tuple[int, int, int]]] = [
|
|
"""
|
|
)
|
|
for triple in make_width_table():
|
|
f.write(f" {triple!r},\n")
|
|
f.write("]\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|