Skip to documentation
SLOP

tiny.termtex.parse

Reference tiny.termtex parse

Defined in tiny.termtex.

API (2)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.termtexparse
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callsprivate sourcelib.termtex.src.parse.ParserparseOptionalRuleLabeltest sourcelib.termtex.src.parsetest: parser builds fractions and rad...test sourcelib.termtex.src.parsetest: parser maps commands and scriptstest sourcelib.termtex.src.parsetest: parser preserves display fracti...test sourcelib.termtex.src.parsetest: parser preserves explicit opera...parseparse
Static calls · unresolved targets: 0 · external targets: 1.

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

Definitions3
Public names3
Members0
Version26.7.0
Revisiondaab053ee433