summaryrefslogtreecommitdiff
path: root/tools/generate_unicode.py
blob: 22f12a61ee57dc6c7d22d3c9403285b4a7884c73 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3

import argparse
import re
import sys
from pathlib import Path

VERSION_PATTERNS = (
    re.compile(r"^#\s*[A-Za-z-]+-(\d+\.\d+(?:\.\d+)?)\.txt\s*$"),
    re.compile(r"^#\s*Version:\s*(\d+\.\d+(?:\.\d+)?)\s*$"),
    re.compile(r"^#\s*Used with Emoji Version (\d+\.\d+(?:\.\d+)?)\b"),
)

def merge(values):
    values = sorted(set(values))
    ranges = []
    for cp in values:
        if ranges and cp == ranges[-1][1] + 1:
            ranges[-1] = (ranges[-1][0], cp)
        else:
            ranges.append((cp, cp))
    return ranges

def field_ranges(path, wanted):
    values = []
    for raw in path.read_text().splitlines():
        line = raw.split("#", 1)[0].strip()
        if not line:
            continue
        span, prop = (part.strip() for part in line.split(";", 1))
        if prop not in wanted:
            continue
        ends = [int(x, 16) for x in span.split("..")]
        values.extend(range(ends[0], ends[-1] + 1))
    return merge(values)

def normalize_version(version):
    parts = version.split(".")
    return ".".join(parts + ["0"] * (3 - len(parts)))

def header_version(path):
    found = set()
    for raw in path.read_text().splitlines():
        if not raw.startswith("#"):
            break
        for pattern in VERSION_PATTERNS:
            match = pattern.match(raw)
            if match:
                found.add(normalize_version(match.group(1)))
    if len(found) != 1:
        sys.exit(f"{path}: expected one Unicode version in the header, found {sorted(found) or 'none'}")
    return found.pop()

def unicode_categories(path):
    by_category = {}
    pending = None
    for raw in path.read_text().splitlines():
        fields = raw.split(";")
        cp, name, category = int(fields[0], 16), fields[1], fields[2]
        if name.endswith(", First>"):
            pending = (cp, category)
            continue
        if name.endswith(", Last>"):
            start, first_category = pending
            assert category == first_category
            by_category.setdefault(category, []).extend(range(start, cp + 1))
            pending = None
        else:
            by_category.setdefault(category, []).append(cp)
    assert pending is None
    return by_category

def emit_table(out, name, ranges):
    out.append(f"static const struct term_unicode_range {name}[] = {{")
    for i in range(0, len(ranges), 4):
        chunk = ranges[i:i + 4]
        out.append("\t" + ",".join(f"{{0x{lo:X},0x{hi:X}}}" for lo, hi in chunk) + ",")
    out.append("};\n")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("unicode_data", type=Path)
    parser.add_argument("east_asian_width", type=Path)
    parser.add_argument("emoji_data", type=Path)
    parser.add_argument("output", type=Path)
    args = parser.parse_args()

    versions = {path: header_version(path) for path in (args.east_asian_width, args.emoji_data)}
    if len(set(versions.values())) != 1:
        sys.exit("input files disagree on the Unicode version: " +
                 ", ".join(f"{path}={version}" for path, version in versions.items()))
    version = next(iter(versions.values()))

    categories = unicode_categories(args.unicode_data)
    assigned = {cp for cps in categories.values() for cp in cps}
    # Conjoining Hangul vowels and finals draw inside the leading consonant.
    jamo = list(range(0x1160, 0x1200)) + [cp for cp in range(0xD7B0, 0xD800) if cp in assigned]
    zero = merge(categories.get("Mn", []) + categories.get("Me", []) +
                 categories.get("Cf", []) + jamo)
    nonprint = merge(categories.get("Cc", []) + categories.get("Cs", []))
    wide = field_ranges(args.east_asian_width, {"W", "F"})
    spacing = merge(categories.get("Mc", []))
    pictographic = field_ranges(args.emoji_data, {"Extended_Pictographic"})
    emoji_presentation = field_ranges(args.emoji_data, {"Emoji_Presentation"})

    out = [f"/* Generated from Unicode {version} data by tools/generate_unicode.py. */", "",
           f'#define TERM_UNICODE_DATA_VERSION "{version}"', ""]
    emit_table(out, "term_zero_width", zero)
    emit_table(out, "term_nonprintable", nonprint)
    emit_table(out, "term_unicode_wide", wide)
    emit_table(out, "term_spacing_mark", spacing)
    emit_table(out, "term_extended_pictographic", pictographic)
    emit_table(out, "term_emoji_presentation", emoji_presentation)
    args.output.write_text("\n".join(out))

if __name__ == "__main__":
    main()