Skip to documentation
SLOP

tiny.markdown.table

Reference tiny.markdown table

Defined in tiny.markdown.

API (13)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Source: lib/markdown/src/root.zig:11

zig
pub const table = @import("table.zig");

Source: lib/markdown/src/table.zig

zig
const std = @import("std");/// Sixteen columns cover dense comparison tables without unbounded descriptors.pub const max_columns: usize = 16;/// Sixty-four header and body rows cap one table at 1,024 cell ranges.pub const max_rows: usize = 64;pub const max_cells: usize = max_columns * max_rows;pub const Alignment = enum {    none,    left,    center,    right,};pub const SourceRange = struct {    start: usize,    end: usize,    pub fn bytes(self: SourceRange, source: []const u8) []const u8 {        std.debug.assert(self.start <= self.end);        std.debug.assert(self.end <= source.len);        return source[self.start..self.end];    }};/// One cell contains a borrowed inline Markdown source range.pub const Cell = struct {    source: SourceRange,};pub const Row = struct {    source: SourceRange,    cells: []const Cell,};pub const Delimiter = struct {    source: SourceRange,    alignments: []const Alignment,};pub const Table = struct {    source: SourceRange,    header: Row,    delimiter: Delimiter,    body: []const Row,};/// Callers own this fixed parser storage and retain it while using a table./// Over-limit candidates reject before rendering and remain paragraphs.pub const Storage = struct {    rows: [max_rows]Row = undefined,    cells: [max_cells]Cell = undefined,    alignments: [max_columns]Alignment = undefined,};pub const Rejection = enum {    columns,    rows,};pub const Result = union(enum) {    paragraph,    rejected: Rejection,    table: Table,};const Line = struct {    content: SourceRange,    next: usize,};const RowInfo = struct {    cells: usize,    has_pipe: bool,};const DelimiterInfo = struct {    cells: usize,    has_pipe: bool,    valid: bool,};pub fn parse(storage: *Storage, source: []const u8) Result {    var cursor: usize = 0;    const header_line = nextLine(source, &cursor) orelse return .paragraph;    const delimiter_line = nextLine(source, &cursor) orelse return .paragraph;    const header_info = scanRow(source, header_line.content, null);    if (!header_info.has_pipe or header_info.cells == 0) return .paragraph;    const delimiter_info = scanDelimiter(source, delimiter_line.content, null);    if (!delimiter_info.has_pipe or !delimiter_info.valid or        delimiter_info.cells != header_info.cells)    {        return .paragraph;    }    if (header_info.cells > max_columns) return .{ .rejected = .columns };    const columns = header_info.cells;    _ = scanRow(source, header_line.content, storage.cells[0..columns]);    _ = scanDelimiter(source, delimiter_line.content, storage.alignments[0..columns]);    storage.rows[0] = .{        .source = header_line.content,        .cells = storage.cells[0..columns],    };    var row_count: usize = 1;    var cell_count = columns;    var table_end = delimiter_line.next;    while (nextLine(source, &cursor)) |line| {        const trimmed = trimRange(source, line.content);        if (trimmed.start == trimmed.end) break;        const info = scanRow(source, line.content, null);        if (!info.has_pipe) break;        if (info.cells > max_columns) return .{ .rejected = .columns };        if (info.cells != columns) return .paragraph;        if (row_count == max_rows) return .{ .rejected = .rows };        std.debug.assert(cell_count <= max_cells - columns);        const cells = storage.cells[cell_count..][0..columns];        _ = scanRow(source, line.content, cells);        storage.rows[row_count] = .{ .source = line.content, .cells = cells };        row_count += 1;        cell_count += columns;        table_end = line.next;    }    std.debug.assert(row_count <= max_rows);    std.debug.assert(cell_count == row_count * columns);    return .{ .table = .{        .source = .{ .start = 0, .end = table_end },        .header = storage.rows[0],        .delimiter = .{            .source = delimiter_line.content,            .alignments = storage.alignments[0..columns],        },        .body = storage.rows[1..row_count],    } };}const RowScanner = struct {    source: []const u8,    start: usize,    end: usize,    cursor: usize,    has_pipe: bool,    done: bool = false,    fn init(source: []const u8, line: SourceRange) RowScanner {        std.debug.assert(line.start <= line.end);        std.debug.assert(line.end <= source.len);        var content = trimRange(source, line);        var has_pipe = false;        if (content.start < content.end and source[content.start] == '|') {            content.start += 1;            has_pipe = true;        }        if (content.start < content.end and source[content.end - 1] == '|' and            !escapedPipe(source, content.end - 1, content.start))        {            content.end -= 1;            has_pipe = true;        }        return .{            .source = source,            .start = content.start,            .end = content.end,            .cursor = content.start,            .has_pipe = has_pipe,        };    }    fn next(self: *RowScanner) ?SourceRange {        std.debug.assert(self.start <= self.cursor);        std.debug.assert(self.cursor <= self.end);        std.debug.assert(self.end <= self.source.len);        if (self.done) return null;        const cell_start = self.cursor;        var index = self.cursor;        while (index < self.end) : (index += 1) {            if (self.source[index] != '|') continue;            if (escapedPipe(self.source, index, self.start)) continue;            self.has_pipe = true;            self.cursor = index + 1;            return trimRange(self.source, .{ .start = cell_start, .end = index });        }        self.done = true;        self.cursor = self.end;        return trimRange(self.source, .{ .start = cell_start, .end = self.end });    }};fn scanRow(source: []const u8, line: SourceRange, output: ?[]Cell) RowInfo {    std.debug.assert(line.start <= line.end);    std.debug.assert(line.end <= source.len);    if (output) |cells| std.debug.assert(cells.len <= max_columns);    var scanner = RowScanner.init(source, line);    var count: usize = 0;    while (scanner.next()) |inline_source| {        if (output) |cells| {            std.debug.assert(count < cells.len);            cells[count] = .{ .source = inline_source };        }        count += 1;    }    if (output) |cells| std.debug.assert(count == cells.len);    return .{ .cells = count, .has_pipe = scanner.has_pipe };}fn scanDelimiter(    source: []const u8,    line: SourceRange,    output: ?[]Alignment,) DelimiterInfo {    std.debug.assert(line.start <= line.end);    std.debug.assert(line.end <= source.len);    if (output) |alignments| std.debug.assert(alignments.len <= max_columns);    var scanner = RowScanner.init(source, line);    var count: usize = 0;    var valid = true;    while (scanner.next()) |cell| {        const alignment = parseAlignment(source, cell) orelse {            valid = false;            count += 1;            continue;        };        if (output) |alignments| {            std.debug.assert(count < alignments.len);            alignments[count] = alignment;        }        count += 1;    }    if (output) |alignments| std.debug.assert(count == alignments.len);    return .{        .cells = count,        .has_pipe = scanner.has_pipe,        .valid = valid,    };}fn parseAlignment(source: []const u8, range: SourceRange) ?Alignment {    std.debug.assert(range.start <= range.end);    std.debug.assert(range.end <= source.len);    var start = range.start;    var end = range.end;    const left = start < end and source[start] == ':';    if (left) start += 1;    const right = start < end and source[end - 1] == ':';    if (right) end -= 1;    if (end - start < 3) return null;    for (source[start..end]) |byte| {        if (byte != '-') return null;    }    if (left and right) return .center;    if (left) return .left;    if (right) return .right;    return .none;}fn nextLine(source: []const u8, cursor: *usize) ?Line {    std.debug.assert(cursor.* <= source.len);    if (cursor.* >= source.len) return null;    const start = cursor.*;    const newline = std.mem.indexOfScalarPos(u8, source, start, '\n');    const raw_end = newline orelse source.len;    cursor.* = if (newline != null) raw_end + 1 else raw_end;    const end = if (raw_end > start and source[raw_end - 1] == '\r')        raw_end - 1    else        raw_end;    std.debug.assert(start <= end);    std.debug.assert(end <= cursor.*);    return .{ .content = .{ .start = start, .end = end }, .next = cursor.* };}fn trimRange(source: []const u8, range: SourceRange) SourceRange {    std.debug.assert(range.start <= range.end);    std.debug.assert(range.end <= source.len);    var start = range.start;    var end = range.end;    while (start < end and (source[start] == ' ' or source[start] == '\t')) start += 1;    while (end > start and (source[end - 1] == ' ' or source[end - 1] == '\t')) end -= 1;    return .{ .start = start, .end = end };}fn escapedPipe(source: []const u8, pipe: usize, lower: usize) bool {    std.debug.assert(lower <= pipe);    std.debug.assert(pipe < source.len);    var slash_count: usize = 0;    var cursor = pipe;    while (cursor > lower and source[cursor - 1] == '\\') {        slash_count += 1;        cursor -= 1;    }    return slash_count % 2 == 1;}fn expectTable(result: Result) !Table {    return switch (result) {        .table => |table| table,        .paragraph, .rejected => error.ExpectedTable,    };}test "pipe table classifies header delimiter body and inline source" {    const source =        "| Name | Detail |\n" ++        "| --- | --- |\n" ++        "| **fast** | [docs](/docs) and `code` |\n" ++        "\nAfter";    var storage: Storage = .{};    const table = try expectTable(parse(&storage, source));    try std.testing.expectEqual(@as(usize, 2), table.header.cells.len);    try std.testing.expectEqualStrings("Name", table.header.cells[0].source.bytes(source));    try std.testing.expectEqualStrings("| --- | --- |", table.delimiter.source.bytes(source));    try std.testing.expectEqual(@as(usize, 1), table.body.len);    try std.testing.expectEqualStrings("**fast**", table.body[0].cells[0].source.bytes(source));    try std.testing.expectEqualStrings(        "[docs](/docs) and `code`",        table.body[0].cells[1].source.bytes(source),    );    try std.testing.expectEqualStrings(        source[0..table.source.end],        table.source.bytes(source),    );}test "pipe table classifies delimiter alignment colons" {    const source =        "| Default | Left | Center | Right |\n" ++        "| --- | :--- | :---: | ---: |\n" ++        "| a | b | c | d |\n";    var storage: Storage = .{};    const table = try expectTable(parse(&storage, source));    try std.testing.expectEqualSlices(        Alignment,        &.{ .none, .left, .center, .right },        table.delimiter.alignments,    );}test "pipe table keeps degenerate candidates as paragraphs" {    var storage: Storage = .{};    const lone = parse(&storage, "| lone |\n");    try std.testing.expectEqual(.paragraph, std.meta.activeTag(lone));    const delimiter_first = parse(&storage, "| --- | --- |\n| a | b |\n");    try std.testing.expectEqual(.paragraph, std.meta.activeTag(delimiter_first));    const malformed = parse(&storage, "| a | b |\n| -- | --- |\n");    try std.testing.expectEqual(.paragraph, std.meta.activeTag(malformed));    const mismatched = parse(&storage, "| a | b |\n| --- | --- |\n| only one |\n");    try std.testing.expectEqual(.paragraph, std.meta.activeTag(mismatched));}fn appendRow(    allocator: std.mem.Allocator,    output: *std.ArrayList(u8),    columns: usize,    value: []const u8,) !void {    try output.append(allocator, '|');    for (0..columns) |_| {        try output.append(allocator, ' ');        try output.appendSlice(allocator, value);        try output.appendSlice(allocator, " |");    }    try output.append(allocator, '\n');}test "pipe table accepts maximum columns and rejects maximum plus one" {    const allocator = std.testing.allocator;    var source: std.ArrayList(u8) = .empty;    defer source.deinit(allocator);    var storage: Storage = .{};    try appendRow(allocator, &source, max_columns, "head");    try appendRow(allocator, &source, max_columns, "---");    try appendRow(allocator, &source, max_columns, "body");    const table = try expectTable(parse(&storage, source.items));    try std.testing.expectEqual(max_columns, table.header.cells.len);    source.clearRetainingCapacity();    try appendRow(allocator, &source, max_columns + 1, "head");    try appendRow(allocator, &source, max_columns + 1, "---");    const rejected = parse(&storage, source.items);    try std.testing.expectEqual(Result{ .rejected = .columns }, rejected);}test "pipe table accepts maximum rows and rejects maximum plus one" {    const allocator = std.testing.allocator;    var source: std.ArrayList(u8) = .empty;    defer source.deinit(allocator);    var storage: Storage = .{};    try appendRow(allocator, &source, 2, "head");    try appendRow(allocator, &source, 2, "---");    for (0..max_rows - 1) |_| try appendRow(allocator, &source, 2, "body");    const table = try expectTable(parse(&storage, source.items));    try std.testing.expectEqual(max_rows - 1, table.body.len);    try appendRow(allocator, &source, 2, "overflow");    const rejected = parse(&storage, source.items);    try std.testing.expectEqual(Result{ .rejected = .rows }, rejected);}

Audit

Definitions1
Public names1
Members0
Version26.7.0
Revisiondaab053ee433