#!/usr/bin/env python3 """Generate internal/cbm/pdf/pdf_unicode.c: the Unicode facts the PDF mention scanner needs, exactly as CPython sees them (the field test's scanner ran on CPython's str and re). usage: scripts/gen-pdf-unicode.py internal/cbm/pdf/pdf_unicode.c (then clang-format -i it) The output depends on the CPython version (its Unicode data): the committed tables were made with CPython 3.14.6 (Unicode 16.0.0). """ import sys import unicodedata MAXCP = 0x110000 def is_surrogate(c): return 0xD800 <= c <= 0xDFFF def ranges(pred): out = [] start = None for c in range(MAXCP): ok = not is_surrogate(c) and pred(chr(c)) if ok and start is None: start = c elif not ok and start is not None: out.append((start, c - 1)) start = None if start is not None: out.append((start, MAXCP - 1)) return out def emit_ranges(f, name, rs): f.write("const pdf_urange_t %s[] = {\n" % name) for i in range(0, len(rs), 4): f.write(" " + " ".join("{0x%X, 0x%X}," % r for r in rs[i:i + 4]) + "\n") f.write("};\nconst int %s_COUNT = (int)(sizeof(%s) / sizeof(%s[0]));\n\n" % (name, name, name)) def main(): out = sys.argv[1] # per-character NFKC nfkc = [] seq = [] for c in range(MAXCP): if is_surrogate(c): continue ch = chr(c) m = unicodedata.normalize("NFKC", ch) if m != ch: nfkc.append((c, len(seq), len(m))) seq.extend(ord(x) for x in m) # canonical composition pairs: a + b -> c when NFC composes them comp = [] for c in range(MAXCP): if is_surrogate(c): continue d = unicodedata.decomposition(chr(c)) if not d or d.startswith("<"): continue parts = [int(x, 16) for x in d.split()] if len(parts) != 2: continue a, b = parts if unicodedata.normalize("NFC", chr(a) + chr(b)) == chr(c): comp.append((a, b, c)) comp.sort() with open(out, "w", encoding="utf-8") as f: f.write("/*\n * pdf_unicode.c -- Unicode %s facts for the PDF mention scanner (generated).\n" " *\n * The field test's scanner ran on CPython's str and re; these tables are what\n" " * CPython %s answers: NFKC of each code point, the canonical compositions,\n" " * str.isspace, the re module's \\w (str.isalnum or '_'), str.isalpha, and the\n" " * one-character str.isupper / str.islower / titlecase. Generated by\n" " * scripts/gen-pdf-unicode.py; do not edit by hand. Data of the Unicode\n" " * Character Database, (c) Unicode, Inc., Unicode License v3\n" " * (THIRD_PARTY.md).\n */\n" % (unicodedata.unidata_version, sys.version.split()[0])) f.write("#include \"pdf_unicode.h\"\n\n") f.write("const pdf_nfkc_t PDF_NFKC[] = {\n") for i in range(0, len(nfkc), 4): f.write(" " + " ".join("{0x%X, %d, %d}," % e for e in nfkc[i:i + 4]) + "\n") f.write("};\nconst int PDF_NFKC_COUNT = (int)(sizeof(PDF_NFKC) / sizeof(PDF_NFKC[0]));\n\n") f.write("const uint32_t PDF_NFKC_SEQ[] = {\n") for i in range(0, len(seq), 10): f.write(" " + " ".join("0x%X," % x for x in seq[i:i + 10]) + "\n") f.write("};\n\n") f.write("const pdf_ucomp_t PDF_UCOMP[] = {\n") for i in range(0, len(comp), 3): f.write(" " + " ".join("{0x%X, 0x%X, 0x%X}," % e for e in comp[i:i + 3]) + "\n") f.write("};\nconst int PDF_UCOMP_COUNT = (int)(sizeof(PDF_UCOMP) / sizeof(PDF_UCOMP[0]));\n\n") emit_ranges(f, "PDF_U_SPACE", ranges(lambda c: c.isspace())) emit_ranges(f, "PDF_U_WORD", ranges(lambda c: c.isalnum() or c == "_")) emit_ranges(f, "PDF_U_ALPHA", ranges(lambda c: c.isalpha())) emit_ranges(f, "PDF_U_UPPER", ranges(lambda c: c.isupper())) emit_ranges(f, "PDF_U_LOWER", ranges(lambda c: c.islower())) emit_ranges(f, "PDF_U_TITLE", ranges(lambda c: c.istitle() and not c.isupper())) print("nfkc", len(nfkc), "seq", len(seq), "comp", len(comp)) if __name__ == "__main__": main()