Files
alibaba--zvec/scripts/generate_standard_tokenizer_unicode.py
2026-07-13 12:47:42 +08:00

178 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
#
# Generate Unicode lookup tables for the standard tokenizer.
#
# Usage:
# scripts/generate_standard_tokenizer_unicode.py \
# --ucd-dir /path/to/Public/17.0.0/ucd \
# --out src/db/index/column/fts_column/tokenizer/standard_tokenizer_unicode.inc
from __future__ import annotations
import argparse
import pathlib
WORD_BREAK_CLASSES = {
"ALetter": "ALetter",
"CR": "CR",
"Double_Quote": "DoubleQuote",
"Extend": "Extend",
"ExtendNumLet": "ExtendNumLet",
"Format": "Format",
"Hebrew_Letter": "HebrewLetter",
"Katakana": "Katakana",
"LF": "LF",
"MidLetter": "MidLetter",
"MidNum": "MidNum",
"MidNumLet": "MidNumLet",
"Newline": "Newline",
"Numeric": "Numeric",
"Regional_Indicator": "RegionalIndicator",
"Single_Quote": "SingleQuote",
"WSegSpace": "WSegSpace",
"ZWJ": "ZWJ",
}
SCRIPT_CLASSES = {
"Han": "Ideographic",
"Hangul": "Hangul",
"Hiragana": "Hiragana",
}
LINE_BREAK_COMPLEX_CONTEXT = {
"Complex_Context",
"SA",
}
def parse_codepoint_range(field):
if ".." in field:
start, end = field.split("..", 1)
return int(start, 16), int(end, 16)
cp = int(field, 16)
return cp, cp
def parse_property_file(path, accepted_properties):
ranges = []
with path.open("r", encoding="utf-8") as fin:
for raw_line in fin:
line = raw_line.split("#", 1)[0].strip()
if not line:
continue
fields = [field.strip() for field in line.split(";")]
if len(fields) < 2:
continue
prop = fields[1]
if prop not in accepted_properties:
continue
start, end = parse_codepoint_range(fields[0])
ranges.append((start, end, accepted_properties[prop]))
return merge_class_ranges(ranges)
def parse_range_properties(path, accepted_properties):
ranges = []
with path.open("r", encoding="utf-8") as fin:
for raw_line in fin:
line = raw_line.split("#", 1)[0].strip()
if not line:
continue
fields = [field.strip() for field in line.split(";")]
if len(fields) < 2:
continue
if fields[1] not in accepted_properties:
continue
ranges.append(parse_codepoint_range(fields[0]))
return merge_ranges(ranges)
def merge_class_ranges(ranges):
merged = []
for start, end, cls in sorted(ranges):
if merged and merged[-1][2] == cls and merged[-1][1] + 1 == start:
merged[-1] = (merged[-1][0], end, cls)
else:
merged.append((start, end, cls))
return merged
def merge_ranges(ranges):
merged = []
for start, end in sorted(ranges):
if merged and merged[-1][1] + 1 >= start:
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
else:
merged.append((start, end))
return merged
def parse_binary_property(path, property_name):
return parse_range_properties(path, {property_name})
def write_class_table(out, name, ranges):
out.write(f"constexpr UnicodeClassRange {name}[] = {{\n")
for start, end, cls in ranges:
out.write(f" {{0x{start:04X}, 0x{end:04X}, WordBreakClass::{cls}}},\n")
out.write("};\n\n")
def write_range_table(out, name, ranges):
out.write(f"constexpr UnicodeRange {name}[] = {{\n")
for start, end in ranges:
out.write(f" {{0x{start:04X}, 0x{end:04X}}},\n")
out.write("};\n\n")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--ucd-dir", required=True, type=pathlib.Path)
parser.add_argument("--out", required=True, type=pathlib.Path)
args = parser.parse_args()
word_break_path = args.ucd_dir / "auxiliary" / "WordBreakProperty.txt"
emoji_data_path = args.ucd_dir / "emoji" / "emoji-data.txt"
line_break_path = args.ucd_dir / "LineBreak.txt"
scripts_path = args.ucd_dir / "Scripts.txt"
word_break_ranges = parse_property_file(word_break_path, WORD_BREAK_CLASSES)
script_ranges = parse_property_file(scripts_path, SCRIPT_CLASSES)
extended_pictographic_ranges = parse_binary_property(
emoji_data_path, "Extended_Pictographic"
)
emoji_modifier_base_ranges = parse_binary_property(
emoji_data_path, "Emoji_Modifier_Base"
)
emoji_modifier_ranges = parse_binary_property(emoji_data_path, "Emoji_Modifier")
line_break_complex_context_ranges = parse_range_properties(
line_break_path, LINE_BREAK_COMPLEX_CONTEXT
)
args.out.parent.mkdir(parents=True, exist_ok=True)
with args.out.open("w", encoding="utf-8", newline="\n") as fout:
fout.write("// Generated by scripts/generate_standard_tokenizer_unicode.py\n")
fout.write(
"// Source: Unicode 17.0.0 WordBreakProperty, emoji-data, LineBreak, Scripts.\n"
)
fout.write(
"// Derived from Unicode data files licensed under Unicode License V3; see NOTICE.\n"
)
fout.write("// Do not edit by hand.\n\n")
fout.write("// clang-format off\n\n")
write_class_table(fout, "kWordBreakRanges", word_break_ranges)
write_class_table(fout, "kScriptClassRanges", script_ranges)
write_range_table(
fout, "kExtendedPictographicRanges", extended_pictographic_ranges
)
write_range_table(fout, "kEmojiModifierBaseRanges", emoji_modifier_base_ranges)
write_range_table(fout, "kEmojiModifierRanges", emoji_modifier_ranges)
write_range_table(
fout, "kLineBreakComplexContextRanges", line_break_complex_context_ranges
)
fout.write("// clang-format on\n")
if __name__ == "__main__":
main()