PranitInitial commit439f83c3 months agoHistory
487 lines · 16.1 KB · Zig
RawDownload
const std = @import("std");const Allocator = std.mem.Allocator;const display_width = @import("../../shared/display_width.zig");const ansi = @import("ansi.zig");const tu = @import("text_util.zig");const bp = @import("block_parse.zig");const payload = @import("payload.zig");const inline_render = @import("inline_render.zig"); pub fn writeHeading(    alloc: Allocator,    level: usize,    content: []const u8,    out: *std.ArrayList(u8),    footnotes: ?*const payload.FootnoteSink,    link_id: *u32,) !void {    switch (level) {        1 => {            try out.appendSlice(alloc, ansi.bold_open);            try out.appendSlice(alloc, ansi.underline_open);        },        2 => try out.appendSlice(alloc, ansi.bold_open),        3 => try out.appendSlice(alloc, ansi.underline_open),        4 => {            try out.appendSlice(alloc, ansi.bold_open);            try out.appendSlice(alloc, ansi.dim_open);        },        5 => {            try out.appendSlice(alloc, ansi.dim_open);            try out.appendSlice(alloc, ansi.underline_open);        },        6 => try out.appendSlice(alloc, ansi.dim_open),        else => unreachable,    }    try inline_render.writeInlineNoBold(        alloc,        content,        out,        level == 1 or level == 3 or level == 5,        footnotes,        link_id,    );    switch (level) {        1 => {            try out.appendSlice(alloc, ansi.underline_close);            try out.appendSlice(alloc, ansi.bold_close);        },        2, 4, 6 => try out.appendSlice(alloc, ansi.bold_close),        3 => try out.appendSlice(alloc, ansi.underline_close),        5 => {            try out.appendSlice(alloc, ansi.underline_close);            try out.appendSlice(alloc, ansi.dim_close);        },        else => unreachable,    }} pub fn writeBlockquoteLine(    alloc: Allocator,    blockquote: bp.BlockquotePrefix,    content: []const u8,    line_has_lf: bool,    out: *std.ArrayList(u8),    footnotes: ?*const payload.FootnoteSink,    link_id: *u32,) !void {    try out.appendNTimes(alloc, ' ', blockquote.indent);    for (0..blockquote.depth) |_| try ansi.writeDim(alloc, out, ansi.vertical_rule_prefix);    try inline_render.writeInline(alloc, tu.withoutTerminalHardBreakMarker(content, line_has_lf), out, false, footnotes, link_id);} pub fn writeDefinitionLine(    alloc: Allocator,    body: []const u8,    line_has_lf: bool,    out: *std.ArrayList(u8),    footnotes: ?*const payload.FootnoteSink,    link_id: *u32,) !void {    try ansi.writeDim(alloc, out, "  ");    try inline_render.writeInline(alloc, tu.withoutTerminalHardBreakMarker(body, line_has_lf), out, false, footnotes, link_id);    try out.append(alloc, '\n');} pub fn writeTaskListMarker(alloc: Allocator, out: *std.ArrayList(u8), task: bp.ParsedTaskListItem) !void {    if (task.completed) {        try out.appendSlice(alloc, ansi.task_completed_open);        try out.appendSlice(alloc, ansi.task_completed_marker);        try out.appendSlice(alloc, ansi.task_completed_close);        if (task.has_separator) try out.append(alloc, ' ');        return;    }     try out.appendSlice(alloc, ansi.dim_open);    try out.appendSlice(alloc, ansi.task_pending_marker);    if (task.has_separator) try out.append(alloc, ' ');    try out.appendSlice(alloc, ansi.dim_close);} // Width excludes ANSI escape bytes so table dividers stay aligned.const RenderedCell = struct {    bytes: std.ArrayList(u8),    width: usize,}; const RenderedRow = std.ArrayList(RenderedCell);const RenderedTable = std.ArrayList(RenderedRow); pub fn renderTable(    alloc: Allocator,    buf: []const u8,    out: *std.ArrayList(u8),    footnotes: ?*const payload.FootnoteSink,    link_id: *u32,) !void {    var table: RenderedTable = .empty;    defer deinitRenderedTable(alloc, &table);     try parseAndRenderRows(alloc, buf, &table, footnotes, link_id);     const col_count = maxRowLen(table.items);    if (table.items.len == 0 or col_count == 0) return;     const widths = try alloc.alloc(usize, col_count);    defer alloc.free(widths);    computeColumnWidths(table.items, widths);     const alignments = try alloc.alloc(payload.TableColumnAlign, col_count);    defer alloc.free(alignments);    parseColumnAlignments(alloc, buf, alignments);     for (table.items, 0..) |row, row_idx| {        const is_header = row_idx == 0;        try writeTableRow(alloc, row.items, widths, alignments, is_header, out);        if (is_header) try writeTableSeparator(alloc, widths, out);    }} fn parseAndRenderRows(    alloc: Allocator,    buf: []const u8,    table: *RenderedTable,    footnotes: ?*const payload.FootnoteSink,    link_id: *u32,) !void {    var line_idx: usize = 0;    var start: usize = 0;    while (start < buf.len) {        const end = std.mem.indexOfScalarPos(u8, buf, start, '\n') orelse buf.len;        defer {            line_idx += 1;            start = end + 1;        }        if (line_idx == 1) continue;         var raw: std.ArrayList([]const u8) = .empty;        defer raw.deinit(alloc);        try splitCells(buf[start..end], alloc, &raw);         var row = try renderRowCells(alloc, raw.items, footnotes, link_id);        errdefer deinitRenderedRow(alloc, &row);        try table.append(alloc, row);    }} fn renderRowCells(    alloc: Allocator,    raw_cells: []const []const u8,    footnotes: ?*const payload.FootnoteSink,    link_id: *u32,) !RenderedRow {    var row: RenderedRow = .empty;    errdefer deinitRenderedRow(alloc, &row);     for (raw_cells) |raw_cell| {        var bytes: std.ArrayList(u8) = .empty;        errdefer bytes.deinit(alloc);        try inline_render.writeInline(alloc, raw_cell, &bytes, false, footnotes, link_id);        const width = display_width.visibleWidthIgnoringAnsi(bytes.items);        try row.append(alloc, .{ .bytes = bytes, .width = width });    }    return row;} fn splitCells(line: []const u8, alloc: Allocator, cells: *std.ArrayList([]const u8)) !void {    var rest = tu.leftTrim(line);    if (rest.len > 0 and rest[0] == '|') rest = rest[1..];    while (rest.len > 0 and (rest[rest.len - 1] == ' ' or rest[rest.len - 1] == '\t')) {        rest = rest[0 .. rest.len - 1];    }    if (rest.len > 0 and rest[rest.len - 1] == '|' and !tu.isEscapedPunctuationAt(rest, rest.len - 1)) {        rest = rest[0 .. rest.len - 1];    }     var cell_start: usize = 0;    var i: usize = 0;    while (i <= rest.len) : (i += 1) {        if (i == rest.len or (rest[i] == '|' and !tu.isEscapedPunctuationAt(rest, i))) {            const cell = std.mem.trim(u8, rest[cell_start..i], " \t");            try cells.append(alloc, cell);            cell_start = i + 1;        }    }} fn computeColumnWidths(rows: []const RenderedRow, widths: []usize) void {    @memset(widths, 0);    for (rows) |row| {        for (row.items, 0..) |cell, col| {            if (cell.width > widths[col]) widths[col] = cell.width;        }    }} fn maxRowLen(rows: []const RenderedRow) usize {    var max: usize = 0;    for (rows) |row| {        if (row.items.len > max) max = row.items.len;    }    return max;} // ANSI escape bytes do not contribute to table column width.fn writeTableRow(    alloc: Allocator,    cells: []const RenderedCell,    widths: []const usize,    alignments: []const payload.TableColumnAlign,    is_header: bool,    out: *std.ArrayList(u8),) !void {    for (widths, 0..) |col_width, col| {        if (col > 0) try out.appendSlice(alloc, ansi.table_column_sep);         const col_align: payload.TableColumnAlign = if (is_header) .left else alignments[col];         if (col < cells.len) {            const cell = cells[col];            const pad = col_width -| cell.width;            const left_pad: usize = switch (col_align) {                .left => 0,                .right => pad,                .center => pad / 2,            };            const right_pad: usize = pad - left_pad;             try out.appendNTimes(alloc, ' ', left_pad);            if (is_header) {                try writeTableHeaderCell(alloc, out, cell.bytes.items);            } else {                try out.appendSlice(alloc, cell.bytes.items);            }            try out.appendNTimes(alloc, ' ', right_pad);        } else {            try out.appendNTimes(alloc, ' ', col_width);        }    }    try out.append(alloc, '\n');} pub fn writeTableHeaderCell(alloc: Allocator, out: *std.ArrayList(u8), cell: []const u8) !void {    try out.appendSlice(alloc, ansi.bold_open);    var remaining = cell;    while (std.mem.indexOf(u8, remaining, ansi.bold_close)) |close_index| {        try out.appendSlice(alloc, remaining[0..close_index]);        try out.appendSlice(alloc, ansi.bold_close);        try out.appendSlice(alloc, ansi.bold_open);        remaining = remaining[close_index + ansi.bold_close.len ..];    }    try out.appendSlice(alloc, remaining);    try out.appendSlice(alloc, ansi.bold_close);} fn writeTableSeparator(alloc: Allocator, widths: []const usize, out: *std.ArrayList(u8)) !void {    for (widths, 0..) |col_width, col| {        if (col > 0) try out.appendSlice(alloc, ansi.table_junction);        var remaining = col_width;        while (remaining > 0) : (remaining -= 1) try out.appendSlice(alloc, ansi.table_horiz);    }    try out.append(alloc, '\n');} fn parseColumnAlignments(alloc: Allocator, buf: []const u8, alignments: []payload.TableColumnAlign) void {    @memset(alignments, .left);    const sep_line = tu.nthLine(buf, 1) orelse return;     var cells: std.ArrayList([]const u8) = .empty;    defer cells.deinit(alloc);    splitCells(sep_line, alloc, &cells) catch return;     for (cells.items, 0..) |cell, col| {        if (col >= alignments.len) break;        const trimmed = std.mem.trim(u8, cell, " \t");        if (trimmed.len == 0) continue;        const starts = trimmed[0] == ':';        const ends = trimmed[trimmed.len - 1] == ':';        alignments[col] = if (starts and ends) .center else if (ends) .right else .left;    }} pub fn renderTablePayload(alloc: Allocator, table: payload.TablePayload, out: *std.ArrayList(u8)) !void {    if (table.rows.len == 0 or table.column_count == 0) return;     const widths = try alloc.alloc(usize, table.column_count);    defer alloc.free(widths);    computeTablePayloadWidths(table.rows, widths);     for (table.rows, 0..) |row, row_idx| {        const is_header = row_idx == 0;        try writeTablePayloadRow(alloc, row.cells, widths, table.alignments, is_header, out);        if (is_header) try writeTableSeparator(alloc, widths, out);    }} fn computeTablePayloadWidths(rows: []const payload.TableRow, widths: []usize) void {    @memset(widths, 0);    for (rows) |row| {        for (row.cells, 0..) |cell, col| {            widths[col] = @max(widths[col], display_width.visibleWidthIgnoringAnsi(cell));        }    }} fn writeTablePayloadRow(    alloc: Allocator,    cells: []const []u8,    widths: []const usize,    alignments: []const payload.TableColumnAlign,    is_header: bool,    out: *std.ArrayList(u8),) !void {    for (widths, 0..) |col_width, col| {        if (col > 0) try out.appendSlice(alloc, ansi.table_column_sep);         const col_align: payload.TableColumnAlign = if (is_header) .left else alignments[col];        if (col < cells.len) {            const cell = cells[col];            const pad = col_width -| display_width.visibleWidthIgnoringAnsi(cell);            const left_pad: usize = switch (col_align) {                .left => 0,                .right => pad,                .center => pad / 2,            };            try out.appendNTimes(alloc, ' ', left_pad);            if (is_header) {                try writeTableHeaderCell(alloc, out, cell);            } else {                try out.appendSlice(alloc, cell);            }            try out.appendNTimes(alloc, ' ', pad - left_pad);        } else {            try out.appendNTimes(alloc, ' ', col_width);        }    }    try out.append(alloc, '\n');} fn maxTablePayloadRowLen(rows: []const payload.TableRow) usize {    var max: usize = 0;    for (rows) |row| max = @max(max, row.cells.len);    return max;} pub fn parseTablePayload(alloc: Allocator, buf: []const u8, link_id: *u32) !payload.TablePayload {    return parseTablePayloadWithFootnotes(alloc, buf, null, link_id);} pub fn parseTablePayloadWithFootnotes(    alloc: Allocator,    buf: []const u8,    footnotes: ?*const payload.FootnoteSink,    link_id: *u32,) !payload.TablePayload {    var rows: std.ArrayList(payload.TableRow) = .empty;    errdefer {        payload.deinitTableRows(alloc, rows.items);        rows.deinit(alloc);    }     var line_idx: usize = 0;    var start: usize = 0;    while (start < buf.len) {        const end = std.mem.indexOfScalarPos(u8, buf, start, '\n') orelse buf.len;        defer {            line_idx += 1;            start = end + 1;        }        if (line_idx == 1) continue;         var raw: std.ArrayList([]const u8) = .empty;        defer raw.deinit(alloc);        try splitCells(buf[start..end], alloc, &raw);         var rendered_cells: std.ArrayList([]u8) = .empty;        errdefer {            for (rendered_cells.items) |cell| alloc.free(cell);            rendered_cells.deinit(alloc);        }        try rendered_cells.ensureTotalCapacity(alloc, raw.items.len);        for (raw.items) |raw_cell| {            var rendered: std.ArrayList(u8) = .empty;            errdefer rendered.deinit(alloc);            try inline_render.writeInline(alloc, raw_cell, &rendered, false, footnotes, link_id);            rendered_cells.appendAssumeCapacity(try rendered.toOwnedSlice(alloc));        }         const cells = try rendered_cells.toOwnedSlice(alloc);         var handed_off = false;        errdefer if (!handed_off) {            for (cells) |cell| alloc.free(cell);            alloc.free(cells);        };        try rows.append(alloc, .{ .cells = cells });        handed_off = true;    }     const column_count = maxTablePayloadRowLen(rows.items);    const alignments = try alloc.alloc(payload.TableColumnAlign, column_count);    errdefer alloc.free(alignments);    parseColumnAlignments(alloc, buf, alignments);    return .{        .rows = try rows.toOwnedSlice(alloc),        .alignments = alignments,        .column_count = column_count,    };} fn deinitRenderedTable(alloc: Allocator, table: *RenderedTable) void {    for (table.items) |*row| deinitRenderedRow(alloc, row);    table.deinit(alloc);} fn deinitRenderedRow(alloc: Allocator, row: *RenderedRow) void {    for (row.items) |*cell| cell.bytes.deinit(alloc);    row.deinit(alloc);} pub fn writeFootnoteDefinitionMarker(alloc: Allocator, out: *std.ArrayList(u8), number: usize) !void {    var marker: [32]u8 = undefined;    const bytes = try std.fmt.bufPrint(&marker, "[{d}] ", .{number});    try ansi.writeDim(alloc, out, bytes);} pub fn writeFootnoteBody(    alloc: Allocator,    body: []const u8,    out: *std.ArrayList(u8),    sink: *const payload.FootnoteSink,    number: usize,    link_id: *u32,) !void {    var start: usize = 0;    var is_first = true;    while (start <= body.len) {        const end = std.mem.indexOfScalarPos(u8, body, start, '\n') orelse body.len;        if (!is_first) {            try out.append(alloc, '\n');            var marker: [32]u8 = undefined;            const marker_bytes = try std.fmt.bufPrint(&marker, "[{d}] ", .{number});            @memset(marker[0..marker_bytes.len], ' ');            try ansi.writeDim(alloc, out, marker[0..marker_bytes.len]);        }        try inline_render.writeInline(alloc, body[start..end], out, false, sink, link_id);        if (end == body.len) break;        is_first = false;        start = end + 1;    }    try out.append(alloc, '\n');} pub fn renderCodeBlockPayload(    alloc: Allocator,    block: payload.CodeBlockPayload,    out: *std.ArrayList(u8),) !void {    var start: usize = 0;    while (start < block.code.len) {        const end = std.mem.indexOfScalarPos(u8, block.code, start, '\n') orelse block.code.len;        try ansi.writeDim(alloc, out, ansi.vertical_rule_prefix);        try out.appendSlice(alloc, block.code[start..end]);        try out.append(alloc, '\n');        if (end == block.code.len) break;        start = end + 1;    }}