from __future__ import annotations
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
UCD_PATH = REPO_ROOT / "src" / "unicodedata" / "UnicodeData.txt"
CATEGORIES_PATH = REPO_ROOT / "src" / "unicodedata" / "categories.txt"
NFD_BASES_PATH = REPO_ROOT / "src" / "unicodedata" / "nfd_bases.txt"
CATEGORIES = [
"Lu", "Ll", "Lt", "Lm", "Lo",
"Mn", "Mc", "Me",
"Nd", "Nl", "No",
"Pc", "Pd", "Ps", "Pe", "Pi", "Pf", "Po",
"Sm", "Sc", "Sk", "So",
"Zs", "Zl", "Zp",
"Cc", "Cf", "Cs", "Co", "Cn",
]
MAX_CP = 0x10FFFF
def parse_unicode_data(path: Path) -> dict[int, str]:
entries: dict[int, str] = {}
lines = [l for l in path.read_text().splitlines() if l]
i = 0
while i < len(lines):
fields = lines[i].split(";")
cp = int(fields[0], 16)
name = fields[1]
cat = fields[2]
if name.endswith(", First>"):
i += 1
fields2 = lines[i].split(";")
cp_end = int(fields2[0], 16)
assert fields2[1].endswith(", Last>"), fields2
for c in range(cp, cp_end + 1):
entries[c] = cat
else:
entries[cp] = cat
i += 1
return entries
def parse_canonical_decompositions(path: Path) -> dict[int, int]:
immediate: dict[int, int] = {}
for line in path.read_text().splitlines():
if not line:
continue
fields = line.split(";")
cp = int(fields[0], 16)
decomp = fields[5]
if not decomp or decomp.startswith("<"):
continue
first = int(decomp.split()[0], 16)
if first != cp:
immediate[cp] = first
recursive: dict[int, int] = {}
for cp in immediate:
seen: set[int] = set()
current = cp
while current in immediate and current not in seen:
seen.add(current)
current = immediate[current]
if current != cp:
recursive[cp] = current
return recursive
def build_ranges(entries: dict[int, str]) -> list[tuple[int, int, str]]:
ranges: list[tuple[int, int, str]] = []
current_cat: str | None = None
current_start = 0
for cp in range(MAX_CP + 1):
cat = entries.get(cp, "Cn")
if cat != current_cat:
if current_cat is not None:
ranges.append((current_start, cp - 1, current_cat))
current_cat = cat
current_start = cp
assert current_cat is not None
ranges.append((current_start, MAX_CP, current_cat))
return ranges
def emit_categories(ranges: list[tuple[int, int, str]], path: Path) -> None:
seen_cats = sorted({r[2] for r in ranges})
unknown = [c for c in seen_cats if c not in CATEGORIES]
if unknown:
sys.exit(f"Unknown categories in UnicodeData.txt: {unknown}")
lines = [f"{end:x} {cat}" for _, end, cat in ranges]
path.write_text("\n".join(lines) + "\n")
def emit_nfd_bases(nfd_bases: dict[int, int], path: Path) -> None:
lines = [f"{cp:x} {nfd_bases[cp]:x}" for cp in sorted(nfd_bases)]
path.write_text("\n".join(lines) + "\n")
def main() -> None:
entries = parse_unicode_data(UCD_PATH)
ranges = build_ranges(entries)
nfd_bases = parse_canonical_decompositions(UCD_PATH)
emit_categories(ranges, CATEGORIES_PATH)
emit_nfd_bases(nfd_bases, NFD_BASES_PATH)
print(
f"Wrote {CATEGORIES_PATH} ({len(ranges)} ranges) and "
f"{NFD_BASES_PATH} ({len(nfd_bases)} NFD base entries)."
)
if __name__ == "__main__":
main()