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 }