tiny.markdown.table
Defined in tiny.markdown.
API (13)
Actions
Public operations.
Types and contracts
Public types and contracts.
AlignmentCell: One cell contains a borrowed inline Markdown source range.DelimiterRejectionResultRowSourceRangeStorage: Callers own this fixed parser storage and retain it while using a table.Table
Values and defaults
Public values and defaults.
max_cellsmax_columns: Sixteen columns cover dense comparison tables without unbounded descriptors.max_rows: Sixty-four header and body rows cap one table at 1,024 cell ranges.
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
| Definitions | 1 |
|---|---|
| Public names | 1 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |