lib/zen/src/math/symbol.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const Kind = enum {
4 identifier,
5 function,
6 operator,
7 fence,
8 largeop,
9 integral,
10 movableop,
11 space,
12 accent,
13 };
14
15 pub const Entry = struct {
16 kind: Kind,
17 text: []const u8,
18 upright: bool = false,
19 };
20
21 pub const commands = std.StaticStringMap(Entry).initComptime(.{
22 .{ "alpha", Entry{ .kind = .identifier, .text = "α" } },
23 .{ "beta", Entry{ .kind = .identifier, .text = "β" } },
24 .{ "gamma", Entry{ .kind = .identifier, .text = "γ" } },
25 .{ "delta", Entry{ .kind = .identifier, .text = "δ" } },
26 .{ "epsilon", Entry{ .kind = .identifier, .text = "ϵ" } },
27 .{ "varepsilon", Entry{ .kind = .identifier, .text = "ε" } },
28 .{ "zeta", Entry{ .kind = .identifier, .text = "ζ" } },
29 .{ "eta", Entry{ .kind = .identifier, .text = "η" } },
30 .{ "theta", Entry{ .kind = .identifier, .text = "θ" } },
31 .{ "vartheta", Entry{ .kind = .identifier, .text = "ϑ" } },
32 .{ "iota", Entry{ .kind = .identifier, .text = "ι" } },
33 .{ "kappa", Entry{ .kind = .identifier, .text = "κ" } },
34 .{ "lambda", Entry{ .kind = .identifier, .text = "λ" } },
35 .{ "mu", Entry{ .kind = .identifier, .text = "μ" } },
36 .{ "nu", Entry{ .kind = .identifier, .text = "ν" } },
37 .{ "xi", Entry{ .kind = .identifier, .text = "ξ" } },
38 .{ "pi", Entry{ .kind = .identifier, .text = "π" } },
39 .{ "varpi", Entry{ .kind = .identifier, .text = "ϖ" } },
40 .{ "rho", Entry{ .kind = .identifier, .text = "ρ" } },
41 .{ "varrho", Entry{ .kind = .identifier, .text = "ϱ" } },
42 .{ "sigma", Entry{ .kind = .identifier, .text = "σ" } },
43 .{ "varsigma", Entry{ .kind = .identifier, .text = "ς" } },
44 .{ "tau", Entry{ .kind = .identifier, .text = "τ" } },
45 .{ "upsilon", Entry{ .kind = .identifier, .text = "υ" } },
46 .{ "phi", Entry{ .kind = .identifier, .text = "ϕ" } },
47 .{ "varphi", Entry{ .kind = .identifier, .text = "φ" } },
48 .{ "chi", Entry{ .kind = .identifier, .text = "χ" } },
49 .{ "psi", Entry{ .kind = .identifier, .text = "ψ" } },
50 .{ "omega", Entry{ .kind = .identifier, .text = "ω" } },
51 .{ "Gamma", Entry{ .kind = .identifier, .text = "Γ", .upright = true } },
52 .{ "Delta", Entry{ .kind = .identifier, .text = "Δ", .upright = true } },
53 .{ "Theta", Entry{ .kind = .identifier, .text = "Θ", .upright = true } },
54 .{ "Lambda", Entry{ .kind = .identifier, .text = "Λ", .upright = true } },
55 .{ "Xi", Entry{ .kind = .identifier, .text = "Ξ", .upright = true } },
56 .{ "Pi", Entry{ .kind = .identifier, .text = "Π", .upright = true } },
57 .{ "Sigma", Entry{ .kind = .identifier, .text = "Σ", .upright = true } },
58 .{ "Upsilon", Entry{ .kind = .identifier, .text = "Υ", .upright = true } },
59 .{ "Phi", Entry{ .kind = .identifier, .text = "Φ", .upright = true } },
60 .{ "Psi", Entry{ .kind = .identifier, .text = "Ψ", .upright = true } },
61 .{ "Omega", Entry{ .kind = .identifier, .text = "Ω", .upright = true } },
62 .{ "infty", Entry{ .kind = .identifier, .text = "∞", .upright = true } },
63 .{ "partial", Entry{ .kind = .identifier, .text = "∂" } },
64 .{ "nabla", Entry{ .kind = .identifier, .text = "∇", .upright = true } },
65 .{ "hbar", Entry{ .kind = .identifier, .text = "ℏ" } },
66 .{ "ell", Entry{ .kind = .identifier, .text = "ℓ" } },
67 .{ "Re", Entry{ .kind = .identifier, .text = "ℜ", .upright = true } },
68 .{ "Im", Entry{ .kind = .identifier, .text = "ℑ", .upright = true } },
69 .{ "aleph", Entry{ .kind = .identifier, .text = "ℵ", .upright = true } },
70 .{ "emptyset", Entry{ .kind = .identifier, .text = "∅", .upright = true } },
71 .{ "varnothing", Entry{ .kind = .identifier, .text = "∅", .upright = true } },
72 .{ "imath", Entry{ .kind = .identifier, .text = "ı" } },
73 .{ "jmath", Entry{ .kind = .identifier, .text = "ȷ" } },
74 .{ "cdot", Entry{ .kind = .operator, .text = "⋅" } },
75 .{ "times", Entry{ .kind = .operator, .text = "×" } },
76 .{ "div", Entry{ .kind = .operator, .text = "÷" } },
77 .{ "pm", Entry{ .kind = .operator, .text = "±" } },
78 .{ "mp", Entry{ .kind = .operator, .text = "∓" } },
79 .{ "le", Entry{ .kind = .operator, .text = "≤" } },
80 .{ "leq", Entry{ .kind = .operator, .text = "≤" } },
81 .{ "ge", Entry{ .kind = .operator, .text = "≥" } },
82 .{ "geq", Entry{ .kind = .operator, .text = "≥" } },
83 .{ "ne", Entry{ .kind = .operator, .text = "≠" } },
84 .{ "neq", Entry{ .kind = .operator, .text = "≠" } },
85 .{ "ll", Entry{ .kind = .operator, .text = "≪" } },
86 .{ "gg", Entry{ .kind = .operator, .text = "≫" } },
87 .{ "approx", Entry{ .kind = .operator, .text = "≈" } },
88 .{ "sim", Entry{ .kind = .operator, .text = "∼" } },
89 .{ "simeq", Entry{ .kind = .operator, .text = "≃" } },
90 .{ "cong", Entry{ .kind = .operator, .text = "≅" } },
91 .{ "equiv", Entry{ .kind = .operator, .text = "≡" } },
92 .{ "propto", Entry{ .kind = .operator, .text = "∝" } },
93 .{ "in", Entry{ .kind = .operator, .text = "∈" } },
94 .{ "notin", Entry{ .kind = .operator, .text = "∉" } },
95 .{ "ni", Entry{ .kind = .operator, .text = "∋" } },
96 .{ "subset", Entry{ .kind = .operator, .text = "⊂" } },
97 .{ "supset", Entry{ .kind = .operator, .text = "⊃" } },
98 .{ "subseteq", Entry{ .kind = .operator, .text = "⊆" } },
99 .{ "supseteq", Entry{ .kind = .operator, .text = "⊇" } },
100 .{ "cup", Entry{ .kind = .operator, .text = "∪" } },
101 .{ "cap", Entry{ .kind = .operator, .text = "∩" } },
102 .{ "setminus", Entry{ .kind = .operator, .text = "∖" } },
103 .{ "wedge", Entry{ .kind = .operator, .text = "∧" } },
104 .{ "land", Entry{ .kind = .operator, .text = "∧" } },
105 .{ "vee", Entry{ .kind = .operator, .text = "∨" } },
106 .{ "lor", Entry{ .kind = .operator, .text = "∨" } },
107 .{ "oplus", Entry{ .kind = .operator, .text = "⊕" } },
108 .{ "ominus", Entry{ .kind = .operator, .text = "⊖" } },
109 .{ "otimes", Entry{ .kind = .operator, .text = "⊗" } },
110 .{ "odot", Entry{ .kind = .operator, .text = "⊙" } },
111 .{ "circ", Entry{ .kind = .operator, .text = "∘" } },
112 .{ "bullet", Entry{ .kind = .operator, .text = "•" } },
113 .{ "star", Entry{ .kind = .operator, .text = "⋆" } },
114 .{ "ast", Entry{ .kind = .operator, .text = "∗" } },
115 .{ "to", Entry{ .kind = .operator, .text = "→" } },
116 .{ "rightarrow", Entry{ .kind = .operator, .text = "→" } },
117 .{ "leftarrow", Entry{ .kind = .operator, .text = "←" } },
118 .{ "gets", Entry{ .kind = .operator, .text = "←" } },
119 .{ "mapsto", Entry{ .kind = .operator, .text = "↦" } },
120 .{ "hookrightarrow", Entry{ .kind = .operator, .text = "↪" } },
121 .{ "Rightarrow", Entry{ .kind = .operator, .text = "⇒" } },
122 .{ "Leftarrow", Entry{ .kind = .operator, .text = "⇐" } },
123 .{ "leftrightarrow", Entry{ .kind = .operator, .text = "↔" } },
124 .{ "Leftrightarrow", Entry{ .kind = .operator, .text = "⇔" } },
125 .{ "iff", Entry{ .kind = .operator, .text = "⇔" } },
126 .{ "implies", Entry{ .kind = .operator, .text = "⇒" } },
127 .{ "uparrow", Entry{ .kind = .operator, .text = "↑" } },
128 .{ "downarrow", Entry{ .kind = .operator, .text = "↓" } },
129 .{ "rightharpoonup", Entry{ .kind = .operator, .text = "⇀" } },
130 .{ "mid", Entry{ .kind = .operator, .text = "∣" } },
131 .{ "parallel", Entry{ .kind = .operator, .text = "∥" } },
132 .{ "perp", Entry{ .kind = .operator, .text = "⊥" } },
133 .{ "vdash", Entry{ .kind = .operator, .text = "⊢" } },
134 .{ "models", Entry{ .kind = .operator, .text = "⊨" } },
135 .{ "angle", Entry{ .kind = .operator, .text = "∠" } },
136 .{ "forall", Entry{ .kind = .operator, .text = "∀" } },
137 .{ "exists", Entry{ .kind = .operator, .text = "∃" } },
138 .{ "neg", Entry{ .kind = .operator, .text = "¬" } },
139 .{ "lnot", Entry{ .kind = .operator, .text = "¬" } },
140 .{ "prime", Entry{ .kind = .operator, .text = "′" } },
141 .{ "dots", Entry{ .kind = .operator, .text = "…" } },
142 .{ "ldots", Entry{ .kind = .operator, .text = "…" } },
143 .{ "cdots", Entry{ .kind = .operator, .text = "⋯" } },
144 .{ "vdots", Entry{ .kind = .operator, .text = "⋮" } },
145 .{ "ddots", Entry{ .kind = .operator, .text = "⋱" } },
146 .{ "backslash", Entry{ .kind = .operator, .text = "\\" } },
147 .{ "bmod", Entry{ .kind = .function, .text = "mod" } },
148 .{ "langle", Entry{ .kind = .fence, .text = "⟨" } },
149 .{ "rangle", Entry{ .kind = .fence, .text = "⟩" } },
150 .{ "lfloor", Entry{ .kind = .fence, .text = "⌊" } },
151 .{ "rfloor", Entry{ .kind = .fence, .text = "⌋" } },
152 .{ "lceil", Entry{ .kind = .fence, .text = "⌈" } },
153 .{ "rceil", Entry{ .kind = .fence, .text = "⌉" } },
154 .{ "lvert", Entry{ .kind = .fence, .text = "|" } },
155 .{ "rvert", Entry{ .kind = .fence, .text = "|" } },
156 .{ "vert", Entry{ .kind = .fence, .text = "|" } },
157 .{ "lVert", Entry{ .kind = .fence, .text = "‖" } },
158 .{ "rVert", Entry{ .kind = .fence, .text = "‖" } },
159 .{ "Vert", Entry{ .kind = .fence, .text = "‖" } },
160 .{ "|", Entry{ .kind = .fence, .text = "‖" } },
161 .{ "{", Entry{ .kind = .fence, .text = "{" } },
162 .{ "}", Entry{ .kind = .fence, .text = "}" } },
163 .{ "$", Entry{ .kind = .identifier, .text = "$", .upright = true } },
164 .{ "%", Entry{ .kind = .identifier, .text = "%", .upright = true } },
165 .{ "#", Entry{ .kind = .identifier, .text = "#", .upright = true } },
166 .{ "&", Entry{ .kind = .identifier, .text = "&", .upright = true } },
167 .{ "_", Entry{ .kind = .identifier, .text = "_", .upright = true } },
168 .{ "sum", Entry{ .kind = .largeop, .text = "∑" } },
169 .{ "prod", Entry{ .kind = .largeop, .text = "∏" } },
170 .{ "coprod", Entry{ .kind = .largeop, .text = "∐" } },
171 .{ "bigcup", Entry{ .kind = .largeop, .text = "⋃" } },
172 .{ "bigcap", Entry{ .kind = .largeop, .text = "⋂" } },
173 .{ "bigoplus", Entry{ .kind = .largeop, .text = "⨁" } },
174 .{ "bigotimes", Entry{ .kind = .largeop, .text = "⨂" } },
175 .{ "bigodot", Entry{ .kind = .largeop, .text = "⨀" } },
176 .{ "bigwedge", Entry{ .kind = .largeop, .text = "⋀" } },
177 .{ "bigvee", Entry{ .kind = .largeop, .text = "⋁" } },
178 .{ "int", Entry{ .kind = .integral, .text = "∫" } },
179 .{ "iint", Entry{ .kind = .integral, .text = "∬" } },
180 .{ "iiint", Entry{ .kind = .integral, .text = "∭" } },
181 .{ "oint", Entry{ .kind = .integral, .text = "∮" } },
182 .{ "lim", Entry{ .kind = .movableop, .text = "lim" } },
183 .{ "limsup", Entry{ .kind = .movableop, .text = "lim sup" } },
184 .{ "liminf", Entry{ .kind = .movableop, .text = "lim inf" } },
185 .{ "max", Entry{ .kind = .movableop, .text = "max" } },
186 .{ "min", Entry{ .kind = .movableop, .text = "min" } },
187 .{ "sup", Entry{ .kind = .movableop, .text = "sup" } },
188 .{ "inf", Entry{ .kind = .movableop, .text = "inf" } },
189 .{ "argmax", Entry{ .kind = .movableop, .text = "arg max" } },
190 .{ "argmin", Entry{ .kind = .movableop, .text = "arg min" } },
191 .{ "log", Entry{ .kind = .function, .text = "log" } },
192 .{ "ln", Entry{ .kind = .function, .text = "ln" } },
193 .{ "lg", Entry{ .kind = .function, .text = "lg" } },
194 .{ "exp", Entry{ .kind = .function, .text = "exp" } },
195 .{ "sin", Entry{ .kind = .function, .text = "sin" } },
196 .{ "cos", Entry{ .kind = .function, .text = "cos" } },
197 .{ "tan", Entry{ .kind = .function, .text = "tan" } },
198 .{ "cot", Entry{ .kind = .function, .text = "cot" } },
199 .{ "sec", Entry{ .kind = .function, .text = "sec" } },
200 .{ "csc", Entry{ .kind = .function, .text = "csc" } },
201 .{ "arcsin", Entry{ .kind = .function, .text = "arcsin" } },
202 .{ "arccos", Entry{ .kind = .function, .text = "arccos" } },
203 .{ "arctan", Entry{ .kind = .function, .text = "arctan" } },
204 .{ "sinh", Entry{ .kind = .function, .text = "sinh" } },
205 .{ "cosh", Entry{ .kind = .function, .text = "cosh" } },
206 .{ "tanh", Entry{ .kind = .function, .text = "tanh" } },
207 .{ "coth", Entry{ .kind = .function, .text = "coth" } },
208 .{ "det", Entry{ .kind = .function, .text = "det" } },
209 .{ "dim", Entry{ .kind = .function, .text = "dim" } },
210 .{ "ker", Entry{ .kind = .function, .text = "ker" } },
211 .{ "deg", Entry{ .kind = .function, .text = "deg" } },
212 .{ "gcd", Entry{ .kind = .function, .text = "gcd" } },
213 .{ "Pr", Entry{ .kind = .function, .text = "Pr" } },
214 .{ "arg", Entry{ .kind = .function, .text = "arg" } },
215 .{ ",", Entry{ .kind = .space, .text = "0.167em" } },
216 .{ ":", Entry{ .kind = .space, .text = "0.222em" } },
217 .{ ";", Entry{ .kind = .space, .text = "0.278em" } },
218 .{ " ", Entry{ .kind = .space, .text = "0.25em" } },
219 .{ "quad", Entry{ .kind = .space, .text = "1em" } },
220 .{ "qquad", Entry{ .kind = .space, .text = "2em" } },
221 .{ "hat", Entry{ .kind = .accent, .text = "ˆ" } },
222 .{ "widehat", Entry{ .kind = .accent, .text = "ˆ" } },
223 .{ "bar", Entry{ .kind = .accent, .text = "¯" } },
224 .{ "overline", Entry{ .kind = .accent, .text = "¯" } },
225 .{ "tilde", Entry{ .kind = .accent, .text = "˜" } },
226 .{ "widetilde", Entry{ .kind = .accent, .text = "˜" } },
227 .{ "vec", Entry{ .kind = .accent, .text = "⃗" } },
228 .{ "dot", Entry{ .kind = .accent, .text = "˙" } },
229 .{ "ddot", Entry{ .kind = .accent, .text = "¨" } },
230 .{ "check", Entry{ .kind = .accent, .text = "ˇ" } },
231 .{ "breve", Entry{ .kind = .accent, .text = "˘" } },
232 .{ "mathring", Entry{ .kind = .accent, .text = "˚" } },
233 });
234
235 pub const Environment = struct {
236 open: ?[]const u8 = null,
237 close: ?[]const u8 = null,
238 class: ?[]const u8 = null,
239 display_cells: bool = false,
240 };
241
242 pub const environments = std.StaticStringMap(Environment).initComptime(.{
243 .{ "matrix", Environment{} },
244 .{ "pmatrix", Environment{ .open = "(", .close = ")" } },
245 .{ "bmatrix", Environment{ .open = "[", .close = "]" } },
246 .{ "Bmatrix", Environment{ .open = "{", .close = "}" } },
247 .{ "vmatrix", Environment{ .open = "|", .close = "|" } },
248 .{ "Vmatrix", Environment{ .open = "‖", .close = "‖" } },
249 .{ "cases", Environment{ .open = "{", .class = "zen-cases" } },
250 .{ "aligned", Environment{ .class = "zen-aligned", .display_cells = true } },
251 });
252
253 pub const Style = enum { bb, cal, frak, bf };
254
255 pub fn styled(style: Style, char: u8) ?u21 {
256 return switch (style) {
257 .bb => styledBb(char),
258 .cal => styledCal(char),
259 .frak => styledFrak(char),
260 .bf => styledBf(char),
261 };
262 }
263
264 fn styledBb(char: u8) ?u21 {
265 switch (char) {
266 'C' => return 0x2102,
267 'H' => return 0x210D,
268 'N' => return 0x2115,
269 'P' => return 0x2119,
270 'Q' => return 0x211A,
271 'R' => return 0x211D,
272 'Z' => return 0x2124,
273 else => {},
274 }
275 if (char >= 'A' and char <= 'Z') return 0x1D538 + @as(u21, char - 'A');
276 if (char >= 'a' and char <= 'z') return 0x1D552 + @as(u21, char - 'a');
277 if (char >= '0' and char <= '9') return 0x1D7D8 + @as(u21, char - '0');
278 return null;
279 }
280
281 fn styledCal(char: u8) ?u21 {
282 switch (char) {
283 'B' => return 0x212C,
284 'E' => return 0x2130,
285 'F' => return 0x2131,
286 'H' => return 0x210B,
287 'I' => return 0x2110,
288 'L' => return 0x2112,
289 'M' => return 0x2133,
290 'R' => return 0x211B,
291 'e' => return 0x212F,
292 'g' => return 0x210A,
293 'o' => return 0x2134,
294 else => {},
295 }
296 if (char >= 'A' and char <= 'Z') return 0x1D49C + @as(u21, char - 'A');
297 if (char >= 'a' and char <= 'z') return 0x1D4B6 + @as(u21, char - 'a');
298 return null;
299 }
300
301 fn styledFrak(char: u8) ?u21 {
302 switch (char) {
303 'C' => return 0x212D,
304 'H' => return 0x210C,
305 'I' => return 0x2111,
306 'R' => return 0x211C,
307 'Z' => return 0x2128,
308 else => {},
309 }
310 if (char >= 'A' and char <= 'Z') return 0x1D504 + @as(u21, char - 'A');
311 if (char >= 'a' and char <= 'z') return 0x1D51E + @as(u21, char - 'a');
312 return null;
313 }
314
315 fn styledBf(char: u8) ?u21 {
316 if (char >= 'A' and char <= 'Z') return 0x1D400 + @as(u21, char - 'A');
317 if (char >= 'a' and char <= 'z') return 0x1D41A + @as(u21, char - 'a');
318 if (char >= '0' and char <= '9') return 0x1D7CE + @as(u21, char - '0');
319 return null;
320 }
321
322 test "symbol table resolves commands and classes" {
323 try std.testing.expectEqualStrings("∑", commands.get("sum").?.text);
324 try std.testing.expect(commands.get("sum").?.kind == .largeop);
325 try std.testing.expect(commands.get("int").?.kind == .integral);
326 try std.testing.expect(commands.get("lim").?.kind == .movableop);
327 try std.testing.expect(commands.get("nonesuch") == null);
328 }
329
330 test "environment table resolves fences and classes" {
331 try std.testing.expectEqualStrings("(", environments.get("pmatrix").?.open.?);
332 try std.testing.expectEqualStrings("‖", environments.get("Vmatrix").?.close.?);
333 try std.testing.expect(environments.get("matrix").?.open == null);
334 const cases = environments.get("cases").?;
335 try std.testing.expectEqualStrings("{", cases.open.?);
336 try std.testing.expect(cases.close == null);
337 try std.testing.expectEqualStrings("zen-cases", cases.class.?);
338 const aligned = environments.get("aligned").?;
339 try std.testing.expect(aligned.display_cells);
340 try std.testing.expect(environments.get("align") == null);
341 }
342
343 test "styled alphabets honor unicode exception slots" {
344 try std.testing.expectEqual(@as(?u21, 0x211D), styled(.bb, 'R'));
345 try std.testing.expectEqual(@as(?u21, 0x1D538), styled(.bb, 'A'));
346 try std.testing.expectEqual(@as(?u21, 0x2112), styled(.cal, 'L'));
347 try std.testing.expectEqual(@as(?u21, 0x212F), styled(.cal, 'e'));
348 try std.testing.expectEqual(@as(?u21, 0x211C), styled(.frak, 'R'));
349 try std.testing.expectEqual(@as(?u21, 0x1D41A), styled(.bf, 'a'));
350 try std.testing.expectEqual(@as(?u21, null), styled(.cal, '3'));
351 }