| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
The Box Drawing and Block Elements blocks are named systematically, so the 160 |
| 5 |
recipes are already written down: BOX DRAWINGS DOWN LIGHT AND RIGHT HEAVY says |
| 6 |
which arms a glyph has and how heavy each one is, and LEFT THREE EIGHTHS BLOCK |
| 7 |
says which fraction of the cell is filled. Typing that out by hand would be 160 |
| 8 |
chances to transpose two arms in a way nothing catches until a border looks |
| 9 |
wrong in one corner. |
| 10 |
|
| 11 |
So the table is derived rather than authored, from the `unicodedata` module in |
| 12 |
the Python standard library, and the result is committed. Re-run it when the |
| 13 |
shape vocabulary changes: |
| 14 |
|
| 15 |
python3 scripts/derive-cell-table.py > src/cells/table.rs |
| 16 |
|
| 17 |
Every name must parse. The script fails rather than emitting a partial table, |
| 18 |
because a name it did not understand is a glyph that would silently come out |
| 19 |
blank. |
| 20 |
|
| 21 |
|
| 22 |
import sys |
| 23 |
import unicodedata as ud |
| 24 |
|
| 25 |
WEIGHT = {"LIGHT": "L", "HEAVY": "H", "DOUBLE": "D", "SINGLE": "L"} |
| 26 |
DIRS = { |
| 27 |
"UP": ["up"], |
| 28 |
"DOWN": ["down"], |
| 29 |
"LEFT": ["left"], |
| 30 |
"RIGHT": ["right"], |
| 31 |
"HORIZONTAL": ["left", "right"], |
| 32 |
"VERTICAL": ["up", "down"], |
| 33 |
} |
| 34 |
ARMS = ["left", "right", "up", "down"] |
| 35 |
FRACTION = { |
| 36 |
"ONE": 1, "TWO": 2, "THREE": 3, "FOUR": 4, |
| 37 |
"FIVE": 5, "SIX": 6, "SEVEN": 7, "EIGHT": 8, |
| 38 |
} |
| 39 |
|
| 40 |
|
| 41 |
def die(msg): |
| 42 |
print(f"derive-cell-table: {msg}", file=sys.stderr) |
| 43 |
sys.exit(1) |
| 44 |
|
| 45 |
|
| 46 |
def parse_box(cp): |
| 47 |
|
| 48 |
name = ud.name(chr(cp)) |
| 49 |
body = name.replace("BOX DRAWINGS ", "") |
| 50 |
|
| 51 |
if "DIAGONAL" in body: |
| 52 |
if "UPPER RIGHT TO LOWER LEFT" in body: |
| 53 |
return ("diagonal", "Rising", {}) |
| 54 |
if "UPPER LEFT TO LOWER RIGHT" in body: |
| 55 |
return ("diagonal", "Falling", {}) |
| 56 |
if "CROSS" in body: |
| 57 |
return ("diagonal", "Cross", {}) |
| 58 |
die(f"U+{cp:04X} {name}: unrecognised diagonal") |
| 59 |
|
| 60 |
kind = "stems" |
| 61 |
if "ARC " in body: |
| 62 |
kind = "arc" |
| 63 |
body = body.replace("ARC ", "") |
| 64 |
|
| 65 |
dash = "None" |
| 66 |
for spelling, variant in [ |
| 67 |
("QUADRUPLE DASH", "Quadruple"), |
| 68 |
("TRIPLE DASH", "Triple"), |
| 69 |
("DOUBLE DASH", "Double"), |
| 70 |
]: |
| 71 |
if spelling in body: |
| 72 |
dash = variant |
| 73 |
body = body.replace(spelling + " ", "") |
| 74 |
|
| 75 |
arms = {} |
| 76 |
for clause in body.split(" AND "): |
| 77 |
tokens = clause.split() |
| 78 |
weights = [t for t in tokens if t in WEIGHT] |
| 79 |
directions = [t for t in tokens if t in DIRS] |
| 80 |
leftover = [t for t in tokens if t not in WEIGHT and t not in DIRS] |
| 81 |
if leftover or not directions: |
| 82 |
die(f"U+{cp:04X} {name}: cannot read clause {clause!r}") |
| 83 |
weight = WEIGHT[weights[0]] if weights else None |
| 84 |
for direction in directions: |
| 85 |
for arm in DIRS[direction]: |
| 86 |
arms[arm] = weight |
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
|
| 91 |
stated = [w for w in arms.values() if w] |
| 92 |
if not stated: |
| 93 |
die(f"U+{cp:04X} {name}: no weight anywhere") |
| 94 |
for arm, weight in arms.items(): |
| 95 |
if weight is None: |
| 96 |
arms[arm] = stated[0] |
| 97 |
return (kind, dash, arms) |
| 98 |
|
| 99 |
|
| 100 |
def parse_block(cp): |
| 101 |
|
| 102 |
name = ud.name(chr(cp)) |
| 103 |
if name == "FULL BLOCK": |
| 104 |
return ("fill", [(0, 0, 8, 8)]) |
| 105 |
if name.endswith("SHADE"): |
| 106 |
return ("shade", name.split()[0].capitalize()) |
| 107 |
if name.startswith("QUADRANT"): |
| 108 |
quads = { |
| 109 |
"UPPER LEFT": (0, 4, 4, 8), |
| 110 |
"UPPER RIGHT": (4, 4, 8, 8), |
| 111 |
"LOWER LEFT": (0, 0, 4, 4), |
| 112 |
"LOWER RIGHT": (4, 0, 8, 4), |
| 113 |
} |
| 114 |
rects = [] |
| 115 |
for part in name[len("QUADRANT "):].split(" AND "): |
| 116 |
if part not in quads: |
| 117 |
die(f"U+{cp:04X} {name}: unknown quadrant {part!r}") |
| 118 |
rects.append(quads[part]) |
| 119 |
return ("fill", rects) |
| 120 |
|
| 121 |
tokens = name.split() |
| 122 |
side, count, unit = tokens[0], tokens[1], tokens[2] |
| 123 |
if unit.startswith("QUARTER"): |
| 124 |
eighths = FRACTION[count] * 2 |
| 125 |
elif unit.startswith("EIGHTH"): |
| 126 |
eighths = FRACTION[count] |
| 127 |
elif count == "HALF": |
| 128 |
eighths = 4 |
| 129 |
unit = "HALF" |
| 130 |
else: |
| 131 |
die(f"U+{cp:04X} {name}: unknown fraction {count} {unit}") |
| 132 |
if count == "HALF": |
| 133 |
eighths = 4 |
| 134 |
|
| 135 |
if side == "UPPER": |
| 136 |
return ("fill", [(0, 8 - eighths, 8, 8)]) |
| 137 |
if side == "LOWER": |
| 138 |
return ("fill", [(0, 0, 8, eighths)]) |
| 139 |
if side == "LEFT": |
| 140 |
return ("fill", [(0, 0, eighths, 8)]) |
| 141 |
if side == "RIGHT": |
| 142 |
return ("fill", [(8 - eighths, 0, 8, 8)]) |
| 143 |
die(f"U+{cp:04X} {name}: unknown side {side!r}") |
| 144 |
|
| 145 |
|
| 146 |
def main(): |
| 147 |
stems, arcs, diagonals, fills, shades = [], [], [], [], [] |
| 148 |
|
| 149 |
for cp in range(0x2500, 0x2580): |
| 150 |
kind, payload, arms = parse_box(cp) |
| 151 |
if kind == "diagonal": |
| 152 |
diagonals.append((cp, payload)) |
| 153 |
continue |
| 154 |
row = (cp, [arms.get(a, "N") for a in ARMS], payload) |
| 155 |
(arcs if kind == "arc" else stems).append(row) |
| 156 |
|
| 157 |
for cp in range(0x2580, 0x25A0): |
| 158 |
kind, payload = parse_block(cp) |
| 159 |
(shades if kind == "shade" else fills).append((cp, payload)) |
| 160 |
|
| 161 |
counts = (len(stems), len(arcs), len(diagonals), len(fills), len(shades)) |
| 162 |
if sum(counts) != 160: |
| 163 |
die(f"expected 160 codepoints, derived {sum(counts)}") |
| 164 |
|
| 165 |
out = [] |
| 166 |
w = out.append |
| 167 |
w("// Generated by scripts/derive-cell-table.py from Unicode character names.") |
| 168 |
w("// Do not edit by hand: re-run the script instead. See its header for why") |
| 169 |
w("// the table is derived rather than authored.") |
| 170 |
w("//") |
| 171 |
w(f"// {counts[0]} stems, {counts[1]} arcs, {counts[2]} diagonals," |
| 172 |
f" {counts[3]} fills, {counts[4]} shades.") |
| 173 |
w("") |
| 174 |
w("use super::Arm::{D, H, L, N};") |
| 175 |
w("use super::{Dash, Diagonal, Shade};") |
| 176 |
w("") |
| 177 |
w("/// Arms in the order left, right, up, down.") |
| 178 |
w(f"pub(super) const STEMS: [(u32, [super::Arm; 4], Dash); {counts[0]}] = [") |
| 179 |
for cp, arms, dash in stems: |
| 180 |
w(f" (0x{cp:04X}, [{', '.join(arms)}], Dash::{dash}),") |
| 181 |
w("];") |
| 182 |
w("") |
| 183 |
w("/// The rounded corners. Same arms as a stem, drawn as a quarter turn.") |
| 184 |
w(f"pub(super) const ARCS: [(u32, [super::Arm; 4]); {counts[1]}] = [") |
| 185 |
for cp, arms, _ in arcs: |
| 186 |
w(f" (0x{cp:04X}, [{', '.join(arms)}]),") |
| 187 |
w("];") |
| 188 |
w("") |
| 189 |
w(f"pub(super) const DIAGONALS: [(u32, Diagonal); {counts[2]}] = [") |
| 190 |
for cp, kind in diagonals: |
| 191 |
w(f" (0x{cp:04X}, Diagonal::{kind}),") |
| 192 |
w("];") |
| 193 |
w("") |
| 194 |
w("/// Filled rectangles in eighths of the cell: x0, y0, x1, y1, with y up.") |
| 195 |
w(f"pub(super) const FILLS: [(u32, &[(u8, u8, u8, u8)]); {counts[3]}] = [") |
| 196 |
for cp, rects in fills: |
| 197 |
body = ", ".join(f"({a}, {b}, {c}, {d})" for a, b, c, d in rects) |
| 198 |
w(f" (0x{cp:04X}, &[{body}]),") |
| 199 |
w("];") |
| 200 |
w("") |
| 201 |
w(f"pub(super) const SHADES: [(u32, Shade); {counts[4]}] = [") |
| 202 |
for cp, level in shades: |
| 203 |
w(f" (0x{cp:04X}, Shade::{level}),") |
| 204 |
w("];") |
| 205 |
print("\n".join(out)) |
| 206 |
|
| 207 |
|
| 208 |
if __name__ == "__main__": |
| 209 |
main() |
| 210 |
|