tiny.termtex.parse
Defined in tiny.termtex.
API (2)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/termtex/src/parse.zig
zig
const std = @import("std");const ast = @import("ast.zig");const alphabet = @import("alphabet.zig");const symbol = @import("symbol.zig");const ParserError = error{ UnclosedGroup, ExpectedEnvironmentName,};pub const Error = ParserError || std.mem.Allocator.Error;pub fn parse(arena: std.mem.Allocator, source: []const u8) Error!*ast.Expr { var parser = Parser{ .arena = arena, .source = source, }; return parser.parseUntil(null);}const ScriptKind = enum { sub, sup,};const OverUnderMode = enum { over, under,};const GridStyle = struct { fence: ast.GridFence = .none, alignment: ast.GridAlignment = .center,};const GridStop = union(enum) { environment: []const u8, group,};const Infix = enum { over, choose, atop, brack, brace,};const RuleOrder = enum { premises_first, conclusion_first,};const Parser = struct { arena: std.mem.Allocator, source: []const u8, index: usize = 0, fn parseUntil(self: *Parser, stop: ?u8) Error!*ast.Expr { var items: std.ArrayListUnmanaged(*ast.Expr) = .empty; defer items.deinit(self.arena); while (true) { self.skipSpaces(); if (self.index >= self.source.len) { if (stop != null) return error.UnclosedGroup; break; } if (stop) |value| { if (self.source[self.index] == value) { self.index += 1; break; } } if (self.consumeInfix()) |kind| { const numerator = try ast.row(self.arena, items.items); const denominator = try self.parseUntil(stop); return self.makeInfix(kind, numerator, denominator); } if (self.consumeLimitPolicy()) |policy| { try self.applyLimitPolicy(&items, policy); continue; } switch (self.source[self.index]) { '^' => try self.attachScript(&items, .sup), '_' => try self.attachScript(&items, .sub), else => try self.appendAtom(&items), } } return ast.row(self.arena, items.items); } fn parseDelimitedBody(self: *Parser) Error!*ast.Expr { var items: std.ArrayListUnmanaged(*ast.Expr) = .empty; defer items.deinit(self.arena); while (true) { self.skipSpaces(); if (self.index >= self.source.len) return error.UnclosedGroup; if (self.peekCommand("right") != null) break; if (self.consumeInfix()) |kind| { const numerator = try ast.row(self.arena, items.items); const denominator = try self.parseDelimitedBody(); return self.makeInfix(kind, numerator, denominator); } if (self.consumeLimitPolicy()) |policy| { try self.applyLimitPolicy(&items, policy); continue; } switch (self.source[self.index]) { '^' => try self.attachScript(&items, .sup), '_' => try self.attachScript(&items, .sub), else => try self.appendAtom(&items), } } return ast.row(self.arena, items.items); } fn appendAtom(self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr)) Error!void { const item = try self.parseAtom(); if (emptyExpr(item)) return; try items.append(self.arena, item); } fn attachScript(self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr), kind: ScriptKind) Error!void { self.index += 1; const script = try self.parseArgument(); const base = if (items.items.len == 0) try ast.empty(self.arena) else block: { const last = items.items[items.items.len - 1]; items.items.len -= 1; break :block last; }; switch (base.*) { .scripts => |*scripts| { switch (kind) { .sub => scripts.sub = script, .sup => scripts.sup = script, } try items.append(self.arena, base); }, else => { var scripts: ast.Scripts = .{ .base = base }; switch (kind) { .sub => scripts.sub = script, .sup => scripts.sup = script, } try items.append(self.arena, try ast.node(self.arena, .{ .scripts = scripts })); }, } } fn applyLimitPolicy(self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr), policy: ast.LimitPolicy) Error!void { if (items.items.len == 0) return; const index = items.items.len - 1; items.items[index] = try self.withLimitPolicy(items.items[index], policy); } fn withLimitPolicy(self: *Parser, expr: *ast.Expr, policy: ast.LimitPolicy) Error!*ast.Expr { switch (expr.*) { .operator => |*value| { value.limit_policy = policy; return expr; }, .scripts => |*value| { value.base = try self.withLimitPolicy(value.base, policy); return expr; }, else => return ast.operator(self.arena, expr, policy), } } fn parseArgument(self: *Parser) Error!*ast.Expr { self.skipSpaces(); if (self.index >= self.source.len) return ast.empty(self.arena); if (self.source[self.index] == '{') { self.index += 1; return self.parseUntil('}'); } return self.parseAtom(); } fn parseOptionalBracket(self: *Parser) Error!?*ast.Expr { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '[') return null; self.index += 1; return try self.parseUntil(']'); } fn parseAtom(self: *Parser) Error!*ast.Expr { if (self.index >= self.source.len) return ast.empty(self.arena); const char = self.source[self.index]; return switch (char) { '{' => block: { self.index += 1; break :block try self.parseUntil('}'); }, '\\' => self.parseCommand(), '&' => block: { self.index += 1; break :block try ast.text(self.arena, " "); }, '~' => block: { self.index += 1; break :block try ast.space(self.arena, emSpace(1, 3)); }, else => self.parseTextRun(), }; } fn parseTextRun(self: *Parser) Error!*ast.Expr { const start = self.index; while (self.index < self.source.len) { const char = self.source[self.index]; if (isStopByte(char) or isSpace(char)) break; const len = std.unicode.utf8ByteSequenceLength(char) catch 1; self.index += @min(len, self.source.len - self.index); } if (self.index == start) { self.index += 1; return ast.text(self.arena, self.source[start..self.index]); } return ast.text(self.arena, self.source[start..self.index]); } fn parseCommand(self: *Parser) Error!*ast.Expr { self.index += 1; if (self.index >= self.source.len) return ast.text(self.arena, "\\"); const start = self.index; if (!std.ascii.isAlphabetic(self.source[self.index])) { const escaped_start = self.index; const char = self.source[self.index]; self.index += 1; if (escapedSpace(char)) |value| return ast.space(self.arena, value); return ast.text(self.arena, escapedChar(char) orelse self.source[escaped_start..self.index]); } while (self.index < self.source.len and std.ascii.isAlphabetic(self.source[self.index])) { self.index += 1; } const name = self.source[start..self.index]; if (commandExpr(self, name)) |expr| return expr; if (symbol.command(name)) |value| return ast.text(self.arena, value); if (symbol.function(name)) |value| return ast.text(self.arena, value); return ast.text(self.arena, name); } fn commandExpr(self: *Parser, name: []const u8) ?Error!*ast.Expr { if (std.mem.eql(u8, name, "frac") or std.mem.eql(u8, name, "tfrac")) { return makeFraction(self, .text); } if (std.mem.eql(u8, name, "dfrac")) { return makeFraction(self, .display); } if (std.mem.eql(u8, name, "binom") or std.mem.eql(u8, name, "tbinom")) { return makeBinomial(self, .text); } if (std.mem.eql(u8, name, "dbinom")) { return makeBinomial(self, .display); } if (std.mem.eql(u8, name, "inferrule") or std.mem.eql(u8, name, "inference")) { return makeInferenceRule(self, .premises_first); } if (std.mem.eql(u8, name, "infer")) { return makeInferenceRule(self, .conclusion_first); } if (xArrowCommand(name)) |arrow| { return makeXArrow(self, arrow); } if (std.mem.eql(u8, name, "pmod")) { return makeArgumentText(self, " (mod ", ")"); } if (std.mem.eql(u8, name, "pod")) { return makeArgumentText(self, " (", ")"); } if (std.mem.eql(u8, name, "mod")) { return makeArgumentText(self, " mod ", ""); } if (std.mem.eql(u8, name, "bmod")) { return ast.text(self.arena, " mod "); } if (namedSpace(name)) |value| { return ast.space(self.arena, value); } if (std.mem.eql(u8, name, "hspace")) { return makeHSpace(self); } if (std.mem.eql(u8, name, "vspace")) { return self.ignoreSpacingCommand(); } if (std.mem.eql(u8, name, "tag")) { return makeTag(self); } if (referenceCommand(name)) |style| { return makeReference(self, style); } if (std.mem.eql(u8, name, "sqrt")) { return makeSqrt(self); } if (std.mem.eql(u8, name, "begin")) { return makeEnvironment(self); } if (std.mem.eql(u8, name, "substack")) { return makeSubstack(self); } if (std.mem.eql(u8, name, "bar") or std.mem.eql(u8, name, "overline")) { return makeAccent(self, .bar); } if (std.mem.eql(u8, name, "underline")) { return makeAccent(self, .underline); } if (std.mem.eql(u8, name, "hat") or std.mem.eql(u8, name, "widehat")) { return makeAccent(self, .hat); } if (std.mem.eql(u8, name, "tilde") or std.mem.eql(u8, name, "widetilde")) { return makeAccent(self, .tilde); } if (std.mem.eql(u8, name, "check") or std.mem.eql(u8, name, "widecheck")) { return makeAccent(self, .check); } if (std.mem.eql(u8, name, "breve")) { return makeAccent(self, .breve); } if (std.mem.eql(u8, name, "vec")) { return makeAccent(self, .vec); } if (std.mem.eql(u8, name, "overrightarrow")) { return makeAccent(self, .vec); } if (std.mem.eql(u8, name, "overleftarrow")) { return makeAccent(self, .overleft); } if (std.mem.eql(u8, name, "overleftrightarrow")) { return makeAccent(self, .overleftright); } if (std.mem.eql(u8, name, "dot")) { return makeAccent(self, .dot); } if (std.mem.eql(u8, name, "ddot")) { return makeAccent(self, .ddot); } if (std.mem.eql(u8, name, "acute")) { return makeAccent(self, .acute); } if (std.mem.eql(u8, name, "grave")) { return makeAccent(self, .grave); } if (std.mem.eql(u8, name, "mathring")) { return makeAccent(self, .ring); } if (std.mem.eql(u8, name, "overset") or std.mem.eql(u8, name, "stackrel")) { return makeOverUnder(self, .over); } if (std.mem.eql(u8, name, "underset")) { return makeOverUnder(self, .under); } if (std.mem.eql(u8, name, "overbrace")) { return makeAnnotation(self, .overbrace); } if (std.mem.eql(u8, name, "underbrace")) { return makeAnnotation(self, .underbrace); } if (std.mem.eql(u8, name, "boxed")) { return makeAnnotation(self, .boxed); } if (alphabet.command(name)) |mode| { return makeAlphabet(self, mode); } if (std.mem.eql(u8, name, "not")) { return makeNot(self); } if (std.mem.eql(u8, name, "operatorname")) { const limit_policy: ast.LimitPolicy = if (self.index < self.source.len and self.source[self.index] == '*') block: { self.index += 1; break :block .limits; } else .nolimits; return ast.operator(self.arena, try self.parseRawGroupText(), limit_policy); } if (std.mem.eql(u8, name, "mathop")) { return ast.operator(self.arena, try self.parseArgument(), .auto); } if (rawTextCommand(name)) { return self.parseRawGroupText(); } if (transparentCommand(name)) { return self.parseArgument(); } if (ignoredArgumentCommand(name)) { return self.ignoreArgument(); } if (ignoredRawArgumentCommand(name)) { return self.ignoreRawArgument(); } if (declarationCommand(name)) { return self.ignoreDeclaration(name); } if (std.mem.eql(u8, name, "mathnormal") or std.mem.eql(u8, name, "mathdefault") or std.mem.eql(u8, name, "mathregular") or std.mem.eql(u8, name, "symnormal") or std.mem.eql(u8, name, "symliteral")) { return self.parseArgument(); } if (std.mem.eql(u8, name, "displaystyle") or std.mem.eql(u8, name, "textstyle") or std.mem.eql(u8, name, "scriptstyle") or std.mem.eql(u8, name, "scriptscriptstyle") or std.mem.eql(u8, name, "limits") or std.mem.eql(u8, name, "nolimits") or std.mem.eql(u8, name, "hline") or std.mem.eql(u8, name, "notag") or std.mem.eql(u8, name, "nonumber") or std.mem.eql(u8, name, "allowbreak") or std.mem.eql(u8, name, "displaybreak") or std.mem.eql(u8, name, "pagebreak") or std.mem.eql(u8, name, "nopagebreak") or std.mem.eql(u8, name, "relax")) { return ast.empty(self.arena); } if (std.mem.eql(u8, name, "left")) { return makeDelimited(self); } if (std.mem.eql(u8, name, "right") or std.mem.eql(u8, name, "middle") or isBigDelimiterCommand(name)) { return self.parseDelimiterText(); } return null; } fn makeFraction(self: *Parser, style: ast.FractionStyle) Error!*ast.Expr { const numerator = try self.parseArgument(); const denominator = try self.parseArgument(); return self.fraction(numerator, denominator, style); } fn makeBinomial(self: *Parser, style: ast.FractionStyle) Error!*ast.Expr { const numerator = try self.parseArgument(); const denominator = try self.parseArgument(); const fraction_expr = try self.fraction(numerator, denominator, style); return self.oneCellGrid(fraction_expr, .paren); } fn makeXArrow(self: *Parser, arrow: []const u8) Error!*ast.Expr { const sub = try self.parseOptionalBracket(); const sup = try self.parseArgument(); return ast.node(self.arena, .{ .scripts = .{ .base = try ast.text(self.arena, arrow), .sub = sub, .sup = sup, } }); } fn makeInferenceRule(self: *Parser, order: RuleOrder) Error!*ast.Expr { if (self.index < self.source.len and self.source[self.index] == '*') self.index += 1; const label = try self.parseOptionalRuleLabel(); const first = try self.parseRuleArgument(); const second = try self.parseRuleArgument(); const premises = switch (order) { .premises_first => first, .conclusion_first => second, }; const conclusion = switch (order) { .premises_first => second, .conclusion_first => first, }; const rule = try self.fraction(premises, conclusion, .text); if (label) |value| { const items = try self.arena.alloc(*ast.Expr, 3); items[0] = rule; items[1] = try ast.text(self.arena, " "); items[2] = value; return ast.row(self.arena, items); } return rule; } fn parseRuleArgument(self: *Parser) Error!*ast.Expr { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '{') return self.parseArgument(); self.index += 1; return self.parseGridBody(.group, .{}); } fn parseOptionalRuleLabel(self: *Parser) Error!?*ast.Expr { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '[') return null; const raw = try self.parseRawBracketText(); const value = ruleLabelValue(raw); if (value.len == 0) return null; return parse(self.arena, value); } fn makeArgumentText(self: *Parser, prefix: []const u8, suffix: []const u8) Error!*ast.Expr { const body = try self.parseArgument(); var count: usize = 1; if (prefix.len != 0) count += 1; if (suffix.len != 0) count += 1; const items = try self.arena.alloc(*ast.Expr, count); var index: usize = 0; if (prefix.len != 0) { items[index] = try ast.text(self.arena, prefix); index += 1; } items[index] = body; index += 1; if (suffix.len != 0) items[index] = try ast.text(self.arena, suffix); return ast.row(self.arena, items); } fn makeTag(self: *Parser) Error!*ast.Expr { if (self.index < self.source.len and self.source[self.index] == '*') { self.index += 1; return makeArgumentText(self, " ", ""); } return makeArgumentText(self, " (", ")"); } fn makeReference(self: *Parser, style: ReferenceStyle) Error!*ast.Expr { try self.skipRawGroup(); return ast.text(self.arena, switch (style) { .bare => "?", .paren => "(?)", }); } fn makeInfix(self: *Parser, kind: Infix, numerator: *ast.Expr, denominator: *ast.Expr) Error!*ast.Expr { return switch (kind) { .over => self.fraction(numerator, denominator, .text), .choose => self.stack(numerator, denominator, .paren), .atop => self.stack(numerator, denominator, .none), .brack => self.stack(numerator, denominator, .bracket), .brace => self.stack(numerator, denominator, .brace), }; } fn fraction(self: *Parser, numerator: *ast.Expr, denominator: *ast.Expr, style: ast.FractionStyle) Error!*ast.Expr { return ast.node(self.arena, .{ .fraction = .{ .numerator = numerator, .denominator = denominator, .style = style, } }); } fn oneCellGrid(self: *Parser, cell: *ast.Expr, fence: ast.GridFence) Error!*ast.Expr { const cells = try self.arena.alloc(*ast.Expr, 1); cells[0] = cell; const rows = try self.arena.alloc(ast.GridRow, 1); rows[0] = .{ .cells = cells }; return ast.node(self.arena, .{ .grid = .{ .rows = rows, .fence = fence, } }); } fn stack(self: *Parser, numerator: *ast.Expr, denominator: *ast.Expr, fence: ast.GridFence) Error!*ast.Expr { const top = try self.arena.alloc(*ast.Expr, 1); top[0] = numerator; const bottom = try self.arena.alloc(*ast.Expr, 1); bottom[0] = denominator; const rows = try self.arena.alloc(ast.GridRow, 2); rows[0] = .{ .cells = top }; rows[1] = .{ .cells = bottom }; return ast.node(self.arena, .{ .grid = .{ .rows = rows, .fence = fence, } }); } fn makeSqrt(self: *Parser) Error!*ast.Expr { const index = try self.parseOptionalBracket(); const body = try self.parseArgument(); return ast.node(self.arena, .{ .sqrt = .{ .index = index, .body = body, } }); } fn makeAccent(self: *Parser, mark: ast.AccentMark) Error!*ast.Expr { const body = try self.parseArgument(); return ast.node(self.arena, .{ .accent = .{ .mark = mark, .body = body, } }); } fn makeAnnotation(self: *Parser, kind: ast.AnnotationKind) Error!*ast.Expr { const body = try self.parseArgument(); return ast.node(self.arena, .{ .annotation = .{ .base = body, .kind = kind, } }); } fn makeOverUnder(self: *Parser, mode: OverUnderMode) Error!*ast.Expr { const mark = try self.parseArgument(); const base = try self.parseArgument(); var annotation: ast.Annotation = .{ .base = base }; switch (mode) { .over => annotation.over = mark, .under => annotation.under = mark, } return ast.node(self.arena, .{ .annotation = annotation }); } fn makeEnvironment(self: *Parser) Error!*ast.Expr { const name = try self.parseNameGroup(); const style = environmentStyle(name); const base = environmentBase(name); if (std.mem.eql(u8, base, "array")) { _ = try self.parseOptionalBracket(); try self.skipRawGroup(); } else if (std.mem.eql(u8, base, "alignedat")) { try self.skipRawGroup(); } return self.parseGridBody(.{ .environment = name }, style); } fn makeSubstack(self: *Parser) Error!*ast.Expr { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '{') return self.parseArgument(); self.index += 1; return self.parseGridBody(.group, .{}); } fn makeAlphabet(self: *Parser, mode: alphabet.Mode) Error!*ast.Expr { const body = try self.parseArgument(); return alphabet.apply(self.arena, mode, body); } fn makeNot(self: *Parser) Error!*ast.Expr { const body = try self.parseArgument(); if (ast.textValue(body)) |value| { if (symbol.negated(value)) |negated| return ast.text(self.arena, negated); } const items = try self.arena.alloc(*ast.Expr, 2); items[0] = try ast.text(self.arena, "¬"); items[1] = body; return ast.row(self.arena, items); } fn makeHSpace(self: *Parser) Error!*ast.Expr { if (self.index < self.source.len and self.source[self.index] == '*') self.index += 1; try self.skipRawBracket(); const raw = try self.parseRawGroupValue() orelse return ast.empty(self.arena); const value = parseSpaceValue(raw) orelse return ast.empty(self.arena); return ast.space(self.arena, value); } fn makeDelimited(self: *Parser) Error!*ast.Expr { const left = try self.parseDelimiterValue(); const body = try self.parseDelimitedBody(); const right = try self.consumeRightDelimiter() orelse return error.UnclosedGroup; return ast.node(self.arena, .{ .delimited = .{ .left = left, .body = body, .right = right, } }); } fn ignoreArgument(self: *Parser) Error!*ast.Expr { _ = try self.parseArgument(); return ast.empty(self.arena); } fn ignoreRawArgument(self: *Parser) Error!*ast.Expr { try self.skipRawGroup(); return ast.empty(self.arena); } fn ignoreSpacingCommand(self: *Parser) Error!*ast.Expr { if (self.index < self.source.len and self.source[self.index] == '*') self.index += 1; try self.skipRawBracket(); try self.skipRawGroup(); return ast.empty(self.arena); } fn ignoreDeclaration(self: *Parser, name: []const u8) Error!*ast.Expr { if (std.mem.eql(u8, name, "DeclareMathOperator") and self.index < self.source.len and self.source[self.index] == '*') { self.index += 1; } try self.skipRawGroup(); try self.skipRawBracket(); try self.skipRawGroup(); return ast.empty(self.arena); } fn parseRawGroupText(self: *Parser) Error!*ast.Expr { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '{') return self.parseArgument(); self.index += 1; const start = self.index; var depth: usize = 1; while (self.index < self.source.len) { const char = self.source[self.index]; if (char == '\\') { self.index += 1; if (self.index < self.source.len) self.index += 1; continue; } if (char == '{') { depth += 1; } else if (char == '}') { depth -= 1; if (depth == 0) { const raw = std.mem.trim(u8, self.source[start..self.index], " \t\r\n"); self.index += 1; return ast.text(self.arena, raw); } } self.index += 1; } return error.UnclosedGroup; } fn parseGridBody(self: *Parser, stop: GridStop, style: GridStyle) Error!*ast.Expr { var rows: std.ArrayListUnmanaged(ast.GridRow) = .empty; defer rows.deinit(self.arena); var cells: std.ArrayListUnmanaged(*ast.Expr) = .empty; defer cells.deinit(self.arena); var items: std.ArrayListUnmanaged(*ast.Expr) = .empty; defer items.deinit(self.arena); while (true) { self.skipSpaces(); if (self.index >= self.source.len) return error.UnclosedGroup; if (try self.consumeGridStop(stop)) { if (items.items.len != 0 or cells.items.len != 0) try self.finishGridRow(&items, &cells, &rows); break; } if (try self.consumeGridLineBreak()) { try self.finishGridRow(&items, &cells, &rows); continue; } if (self.consumeLimitPolicy()) |policy| { try self.applyLimitPolicy(&items, policy); continue; } switch (self.source[self.index]) { '&' => { self.index += 1; try self.finishGridCell(&items, &cells); }, '^' => try self.attachScript(&items, .sup), '_' => try self.attachScript(&items, .sub), else => try self.appendAtom(&items), } } const owned = try self.arena.dupe(ast.GridRow, rows.items); return ast.node(self.arena, .{ .grid = .{ .rows = owned, .fence = style.fence, .alignment = style.alignment, } }); } fn consumeGridStop(self: *Parser, stop: GridStop) Error!bool { return switch (stop) { .environment => |name| try self.consumeEnvironmentEnd(name), .group => if (self.source[self.index] == '}') block: { self.index += 1; break :block true; } else false, }; } fn finishGridCell( self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr), cells: *std.ArrayListUnmanaged(*ast.Expr), ) Error!void { try cells.append(self.arena, try ast.row(self.arena, items.items)); items.clearRetainingCapacity(); } fn finishGridRow( self: *Parser, items: *std.ArrayListUnmanaged(*ast.Expr), cells: *std.ArrayListUnmanaged(*ast.Expr), rows: *std.ArrayListUnmanaged(ast.GridRow), ) Error!void { try self.finishGridCell(items, cells); const owned = try self.arena.dupe(*ast.Expr, cells.items); try rows.append(self.arena, .{ .cells = owned }); cells.clearRetainingCapacity(); } fn consumeGridLineBreak(self: *Parser) Error!bool { if (self.index + 1 < self.source.len and self.source[self.index] == '\\' and self.source[self.index + 1] == '\\') { self.index += 2; try self.skipRawBracket(); return true; } if (self.peekCommand("cr")) |end| { self.index = end; try self.skipRawBracket(); return true; } return false; } fn consumeEnvironmentEnd(self: *Parser, name: []const u8) Error!bool { const end = self.peekCommand("end") orelse return false; const saved = self.index; self.index = end; const found = try self.parseNameGroup(); if (sameEnvironment(name, found)) return true; self.index = saved; return false; } fn peekCommand(self: *const Parser, name: []const u8) ?usize { if (self.index >= self.source.len or self.source[self.index] != '\\') return null; var index = self.index + 1; const start = index; while (index < self.source.len and std.ascii.isAlphabetic(self.source[index])) index += 1; if (index == start) return null; if (std.mem.eql(u8, self.source[start..index], name)) return index; return null; } fn consumeInfix(self: *Parser) ?Infix { if (self.index >= self.source.len or self.source[self.index] != '\\') return null; var index = self.index + 1; const start = index; while (index < self.source.len and std.ascii.isAlphabetic(self.source[index])) index += 1; if (index == start) return null; const name = self.source[start..index]; const kind = infixCommand(name) orelse return null; self.index = index; return kind; } fn consumeLimitPolicy(self: *Parser) ?ast.LimitPolicy { if (self.peekCommand("limits")) |end| { self.index = end; return .limits; } if (self.peekCommand("nolimits")) |end| { self.index = end; return .nolimits; } return null; } fn consumeRightDelimiter(self: *Parser) Error!?ast.Delimiter { const end = self.peekCommand("right") orelse return null; self.index = end; return try self.parseDelimiterValue(); } fn parseNameGroup(self: *Parser) Error![]const u8 { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '{') return error.ExpectedEnvironmentName; self.index += 1; const start = self.index; while (self.index < self.source.len and self.source[self.index] != '}') self.index += 1; if (self.index >= self.source.len) return error.UnclosedGroup; const value = std.mem.trim(u8, self.source[start..self.index], " \t\r\n"); self.index += 1; return value; } fn skipRawGroup(self: *Parser) Error!void { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '{') return; self.index += 1; var depth: usize = 1; while (self.index < self.source.len) { const char = self.source[self.index]; if (char == '\\') { self.index += 1; if (self.index < self.source.len) self.index += 1; continue; } if (char == '{') { depth += 1; } else if (char == '}') { depth -= 1; if (depth == 0) { self.index += 1; return; } } self.index += 1; } return error.UnclosedGroup; } fn skipRawBracket(self: *Parser) Error!void { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '[') return; self.index += 1; var depth: usize = 1; while (self.index < self.source.len) { const char = self.source[self.index]; if (char == '\\') { self.index += 1; if (self.index < self.source.len) self.index += 1; continue; } if (char == '[') { depth += 1; } else if (char == ']') { depth -= 1; if (depth == 0) { self.index += 1; return; } } self.index += 1; } return error.UnclosedGroup; } fn parseRawBracketText(self: *Parser) Error![]const u8 { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '[') return ""; self.index += 1; const start = self.index; var depth: usize = 1; while (self.index < self.source.len) { const char = self.source[self.index]; if (char == '\\') { self.index += 1; if (self.index < self.source.len) self.index += 1; continue; } if (char == '[') { depth += 1; } else if (char == ']') { depth -= 1; if (depth == 0) { const raw = std.mem.trim(u8, self.source[start..self.index], " \t\r\n"); self.index += 1; return raw; } } self.index += 1; } return error.UnclosedGroup; } fn parseRawGroupValue(self: *Parser) Error!?[]const u8 { self.skipSpaces(); if (self.index >= self.source.len or self.source[self.index] != '{') return null; self.index += 1; const start = self.index; var depth: usize = 1; while (self.index < self.source.len) { const char = self.source[self.index]; if (char == '\\') { self.index += 1; if (self.index < self.source.len) self.index += 1; continue; } if (char == '{') { depth += 1; } else if (char == '}') { depth -= 1; if (depth == 0) { const raw = std.mem.trim(u8, self.source[start..self.index], " \t\r\n"); self.index += 1; return raw; } } self.index += 1; } return error.UnclosedGroup; } fn parseDelimiterText(self: *Parser) Error!*ast.Expr { const delimiter = try self.parseDelimiterValue(); return switch (delimiter) { .none => ast.empty(self.arena), .shape => |shape| ast.text(self.arena, delimiterShapeText(shape)), .text => |value| ast.text(self.arena, value), }; } fn parseDelimiterValue(self: *Parser) Error!ast.Delimiter { self.skipSpaces(); if (self.index >= self.source.len) return .none; if (self.source[self.index] == '\\') { self.index += 1; if (self.index >= self.source.len) return .none; const start = self.index; if (!std.ascii.isAlphabetic(self.source[self.index])) { const char = self.source[self.index]; self.index += 1; if (escapedDelimiterChar(char)) |delimiter| return delimiter; return .{ .text = try self.arena.dupe(u8, self.source[start..self.index]) }; } while (self.index < self.source.len and std.ascii.isAlphabetic(self.source[self.index])) { self.index += 1; } const name = self.source[start..self.index]; if (delimiterCommand(name)) |delimiter| return delimiter; if (symbol.command(name)) |value| return .{ .text = value }; return .{ .text = try self.arena.dupe(u8, name) }; } const start = self.index; const char = self.source[self.index]; const len = std.unicode.utf8ByteSequenceLength(char) catch 1; self.index += @min(len, self.source.len - self.index); if (len == 1) { if (delimiterChar(char)) |delimiter| return delimiter; } return .{ .text = try self.arena.dupe(u8, self.source[start..self.index]) }; } fn skipSpaces(self: *Parser) void { while (self.index < self.source.len and isSpace(self.source[self.index])) self.index += 1; }};fn isStopByte(char: u8) bool { return switch (char) { '{', '}', '[', ']', '(', ')', '_', '^', '\\', '&', '~', '+', '-', '*', '/', '=', '<', '>', ',', ';' => true, else => false, };}fn isSpace(char: u8) bool { return char == ' ' or char == '\t' or char == '\n' or char == '\r';}fn emptyExpr(expr: *const ast.Expr) bool { const value = ast.textValue(expr) orelse return false; return value.len == 0;}fn infixCommand(name: []const u8) ?Infix { if (std.mem.eql(u8, name, "over")) return .over; if (std.mem.eql(u8, name, "choose")) return .choose; if (std.mem.eql(u8, name, "atop")) return .atop; if (std.mem.eql(u8, name, "brack")) return .brack; if (std.mem.eql(u8, name, "brace")) return .brace; return null;}fn ruleLabelValue(raw: []const u8) []const u8 { var value = std.mem.trim(u8, raw, " \t\r\n"); if (std.mem.indexOfScalar(u8, value, '=')) |index| { value = std.mem.trim(u8, value[index + 1 ..], " \t\r\n"); } var depth: usize = 0; for (value, 0..) |char, index| { switch (char) { '{', '[', '(' => depth += 1, '}', ']', ')' => { if (depth != 0) depth -= 1; }, ',' => if (depth == 0) return std.mem.trim(u8, value[0..index], " \t\r\n"), else => {}, } } return value;}fn xArrowCommand(name: []const u8) ?[]const u8 { inline for (xArrowCommands) |entry| { if (std.mem.eql(u8, name, entry.name)) return entry.value; } return null;}const XArrowCommand = struct { name: []const u8, value: []const u8,};const xArrowCommands = [_]XArrowCommand{ .{ .name = "xrightarrow", .value = "→" }, .{ .name = "xleftarrow", .value = "←" }, .{ .name = "xleftrightarrow", .value = "↔" }, .{ .name = "xRightarrow", .value = "⇒" }, .{ .name = "xLeftarrow", .value = "⇐" }, .{ .name = "xLeftrightarrow", .value = "⇔" }, .{ .name = "xlongrightarrow", .value = "⟶" }, .{ .name = "xlongleftarrow", .value = "⟵" }, .{ .name = "xlongleftrightarrow", .value = "⟷" }, .{ .name = "xLongrightarrow", .value = "⟹" }, .{ .name = "xLongleftarrow", .value = "⟸" }, .{ .name = "xLongleftrightarrow", .value = "⟺" }, .{ .name = "xmapsto", .value = "↦" }, .{ .name = "xlongmapsto", .value = "⟼" }, .{ .name = "xhookrightarrow", .value = "↪" }, .{ .name = "xhookleftarrow", .value = "↩" }, .{ .name = "xtwoheadrightarrow", .value = "↠" }, .{ .name = "xtwoheadleftarrow", .value = "↞" }, .{ .name = "xrightharpoonup", .value = "⇀" }, .{ .name = "xrightharpoondown", .value = "⇁" }, .{ .name = "xleftharpoonup", .value = "↼" }, .{ .name = "xleftharpoondown", .value = "↽" }, .{ .name = "xrightleftharpoons", .value = "⇌" }, .{ .name = "xleadsto", .value = "↝" }, .{ .name = "xrightsquigarrow", .value = "⇝" }, .{ .name = "xmultimap", .value = "⊸" },};const ReferenceStyle = enum { bare, paren,};fn referenceCommand(name: []const u8) ?ReferenceStyle { inline for (.{ "ref", "pageref", "autoref", "Autoref", "cref", "Cref", }) |candidate| { if (std.mem.eql(u8, name, candidate)) return .bare; } if (std.mem.eql(u8, name, "eqref")) return .paren; return null;}fn rawTextCommand(name: []const u8) bool { inline for (.{ "text", "mathrm", "textrm", "textnormal", "textup", "operatorname", "mbox", "hbox", }) |candidate| { if (std.mem.eql(u8, name, candidate)) return true; } return false;}fn transparentCommand(name: []const u8) bool { inline for (.{ "mathop", "mathrel", "mathbin", "mathord", "mathopen", "mathclose", "mathpunct", "mathinner", "ensuremath", "smash", "mathclap", "mathllap", "mathrlap", "clap", "llap", "rlap", }) |candidate| { if (std.mem.eql(u8, name, candidate)) return true; } return false;}fn ignoredArgumentCommand(name: []const u8) bool { inline for (.{ "phantom", "vphantom", "hphantom", }) |candidate| { if (std.mem.eql(u8, name, candidate)) return true; } return false;}fn ignoredRawArgumentCommand(name: []const u8) bool { inline for (.{ "label", }) |candidate| { if (std.mem.eql(u8, name, candidate)) return true; } return false;}fn declarationCommand(name: []const u8) bool { inline for (.{ "DeclareMathOperator", }) |candidate| { if (std.mem.eql(u8, name, candidate)) return true; } return false;}fn escapedChar(char: u8) ?[]const u8 { return switch (char) { '{' => "{", '}' => "}", '_' => "_", '^' => "^", '$' => "$", '%' => "%", '&' => "&", '#' => "#", '|' => "‖", else => null, };}fn namedSpace(name: []const u8) ?ast.Space { inline for (.{ .{ .name = "quad", .space = emSpace(1, 1) }, .{ .name = "qquad", .space = emSpace(2, 1) }, .{ .name = "enspace", .space = emSpace(1, 2) }, .{ .name = "thinspace", .space = emSpace(3, 18) }, .{ .name = "medspace", .space = emSpace(4, 18) }, .{ .name = "thickspace", .space = emSpace(5, 18) }, .{ .name = "negthinspace", .space = emSpace(-3, 18) }, .{ .name = "negmedspace", .space = emSpace(-4, 18) }, .{ .name = "negthickspace", .space = emSpace(-5, 18) }, }) |entry| { if (std.mem.eql(u8, name, entry.name)) return entry.space; } return null;}fn escapedSpace(char: u8) ?ast.Space { return switch (char) { '\\', ' ' => emSpace(1, 3), ',' => emSpace(3, 18), ':' => emSpace(4, 18), ';' => emSpace(5, 18), '!' => emSpace(-3, 18), else => null, };}fn parseSpaceValue(raw: []const u8) ?ast.Space { const value = std.mem.trim(u8, raw, " \t\r\n"); if (value.len == 0) return null; var index: usize = 0; var sign: i64 = 1; if (value[index] == '+' or value[index] == '-') { if (value[index] == '-') sign = -1; index += 1; if (index >= value.len) return null; } var whole: i64 = 0; var seen_digit = false; while (index < value.len and std.ascii.isDigit(value[index])) : (index += 1) { seen_digit = true; whole = whole * 10 + @as(i64, value[index] - '0'); } var fraction: i64 = 0; var scale: i64 = 1; if (index < value.len and value[index] == '.') { index += 1; while (index < value.len and std.ascii.isDigit(value[index])) : (index += 1) { seen_digit = true; fraction = fraction * 10 + @as(i64, value[index] - '0'); scale *= 10; } } if (!seen_digit) return null; const unit = std.mem.trim(u8, value[index..], " \t\r\n"); const numerator = sign * (whole * scale + fraction); var denominator = scale; if (!std.mem.eql(u8, unit, "em")) { if (std.mem.eql(u8, unit, "mu")) { denominator *= 18; } else { return null; } } return reduceSpace(numerator, denominator);}fn emSpace(numerator: i32, denominator: u32) ast.Space { return .{ .numerator = numerator, .denominator = denominator };}fn reduceSpace(numerator: i64, denominator: i64) ?ast.Space { if (denominator <= 0) return null; const abs_numerator: u64 = @intCast(if (numerator < 0) -numerator else numerator); const divisor: i64 = @intCast(gcd(abs_numerator, @intCast(denominator))); const reduced_numerator = @divTrunc(numerator, divisor); const reduced_denominator = @divTrunc(denominator, divisor); if (reduced_numerator < std.math.minInt(i32) or reduced_numerator > std.math.maxInt(i32)) return null; if (reduced_denominator <= 0 or reduced_denominator > std.math.maxInt(u32)) return null; return .{ .numerator = @intCast(reduced_numerator), .denominator = @intCast(reduced_denominator), };}fn gcd(a: u64, b: u64) u64 { var x = a; var y = b; while (y != 0) { const next = x % y; x = y; y = next; } return if (x == 0) 1 else x;}fn isBigDelimiterCommand(name: []const u8) bool { inline for (.{ "big", "Big", "bigg", "Bigg", "bigl", "bigr", "bigm", "Bigl", "Bigr", "Bigm", "biggl", "biggr", "biggm", "Biggl", "Biggr", "Biggm", }) |candidate| { if (std.mem.eql(u8, name, candidate)) return true; } return false;}fn delimiterCommand(name: []const u8) ?ast.Delimiter { inline for (delimiterCommands) |entry| { if (std.mem.eql(u8, name, entry.name)) return .{ .shape = entry.shape }; } return null;}const DelimiterCommand = struct { name: []const u8, shape: ast.DelimiterShape,};const delimiterCommands = [_]DelimiterCommand{ .{ .name = "lparen", .shape = .left_paren }, .{ .name = "rparen", .shape = .right_paren }, .{ .name = "lbrack", .shape = .left_bracket }, .{ .name = "rbrack", .shape = .right_bracket }, .{ .name = "lbrace", .shape = .left_brace }, .{ .name = "rbrace", .shape = .right_brace }, .{ .name = "vert", .shape = .bar }, .{ .name = "lvert", .shape = .bar }, .{ .name = "rvert", .shape = .bar }, .{ .name = "Vert", .shape = .double_bar }, .{ .name = "lVert", .shape = .double_bar }, .{ .name = "rVert", .shape = .double_bar }, .{ .name = "langle", .shape = .left_angle }, .{ .name = "rangle", .shape = .right_angle }, .{ .name = "llangle", .shape = .left_double_angle }, .{ .name = "rrangle", .shape = .right_double_angle }, .{ .name = "llbracket", .shape = .left_double_bracket }, .{ .name = "rrbracket", .shape = .right_double_bracket }, .{ .name = "lfloor", .shape = .left_floor }, .{ .name = "rfloor", .shape = .right_floor }, .{ .name = "lceil", .shape = .left_ceil }, .{ .name = "rceil", .shape = .right_ceil },};fn escapedDelimiterChar(char: u8) ?ast.Delimiter { if (char == '|') return .{ .shape = .double_bar }; return delimiterChar(char);}fn delimiterChar(char: u8) ?ast.Delimiter { return switch (char) { '.' => .none, '(' => .{ .shape = .left_paren }, ')' => .{ .shape = .right_paren }, '[' => .{ .shape = .left_bracket }, ']' => .{ .shape = .right_bracket }, '{' => .{ .shape = .left_brace }, '}' => .{ .shape = .right_brace }, '|' => .{ .shape = .bar }, else => null, };}fn delimiterShapeText(shape: ast.DelimiterShape) []const u8 { return switch (shape) { .left_paren => "(", .right_paren => ")", .left_bracket => "[", .right_bracket => "]", .left_brace => "{", .right_brace => "}", .bar => "|", .double_bar => "‖", .left_angle => "⟨", .right_angle => "⟩", .left_double_angle => "⟪", .right_double_angle => "⟫", .left_double_bracket => "⟦", .right_double_bracket => "⟧", .left_floor => "⌊", .right_floor => "⌋", .left_ceil => "⌈", .right_ceil => "⌉", };}fn environmentStyle(name: []const u8) GridStyle { const base = environmentBase(name); if (std.mem.eql(u8, base, "pmatrix")) return .{ .fence = .paren }; if (std.mem.eql(u8, base, "bmatrix")) return .{ .fence = .bracket }; if (std.mem.eql(u8, base, "Bmatrix")) return .{ .fence = .brace }; if (std.mem.eql(u8, base, "vmatrix")) return .{ .fence = .bar }; if (std.mem.eql(u8, base, "Vmatrix")) return .{ .fence = .double_bar }; if (std.mem.eql(u8, base, "cases")) return .{ .fence = .left_brace, .alignment = .left }; if (std.mem.eql(u8, base, "aligned") or std.mem.eql(u8, base, "alignedat") or std.mem.eql(u8, base, "align") or std.mem.eql(u8, base, "split") or std.mem.eql(u8, base, "array")) { return .{ .alignment = .left }; } return .{};}fn environmentBase(name: []const u8) []const u8 { if (name.len != 0 and name[name.len - 1] == '*') return name[0 .. name.len - 1]; return name;}fn sameEnvironment(left: []const u8, right: []const u8) bool { return std.mem.eql(u8, environmentBase(left), environmentBase(right));}test "parser maps commands and scripts" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const expr = try parse(arena.allocator(), "\\alpha_i^2"); const scripts = expr.scripts; try std.testing.expectEqualStrings("α", scripts.base.text); try std.testing.expectEqualStrings("i", scripts.sub.?.text); try std.testing.expectEqualStrings("2", scripts.sup.?.text);}test "parser preserves explicit operator limit policy" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const starred = try parse(arena.allocator(), "\\operatorname*{argmin}_{x}"); const starred_scripts = starred.scripts; try std.testing.expectEqual(ast.LimitPolicy.limits, starred_scripts.base.operator.limit_policy); try std.testing.expectEqualStrings("argmin", starred_scripts.base.operator.body.text); const plain = try parse(arena.allocator(), "\\operatorname{argmin}_{x}"); const plain_scripts = plain.scripts; try std.testing.expectEqual(ast.LimitPolicy.nolimits, plain_scripts.base.operator.limit_policy); try std.testing.expectEqualStrings("argmin", plain_scripts.base.operator.body.text); const explicit = try parse(arena.allocator(), "\\sum\\nolimits_{i=0}"); const explicit_scripts = explicit.scripts; try std.testing.expectEqual(ast.LimitPolicy.nolimits, explicit_scripts.base.operator.limit_policy); try std.testing.expectEqualStrings("∑", explicit_scripts.base.operator.body.text); const mathop = try parse(arena.allocator(), "\\mathop{dom}\\limits_{x}"); const mathop_scripts = mathop.scripts; try std.testing.expectEqual(ast.LimitPolicy.limits, mathop_scripts.base.operator.limit_policy); try std.testing.expectEqualStrings("dom", mathop_scripts.base.operator.body.text);}test "parser builds fractions and radicals" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const expr = try parse(arena.allocator(), "\\sqrt[3]{\\frac{x}{y}}"); const radical = expr.sqrt; try std.testing.expect(radical.index != null); try std.testing.expect(std.meta.activeTag(radical.body.*) == .fraction);}test "parser preserves display fraction commands" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const text = try parse(arena.allocator(), "\\frac{x}{y}"); try std.testing.expectEqual(ast.FractionStyle.text, text.fraction.style); const text_alias = try parse(arena.allocator(), "\\tfrac{x}{y}"); try std.testing.expectEqual(ast.FractionStyle.text, text_alias.fraction.style); const display = try parse(arena.allocator(), "\\dfrac{x}{y}"); try std.testing.expectEqual(ast.FractionStyle.display, display.fraction.style); const display_binomial = try parse(arena.allocator(), "\\dbinom{x}{y}"); try std.testing.expectEqual(ast.FractionStyle.display, display_binomial.grid.rows[0].cells[0].fraction.style);}Source: lib/termtex/src/root.zig:15
zig
pub const parse = @import("parse.zig");Audit
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |