lib/termtex/src/alphabet.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const ast = @import("ast.zig");
3
4 pub const Mode = enum {
5 bold,
6 italic,
7 bold_italic,
8 script,
9 bold_script,
10 fraktur,
11 bold_fraktur,
12 double_struck,
13 double_struck_italic,
14 sans,
15 sans_bold,
16 sans_italic,
17 sans_bold_italic,
18 monospace,
19 small_caps,
20 };
21
22 const Command = struct {
23 name: []const u8,
24 mode: Mode,
25 };
26
27 pub fn command(name: []const u8) ?Mode {
28 inline for (commands) |entry| {
29 if (std.mem.eql(u8, name, entry.name)) return entry.mode;
30 }
31 return null;
32 }
33
34 pub fn apply(arena: std.mem.Allocator, mode: Mode, expr: *const ast.Expr) std.mem.Allocator.Error!*ast.Expr {
35 return switch (expr.*) {
36 .row => |items| block: {
37 const out = try arena.alloc(*ast.Expr, items.len);
38 for (items, 0..) |item, index| out[index] = try apply(arena, mode, item);
39 break :block ast.node(arena, .{ .row = out });
40 },
41 .text => |value| mapText(arena, mode, value),
42 .operator => |value| ast.node(arena, .{ .operator = .{
43 .body = try apply(arena, mode, value.body),
44 .limit_policy = value.limit_policy,
45 } }),
46 .space => |value| ast.space(arena, value),
47 .fraction => |value| ast.node(arena, .{ .fraction = .{
48 .numerator = try apply(arena, mode, value.numerator),
49 .denominator = try apply(arena, mode, value.denominator),
50 .style = value.style,
51 } }),
52 .sqrt => |value| ast.node(arena, .{ .sqrt = .{
53 .index = if (value.index) |index| try apply(arena, mode, index) else null,
54 .body = try apply(arena, mode, value.body),
55 } }),
56 .scripts => |value| ast.node(arena, .{ .scripts = .{
57 .base = try apply(arena, mode, value.base),
58 .sub = if (value.sub) |sub| try apply(arena, mode, sub) else null,
59 .sup = if (value.sup) |sup| try apply(arena, mode, sup) else null,
60 } }),
61 .accent => |value| ast.node(arena, .{ .accent = .{
62 .mark = value.mark,
63 .body = try apply(arena, mode, value.body),
64 } }),
65 .grid => |value| block: {
66 const rows = try arena.alloc(ast.GridRow, value.rows.len);
67 for (value.rows, 0..) |row, row_index| {
68 const cells = try arena.alloc(*ast.Expr, row.cells.len);
69 for (row.cells, 0..) |cell, cell_index| cells[cell_index] = try apply(arena, mode, cell);
70 rows[row_index] = .{ .cells = cells };
71 }
72 break :block ast.node(arena, .{ .grid = .{
73 .rows = rows,
74 .fence = value.fence,
75 .alignment = value.alignment,
76 } });
77 },
78 .annotation => |value| ast.node(arena, .{ .annotation = .{
79 .base = try apply(arena, mode, value.base),
80 .over = if (value.over) |over| try apply(arena, mode, over) else null,
81 .under = if (value.under) |under| try apply(arena, mode, under) else null,
82 .kind = value.kind,
83 } }),
84 .delimited => |value| ast.node(arena, .{ .delimited = .{
85 .left = value.left,
86 .body = try apply(arena, mode, value.body),
87 .right = value.right,
88 } }),
89 };
90 }
91
92 fn mapText(arena: std.mem.Allocator, mode: Mode, value: []const u8) std.mem.Allocator.Error!*ast.Expr {
93 var out: std.ArrayListUnmanaged(u8) = .empty;
94 errdefer out.deinit(arena);
95 var index: usize = 0;
96 while (index < value.len) {
97 const len = std.unicode.utf8ByteSequenceLength(value[index]) catch 1;
98 const end = index + @min(len, value.len - index);
99 const glyph = value[index..end];
100 if (mappedGlyph(mode, glyph)) |mapped| {
101 switch (mapped) {
102 .scalar => |scalar| try appendScalar(arena, &out, scalar),
103 .text => |text| try out.appendSlice(arena, text),
104 }
105 } else {
106 try out.appendSlice(arena, glyph);
107 }
108 index = end;
109 }
110 return ast.node(arena, .{ .text = try out.toOwnedSlice(arena) });
111 }
112
113 const Glyph = union(enum) {
114 scalar: u21,
115 text: []const u8,
116 };
117
118 fn mappedGlyph(mode: Mode, glyph: []const u8) ?Glyph {
119 if (glyph.len == 1) {
120 if (asciiGlyph(mode, glyph[0])) |scalar| return .{ .scalar = scalar };
121 }
122 if (greekGlyph(mode, glyph)) |scalar| return .{ .scalar = scalar };
123 return null;
124 }
125
126 fn asciiGlyph(mode: Mode, char: u8) ?u21 {
127 return switch (mode) {
128 .bold => rangedAscii(char, 0x1D400, 0x1D41A, 0x1D7CE),
129 .italic => italicAscii(char),
130 .bold_italic => rangedAscii(char, 0x1D468, 0x1D482, null),
131 .script => scriptAscii(char),
132 .bold_script => rangedAscii(char, 0x1D4D0, 0x1D4EA, null),
133 .fraktur => frakturAscii(char),
134 .bold_fraktur => rangedAscii(char, 0x1D56C, 0x1D586, null),
135 .double_struck => doubleStruckAscii(char),
136 .double_struck_italic => doubleStruckItalicAscii(char),
137 .sans => rangedAscii(char, 0x1D5A0, 0x1D5BA, 0x1D7E2),
138 .sans_bold => rangedAscii(char, 0x1D5D4, 0x1D5EE, 0x1D7EC),
139 .sans_italic => rangedAscii(char, 0x1D608, 0x1D622, null),
140 .sans_bold_italic => rangedAscii(char, 0x1D63C, 0x1D656, null),
141 .monospace => rangedAscii(char, 0x1D670, 0x1D68A, 0x1D7F6),
142 .small_caps => smallCapsAscii(char),
143 };
144 }
145
146 fn rangedAscii(char: u8, upper: ?u21, lower: ?u21, digit: ?u21) ?u21 {
147 if (char >= 'A' and char <= 'Z') {
148 if (upper) |base| return base + offset(char, 'A');
149 }
150 if (char >= 'a' and char <= 'z') {
151 if (lower) |base| return base + offset(char, 'a');
152 }
153 if (char >= '0' and char <= '9') {
154 if (digit) |base| return base + offset(char, '0');
155 }
156 return null;
157 }
158
159 fn italicAscii(char: u8) ?u21 {
160 if (char == 'h') return 0x210E;
161 return rangedAscii(char, 0x1D434, 0x1D44E, null);
162 }
163
164 fn scriptAscii(char: u8) ?u21 {
165 return switch (char) {
166 'B' => 0x212C,
167 'E' => 0x2130,
168 'F' => 0x2131,
169 'H' => 0x210B,
170 'I' => 0x2110,
171 'L' => 0x2112,
172 'M' => 0x2133,
173 'R' => 0x211B,
174 'e' => 0x212F,
175 'g' => 0x210A,
176 'o' => 0x2134,
177 else => rangedAscii(char, 0x1D49C, 0x1D4B6, null),
178 };
179 }
180
181 fn frakturAscii(char: u8) ?u21 {
182 return switch (char) {
183 'C' => 0x212D,
184 'H' => 0x210C,
185 'I' => 0x2111,
186 'R' => 0x211C,
187 'Z' => 0x2128,
188 else => rangedAscii(char, 0x1D504, 0x1D51E, null),
189 };
190 }
191
192 fn doubleStruckAscii(char: u8) ?u21 {
193 return switch (char) {
194 'C' => 0x2102,
195 'H' => 0x210D,
196 'N' => 0x2115,
197 'P' => 0x2119,
198 'Q' => 0x211A,
199 'R' => 0x211D,
200 'Z' => 0x2124,
201 else => rangedAscii(char, 0x1D538, 0x1D552, 0x1D7D8),
202 };
203 }
204
205 fn doubleStruckItalicAscii(char: u8) ?u21 {
206 return switch (char) {
207 'D' => 0x2145,
208 'd' => 0x2146,
209 'e' => 0x2147,
210 'i' => 0x2148,
211 'j' => 0x2149,
212 else => null,
213 };
214 }
215
216 fn smallCapsAscii(char: u8) ?u21 {
217 return switch (std.ascii.toUpper(char)) {
218 'A' => 0x1D00,
219 'B' => 0x0299,
220 'C' => 0x1D04,
221 'D' => 0x1D05,
222 'E' => 0x1D07,
223 'F' => 0xA730,
224 'G' => 0x0262,
225 'H' => 0x029C,
226 'I' => 0x026A,
227 'J' => 0x1D0A,
228 'K' => 0x1D0B,
229 'L' => 0x029F,
230 'M' => 0x1D0D,
231 'N' => 0x0274,
232 'O' => 0x1D0F,
233 'P' => 0x1D18,
234 'Q' => 0xA7AF,
235 'R' => 0x0280,
236 'S' => 0xA731,
237 'T' => 0x1D1B,
238 'U' => 0x1D1C,
239 'V' => 0x1D20,
240 'W' => 0x1D21,
241 'X' => 'x',
242 'Y' => 0x028F,
243 'Z' => 0x1D22,
244 else => null,
245 };
246 }
247
248 fn greekGlyph(mode: Mode, glyph: []const u8) ?u21 {
249 const set = switch (mode) {
250 .bold => GreekSet{ .upper = 0x1D6A8, .nabla = 0x1D6C1, .lower = 0x1D6C2, .partial = 0x1D6DB, .symbol = 0x1D6DC },
251 .italic => GreekSet{ .upper = 0x1D6E2, .nabla = 0x1D6FB, .lower = 0x1D6FC, .partial = 0x1D715, .symbol = 0x1D716 },
252 .bold_italic => GreekSet{ .upper = 0x1D71C, .nabla = 0x1D735, .lower = 0x1D736, .partial = 0x1D74F, .symbol = 0x1D750 },
253 else => return null,
254 };
255 inline for (greekUpper, 0..) |value, index| {
256 if (std.mem.eql(u8, glyph, value)) return set.upper + @as(u21, @intCast(index));
257 }
258 if (std.mem.eql(u8, glyph, "∇")) return set.nabla;
259 inline for (greekLower, 0..) |value, index| {
260 if (std.mem.eql(u8, glyph, value)) return set.lower + @as(u21, @intCast(index));
261 }
262 if (std.mem.eql(u8, glyph, "∂")) return set.partial;
263 inline for (greekSymbols, 0..) |value, index| {
264 if (std.mem.eql(u8, glyph, value)) return set.symbol + @as(u21, @intCast(index));
265 }
266 return null;
267 }
268
269 const GreekSet = struct {
270 upper: u21,
271 nabla: u21,
272 lower: u21,
273 partial: u21,
274 symbol: u21,
275 };
276
277 fn appendScalar(arena: std.mem.Allocator, out: *std.ArrayListUnmanaged(u8), scalar: u21) std.mem.Allocator.Error!void {
278 var buffer: [4]u8 = undefined;
279 const len = std.unicode.utf8Encode(scalar, &buffer) catch unreachable;
280 try out.appendSlice(arena, buffer[0..len]);
281 }
282
283 fn offset(char: u8, base: u8) u21 {
284 return @intCast(char - base);
285 }
286
287 const commands = [_]Command{
288 .{ .name = "mathbf", .mode = .bold },
289 .{ .name = "textbf", .mode = .bold },
290 .{ .name = "mathbold", .mode = .bold },
291 .{ .name = "mathbfup", .mode = .bold },
292 .{ .name = "symbf", .mode = .bold },
293 .{ .name = "symbfup", .mode = .bold },
294 .{ .name = "mathit", .mode = .italic },
295 .{ .name = "textit", .mode = .italic },
296 .{ .name = "emph", .mode = .italic },
297 .{ .name = "symit", .mode = .italic },
298 .{ .name = "mathbfit", .mode = .bold_italic },
299 .{ .name = "boldsymbol", .mode = .bold_italic },
300 .{ .name = "bm", .mode = .bold_italic },
301 .{ .name = "symbfit", .mode = .bold_italic },
302 .{ .name = "mathcal", .mode = .script },
303 .{ .name = "mathscr", .mode = .script },
304 .{ .name = "symcal", .mode = .script },
305 .{ .name = "symscr", .mode = .script },
306 .{ .name = "mathbcal", .mode = .bold_script },
307 .{ .name = "mathbfcal", .mode = .bold_script },
308 .{ .name = "mathbfscr", .mode = .bold_script },
309 .{ .name = "symbfcal", .mode = .bold_script },
310 .{ .name = "symbfscr", .mode = .bold_script },
311 .{ .name = "mathfrak", .mode = .fraktur },
312 .{ .name = "symfrak", .mode = .fraktur },
313 .{ .name = "mathbffrak", .mode = .bold_fraktur },
314 .{ .name = "symbffrak", .mode = .bold_fraktur },
315 .{ .name = "mathbb", .mode = .double_struck },
316 .{ .name = "mathds", .mode = .double_struck },
317 .{ .name = "symbb", .mode = .double_struck },
318 .{ .name = "mathbbit", .mode = .double_struck_italic },
319 .{ .name = "symbbit", .mode = .double_struck_italic },
320 .{ .name = "mathsf", .mode = .sans },
321 .{ .name = "mathsfup", .mode = .sans },
322 .{ .name = "textsf", .mode = .sans },
323 .{ .name = "symsf", .mode = .sans },
324 .{ .name = "symsfup", .mode = .sans },
325 .{ .name = "mathbfsf", .mode = .sans_bold },
326 .{ .name = "mathbfsfup", .mode = .sans_bold },
327 .{ .name = "mathsfbf", .mode = .sans_bold },
328 .{ .name = "symbfsf", .mode = .sans_bold },
329 .{ .name = "symbfsfup", .mode = .sans_bold },
330 .{ .name = "symsfbf", .mode = .sans_bold },
331 .{ .name = "mathsfit", .mode = .sans_italic },
332 .{ .name = "symsfit", .mode = .sans_italic },
333 .{ .name = "mathbfsfit", .mode = .sans_bold_italic },
334 .{ .name = "mathsfbfit", .mode = .sans_bold_italic },
335 .{ .name = "symbfsfit", .mode = .sans_bold_italic },
336 .{ .name = "symsfbfit", .mode = .sans_bold_italic },
337 .{ .name = "mathtt", .mode = .monospace },
338 .{ .name = "texttt", .mode = .monospace },
339 .{ .name = "symtt", .mode = .monospace },
340 .{ .name = "textsc", .mode = .small_caps },
341 };
342
343 const greekUpper = [_][]const u8{
344 "Α",
345 "Β",
346 "Γ",
347 "Δ",
348 "Ε",
349 "Ζ",
350 "Η",
351 "Θ",
352 "Ι",
353 "Κ",
354 "Λ",
355 "Μ",
356 "Ν",
357 "Ξ",
358 "Ο",
359 "Π",
360 "Ρ",
361 "ϴ",
362 "Σ",
363 "Τ",
364 "Υ",
365 "Φ",
366 "Χ",
367 "Ψ",
368 "Ω",
369 };
370
371 const greekLower = [_][]const u8{
372 "α",
373 "β",
374 "γ",
375 "δ",
376 "ε",
377 "ζ",
378 "η",
379 "θ",
380 "ι",
381 "κ",
382 "λ",
383 "μ",
384 "ν",
385 "ξ",
386 "ο",
387 "π",
388 "ρ",
389 "ς",
390 "σ",
391 "τ",
392 "υ",
393 "φ",
394 "χ",
395 "ψ",
396 "ω",
397 };
398
399 const greekSymbols = [_][]const u8{
400 "ϵ",
401 "ϑ",
402 "ϰ",
403 "ϕ",
404 "ϱ",
405 "ϖ",
406 };
407
408 test "alphabet maps common mathematical alphabets" {
409 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
410 defer arena.deinit();
411 const expr = try ast.text(arena.allocator(), "Az09");
412 const bold = try apply(arena.allocator(), .bold, expr);
413 try std.testing.expectEqualStrings("𝐀𝐳𝟎𝟗", bold.text);
414 const blackboard = try apply(arena.allocator(), .double_struck, expr);
415 try std.testing.expectEqualStrings("𝔸𝕫𝟘𝟡", blackboard.text);
416 }
417
418 test "alphabet maps unicode math aliases" {
419 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
420 defer arena.deinit();
421
422 const bold = try apply(arena.allocator(), command("symbfup").?, try ast.text(arena.allocator(), "A"));
423 try std.testing.expectEqualStrings("𝐀", bold.text);
424
425 const sans = try apply(arena.allocator(), command("symsfup").?, try ast.text(arena.allocator(), "B"));
426 try std.testing.expectEqualStrings("𝖡", sans.text);
427
428 const bold_sans = try apply(arena.allocator(), command("symbfsfup").?, try ast.text(arena.allocator(), "C"));
429 try std.testing.expectEqualStrings("𝗖", bold_sans.text);
430
431 const bold_sans_italic = try apply(arena.allocator(), command("symbfsfit").?, try ast.text(arena.allocator(), "D"));
432 try std.testing.expectEqualStrings("𝘿", bold_sans_italic.text);
433
434 const blackboard_italic = try apply(arena.allocator(), command("symbbit").?, try ast.text(arena.allocator(), "Ddeijx"));
435 try std.testing.expectEqualStrings("ⅅⅆⅇⅈⅉx", blackboard_italic.text);
436 }
437
438 test "alphabet maps script exceptions and bold Greek" {
439 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
440 defer arena.deinit();
441 const script = try apply(arena.allocator(), .script, try ast.text(arena.allocator(), "BFeo"));
442 try std.testing.expectEqualStrings("ℬℱℯℴ", script.text);
443 const greek = try apply(arena.allocator(), .bold_italic, try ast.text(arena.allocator(), "αϕ∂"));
444 try std.testing.expectEqualStrings("𝜶𝝓𝝏", greek.text);
445 const small_caps = try apply(arena.allocator(), .small_caps, try ast.text(arena.allocator(), "E-App"));
446 try std.testing.expectEqualStrings("ᴇ-ᴀᴘᴘ", small_caps.text);
447 }