| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 |
|
| 13 | import unicodedata
|
| 14 | from unidecode import unidecode
|
| 15 | import sys
|
| 16 | import argparse
|
| 17 | from collections import defaultdict
|
| 18 |
|
| 19 | |
| 20 | try:
|
| 21 | from importlib.metadata import version
|
| 22 | unidecode_version = version('unidecode')
|
| 23 | except:
|
| 24 | unidecode_version = 'unknown'
|
| 25 |
|
| 26 | |
| 27 | from pathlib import Path
|
| 28 | this_file = Path(__file__).name
|
| 29 |
|
| 30 | |
| 31 | DEFAULT_OUT_FILE = "ucs_fallback_table.h"
|
| 32 |
|
| 33 | |
| 34 | RANGE_MARKER = 0x00
|
| 35 |
|
| 36 | def generate_fallback_map():
|
| 37 |
|
| 38 | fallback_map = {}
|
| 39 |
|
| 40 | |
| 41 | for cp in range(0x0080, 0x10000): |
| 42 | char = chr(cp)
|
| 43 |
|
| 44 | |
| 45 | try:
|
| 46 | if not unicodedata.name(char, ''):
|
| 47 | continue
|
| 48 | except ValueError:
|
| 49 | continue
|
| 50 |
|
| 51 | |
| 52 | ascii_version = unidecode(char)
|
| 53 |
|
| 54 | |
| 55 | if len(ascii_version) == 1:
|
| 56 | fallback_map[cp] = ord(ascii_version)
|
| 57 |
|
| 58 | |
| 59 | fallback_map.update(get_special_overrides())
|
| 60 |
|
| 61 | return fallback_map
|
| 62 |
|
| 63 | def get_special_overrides():
|
| 64 | |
| 65 | provides... or doesn't provide at all.
|
| 66 |
|
| 67 | overrides = {}
|
| 68 |
|
| 69 | |
| 70 | |
| 71 | |
| 72 | overrides[0x00C6] = ord('E') |
| 73 | overrides[0x00E6] = ord('e') |
| 74 | overrides[0x0152] = ord('E') |
| 75 | overrides[0x0153] = ord('e') |
| 76 | overrides[0x00DF] = ord('s') |
| 77 |
|
| 78 | |
| 79 | overrides[0x2264] = ord('<') |
| 80 | overrides[0x2265] = ord('>') |
| 81 |
|
| 82 | |
| 83 | overrides[0x2260] = ord('#') |
| 84 |
|
| 85 | |
| 86 | for cp in range(0x2596, 0x259F+1):
|
| 87 | overrides[cp] = ord('#') |
| 88 |
|
| 89 | |
| 90 | |
| 91 | overrides[0x2192] = ord('>') |
| 92 | overrides[0x2190] = ord('<') |
| 93 | overrides[0x2191] = ord('^') |
| 94 | overrides[0x2193] = ord('v') |
| 95 |
|
| 96 | |
| 97 | overrides[0x21D0] = ord('<') |
| 98 | overrides[0x21D1] = ord('^') |
| 99 | overrides[0x21D2] = ord('>') |
| 100 | overrides[0x21D3] = ord('v') |
| 101 |
|
| 102 | |
| 103 | |
| 104 | overrides[0xFFE9] = ord('<') |
| 105 | overrides[0xFFEA] = ord('^') |
| 106 | overrides[0xFFEB] = ord('>') |
| 107 | overrides[0xFFEC] = ord('v') |
| 108 |
|
| 109 | |
| 110 | overrides[0x00A2] = ord('c') |
| 111 | overrides[0x00A3] = ord('L') |
| 112 | overrides[0x00A5] = ord('Y') |
| 113 | overrides[0x20AC] = ord('E') |
| 114 |
|
| 115 | |
| 116 | overrides[0x00A7] = ord('S') |
| 117 | overrides[0x00A9] = ord('C') |
| 118 | overrides[0x00AE] = ord('R') |
| 119 | overrides[0x2122] = ord('T') |
| 120 |
|
| 121 | |
| 122 | overrides[0x00B0] = ord('o') |
| 123 | overrides[0x2103] = ord('C') |
| 124 | overrides[0x2109] = ord('F') |
| 125 |
|
| 126 | |
| 127 | overrides[0x00AB] = ord('<') |
| 128 | overrides[0x00BB] = ord('>') |
| 129 |
|
| 130 | |
| 131 | overrides[0x2218] = ord('o') |
| 132 | overrides[0x2219] = ord('.') |
| 133 |
|
| 134 | |
| 135 | |
| 136 | for cp in (0x2204, 0x2209, 0x220C, 0x2224, 0x2226, 0x226E, 0x226F, 0x2280, 0x2281, 0x2284, 0x2285):
|
| 137 | overrides[cp] = ord('!') |
| 138 |
|
| 139 | |
| 140 | for cp in (0x2241, 0x2244, 0x2249, 0x2262, 0x2268, 0x2269, 0x226D, 0x228A, 0x228B):
|
| 141 | overrides[cp] = ord('#') |
| 142 |
|
| 143 | |
| 144 | for cp in (0x219A, 0x219B, 0x21AE, 0x21CD, 0x21CE, 0x21CF):
|
| 145 | overrides[cp] = ord('!') |
| 146 |
|
| 147 | |
| 148 | for cp in (0x2010, 0x2011, 0x2012, 0x2013, 0x2014, 0x2015, 0x2043, 0x2052):
|
| 149 | overrides[cp] = ord('-') |
| 150 |
|
| 151 | |
| 152 | for cp in (0x203D, 0x2047, 0x2048):
|
| 153 | overrides[cp] = ord('?') |
| 154 |
|
| 155 | |
| 156 | for cp in (0x203C, 0x2049):
|
| 157 | overrides[cp] = ord('!') |
| 158 |
|
| 159 | |
| 160 | for cp in (0x2042, 0x2051, 0x2055):
|
| 161 | overrides[cp] = ord('*')
|
| 162 |
|
| 163 | |
| 164 | overrides[0x201E] = ord('"') |
| 165 | overrides[0x2023] = ord('>') |
| 166 | overrides[0x2026] = ord('.') |
| 167 | overrides[0x2033] = ord('"') |
| 168 | overrides[0x204B] = ord('P') |
| 169 | overrides[0x204C] = ord('<') |
| 170 | overrides[0x204D] = ord('>') |
| 171 | overrides[0x204F] = ord(';') |
| 172 | overrides[0x205B] = ord(':') |
| 173 |
|
| 174 | |
| 175 | overrides[0x2713] = ord('v') |
| 176 | overrides[0x2714] = ord('V') |
| 177 |
|
| 178 | |
| 179 | for cp in (0x2715, 0x2717):
|
| 180 | overrides[cp] = ord('x') |
| 181 | for cp in (0x2716, 0x2718):
|
| 182 | overrides[cp] = ord('X') |
| 183 |
|
| 184 | |
| 185 | for cp in (0x2605, 0x2606, 0x262A, 0x269D, 0x2698):
|
| 186 | overrides[cp] = ord('*') |
| 187 | for cp in range(0x2721, 0x2746+1):
|
| 188 | overrides[cp] = ord('*') |
| 189 | for cp in range(0x2749, 0x274B+1):
|
| 190 | overrides[cp] = ord('*') |
| 191 | for cp in (0x229B, 0x22C6, 0x235F, 0x2363):
|
| 192 | overrides[cp] = ord('*') |
| 193 |
|
| 194 | |
| 195 | |
| 196 |
|
| 197 | |
| 198 | overrides[0x2028] = 0 |
| 199 |
|
| 200 | |
| 201 | |
| 202 | |
| 203 | |
| 204 | |
| 205 | for cp in range(0xFF01, 0xFF5E + 1):
|
| 206 | overrides[cp] = 0 |
| 207 |
|
| 208 | return overrides
|
| 209 |
|
| 210 | def organize_by_pages(fallback_map):
|
| 211 |
|
| 212 | |
| 213 | page_groups = defaultdict(list)
|
| 214 | for code, fallback in fallback_map.items():
|
| 215 | |
| 216 | if fallback == 0:
|
| 217 | continue
|
| 218 |
|
| 219 | page = code >> 8 |
| 220 | offset = code & 0xFF |
| 221 | page_groups[page].append((offset, fallback))
|
| 222 |
|
| 223 | |
| 224 | for page in page_groups:
|
| 225 | page_groups[page].sort()
|
| 226 |
|
| 227 | return page_groups
|
| 228 |
|
| 229 | def compress_ranges(page_groups):
|
| 230 | |
| 231 | A range is only compressed if it contains 3 or more consecutive entries.
|
| 232 |
|
| 233 | compressed_pages = {}
|
| 234 |
|
| 235 | for page, entries in page_groups.items():
|
| 236 | compressed_entries = []
|
| 237 | i = 0
|
| 238 | while i < len(entries):
|
| 239 | start_offset, fallback = entries[i]
|
| 240 |
|
| 241 | |
| 242 | j = i + 1
|
| 243 | while (j < len(entries) and
|
| 244 | entries[j][0] == entries[j-1][0] + 1 and |
| 245 | entries[j][1] == fallback): |
| 246 | j += 1
|
| 247 |
|
| 248 | |
| 249 | end_offset = entries[j-1][0]
|
| 250 |
|
| 251 | |
| 252 | if j - i >= 3:
|
| 253 | |
| 254 | compressed_entries.append((start_offset, RANGE_MARKER))
|
| 255 | compressed_entries.append((end_offset, fallback))
|
| 256 | else:
|
| 257 | |
| 258 | for k in range(i, j):
|
| 259 | compressed_entries.append(entries[k])
|
| 260 |
|
| 261 | i = j
|
| 262 |
|
| 263 | compressed_pages[page] = compressed_entries
|
| 264 |
|
| 265 | return compressed_pages
|
| 266 |
|
| 267 | def cp_name(cp):
|
| 268 |
|
| 269 | try:
|
| 270 | return unicodedata.name(chr(cp))
|
| 271 | except:
|
| 272 | return f"U+{cp:04X}"
|
| 273 |
|
| 274 | def generate_fallback_tables(out_file=DEFAULT_OUT_FILE):
|
| 275 |
|
| 276 | |
| 277 | fallback_map = generate_fallback_map()
|
| 278 | print(f"Generated {len(fallback_map)} total fallback mappings")
|
| 279 |
|
| 280 | |
| 281 | page_groups = organize_by_pages(fallback_map)
|
| 282 | print(f"Organized into {len(page_groups)} pages")
|
| 283 |
|
| 284 | |
| 285 | compressed_pages = compress_ranges(page_groups)
|
| 286 | total_compressed_entries = sum(len(entries) for entries in compressed_pages.values())
|
| 287 | print(f"Total compressed entries: {total_compressed_entries}")
|
| 288 |
|
| 289 | |
| 290 | with open(out_file, 'w') as f:
|
| 291 | f.write(f"""\
|
| 292 | /* SPDX-License-Identifier: GPL-2.0 */
|
| 293 | /*
|
| 294 | * {out_file} - Unicode character fallback table
|
| 295 | *
|
| 296 | * Auto-generated by {this_file}
|
| 297 | *
|
| 298 | * Unicode Version: {unicodedata.unidata_version}
|
| 299 | * Unidecode Version: {unidecode_version}
|
| 300 | *
|
| 301 | * This file contains optimized tables that map complex Unicode characters
|
| 302 | * to simpler fallback characters for terminal display when corresponding
|
| 303 | * glyphs are unavailable.
|
| 304 | */
|
| 305 |
|
| 306 | static const struct ucs_page_desc ucs_fallback_pages[] = {{
|
| 307 | """)
|
| 308 |
|
| 309 | |
| 310 | sorted_pages = sorted(compressed_pages.items())
|
| 311 |
|
| 312 | |
| 313 | start_index = 0
|
| 314 |
|
| 315 | |
| 316 | for page, entries in sorted_pages:
|
| 317 | count = len(entries)
|
| 318 | f.write(f"\t{{ 0x{page:02X}, {count}, {start_index} }},\n")
|
| 319 | start_index += count
|
| 320 |
|
| 321 | |
| 322 | f.write("""\
|
| 323 | };
|
| 324 |
|
| 325 | /* Page entries array (referenced by page descriptors) */
|
| 326 | static const struct ucs_page_entry ucs_fallback_entries[] = {
|
| 327 | """)
|
| 328 |
|
| 329 | |
| 330 | for page, entries in sorted_pages:
|
| 331 | page_hex = f"0x{page:02X}"
|
| 332 | f.write(f"\t/* Entries for page {page_hex} */\n")
|
| 333 |
|
| 334 | for i, (offset, fallback) in enumerate(entries):
|
| 335 | |
| 336 | offset_hex = f"0x{offset:02X}"
|
| 337 | fallback_hex = f"0x{fallback:02X}"
|
| 338 |
|
| 339 | |
| 340 | codepoint = (page << 8) | offset
|
| 341 |
|
| 342 | if fallback == RANGE_MARKER:
|
| 343 | comment = f"{cp_name(codepoint)} -> ..."
|
| 344 | else:
|
| 345 | comment = f"{cp_name(codepoint)} -> '{chr(fallback)}'"
|
| 346 | f.write(f"\t{{ 0x{offset:02X}, 0x{fallback:02X} }}, /* {comment} */\n")
|
| 347 |
|
| 348 | f.write(f"""\
|
| 349 | }};
|
| 350 |
|
| 351 | #define UCS_PAGE_ENTRY_RANGE_MARKER {RANGE_MARKER}
|
| 352 | """)
|
| 353 |
|
| 354 | if __name__ == "__main__":
|
| 355 | parser = argparse.ArgumentParser(description="Generate Unicode fallback character tables")
|
| 356 | parser.add_argument("-o", "--output", dest="output_file", default=DEFAULT_OUT_FILE,
|
| 357 | help=f"Output file name (default: {DEFAULT_OUT_FILE})")
|
| 358 | args = parser.parse_args()
|
| 359 |
|
| 360 | generate_fallback_tables(out_file=args.output_file)
|