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 }