lib/termtex/src/parse.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const ast = @import("ast.zig");
3 const alphabet = @import("alphabet.zig");
4 const symbol = @import("symbol.zig");
5
6 const ParserError = error{
7 UnclosedGroup,
8 ExpectedEnvironmentName,
9 };
10
11 pub const Error = ParserError || std.mem.Allocator.Error;
12
13 pub fn parse(arena: std.mem.Allocator, source: []const u8) Error!*ast.Expr {
14 var parser = Parser{
15 .arena = arena,
16 .source = source,
17 };
18 return parser.parseUntil(null);
19 }
20
21 const ScriptKind = enum {
22 sub,
23 sup,
24 };
25
26 const OverUnderMode = enum {
27 over,
28 under,
29 };
30
31 const GridStyle = struct {
32 fence: ast.GridFence = .none,
33 alignment: ast.GridAlignment = .center,
34 };
35
36 const GridStop = union(enum) {
37 environment: []const u8,
38 group,
39 };
40
41 const Infix = enum {
42 over,
43 choose,
44 atop,
45 brack,
46 brace,
47 };
48
49 const RuleOrder = enum {
50 premises_first,
51 conclusion_first,
52 };
53
54 const Parser = struct {
55 arena: std.mem.Allocator,
56 source: []const u8,
57 index: usize = 0,
58
59 fn parseUntil(self: *Parser, stop: ?u8) Error!*ast.Expr {
60 var items: std.ArrayListUnmanaged(*ast.Expr) = .empty;
61 defer items.deinit(self.arena);
62 while (true) {
63 self.skipSpaces();
64 if (self.index >= self.source.len) {
65 if (stop != null) return error.UnclosedGroup;
66 break;
67 }
68 if (stop) |value| {
69 if (self.source[self.index] == value) {
70 self.index += 1;
71 break;
72 }
73 }
74 if (self.consumeInfix()) |kind| {
75 const numerator = try ast.row(self.arena, items.items);
76 const denominator = try self.parseUntil(stop);
77 return self.makeInfix(kind, numerator, denominator);
78 }
79 if (self.consumeLimitPolicy()) |policy| {
80 try self.applyLimitPolicy(&items, policy);
81 continue;
82 }
83 switch (self.source[self.index]) {
84 '^' => try self.attachScript(&items, .sup),
85 '_' => try self.attachScript(&items, .sub),
86 else => try self.appendAtom(&items),
87 }
88 }
89 return ast.row(self.arena, items.items);
90 }
91
92 fn parseDelimitedBody(self: *Parser) Error!*ast.Expr {
93 var items: std.ArrayListUnmanaged(*ast.Expr) = .empty;
94 defer items.deinit(self.arena);
95 while (true) {
96 self.skipSpaces();
97 if (self.index >= self.source.len) return error.UnclosedGroup;
98 if (self.peekCommand("right") != null) break;
99 if (self.consumeInfix()) |kind| {
100 const numerator = try ast.row(self.arena, items.items);
101 const denominator = try self.parseDelimitedBody();
102 return self.makeInfix(kind, numerator, denominator);
103 }
104 if (self.consumeLimitPolicy()) |policy| {
105 try self.applyLimitPolicy(&items, policy);
106 continue;
107 }
108 switch (self.source[self.index]) {
109 '^' => try self.attachScript(&items, .sup),
110 '_' => try self.attachScript(&items, .sub),
111 else => try self.appendAtom(&items),
112 }
113 }
114 return ast.row(self.arena, items.items);
115 }
116
117 fn appendAtom(self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr)) Error!void {
118 const item = try self.parseAtom();
119 if (emptyExpr(item)) return;
120 try items.append(self.arena, item);
121 }
122
123 fn attachScript(self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr), kind: ScriptKind) Error!void {
124 self.index += 1;
125 const script = try self.parseArgument();
126 const base = if (items.items.len == 0) try ast.empty(self.arena) else block: {
127 const last = items.items[items.items.len - 1];
128 items.items.len -= 1;
129 break :block last;
130 };
131 switch (base.*) {
132 .scripts => |*scripts| {
133 switch (kind) {
134 .sub => scripts.sub = script,
135 .sup => scripts.sup = script,
136 }
137 try items.append(self.arena, base);
138 },
139 else => {
140 var scripts: ast.Scripts = .{ .base = base };
141 switch (kind) {
142 .sub => scripts.sub = script,
143 .sup => scripts.sup = script,
144 }
145 try items.append(self.arena, try ast.node(self.arena, .{ .scripts = scripts }));
146 },
147 }
148 }
149
150 fn applyLimitPolicy(self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr), policy: ast.LimitPolicy) Error!void {
151 if (items.items.len == 0) return;
152 const index = items.items.len - 1;
153 items.items[index] = try self.withLimitPolicy(items.items[index], policy);
154 }
155
156 fn withLimitPolicy(self: *Parser, expr: *ast.Expr, policy: ast.LimitPolicy) Error!*ast.Expr {
157 switch (expr.*) {
158 .operator => |*value| {
159 value.limit_policy = policy;
160 return expr;
161 },
162 .scripts => |*value| {
163 value.base = try self.withLimitPolicy(value.base, policy);
164 return expr;
165 },
166 else => return ast.operator(self.arena, expr, policy),
167 }
168 }
169
170 fn parseArgument(self: *Parser) Error!*ast.Expr {
171 self.skipSpaces();
172 if (self.index >= self.source.len) return ast.empty(self.arena);
173 if (self.source[self.index] == '{') {
174 self.index += 1;
175 return self.parseUntil('}');
176 }
177 return self.parseAtom();
178 }
179
180 fn parseOptionalBracket(self: *Parser) Error!?*ast.Expr {
181 self.skipSpaces();
182 if (self.index >= self.source.len or self.source[self.index] != '[') return null;
183 self.index += 1;
184 return try self.parseUntil(']');
185 }
186
187 fn parseAtom(self: *Parser) Error!*ast.Expr {
188 if (self.index >= self.source.len) return ast.empty(self.arena);
189 const char = self.source[self.index];
190 return switch (char) {
191 '{' => block: {
192 self.index += 1;
193 break :block try self.parseUntil('}');
194 },
195 '\\' => self.parseCommand(),
196 '&' => block: {
197 self.index += 1;
198 break :block try ast.text(self.arena, " ");
199 },
200 '~' => block: {
201 self.index += 1;
202 break :block try ast.space(self.arena, emSpace(1, 3));
203 },
204 else => self.parseTextRun(),
205 };
206 }
207
208 fn parseTextRun(self: *Parser) Error!*ast.Expr {
209 const start = self.index;
210 while (self.index < self.source.len) {
211 const char = self.source[self.index];
212 if (isStopByte(char) or isSpace(char)) break;
213 const len = std.unicode.utf8ByteSequenceLength(char) catch 1;
214 self.index += @min(len, self.source.len - self.index);
215 }
216 if (self.index == start) {
217 self.index += 1;
218 return ast.text(self.arena, self.source[start..self.index]);
219 }
220 return ast.text(self.arena, self.source[start..self.index]);
221 }
222
223 fn parseCommand(self: *Parser) Error!*ast.Expr {
224 self.index += 1;
225 if (self.index >= self.source.len) return ast.text(self.arena, "\\");
226 const start = self.index;
227 if (!std.ascii.isAlphabetic(self.source[self.index])) {
228 const escaped_start = self.index;
229 const char = self.source[self.index];
230 self.index += 1;
231 if (escapedSpace(char)) |value| return ast.space(self.arena, value);
232 return ast.text(self.arena, escapedChar(char) orelse self.source[escaped_start..self.index]);
233 }
234 while (self.index < self.source.len and std.ascii.isAlphabetic(self.source[self.index])) {
235 self.index += 1;
236 }
237 const name = self.source[start..self.index];
238 if (commandExpr(self, name)) |expr| return expr;
239 if (symbol.command(name)) |value| return ast.text(self.arena, value);
240 if (symbol.function(name)) |value| return ast.text(self.arena, value);
241 return ast.text(self.arena, name);
242 }
243
244 fn commandExpr(self: *Parser, name: []const u8) ?Error!*ast.Expr {
245 if (std.mem.eql(u8, name, "frac") or std.mem.eql(u8, name, "tfrac")) {
246 return makeFraction(self, .text);
247 }
248 if (std.mem.eql(u8, name, "dfrac")) {
249 return makeFraction(self, .display);
250 }
251 if (std.mem.eql(u8, name, "binom") or std.mem.eql(u8, name, "tbinom")) {
252 return makeBinomial(self, .text);
253 }
254 if (std.mem.eql(u8, name, "dbinom")) {
255 return makeBinomial(self, .display);
256 }
257 if (std.mem.eql(u8, name, "inferrule") or std.mem.eql(u8, name, "inference")) {
258 return makeInferenceRule(self, .premises_first);
259 }
260 if (std.mem.eql(u8, name, "infer")) {
261 return makeInferenceRule(self, .conclusion_first);
262 }
263 if (xArrowCommand(name)) |arrow| {
264 return makeXArrow(self, arrow);
265 }
266 if (std.mem.eql(u8, name, "pmod")) {
267 return makeArgumentText(self, " (mod ", ")");
268 }
269 if (std.mem.eql(u8, name, "pod")) {
270 return makeArgumentText(self, " (", ")");
271 }
272 if (std.mem.eql(u8, name, "mod")) {
273 return makeArgumentText(self, " mod ", "");
274 }
275 if (std.mem.eql(u8, name, "bmod")) {
276 return ast.text(self.arena, " mod ");
277 }
278 if (namedSpace(name)) |value| {
279 return ast.space(self.arena, value);
280 }
281 if (std.mem.eql(u8, name, "hspace")) {
282 return makeHSpace(self);
283 }
284 if (std.mem.eql(u8, name, "vspace")) {
285 return self.ignoreSpacingCommand();
286 }
287 if (std.mem.eql(u8, name, "tag")) {
288 return makeTag(self);
289 }
290 if (referenceCommand(name)) |style| {
291 return makeReference(self, style);
292 }
293 if (std.mem.eql(u8, name, "sqrt")) {
294 return makeSqrt(self);
295 }
296 if (std.mem.eql(u8, name, "begin")) {
297 return makeEnvironment(self);
298 }
299 if (std.mem.eql(u8, name, "substack")) {
300 return makeSubstack(self);
301 }
302 if (std.mem.eql(u8, name, "bar") or std.mem.eql(u8, name, "overline")) {
303 return makeAccent(self, .bar);
304 }
305 if (std.mem.eql(u8, name, "underline")) {
306 return makeAccent(self, .underline);
307 }
308 if (std.mem.eql(u8, name, "hat") or std.mem.eql(u8, name, "widehat")) {
309 return makeAccent(self, .hat);
310 }
311 if (std.mem.eql(u8, name, "tilde") or std.mem.eql(u8, name, "widetilde")) {
312 return makeAccent(self, .tilde);
313 }
314 if (std.mem.eql(u8, name, "check") or std.mem.eql(u8, name, "widecheck")) {
315 return makeAccent(self, .check);
316 }
317 if (std.mem.eql(u8, name, "breve")) {
318 return makeAccent(self, .breve);
319 }
320 if (std.mem.eql(u8, name, "vec")) {
321 return makeAccent(self, .vec);
322 }
323 if (std.mem.eql(u8, name, "overrightarrow")) {
324 return makeAccent(self, .vec);
325 }
326 if (std.mem.eql(u8, name, "overleftarrow")) {
327 return makeAccent(self, .overleft);
328 }
329 if (std.mem.eql(u8, name, "overleftrightarrow")) {
330 return makeAccent(self, .overleftright);
331 }
332 if (std.mem.eql(u8, name, "dot")) {
333 return makeAccent(self, .dot);
334 }
335 if (std.mem.eql(u8, name, "ddot")) {
336 return makeAccent(self, .ddot);
337 }
338 if (std.mem.eql(u8, name, "acute")) {
339 return makeAccent(self, .acute);
340 }
341 if (std.mem.eql(u8, name, "grave")) {
342 return makeAccent(self, .grave);
343 }
344 if (std.mem.eql(u8, name, "mathring")) {
345 return makeAccent(self, .ring);
346 }
347 if (std.mem.eql(u8, name, "overset") or std.mem.eql(u8, name, "stackrel")) {
348 return makeOverUnder(self, .over);
349 }
350 if (std.mem.eql(u8, name, "underset")) {
351 return makeOverUnder(self, .under);
352 }
353 if (std.mem.eql(u8, name, "overbrace")) {
354 return makeAnnotation(self, .overbrace);
355 }
356 if (std.mem.eql(u8, name, "underbrace")) {
357 return makeAnnotation(self, .underbrace);
358 }
359 if (std.mem.eql(u8, name, "boxed")) {
360 return makeAnnotation(self, .boxed);
361 }
362 if (alphabet.command(name)) |mode| {
363 return makeAlphabet(self, mode);
364 }
365 if (std.mem.eql(u8, name, "not")) {
366 return makeNot(self);
367 }
368 if (std.mem.eql(u8, name, "operatorname")) {
369 const limit_policy: ast.LimitPolicy = if (self.index < self.source.len and self.source[self.index] == '*') block: {
370 self.index += 1;
371 break :block .limits;
372 } else .nolimits;
373 return ast.operator(self.arena, try self.parseRawGroupText(), limit_policy);
374 }
375 if (std.mem.eql(u8, name, "mathop")) {
376 return ast.operator(self.arena, try self.parseArgument(), .auto);
377 }
378 if (rawTextCommand(name)) {
379 return self.parseRawGroupText();
380 }
381 if (transparentCommand(name)) {
382 return self.parseArgument();
383 }
384 if (ignoredArgumentCommand(name)) {
385 return self.ignoreArgument();
386 }
387 if (ignoredRawArgumentCommand(name)) {
388 return self.ignoreRawArgument();
389 }
390 if (declarationCommand(name)) {
391 return self.ignoreDeclaration(name);
392 }
393 if (std.mem.eql(u8, name, "mathnormal") or
394 std.mem.eql(u8, name, "mathdefault") or
395 std.mem.eql(u8, name, "mathregular") or
396 std.mem.eql(u8, name, "symnormal") or
397 std.mem.eql(u8, name, "symliteral"))
398 {
399 return self.parseArgument();
400 }
401 if (std.mem.eql(u8, name, "displaystyle") or
402 std.mem.eql(u8, name, "textstyle") or
403 std.mem.eql(u8, name, "scriptstyle") or
404 std.mem.eql(u8, name, "scriptscriptstyle") or
405 std.mem.eql(u8, name, "limits") or
406 std.mem.eql(u8, name, "nolimits") or
407 std.mem.eql(u8, name, "hline") or
408 std.mem.eql(u8, name, "notag") or
409 std.mem.eql(u8, name, "nonumber") or
410 std.mem.eql(u8, name, "allowbreak") or
411 std.mem.eql(u8, name, "displaybreak") or
412 std.mem.eql(u8, name, "pagebreak") or
413 std.mem.eql(u8, name, "nopagebreak") or
414 std.mem.eql(u8, name, "relax"))
415 {
416 return ast.empty(self.arena);
417 }
418 if (std.mem.eql(u8, name, "left")) {
419 return makeDelimited(self);
420 }
421 if (std.mem.eql(u8, name, "right") or std.mem.eql(u8, name, "middle") or isBigDelimiterCommand(name)) {
422 return self.parseDelimiterText();
423 }
424 return null;
425 }
426
427 fn makeFraction(self: *Parser, style: ast.FractionStyle) Error!*ast.Expr {
428 const numerator = try self.parseArgument();
429 const denominator = try self.parseArgument();
430 return self.fraction(numerator, denominator, style);
431 }
432
433 fn makeBinomial(self: *Parser, style: ast.FractionStyle) Error!*ast.Expr {
434 const numerator = try self.parseArgument();
435 const denominator = try self.parseArgument();
436 const fraction_expr = try self.fraction(numerator, denominator, style);
437 return self.oneCellGrid(fraction_expr, .paren);
438 }
439
440 fn makeXArrow(self: *Parser, arrow: []const u8) Error!*ast.Expr {
441 const sub = try self.parseOptionalBracket();
442 const sup = try self.parseArgument();
443 return ast.node(self.arena, .{ .scripts = .{
444 .base = try ast.text(self.arena, arrow),
445 .sub = sub,
446 .sup = sup,
447 } });
448 }
449
450 fn makeInferenceRule(self: *Parser, order: RuleOrder) Error!*ast.Expr {
451 if (self.index < self.source.len and self.source[self.index] == '*') self.index += 1;
452 const label = try self.parseOptionalRuleLabel();
453 const first = try self.parseRuleArgument();
454 const second = try self.parseRuleArgument();
455 const premises = switch (order) {
456 .premises_first => first,
457 .conclusion_first => second,
458 };
459 const conclusion = switch (order) {
460 .premises_first => second,
461 .conclusion_first => first,
462 };
463 const rule = try self.fraction(premises, conclusion, .text);
464 if (label) |value| {
465 const items = try self.arena.alloc(*ast.Expr, 3);
466 items[0] = rule;
467 items[1] = try ast.text(self.arena, " ");
468 items[2] = value;
469 return ast.row(self.arena, items);
470 }
471 return rule;
472 }
473
474 fn parseRuleArgument(self: *Parser) Error!*ast.Expr {
475 self.skipSpaces();
476 if (self.index >= self.source.len or self.source[self.index] != '{') return self.parseArgument();
477 self.index += 1;
478 return self.parseGridBody(.group, .{});
479 }
480
481 fn parseOptionalRuleLabel(self: *Parser) Error!?*ast.Expr {
482 self.skipSpaces();
483 if (self.index >= self.source.len or self.source[self.index] != '[') return null;
484 const raw = try self.parseRawBracketText();
485 const value = ruleLabelValue(raw);
486 if (value.len == 0) return null;
487 return parse(self.arena, value);
488 }
489
490 fn makeArgumentText(self: *Parser, prefix: []const u8, suffix: []const u8) Error!*ast.Expr {
491 const body = try self.parseArgument();
492 var count: usize = 1;
493 if (prefix.len != 0) count += 1;
494 if (suffix.len != 0) count += 1;
495 const items = try self.arena.alloc(*ast.Expr, count);
496 var index: usize = 0;
497 if (prefix.len != 0) {
498 items[index] = try ast.text(self.arena, prefix);
499 index += 1;
500 }
501 items[index] = body;
502 index += 1;
503 if (suffix.len != 0) items[index] = try ast.text(self.arena, suffix);
504 return ast.row(self.arena, items);
505 }
506
507 fn makeTag(self: *Parser) Error!*ast.Expr {
508 if (self.index < self.source.len and self.source[self.index] == '*') {
509 self.index += 1;
510 return makeArgumentText(self, " ", "");
511 }
512 return makeArgumentText(self, " (", ")");
513 }
514
515 fn makeReference(self: *Parser, style: ReferenceStyle) Error!*ast.Expr {
516 try self.skipRawGroup();
517 return ast.text(self.arena, switch (style) {
518 .bare => "?",
519 .paren => "(?)",
520 });
521 }
522
523 fn makeInfix(self: *Parser, kind: Infix, numerator: *ast.Expr, denominator: *ast.Expr) Error!*ast.Expr {
524 return switch (kind) {
525 .over => self.fraction(numerator, denominator, .text),
526 .choose => self.stack(numerator, denominator, .paren),
527 .atop => self.stack(numerator, denominator, .none),
528 .brack => self.stack(numerator, denominator, .bracket),
529 .brace => self.stack(numerator, denominator, .brace),
530 };
531 }
532
533 fn fraction(self: *Parser, numerator: *ast.Expr, denominator: *ast.Expr, style: ast.FractionStyle) Error!*ast.Expr {
534 return ast.node(self.arena, .{ .fraction = .{
535 .numerator = numerator,
536 .denominator = denominator,
537 .style = style,
538 } });
539 }
540
541 fn oneCellGrid(self: *Parser, cell: *ast.Expr, fence: ast.GridFence) Error!*ast.Expr {
542 const cells = try self.arena.alloc(*ast.Expr, 1);
543 cells[0] = cell;
544 const rows = try self.arena.alloc(ast.GridRow, 1);
545 rows[0] = .{ .cells = cells };
546 return ast.node(self.arena, .{ .grid = .{
547 .rows = rows,
548 .fence = fence,
549 } });
550 }
551
552 fn stack(self: *Parser, numerator: *ast.Expr, denominator: *ast.Expr, fence: ast.GridFence) Error!*ast.Expr {
553 const top = try self.arena.alloc(*ast.Expr, 1);
554 top[0] = numerator;
555 const bottom = try self.arena.alloc(*ast.Expr, 1);
556 bottom[0] = denominator;
557 const rows = try self.arena.alloc(ast.GridRow, 2);
558 rows[0] = .{ .cells = top };
559 rows[1] = .{ .cells = bottom };
560 return ast.node(self.arena, .{ .grid = .{
561 .rows = rows,
562 .fence = fence,
563 } });
564 }
565
566 fn makeSqrt(self: *Parser) Error!*ast.Expr {
567 const index = try self.parseOptionalBracket();
568 const body = try self.parseArgument();
569 return ast.node(self.arena, .{ .sqrt = .{
570 .index = index,
571 .body = body,
572 } });
573 }
574
575 fn makeAccent(self: *Parser, mark: ast.AccentMark) Error!*ast.Expr {
576 const body = try self.parseArgument();
577 return ast.node(self.arena, .{ .accent = .{
578 .mark = mark,
579 .body = body,
580 } });
581 }
582
583 fn makeAnnotation(self: *Parser, kind: ast.AnnotationKind) Error!*ast.Expr {
584 const body = try self.parseArgument();
585 return ast.node(self.arena, .{ .annotation = .{
586 .base = body,
587 .kind = kind,
588 } });
589 }
590
591 fn makeOverUnder(self: *Parser, mode: OverUnderMode) Error!*ast.Expr {
592 const mark = try self.parseArgument();
593 const base = try self.parseArgument();
594 var annotation: ast.Annotation = .{ .base = base };
595 switch (mode) {
596 .over => annotation.over = mark,
597 .under => annotation.under = mark,
598 }
599 return ast.node(self.arena, .{ .annotation = annotation });
600 }
601
602 fn makeEnvironment(self: *Parser) Error!*ast.Expr {
603 const name = try self.parseNameGroup();
604 const style = environmentStyle(name);
605 const base = environmentBase(name);
606 if (std.mem.eql(u8, base, "array")) {
607 _ = try self.parseOptionalBracket();
608 try self.skipRawGroup();
609 } else if (std.mem.eql(u8, base, "alignedat")) {
610 try self.skipRawGroup();
611 }
612 return self.parseGridBody(.{ .environment = name }, style);
613 }
614
615 fn makeSubstack(self: *Parser) Error!*ast.Expr {
616 self.skipSpaces();
617 if (self.index >= self.source.len or self.source[self.index] != '{') return self.parseArgument();
618 self.index += 1;
619 return self.parseGridBody(.group, .{});
620 }
621
622 fn makeAlphabet(self: *Parser, mode: alphabet.Mode) Error!*ast.Expr {
623 const body = try self.parseArgument();
624 return alphabet.apply(self.arena, mode, body);
625 }
626
627 fn makeNot(self: *Parser) Error!*ast.Expr {
628 const body = try self.parseArgument();
629 if (ast.textValue(body)) |value| {
630 if (symbol.negated(value)) |negated| return ast.text(self.arena, negated);
631 }
632 const items = try self.arena.alloc(*ast.Expr, 2);
633 items[0] = try ast.text(self.arena, "¬");
634 items[1] = body;
635 return ast.row(self.arena, items);
636 }
637
638 fn makeHSpace(self: *Parser) Error!*ast.Expr {
639 if (self.index < self.source.len and self.source[self.index] == '*') self.index += 1;
640 try self.skipRawBracket();
641 const raw = try self.parseRawGroupValue() orelse return ast.empty(self.arena);
642 const value = parseSpaceValue(raw) orelse return ast.empty(self.arena);
643 return ast.space(self.arena, value);
644 }
645
646 fn makeDelimited(self: *Parser) Error!*ast.Expr {
647 const left = try self.parseDelimiterValue();
648 const body = try self.parseDelimitedBody();
649 const right = try self.consumeRightDelimiter() orelse return error.UnclosedGroup;
650 return ast.node(self.arena, .{ .delimited = .{
651 .left = left,
652 .body = body,
653 .right = right,
654 } });
655 }
656
657 fn ignoreArgument(self: *Parser) Error!*ast.Expr {
658 _ = try self.parseArgument();
659 return ast.empty(self.arena);
660 }
661
662 fn ignoreRawArgument(self: *Parser) Error!*ast.Expr {
663 try self.skipRawGroup();
664 return ast.empty(self.arena);
665 }
666
667 fn ignoreSpacingCommand(self: *Parser) Error!*ast.Expr {
668 if (self.index < self.source.len and self.source[self.index] == '*') self.index += 1;
669 try self.skipRawBracket();
670 try self.skipRawGroup();
671 return ast.empty(self.arena);
672 }
673
674 fn ignoreDeclaration(self: *Parser, name: []const u8) Error!*ast.Expr {
675 if (std.mem.eql(u8, name, "DeclareMathOperator") and self.index < self.source.len and self.source[self.index] == '*') {
676 self.index += 1;
677 }
678 try self.skipRawGroup();
679 try self.skipRawBracket();
680 try self.skipRawGroup();
681 return ast.empty(self.arena);
682 }
683
684 fn parseRawGroupText(self: *Parser) Error!*ast.Expr {
685 self.skipSpaces();
686 if (self.index >= self.source.len or self.source[self.index] != '{') return self.parseArgument();
687 self.index += 1;
688 const start = self.index;
689 var depth: usize = 1;
690 while (self.index < self.source.len) {
691 const char = self.source[self.index];
692 if (char == '\\') {
693 self.index += 1;
694 if (self.index < self.source.len) self.index += 1;
695 continue;
696 }
697 if (char == '{') {
698 depth += 1;
699 } else if (char == '}') {
700 depth -= 1;
701 if (depth == 0) {
702 const raw = std.mem.trim(u8, self.source[start..self.index], " \t\r\n");
703 self.index += 1;
704 return ast.text(self.arena, raw);
705 }
706 }
707 self.index += 1;
708 }
709 return error.UnclosedGroup;
710 }
711
712 fn parseGridBody(self: *Parser, stop: GridStop, style: GridStyle) Error!*ast.Expr {
713 var rows: std.ArrayListUnmanaged(ast.GridRow) = .empty;
714 defer rows.deinit(self.arena);
715 var cells: std.ArrayListUnmanaged(*ast.Expr) = .empty;
716 defer cells.deinit(self.arena);
717 var items: std.ArrayListUnmanaged(*ast.Expr) = .empty;
718 defer items.deinit(self.arena);
719 while (true) {
720 self.skipSpaces();
721 if (self.index >= self.source.len) return error.UnclosedGroup;
722 if (try self.consumeGridStop(stop)) {
723 if (items.items.len != 0 or cells.items.len != 0) try self.finishGridRow(&items, &cells, &rows);
724 break;
725 }
726 if (try self.consumeGridLineBreak()) {
727 try self.finishGridRow(&items, &cells, &rows);
728 continue;
729 }
730 if (self.consumeLimitPolicy()) |policy| {
731 try self.applyLimitPolicy(&items, policy);
732 continue;
733 }
734 switch (self.source[self.index]) {
735 '&' => {
736 self.index += 1;
737 try self.finishGridCell(&items, &cells);
738 },
739 '^' => try self.attachScript(&items, .sup),
740 '_' => try self.attachScript(&items, .sub),
741 else => try self.appendAtom(&items),
742 }
743 }
744 const owned = try self.arena.dupe(ast.GridRow, rows.items);
745 return ast.node(self.arena, .{ .grid = .{
746 .rows = owned,
747 .fence = style.fence,
748 .alignment = style.alignment,
749 } });
750 }
751
752 fn consumeGridStop(self: *Parser, stop: GridStop) Error!bool {
753 return switch (stop) {
754 .environment => |name| try self.consumeEnvironmentEnd(name),
755 .group => if (self.source[self.index] == '}') block: {
756 self.index += 1;
757 break :block true;
758 } else false,
759 };
760 }
761
762 fn finishGridCell(
763 self: *Parser,
764 items: *std.ArrayListUnmanaged(*ast.Expr),
765 cells: *std.ArrayListUnmanaged(*ast.Expr),
766 ) Error!void {
767 try cells.append(self.arena, try ast.row(self.arena, items.items));
768 items.clearRetainingCapacity();
769 }
770
771 fn finishGridRow(
772 self: *Parser,
773 items: *std.ArrayListUnmanaged(*ast.Expr),
774 cells: *std.ArrayListUnmanaged(*ast.Expr),
775 rows: *std.ArrayListUnmanaged(ast.GridRow),
776 ) Error!void {
777 try self.finishGridCell(items, cells);
778 const owned = try self.arena.dupe(*ast.Expr, cells.items);
779 try rows.append(self.arena, .{ .cells = owned });
780 cells.clearRetainingCapacity();
781 }
782
783 fn consumeGridLineBreak(self: *Parser) Error!bool {
784 if (self.index + 1 < self.source.len and
785 self.source[self.index] == '\\' and
786 self.source[self.index + 1] == '\\')
787 {
788 self.index += 2;
789 try self.skipRawBracket();
790 return true;
791 }
792 if (self.peekCommand("cr")) |end| {
793 self.index = end;
794 try self.skipRawBracket();
795 return true;
796 }
797 return false;
798 }
799
800 fn consumeEnvironmentEnd(self: *Parser, name: []const u8) Error!bool {
801 const end = self.peekCommand("end") orelse return false;
802 const saved = self.index;
803 self.index = end;
804 const found = try self.parseNameGroup();
805 if (sameEnvironment(name, found)) return true;
806 self.index = saved;
807 return false;
808 }
809
810 fn peekCommand(self: *const Parser, name: []const u8) ?usize {
811 if (self.index >= self.source.len or self.source[self.index] != '\\') return null;
812 var index = self.index + 1;
813 const start = index;
814 while (index < self.source.len and std.ascii.isAlphabetic(self.source[index])) index += 1;
815 if (index == start) return null;
816 if (std.mem.eql(u8, self.source[start..index], name)) return index;
817 return null;
818 }
819
820 fn consumeInfix(self: *Parser) ?Infix {
821 if (self.index >= self.source.len or self.source[self.index] != '\\') return null;
822 var index = self.index + 1;
823 const start = index;
824 while (index < self.source.len and std.ascii.isAlphabetic(self.source[index])) index += 1;
825 if (index == start) return null;
826 const name = self.source[start..index];
827 const kind = infixCommand(name) orelse return null;
828 self.index = index;
829 return kind;
830 }
831
832 fn consumeLimitPolicy(self: *Parser) ?ast.LimitPolicy {
833 if (self.peekCommand("limits")) |end| {
834 self.index = end;
835 return .limits;
836 }
837 if (self.peekCommand("nolimits")) |end| {
838 self.index = end;
839 return .nolimits;
840 }
841 return null;
842 }
843
844 fn consumeRightDelimiter(self: *Parser) Error!?ast.Delimiter {
845 const end = self.peekCommand("right") orelse return null;
846 self.index = end;
847 return try self.parseDelimiterValue();
848 }
849
850 fn parseNameGroup(self: *Parser) Error![]const u8 {
851 self.skipSpaces();
852 if (self.index >= self.source.len or self.source[self.index] != '{') return error.ExpectedEnvironmentName;
853 self.index += 1;
854 const start = self.index;
855 while (self.index < self.source.len and self.source[self.index] != '}') self.index += 1;
856 if (self.index >= self.source.len) return error.UnclosedGroup;
857 const value = std.mem.trim(u8, self.source[start..self.index], " \t\r\n");
858 self.index += 1;
859 return value;
860 }
861
862 fn skipRawGroup(self: *Parser) Error!void {
863 self.skipSpaces();
864 if (self.index >= self.source.len or self.source[self.index] != '{') return;
865 self.index += 1;
866 var depth: usize = 1;
867 while (self.index < self.source.len) {
868 const char = self.source[self.index];
869 if (char == '\\') {
870 self.index += 1;
871 if (self.index < self.source.len) self.index += 1;
872 continue;
873 }
874 if (char == '{') {
875 depth += 1;
876 } else if (char == '}') {
877 depth -= 1;
878 if (depth == 0) {
879 self.index += 1;
880 return;
881 }
882 }
883 self.index += 1;
884 }
885 return error.UnclosedGroup;
886 }
887
888 fn skipRawBracket(self: *Parser) Error!void {
889 self.skipSpaces();
890 if (self.index >= self.source.len or self.source[self.index] != '[') return;
891 self.index += 1;
892 var depth: usize = 1;
893 while (self.index < self.source.len) {
894 const char = self.source[self.index];
895 if (char == '\\') {
896 self.index += 1;
897 if (self.index < self.source.len) self.index += 1;
898 continue;
899 }
900 if (char == '[') {
901 depth += 1;
902 } else if (char == ']') {
903 depth -= 1;
904 if (depth == 0) {
905 self.index += 1;
906 return;
907 }
908 }
909 self.index += 1;
910 }
911 return error.UnclosedGroup;
912 }
913
914 fn parseRawBracketText(self: *Parser) Error![]const u8 {
915 self.skipSpaces();
916 if (self.index >= self.source.len or self.source[self.index] != '[') return "";
917 self.index += 1;
918 const start = self.index;
919 var depth: usize = 1;
920 while (self.index < self.source.len) {
921 const char = self.source[self.index];
922 if (char == '\\') {
923 self.index += 1;
924 if (self.index < self.source.len) self.index += 1;
925 continue;
926 }
927 if (char == '[') {
928 depth += 1;
929 } else if (char == ']') {
930 depth -= 1;
931 if (depth == 0) {
932 const raw = std.mem.trim(u8, self.source[start..self.index], " \t\r\n");
933 self.index += 1;
934 return raw;
935 }
936 }
937 self.index += 1;
938 }
939 return error.UnclosedGroup;
940 }
941
942 fn parseRawGroupValue(self: *Parser) Error!?[]const u8 {
943 self.skipSpaces();
944 if (self.index >= self.source.len or self.source[self.index] != '{') return null;
945 self.index += 1;
946 const start = self.index;
947 var depth: usize = 1;
948 while (self.index < self.source.len) {
949 const char = self.source[self.index];
950 if (char == '\\') {
951 self.index += 1;
952 if (self.index < self.source.len) self.index += 1;
953 continue;
954 }
955 if (char == '{') {
956 depth += 1;
957 } else if (char == '}') {
958 depth -= 1;
959 if (depth == 0) {
960 const raw = std.mem.trim(u8, self.source[start..self.index], " \t\r\n");
961 self.index += 1;
962 return raw;
963 }
964 }
965 self.index += 1;
966 }
967 return error.UnclosedGroup;
968 }
969
970 fn parseDelimiterText(self: *Parser) Error!*ast.Expr {
971 const delimiter = try self.parseDelimiterValue();
972 return switch (delimiter) {
973 .none => ast.empty(self.arena),
974 .shape => |shape| ast.text(self.arena, delimiterShapeText(shape)),
975 .text => |value| ast.text(self.arena, value),
976 };
977 }
978
979 fn parseDelimiterValue(self: *Parser) Error!ast.Delimiter {
980 self.skipSpaces();
981 if (self.index >= self.source.len) return .none;
982 if (self.source[self.index] == '\\') {
983 self.index += 1;
984 if (self.index >= self.source.len) return .none;
985 const start = self.index;
986 if (!std.ascii.isAlphabetic(self.source[self.index])) {
987 const char = self.source[self.index];
988 self.index += 1;
989 if (escapedDelimiterChar(char)) |delimiter| return delimiter;
990 return .{ .text = try self.arena.dupe(u8, self.source[start..self.index]) };
991 }
992 while (self.index < self.source.len and std.ascii.isAlphabetic(self.source[self.index])) {
993 self.index += 1;
994 }
995 const name = self.source[start..self.index];
996 if (delimiterCommand(name)) |delimiter| return delimiter;
997 if (symbol.command(name)) |value| return .{ .text = value };
998 return .{ .text = try self.arena.dupe(u8, name) };
999 }
1000 const start = self.index;
1001 const char = self.source[self.index];
1002 const len = std.unicode.utf8ByteSequenceLength(char) catch 1;
1003 self.index += @min(len, self.source.len - self.index);
1004 if (len == 1) {
1005 if (delimiterChar(char)) |delimiter| return delimiter;
1006 }
1007 return .{ .text = try self.arena.dupe(u8, self.source[start..self.index]) };
1008 }
1009
1010 fn skipSpaces(self: *Parser) void {
1011 while (self.index < self.source.len and isSpace(self.source[self.index])) self.index += 1;
1012 }
1013 };
1014
1015 fn isStopByte(char: u8) bool {
1016 return switch (char) {
1017 '{', '}', '[', ']', '(', ')', '_', '^', '\\', '&', '~', '+', '-', '*', '/', '=', '<', '>', ',', ';' => true,
1018 else => false,
1019 };
1020 }
1021
1022 fn isSpace(char: u8) bool {
1023 return char == ' ' or char == '\t' or char == '\n' or char == '\r';
1024 }
1025
1026 fn emptyExpr(expr: *const ast.Expr) bool {
1027 const value = ast.textValue(expr) orelse return false;
1028 return value.len == 0;
1029 }
1030
1031 fn infixCommand(name: []const u8) ?Infix {
1032 if (std.mem.eql(u8, name, "over")) return .over;
1033 if (std.mem.eql(u8, name, "choose")) return .choose;
1034 if (std.mem.eql(u8, name, "atop")) return .atop;
1035 if (std.mem.eql(u8, name, "brack")) return .brack;
1036 if (std.mem.eql(u8, name, "brace")) return .brace;
1037 return null;
1038 }
1039
1040 fn ruleLabelValue(raw: []const u8) []const u8 {
1041 var value = std.mem.trim(u8, raw, " \t\r\n");
1042 if (std.mem.indexOfScalar(u8, value, '=')) |index| {
1043 value = std.mem.trim(u8, value[index + 1 ..], " \t\r\n");
1044 }
1045 var depth: usize = 0;
1046 for (value, 0..) |char, index| {
1047 switch (char) {
1048 '{', '[', '(' => depth += 1,
1049 '}', ']', ')' => {
1050 if (depth != 0) depth -= 1;
1051 },
1052 ',' => if (depth == 0) return std.mem.trim(u8, value[0..index], " \t\r\n"),
1053 else => {},
1054 }
1055 }
1056 return value;
1057 }
1058
1059 fn xArrowCommand(name: []const u8) ?[]const u8 {
1060 inline for (xArrowCommands) |entry| {
1061 if (std.mem.eql(u8, name, entry.name)) return entry.value;
1062 }
1063 return null;
1064 }
1065
1066 const XArrowCommand = struct {
1067 name: []const u8,
1068 value: []const u8,
1069 };
1070
1071 const xArrowCommands = [_]XArrowCommand{
1072 .{ .name = "xrightarrow", .value = "→" },
1073 .{ .name = "xleftarrow", .value = "←" },
1074 .{ .name = "xleftrightarrow", .value = "↔" },
1075 .{ .name = "xRightarrow", .value = "⇒" },
1076 .{ .name = "xLeftarrow", .value = "⇐" },
1077 .{ .name = "xLeftrightarrow", .value = "⇔" },
1078 .{ .name = "xlongrightarrow", .value = "⟶" },
1079 .{ .name = "xlongleftarrow", .value = "⟵" },
1080 .{ .name = "xlongleftrightarrow", .value = "⟷" },
1081 .{ .name = "xLongrightarrow", .value = "⟹" },
1082 .{ .name = "xLongleftarrow", .value = "⟸" },
1083 .{ .name = "xLongleftrightarrow", .value = "⟺" },
1084 .{ .name = "xmapsto", .value = "↦" },
1085 .{ .name = "xlongmapsto", .value = "⟼" },
1086 .{ .name = "xhookrightarrow", .value = "↪" },
1087 .{ .name = "xhookleftarrow", .value = "↩" },
1088 .{ .name = "xtwoheadrightarrow", .value = "↠" },
1089 .{ .name = "xtwoheadleftarrow", .value = "↞" },
1090 .{ .name = "xrightharpoonup", .value = "⇀" },
1091 .{ .name = "xrightharpoondown", .value = "⇁" },
1092 .{ .name = "xleftharpoonup", .value = "↼" },
1093 .{ .name = "xleftharpoondown", .value = "↽" },
1094 .{ .name = "xrightleftharpoons", .value = "⇌" },
1095 .{ .name = "xleadsto", .value = "↝" },
1096 .{ .name = "xrightsquigarrow", .value = "⇝" },
1097 .{ .name = "xmultimap", .value = "⊸" },
1098 };
1099
1100 const ReferenceStyle = enum {
1101 bare,
1102 paren,
1103 };
1104
1105 fn referenceCommand(name: []const u8) ?ReferenceStyle {
1106 inline for (.{
1107 "ref",
1108 "pageref",
1109 "autoref",
1110 "Autoref",
1111 "cref",
1112 "Cref",
1113 }) |candidate| {
1114 if (std.mem.eql(u8, name, candidate)) return .bare;
1115 }
1116 if (std.mem.eql(u8, name, "eqref")) return .paren;
1117 return null;
1118 }
1119
1120 fn rawTextCommand(name: []const u8) bool {
1121 inline for (.{
1122 "text",
1123 "mathrm",
1124 "textrm",
1125 "textnormal",
1126 "textup",
1127 "operatorname",
1128 "mbox",
1129 "hbox",
1130 }) |candidate| {
1131 if (std.mem.eql(u8, name, candidate)) return true;
1132 }
1133 return false;
1134 }
1135
1136 fn transparentCommand(name: []const u8) bool {
1137 inline for (.{
1138 "mathop",
1139 "mathrel",
1140 "mathbin",
1141 "mathord",
1142 "mathopen",
1143 "mathclose",
1144 "mathpunct",
1145 "mathinner",
1146 "ensuremath",
1147 "smash",
1148 "mathclap",
1149 "mathllap",
1150 "mathrlap",
1151 "clap",
1152 "llap",
1153 "rlap",
1154 }) |candidate| {
1155 if (std.mem.eql(u8, name, candidate)) return true;
1156 }
1157 return false;
1158 }
1159
1160 fn ignoredArgumentCommand(name: []const u8) bool {
1161 inline for (.{
1162 "phantom",
1163 "vphantom",
1164 "hphantom",
1165 }) |candidate| {
1166 if (std.mem.eql(u8, name, candidate)) return true;
1167 }
1168 return false;
1169 }
1170
1171 fn ignoredRawArgumentCommand(name: []const u8) bool {
1172 inline for (.{
1173 "label",
1174 }) |candidate| {
1175 if (std.mem.eql(u8, name, candidate)) return true;
1176 }
1177 return false;
1178 }
1179
1180 fn declarationCommand(name: []const u8) bool {
1181 inline for (.{
1182 "DeclareMathOperator",
1183 }) |candidate| {
1184 if (std.mem.eql(u8, name, candidate)) return true;
1185 }
1186 return false;
1187 }
1188
1189 fn escapedChar(char: u8) ?[]const u8 {
1190 return switch (char) {
1191 '{' => "{",
1192 '}' => "}",
1193 '_' => "_",
1194 '^' => "^",
1195 '$' => "$",
1196 '%' => "%",
1197 '&' => "&",
1198 '#' => "#",
1199 '|' => "‖",
1200 else => null,
1201 };
1202 }
1203
1204 fn namedSpace(name: []const u8) ?ast.Space {
1205 inline for (.{
1206 .{ .name = "quad", .space = emSpace(1, 1) },
1207 .{ .name = "qquad", .space = emSpace(2, 1) },
1208 .{ .name = "enspace", .space = emSpace(1, 2) },
1209 .{ .name = "thinspace", .space = emSpace(3, 18) },
1210 .{ .name = "medspace", .space = emSpace(4, 18) },
1211 .{ .name = "thickspace", .space = emSpace(5, 18) },
1212 .{ .name = "negthinspace", .space = emSpace(-3, 18) },
1213 .{ .name = "negmedspace", .space = emSpace(-4, 18) },
1214 .{ .name = "negthickspace", .space = emSpace(-5, 18) },
1215 }) |entry| {
1216 if (std.mem.eql(u8, name, entry.name)) return entry.space;
1217 }
1218 return null;
1219 }
1220
1221 fn escapedSpace(char: u8) ?ast.Space {
1222 return switch (char) {
1223 '\\', ' ' => emSpace(1, 3),
1224 ',' => emSpace(3, 18),
1225 ':' => emSpace(4, 18),
1226 ';' => emSpace(5, 18),
1227 '!' => emSpace(-3, 18),
1228 else => null,
1229 };
1230 }
1231
1232 fn parseSpaceValue(raw: []const u8) ?ast.Space {
1233 const value = std.mem.trim(u8, raw, " \t\r\n");
1234 if (value.len == 0) return null;
1235 var index: usize = 0;
1236 var sign: i64 = 1;
1237 if (value[index] == '+' or value[index] == '-') {
1238 if (value[index] == '-') sign = -1;
1239 index += 1;
1240 if (index >= value.len) return null;
1241 }
1242 var whole: i64 = 0;
1243 var seen_digit = false;
1244 while (index < value.len and std.ascii.isDigit(value[index])) : (index += 1) {
1245 seen_digit = true;
1246 whole = whole * 10 + @as(i64, value[index] - '0');
1247 }
1248 var fraction: i64 = 0;
1249 var scale: i64 = 1;
1250 if (index < value.len and value[index] == '.') {
1251 index += 1;
1252 while (index < value.len and std.ascii.isDigit(value[index])) : (index += 1) {
1253 seen_digit = true;
1254 fraction = fraction * 10 + @as(i64, value[index] - '0');
1255 scale *= 10;
1256 }
1257 }
1258 if (!seen_digit) return null;
1259 const unit = std.mem.trim(u8, value[index..], " \t\r\n");
1260 const numerator = sign * (whole * scale + fraction);
1261 var denominator = scale;
1262 if (!std.mem.eql(u8, unit, "em")) {
1263 if (std.mem.eql(u8, unit, "mu")) {
1264 denominator *= 18;
1265 } else {
1266 return null;
1267 }
1268 }
1269 return reduceSpace(numerator, denominator);
1270 }
1271
1272 fn emSpace(numerator: i32, denominator: u32) ast.Space {
1273 return .{ .numerator = numerator, .denominator = denominator };
1274 }
1275
1276 fn reduceSpace(numerator: i64, denominator: i64) ?ast.Space {
1277 if (denominator <= 0) return null;
1278 const abs_numerator: u64 = @intCast(if (numerator < 0) -numerator else numerator);
1279 const divisor: i64 = @intCast(gcd(abs_numerator, @intCast(denominator)));
1280 const reduced_numerator = @divTrunc(numerator, divisor);
1281 const reduced_denominator = @divTrunc(denominator, divisor);
1282 if (reduced_numerator < std.math.minInt(i32) or reduced_numerator > std.math.maxInt(i32)) return null;
1283 if (reduced_denominator <= 0 or reduced_denominator > std.math.maxInt(u32)) return null;
1284 return .{
1285 .numerator = @intCast(reduced_numerator),
1286 .denominator = @intCast(reduced_denominator),
1287 };
1288 }
1289
1290 fn gcd(a: u64, b: u64) u64 {
1291 var x = a;
1292 var y = b;
1293 while (y != 0) {
1294 const next = x % y;
1295 x = y;
1296 y = next;
1297 }
1298 return if (x == 0) 1 else x;
1299 }
1300
1301 fn isBigDelimiterCommand(name: []const u8) bool {
1302 inline for (.{
1303 "big",
1304 "Big",
1305 "bigg",
1306 "Bigg",
1307 "bigl",
1308 "bigr",
1309 "bigm",
1310 "Bigl",
1311 "Bigr",
1312 "Bigm",
1313 "biggl",
1314 "biggr",
1315 "biggm",
1316 "Biggl",
1317 "Biggr",
1318 "Biggm",
1319 }) |candidate| {
1320 if (std.mem.eql(u8, name, candidate)) return true;
1321 }
1322 return false;
1323 }
1324
1325 fn delimiterCommand(name: []const u8) ?ast.Delimiter {
1326 inline for (delimiterCommands) |entry| {
1327 if (std.mem.eql(u8, name, entry.name)) return .{ .shape = entry.shape };
1328 }
1329 return null;
1330 }
1331
1332 const DelimiterCommand = struct {
1333 name: []const u8,
1334 shape: ast.DelimiterShape,
1335 };
1336
1337 const delimiterCommands = [_]DelimiterCommand{
1338 .{ .name = "lparen", .shape = .left_paren },
1339 .{ .name = "rparen", .shape = .right_paren },
1340 .{ .name = "lbrack", .shape = .left_bracket },
1341 .{ .name = "rbrack", .shape = .right_bracket },
1342 .{ .name = "lbrace", .shape = .left_brace },
1343 .{ .name = "rbrace", .shape = .right_brace },
1344 .{ .name = "vert", .shape = .bar },
1345 .{ .name = "lvert", .shape = .bar },
1346 .{ .name = "rvert", .shape = .bar },
1347 .{ .name = "Vert", .shape = .double_bar },
1348 .{ .name = "lVert", .shape = .double_bar },
1349 .{ .name = "rVert", .shape = .double_bar },
1350 .{ .name = "langle", .shape = .left_angle },
1351 .{ .name = "rangle", .shape = .right_angle },
1352 .{ .name = "llangle", .shape = .left_double_angle },
1353 .{ .name = "rrangle", .shape = .right_double_angle },
1354 .{ .name = "llbracket", .shape = .left_double_bracket },
1355 .{ .name = "rrbracket", .shape = .right_double_bracket },
1356 .{ .name = "lfloor", .shape = .left_floor },
1357 .{ .name = "rfloor", .shape = .right_floor },
1358 .{ .name = "lceil", .shape = .left_ceil },
1359 .{ .name = "rceil", .shape = .right_ceil },
1360 };
1361
1362 fn escapedDelimiterChar(char: u8) ?ast.Delimiter {
1363 if (char == '|') return .{ .shape = .double_bar };
1364 return delimiterChar(char);
1365 }
1366
1367 fn delimiterChar(char: u8) ?ast.Delimiter {
1368 return switch (char) {
1369 '.' => .none,
1370 '(' => .{ .shape = .left_paren },
1371 ')' => .{ .shape = .right_paren },
1372 '[' => .{ .shape = .left_bracket },
1373 ']' => .{ .shape = .right_bracket },
1374 '{' => .{ .shape = .left_brace },
1375 '}' => .{ .shape = .right_brace },
1376 '|' => .{ .shape = .bar },
1377 else => null,
1378 };
1379 }
1380
1381 fn delimiterShapeText(shape: ast.DelimiterShape) []const u8 {
1382 return switch (shape) {
1383 .left_paren => "(",
1384 .right_paren => ")",
1385 .left_bracket => "[",
1386 .right_bracket => "]",
1387 .left_brace => "{",
1388 .right_brace => "}",
1389 .bar => "|",
1390 .double_bar => "‖",
1391 .left_angle => "⟨",
1392 .right_angle => "⟩",
1393 .left_double_angle => "⟪",
1394 .right_double_angle => "⟫",
1395 .left_double_bracket => "⟦",
1396 .right_double_bracket => "⟧",
1397 .left_floor => "⌊",
1398 .right_floor => "⌋",
1399 .left_ceil => "⌈",
1400 .right_ceil => "⌉",
1401 };
1402 }
1403
1404 fn environmentStyle(name: []const u8) GridStyle {
1405 const base = environmentBase(name);
1406 if (std.mem.eql(u8, base, "pmatrix")) return .{ .fence = .paren };
1407 if (std.mem.eql(u8, base, "bmatrix")) return .{ .fence = .bracket };
1408 if (std.mem.eql(u8, base, "Bmatrix")) return .{ .fence = .brace };
1409 if (std.mem.eql(u8, base, "vmatrix")) return .{ .fence = .bar };
1410 if (std.mem.eql(u8, base, "Vmatrix")) return .{ .fence = .double_bar };
1411 if (std.mem.eql(u8, base, "cases")) return .{ .fence = .left_brace, .alignment = .left };
1412 if (std.mem.eql(u8, base, "aligned") or
1413 std.mem.eql(u8, base, "alignedat") or
1414 std.mem.eql(u8, base, "align") or
1415 std.mem.eql(u8, base, "split") or
1416 std.mem.eql(u8, base, "array"))
1417 {
1418 return .{ .alignment = .left };
1419 }
1420 return .{};
1421 }
1422
1423 fn environmentBase(name: []const u8) []const u8 {
1424 if (name.len != 0 and name[name.len - 1] == '*') return name[0 .. name.len - 1];
1425 return name;
1426 }
1427
1428 fn sameEnvironment(left: []const u8, right: []const u8) bool {
1429 return std.mem.eql(u8, environmentBase(left), environmentBase(right));
1430 }
1431
1432 test "parser maps commands and scripts" {
1433 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1434 defer arena.deinit();
1435 const expr = try parse(arena.allocator(), "\\alpha_i^2");
1436 const scripts = expr.scripts;
1437 try std.testing.expectEqualStrings("α", scripts.base.text);
1438 try std.testing.expectEqualStrings("i", scripts.sub.?.text);
1439 try std.testing.expectEqualStrings("2", scripts.sup.?.text);
1440 }
1441
1442 test "parser preserves explicit operator limit policy" {
1443 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1444 defer arena.deinit();
1445
1446 const starred = try parse(arena.allocator(), "\\operatorname*{argmin}_{x}");
1447 const starred_scripts = starred.scripts;
1448 try std.testing.expectEqual(ast.LimitPolicy.limits, starred_scripts.base.operator.limit_policy);
1449 try std.testing.expectEqualStrings("argmin", starred_scripts.base.operator.body.text);
1450
1451 const plain = try parse(arena.allocator(), "\\operatorname{argmin}_{x}");
1452 const plain_scripts = plain.scripts;
1453 try std.testing.expectEqual(ast.LimitPolicy.nolimits, plain_scripts.base.operator.limit_policy);
1454 try std.testing.expectEqualStrings("argmin", plain_scripts.base.operator.body.text);
1455
1456 const explicit = try parse(arena.allocator(), "\\sum\\nolimits_{i=0}");
1457 const explicit_scripts = explicit.scripts;
1458 try std.testing.expectEqual(ast.LimitPolicy.nolimits, explicit_scripts.base.operator.limit_policy);
1459 try std.testing.expectEqualStrings("∑", explicit_scripts.base.operator.body.text);
1460
1461 const mathop = try parse(arena.allocator(), "\\mathop{dom}\\limits_{x}");
1462 const mathop_scripts = mathop.scripts;
1463 try std.testing.expectEqual(ast.LimitPolicy.limits, mathop_scripts.base.operator.limit_policy);
1464 try std.testing.expectEqualStrings("dom", mathop_scripts.base.operator.body.text);
1465 }
1466
1467 test "parser builds fractions and radicals" {
1468 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1469 defer arena.deinit();
1470 const expr = try parse(arena.allocator(), "\\sqrt[3]{\\frac{x}{y}}");
1471 const radical = expr.sqrt;
1472 try std.testing.expect(radical.index != null);
1473 try std.testing.expect(std.meta.activeTag(radical.body.*) == .fraction);
1474 }
1475
1476 test "parser preserves display fraction commands" {
1477 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1478 defer arena.deinit();
1479
1480 const text = try parse(arena.allocator(), "\\frac{x}{y}");
1481 try std.testing.expectEqual(ast.FractionStyle.text, text.fraction.style);
1482
1483 const text_alias = try parse(arena.allocator(), "\\tfrac{x}{y}");
1484 try std.testing.expectEqual(ast.FractionStyle.text, text_alias.fraction.style);
1485
1486 const display = try parse(arena.allocator(), "\\dfrac{x}{y}");
1487 try std.testing.expectEqual(ast.FractionStyle.display, display.fraction.style);
1488
1489 const display_binomial = try parse(arena.allocator(), "\\dbinom{x}{y}");
1490 try std.testing.expectEqual(ast.FractionStyle.display, display_binomial.grid.rows[0].cells[0].fraction.style);
1491 }