import collections
import requests
UNICODE_VERSION = "9.0.0"
UCD_URL = "https://www.unicode.org/Public/%s/ucd/" % UNICODE_VERSION
PREAMBLE = """// Copyright 2012-2018 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// NOTE: The following code was generated by "scripts/unicode.py", do not edit directly
#![allow(missing_docs)]
"""
NormalizationTest = collections.namedtuple(
"NormalizationTest",
["source", "nfc", "nfd", "nfkc", "nfkd"],
)
expanded_categories = {
'Lu': ['LC', 'L'], 'Ll': ['LC', 'L'], 'Lt': ['LC', 'L'],
'Lm': ['L'], 'Lo': ['L'],
'Mn': ['M'], 'Mc': ['M'], 'Me': ['M'],
'Nd': ['N'], 'Nl': ['N'], 'No': ['No'],
'Pc': ['P'], 'Pd': ['P'], 'Ps': ['P'], 'Pe': ['P'],
'Pi': ['P'], 'Pf': ['P'], 'Po': ['P'],
'Sm': ['S'], 'Sc': ['S'], 'Sk': ['S'], 'So': ['S'],
'Zs': ['Z'], 'Zl': ['Z'], 'Zp': ['Z'],
'Cc': ['C'], 'Cf': ['C'], 'Cs': ['C'], 'Co': ['C'], 'Cn': ['C'],
}
class UnicodeData(object):
def __init__(self):
self._load_unicode_data()
self.norm_props = self._load_norm_props()
self.norm_tests = self._load_norm_tests()
self.canon_comp = self._compute_canonical_comp()
self.canon_fully_decomp, self.compat_fully_decomp = self._compute_fully_decomposed()
def stats(name, table):
count = sum(len(v) for v in table.values())
print "%s: %d chars => %d decomposed chars" % (name, len(table), count)
print "Decomposition table stats:"
stats("Canonical decomp", self.canon_decomp)
stats("Compatible decomp", self.compat_decomp)
stats("Canonical fully decomp", self.canon_fully_decomp)
stats("Compatible fully decomp", self.compat_fully_decomp)
self.ss_leading, self.ss_trailing = self._compute_stream_safe_tables()
def _fetch(self, filename):
resp = requests.get(UCD_URL + filename)
return resp.text
def _load_unicode_data(self):
self.combining_classes = {}
self.compat_decomp = {}
self.canon_decomp = {}
self.general_category_mark = []
for line in self._fetch("UnicodeData.txt").splitlines():
pieces = line.split(';')
assert len(pieces) == 15
char, category, cc, decomp = pieces[0], pieces[2], pieces[3], pieces[5]
char_int = int(char, 16)
if cc != '0':
self.combining_classes[char_int] = cc
if decomp.startswith('<'):
self.compat_decomp[char_int] = [int(c, 16) for c in decomp.split()[1:]]
elif decomp != '':
self.canon_decomp[char_int] = [int(c, 16) for c in decomp.split()]
if category == 'M' or 'M' in expanded_categories.get(category, []):
self.general_category_mark.append(char_int)
def _load_norm_props(self):
props = collections.defaultdict(list)
for line in self._fetch("DerivedNormalizationProps.txt").splitlines():
(prop_data, _, _) = line.partition("#")
prop_pieces = prop_data.split(";")
if len(prop_pieces) < 2:
continue
assert len(prop_pieces) <= 3
(low, _, high) = prop_pieces[0].strip().partition("..")
prop = prop_pieces[1].strip()
data = None
if len(prop_pieces) == 3:
data = prop_pieces[2].strip()
props[prop].append((low, high, data))
return props
def _load_norm_tests(self):
tests = []
for line in self._fetch("NormalizationTest.txt").splitlines():
(test_data, _, _) = line.partition("#")
test_pieces = test_data.split(";")
if len(test_pieces) < 5:
continue
source, nfc, nfd, nfkc, nfkd = [[c.strip() for c in p.split()] for p in test_pieces[:5]]
tests.append(NormalizationTest(source, nfc, nfd, nfkc, nfkd))
return tests
def _compute_canonical_comp(self):
canon_comp = {}
comp_exclusions = [
(int(low, 16), int(high or low, 16))
for low, high, _ in self.norm_props["Full_Composition_Exclusion"]
]
for char_int, decomp in self.canon_decomp.items():
if any(lo <= char_int <= hi for lo, hi in comp_exclusions):
continue
assert len(decomp) == 2
assert (decomp[0], decomp[1]) not in canon_comp
canon_comp[(decomp[0], decomp[1])] = char_int
return canon_comp
def _compute_fully_decomposed(self):
S_BASE, L_COUNT, V_COUNT, T_COUNT = 0xAC00, 19, 21, 28
S_COUNT = L_COUNT * V_COUNT * T_COUNT
def _decompose(char_int, compatible):
if char_int <= 0x7f:
yield char_int
return
assert not (S_BASE <= char_int < S_BASE + S_COUNT)
decomp = self.canon_decomp.get(char_int)
if decomp is not None:
for decomposed_ch in decomp:
for fully_decomposed_ch in _decompose(decomposed_ch, compatible):
yield fully_decomposed_ch
return
if compatible and char_int in self.compat_decomp:
for decomposed_ch in self.compat_decomp[char_int]:
for fully_decomposed_ch in _decompose(decomposed_ch, compatible):
yield fully_decomposed_ch
return
yield char_int
return
end_codepoint = max(
max(self.canon_decomp.keys()),
max(self.compat_decomp.keys()),
)
canon_fully_decomp = {}
compat_fully_decomp = {}
for char_int in range(0, end_codepoint + 1):
if S_BASE <= char_int < S_BASE + S_COUNT:
continue
canon = list(_decompose(char_int, False))
if not (len(canon) == 1 and canon[0] == char_int):
canon_fully_decomp[char_int] = canon
compat = list(_decompose(char_int, True))
if not (len(compat) == 1 and compat[0] == char_int):
compat_fully_decomp[char_int] = compat
assert canon_fully_decomp <= compat_fully_decomp
for ch in set(canon_fully_decomp) & set(compat_fully_decomp):
if canon_fully_decomp[ch] == compat_fully_decomp[ch]:
del compat_fully_decomp[ch]
return canon_fully_decomp, compat_fully_decomp
def _compute_stream_safe_tables(self):
leading_nonstarters = {}
trailing_nonstarters = {}
for c in set(self.canon_fully_decomp) | set(self.compat_fully_decomp):
decomposed = self.compat_fully_decomp.get(c) or self.canon_fully_decomp[c]
num_leading = 0
for d in decomposed:
if d not in self.combining_classes:
break
num_leading += 1
num_trailing = 0
for d in reversed(decomposed):
if d not in self.combining_classes:
break
num_trailing += 1
if num_leading > 0:
leading_nonstarters[c] = num_leading
if num_trailing > 0:
trailing_nonstarters[c] = num_trailing
return leading_nonstarters, trailing_nonstarters
hexify = lambda c: hex(c)[2:].upper().rjust(4, '0')
def gen_combining_class(combining_classes, out):
out.write("#[inline]\n")
out.write("pub fn canonical_combining_class(c: char) -> u8 {\n")
out.write(" match c {\n")
for char, combining_class in sorted(combining_classes.items()):
out.write(" '\u{%s}' => %s,\n" % (hexify(char), combining_class))
out.write(" _ => 0,\n")
out.write(" }\n")
out.write("}\n")
def gen_composition_table(canon_comp, out):
out.write("#[inline]\n")
out.write("pub fn composition_table(c1: char, c2: char) -> Option<char> {\n")
out.write(" match (c1, c2) {\n")
for (c1, c2), c3 in sorted(canon_comp.items()):
out.write(" ('\u{%s}', '\u{%s}') => Some('\u{%s}'),\n" % (hexify(c1), hexify(c2), hexify(c3)))
out.write(" _ => None,\n")
out.write(" }\n")
out.write("}\n")
def gen_decomposition_tables(canon_decomp, compat_decomp, out):
tables = [(canon_decomp, 'canonical'), (compat_decomp, 'compatibility')]
for table, name in tables:
out.write("#[inline]\n")
out.write("pub fn %s_fully_decomposed(c: char) -> Option<&'static [char]> {\n" % name)
out.write(" Some(match c {\n")
for char, chars in sorted(table.items()):
d = ", ".join("'\u{%s}'" % hexify(c) for c in chars)
out.write(" '\u{%s}' => &[%s],\n" % (hexify(char), d))
out.write(" _ => return None,\n")
out.write(" })\n")
out.write("}\n")
out.write("\n")
def gen_qc_match(prop_table, out):
out.write(" match c {\n")
for low, high, data in prop_table:
assert data in ('N', 'M')
result = "No" if data == 'N' else "Maybe"
if high:
out.write(r" '\u{%s}'...'\u{%s}' => %s," % (low, high, result))
else:
out.write(r" '\u{%s}' => %s," % (low, result))
out.write("\n")
out.write(" _ => Yes,\n")
out.write(" }\n")
def gen_nfc_qc(prop_tables, out):
out.write("#[inline]\n")
out.write("pub fn qc_nfc(c: char) -> IsNormalized {\n")
gen_qc_match(prop_tables['NFC_QC'], out)
out.write("}\n")
def gen_nfkc_qc(prop_tables, out):
out.write("#[inline]\n")
out.write("pub fn qc_nfkc(c: char) -> IsNormalized {\n")
gen_qc_match(prop_tables['NFKC_QC'], out)
out.write("}\n")
def gen_nfd_qc(prop_tables, out):
out.write("#[inline]\n")
out.write("pub fn qc_nfd(c: char) -> IsNormalized {\n")
gen_qc_match(prop_tables['NFD_QC'], out)
out.write("}\n")
def gen_nfkd_qc(prop_tables, out):
out.write("#[inline]\n")
out.write("pub fn qc_nfkd(c: char) -> IsNormalized {\n")
gen_qc_match(prop_tables['NFKD_QC'], out)
out.write("}\n")
def gen_combining_mark(general_category_mark, out):
out.write("#[inline]\n")
out.write("pub fn is_combining_mark(c: char) -> bool {\n")
out.write(" match c {\n")
for char in general_category_mark:
out.write(" '\u{%s}' => true,\n" % hexify(char))
out.write(" _ => false,\n")
out.write(" }\n")
out.write("}\n")
def gen_stream_safe(leading, trailing, out):
out.write("#[inline]\n")
out.write("pub fn stream_safe_leading_nonstarters(c: char) -> usize {\n")
out.write(" match c {\n")
for char, num_leading in leading.items():
out.write(" '\u{%s}' => %d,\n" % (hexify(char), num_leading))
out.write(" _ => 0,\n")
out.write(" }\n")
out.write("}\n")
out.write("\n")
out.write("#[inline]\n")
out.write("pub fn stream_safe_trailing_nonstarters(c: char) -> usize {\n")
out.write(" match c {\n")
for char, num_trailing in trailing.items():
out.write(" '\u{%s}' => %d,\n" % (hexify(char), num_trailing))
out.write(" _ => 0,\n")
out.write(" }\n")
out.write("}\n")
def gen_tests(tests, out):
out.write("""#[derive(Debug)]
pub struct NormalizationTest {
pub source: &'static str,
pub nfc: &'static str,
pub nfd: &'static str,
pub nfkc: &'static str,
pub nfkd: &'static str,
}
""")
out.write("pub const NORMALIZATION_TESTS: &[NormalizationTest] = &[\n")
str_literal = lambda s: '"%s"' % "".join("\u{%s}" % c for c in s)
for test in tests:
out.write(" NormalizationTest {\n")
out.write(" source: %s,\n" % str_literal(test.source))
out.write(" nfc: %s,\n" % str_literal(test.nfc))
out.write(" nfd: %s,\n" % str_literal(test.nfd))
out.write(" nfkc: %s,\n" % str_literal(test.nfkc))
out.write(" nfkd: %s,\n" % str_literal(test.nfkd))
out.write(" },\n")
out.write("];\n")
if __name__ == '__main__':
data = UnicodeData()
with open("tables.rs", "w") as out:
out.write(PREAMBLE)
out.write("use quick_check::IsNormalized;\n")
out.write("use quick_check::IsNormalized::*;\n")
out.write("\n")
version = "(%s, %s, %s)" % tuple(UNICODE_VERSION.split("."))
out.write("#[allow(unused)]\n")
out.write("pub const UNICODE_VERSION: (u64, u64, u64) = %s;\n\n" % version)
gen_combining_class(data.combining_classes, out)
out.write("\n")
gen_composition_table(data.canon_comp, out)
out.write("\n")
gen_decomposition_tables(data.canon_fully_decomp, data.compat_fully_decomp, out)
gen_combining_mark(data.general_category_mark, out)
out.write("\n")
gen_nfc_qc(data.norm_props, out)
out.write("\n")
gen_nfkc_qc(data.norm_props, out)
out.write("\n")
gen_nfd_qc(data.norm_props, out)
out.write("\n")
gen_nfkd_qc(data.norm_props, out)
out.write("\n")
gen_stream_safe(data.ss_leading, data.ss_trailing, out)
out.write("\n")
with open("normalization_tests.rs", "w") as out:
out.write(PREAMBLE)
gen_tests(data.norm_tests, out)