1# Copyright 2008 The RE2 Authors. All Rights Reserved. 2# Use of this source code is governed by a BSD-style 3# license that can be found in the LICENSE file. 4 5"""Parser for Unicode data files (as distributed by unicode.org).""" 6 7from __future__ import absolute_import 8from __future__ import division 9from __future__ import print_function 10 11import os 12import re 13from six.moves import urllib 14 15# Directory or URL where Unicode tables reside. 16_UNICODE_DIR = "https://www.unicode.org/Public/13.0.0/ucd" 17 18# Largest valid Unicode code value. 19_RUNE_MAX = 0x10FFFF 20 21 22class Error(Exception): 23 """Unicode error base class.""" 24 25 26class InputError(Error): 27 """Unicode input error class. Raised on invalid input.""" 28 29 30def _UInt(s): 31 """Converts string to Unicode code point ('263A' => 0x263a). 32 33 Args: 34 s: string to convert 35 36 Returns: 37 Unicode code point 38 39 Raises: 40 InputError: the string is not a valid Unicode value. 41 """ 42 43 try: 44 v = int(s, 16) 45 except ValueError: 46 v = -1 47 if len(s) < 4 or len(s) > 6 or v < 0 or v > _RUNE_MAX: 48 raise InputError("invalid Unicode value %s" % (s,)) 49 return v 50 51 52def _URange(s): 53 """Converts string to Unicode range. 54 55 '0001..0003' => [1, 2, 3]. 56 '0001' => [1]. 57 58 Args: 59 s: string to convert 60 61 Returns: 62 Unicode range 63 64 Raises: 65 InputError: the string is not a valid Unicode range. 66 """ 67 a = s.split("..") 68 if len(a) == 1: 69 return [_UInt(a[0])] 70 if len(a) == 2: 71 lo = _UInt(a[0]) 72 hi = _UInt(a[1]) 73 if lo < hi: 74 return range(lo, hi + 1) 75 raise InputError("invalid Unicode range %s" % (s,)) 76 77 78def _UStr(v): 79 """Converts Unicode code point to hex string. 80 81 0x263a => '0x263A'. 82 83 Args: 84 v: code point to convert 85 86 Returns: 87 Unicode string 88 89 Raises: 90 InputError: the argument is not a valid Unicode value. 91 """ 92 if v < 0 or v > _RUNE_MAX: 93 raise InputError("invalid Unicode value %s" % (v,)) 94 return "0x%04X" % (v,) 95 96 97def _ParseContinue(s): 98 """Parses a Unicode continuation field. 99 100 These are of the form '<Name, First>' or '<Name, Last>'. 101 Instead of giving an explicit range in a single table entry, 102 some Unicode tables use two entries, one for the first 103 code value in the range and one for the last. 104 The first entry's description is '<Name, First>' instead of 'Name' 105 and the second is '<Name, Last>'. 106 107 '<Name, First>' => ('Name', 'First') 108 '<Name, Last>' => ('Name', 'Last') 109 'Anything else' => ('Anything else', None) 110 111 Args: 112 s: continuation field string 113 114 Returns: 115 pair: name and ('First', 'Last', or None) 116 """ 117 118 match = re.match("<(.*), (First|Last)>", s) 119 if match is not None: 120 return match.groups() 121 return (s, None) 122 123 124def ReadUnicodeTable(filename, nfields, doline): 125 """Generic Unicode table text file reader. 126 127 The reader takes care of stripping out comments and also 128 parsing the two different ways that the Unicode tables specify 129 code ranges (using the .. notation and splitting the range across 130 multiple lines). 131 132 Each non-comment line in the table is expected to have the given 133 number of fields. The first field is known to be the Unicode value 134 and the second field its description. 135 136 The reader calls doline(codes, fields) for each entry in the table. 137 If fn raises an exception, the reader prints that exception, 138 prefixed with the file name and line number, and continues 139 processing the file. When done with the file, the reader re-raises 140 the first exception encountered during the file. 141 142 Arguments: 143 filename: the Unicode data file to read, or a file-like object. 144 nfields: the number of expected fields per line in that file. 145 doline: the function to call for each table entry. 146 147 Raises: 148 InputError: nfields is invalid (must be >= 2). 149 """ 150 151 if nfields < 2: 152 raise InputError("invalid number of fields %d" % (nfields,)) 153 154 if type(filename) == str: 155 if filename.startswith("https://"): 156 fil = urllib.request.urlopen(filename) 157 else: 158 fil = open(filename, "rb") 159 else: 160 fil = filename 161 162 first = None # first code in multiline range 163 expect_last = None # tag expected for "Last" line in multiline range 164 lineno = 0 # current line number 165 for line in fil: 166 lineno += 1 167 try: 168 line = line.decode('latin1') 169 170 # Chop # comments and white space; ignore empty lines. 171 sharp = line.find("#") 172 if sharp >= 0: 173 line = line[:sharp] 174 line = line.strip() 175 if not line: 176 continue 177 178 # Split fields on ";", chop more white space. 179 # Must have the expected number of fields. 180 fields = [s.strip() for s in line.split(";")] 181 if len(fields) != nfields: 182 raise InputError("wrong number of fields %d %d - %s" % 183 (len(fields), nfields, line)) 184 185 # The Unicode text files have two different ways 186 # to list a Unicode range. Either the first field is 187 # itself a range (0000..FFFF), or the range is split 188 # across two lines, with the second field noting 189 # the continuation. 190 codes = _URange(fields[0]) 191 (name, cont) = _ParseContinue(fields[1]) 192 193 if expect_last is not None: 194 # If the last line gave the First code in a range, 195 # this one had better give the Last one. 196 if (len(codes) != 1 or codes[0] <= first or 197 cont != "Last" or name != expect_last): 198 raise InputError("expected Last line for %s" % 199 (expect_last,)) 200 codes = range(first, codes[0] + 1) 201 first = None 202 expect_last = None 203 fields[0] = "%04X..%04X" % (codes[0], codes[-1]) 204 fields[1] = name 205 elif cont == "First": 206 # Otherwise, if this is the First code in a range, 207 # remember it and go to the next line. 208 if len(codes) != 1: 209 raise InputError("bad First line: range given") 210 expect_last = name 211 first = codes[0] 212 continue 213 214 doline(codes, fields) 215 216 except Exception as e: 217 print("%s:%d: %s" % (filename, lineno, e)) 218 raise 219 220 if expect_last is not None: 221 raise InputError("expected Last line for %s; got EOF" % 222 (expect_last,)) 223 224 225def CaseGroups(unicode_dir=_UNICODE_DIR): 226 """Returns list of Unicode code groups equivalent under case folding. 227 228 Each group is a sorted list of code points, 229 and the list of groups is sorted by first code point 230 in the group. 231 232 Args: 233 unicode_dir: Unicode data directory 234 235 Returns: 236 list of Unicode code groups 237 """ 238 239 # Dict mapping lowercase code point to fold-equivalent group. 240 togroup = {} 241 242 def DoLine(codes, fields): 243 """Process single CaseFolding.txt line, updating togroup.""" 244 (_, foldtype, lower, _) = fields 245 if foldtype not in ("C", "S"): 246 return 247 lower = _UInt(lower) 248 togroup.setdefault(lower, [lower]).extend(codes) 249 250 ReadUnicodeTable(unicode_dir+"/CaseFolding.txt", 4, DoLine) 251 252 groups = list(togroup.values()) 253 for g in groups: 254 g.sort() 255 groups.sort() 256 return togroup, groups 257 258 259def Scripts(unicode_dir=_UNICODE_DIR): 260 """Returns dict mapping script names to code lists. 261 262 Args: 263 unicode_dir: Unicode data directory 264 265 Returns: 266 dict mapping script names to code lists 267 """ 268 269 scripts = {} 270 271 def DoLine(codes, fields): 272 """Process single Scripts.txt line, updating scripts.""" 273 (_, name) = fields 274 scripts.setdefault(name, []).extend(codes) 275 276 ReadUnicodeTable(unicode_dir+"/Scripts.txt", 2, DoLine) 277 return scripts 278 279 280def Categories(unicode_dir=_UNICODE_DIR): 281 """Returns dict mapping category names to code lists. 282 283 Args: 284 unicode_dir: Unicode data directory 285 286 Returns: 287 dict mapping category names to code lists 288 """ 289 290 categories = {} 291 292 def DoLine(codes, fields): 293 """Process single UnicodeData.txt line, updating categories.""" 294 category = fields[2] 295 categories.setdefault(category, []).extend(codes) 296 # Add codes from Lu into L, etc. 297 if len(category) > 1: 298 short = category[0] 299 categories.setdefault(short, []).extend(codes) 300 301 ReadUnicodeTable(unicode_dir+"/UnicodeData.txt", 15, DoLine) 302 return categories 303 304