import argparse
import sys
import urllib.request
from pathlib import Path
IVD_URL = "https://www.unicode.org/Public/UCD/latest/ucd/IVD_Sequences.txt"
DEFAULT_COLLECTION = "Adobe-Japan1"
def parse_ivd(lines: list[str]) -> dict[str, list[int]]:
result: dict[str, list[int]] = {}
for line in lines:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(";")
if len(parts) < 3:
continue
seq_hex = parts[0].strip()
collection = parts[1].strip()
codepoints = [int(h, 16) for h in seq_hex.split()]
if not codepoints:
continue
base = codepoints[0] result.setdefault(collection, [])
if base not in result[collection]:
result[collection].append(base)
for v in result.values():
v.sort()
return result
def fetch_ivd(path: Path | None) -> list[str]:
if path and path.exists():
return path.read_text(encoding="utf-8").splitlines()
print(f"Downloading IVD from {IVD_URL} ...", file=sys.stderr)
with urllib.request.urlopen(IVD_URL) as r:
return r.read().decode("utf-8").splitlines()
def emit_rust(base_codepoints: list[int]) -> None:
print("// AUTO-GENERATED stubs — review and fill in canonical targets")
print("// Format: ('variant', 'canonical'),")
for cp in base_codepoints:
ch = chr(cp)
print(f" ('{ch}', '?'), // U+{cp:04X}")
def emit_report(collection: str, base_codepoints: list[int]) -> None:
print(f"Collection: {collection} ({len(base_codepoints)} base characters)\n")
cols = 8
chars = [chr(cp) for cp in base_codepoints]
for i in range(0, len(chars), cols):
row = " ".join(
f"{c} U+{cp:04X}" for c, cp in zip(chars[i:i+cols], base_codepoints[i:i+cols])
)
print(" ", row)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--ivd", metavar="FILE", type=Path,
help="Path to IVD_Sequences.txt (downloaded if omitted)")
parser.add_argument("--collection", default=DEFAULT_COLLECTION,
help=f"IVD collection name (default: {DEFAULT_COLLECTION}, or 'all')")
parser.add_argument("--rust", action="store_true",
help="Emit Rust pair stubs instead of a readable report")
args = parser.parse_args()
lines = fetch_ivd(args.ivd)
data = parse_ivd(lines)
if not data:
print("ERROR: No IVD entries parsed. Is the file valid?", file=sys.stderr)
sys.exit(1)
if args.collection == "all":
collections = sorted(data)
else:
if args.collection not in data:
available = ", ".join(sorted(data))
print(f"ERROR: collection '{args.collection}' not found.", file=sys.stderr)
print(f"Available: {available}", file=sys.stderr)
sys.exit(1)
collections = [args.collection]
for col in collections:
cps = data[col]
if args.rust:
print(f"\n// --- {col} ---")
emit_rust(cps)
else:
emit_report(col, cps)
print()
if __name__ == "__main__":
main()