Add support for table rendering in Markdown

This commit is contained in:
Adrià Arrufat
2026-02-16 21:24:24 +09:00
parent ec0b9de713
commit 425a36aa51

View File

@@ -33,118 +33,137 @@ const State = struct {
index: usize, index: usize,
}; };
list_depth: usize = 0, list_depth: usize = 0,
list_stack: [32]ListState = undefined, list_stack: [32]ListState = undefined,
in_pre: bool = false, in_pre: bool = false,
in_code: bool = false, in_code: bool = false,
in_blockquote: bool = false, in_blockquote: bool = false,
last_char_was_newline: bool = true, in_table: bool = false,
}; table_row_index: usize = 0,
table_col_count: usize = 0,
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void { last_char_was_newline: bool = true,
_ = opts; };
var state = State{};
try render(node, &state, writer, page); pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
if (!state.last_char_was_newline) { _ = opts;
try writer.writeByte('\n'); var state = State{};
try render(node, &state, writer, page);
if (!state.last_char_was_newline) {
try writer.writeByte('\n');
}
} }
}
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void { switch (node._type) {
switch (node._type) { .document, .document_fragment => {
.document, .document_fragment => { try renderChildren(node, state, writer, page);
try renderChildren(node, state, writer, page); },
}, .element => |el| {
.element => |el| { try renderElement(el, state, writer, page);
try renderElement(el, state, writer, page); },
}, .cdata => |cd| {
.cdata => |cd| { if (node.is(Node.CData.Text)) |_| {
if (node.is(Node.CData.Text)) |_| { try renderText(cd.getData(), state, writer);
try renderText(cd.getData(), state, writer); }
} },
}, else => {}, // Ignore other node types
else => {}, // Ignore other node types }
} }
}
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void { var it = parent.childrenIterator();
var it = parent.childrenIterator(); while (it.next()) |child| {
while (it.next()) |child| { try render(child, state, writer, page);
try render(child, state, writer, page); }
} }
}
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void { const tag = el.getTag();
const tag = el.getTag();
// Skip hidden/metadata elements
// Skip hidden/metadata elements switch (tag) {
switch (tag) { .script, .style, .noscript, .template, .head, .meta, .link, .title, .svg => return,
.script, .style, .noscript, .template, .head, .meta, .link, .title, .svg => return, else => {},
else => {}, }
}
// --- Opening Tag Logic ---
// --- Opening Tag Logic ---
// Ensure block elements start on a new line (double newline for paragraphs etc)
// Ensure block elements start on a new line (double newline for paragraphs etc) switch (tag) {
switch (tag) { .p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .pre, .table, .hr => {
.p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .pre, .table, .hr => { if (!state.in_table) {
if (!state.last_char_was_newline) { if (!state.last_char_was_newline) {
try writer.writeByte('\n'); try writer.writeByte('\n');
state.last_char_was_newline = true; state.last_char_was_newline = true;
} }
if (tag == .p or tag == .h1 or tag == .h2 or tag == .h3 or tag == .h4 or tag == .h5 or tag == .h6 or tag == .blockquote or tag == .pre or tag == .table) { if (tag == .p or tag == .h1 or tag == .h2 or tag == .h3 or tag == .h4 or tag == .h5 or tag == .h6 or tag == .blockquote or tag == .pre or tag == .table) {
// Add an extra newline for spacing between blocks // Add an extra newline for spacing between blocks
try writer.writeByte('\n'); try writer.writeByte('\n');
} }
}, }
.li, .tr => { },
if (!state.last_char_was_newline) { .li, .tr => {
try writer.writeByte('\n'); if (!state.last_char_was_newline) {
state.last_char_was_newline = true; try writer.writeByte('\n');
} state.last_char_was_newline = true;
}, }
else => {}, },
} else => {},
}
// Prefixes
switch (tag) { // Prefixes
.h1 => try writer.writeAll("# "), switch (tag) {
.h2 => try writer.writeAll("## "), .h1 => try writer.writeAll("# "),
.h3 => try writer.writeAll("### "), .h2 => try writer.writeAll("## "),
.h4 => try writer.writeAll("#### "), .h3 => try writer.writeAll("### "),
.h5 => try writer.writeAll("##### "), .h4 => try writer.writeAll("#### "),
.h6 => try writer.writeAll("###### "), .h5 => try writer.writeAll("##### "),
.ul => { .h6 => try writer.writeAll("###### "),
if (state.list_depth < state.list_stack.len) { .ul => {
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 }; if (state.list_depth < state.list_stack.len) {
state.list_depth += 1; state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
} state.list_depth += 1;
}, }
.ol => { },
if (state.list_depth < state.list_stack.len) { .ol => {
state.list_stack[state.list_depth] = .{ .type = .ordered, .index = 1 }; if (state.list_depth < state.list_stack.len) {
state.list_depth += 1; state.list_stack[state.list_depth] = .{ .type = .ordered, .index = 1 };
} state.list_depth += 1;
}, }
.li => { },
const indent = if (state.list_depth > 0) state.list_depth - 1 else 0; .li => {
try writeIndentation(indent, writer); const indent = if (state.list_depth > 0) state.list_depth - 1 else 0;
try writeIndentation(indent, writer);
if (state.list_depth > 0) {
const current_list = &state.list_stack[state.list_depth - 1]; if (state.list_depth > 0) {
if (current_list.type == .ordered) { const current_list = &state.list_stack[state.list_depth - 1];
try writer.print("{d}. ", .{current_list.index}); if (current_list.type == .ordered) {
current_list.index += 1; try writer.print("{d}. ", .{current_list.index});
current_list.index += 1;
} else {
try writer.writeAll("- ");
}
} else { } else {
try writer.writeAll("- "); try writer.writeAll("- ");
} }
} else { state.last_char_was_newline = false;
try writer.writeAll("- "); },
} .table => {
state.last_char_was_newline = false; state.in_table = true;
}, state.table_row_index = 0;
.blockquote => { state.table_col_count = 0;
try writer.writeAll("> "); },
.tr => {
state.table_col_count = 0;
try writer.writeByte('|');
},
.td, .th => {
// Note: leading pipe handled by previous cell closing or tr opening
state.last_char_was_newline = false;
// Add spacing
try writer.writeByte(' ');
},
.blockquote => { try writer.writeAll("> ");
state.in_blockquote = true; state.in_blockquote = true;
state.last_char_was_newline = false; state.last_char_was_newline = false;
}, },
@@ -178,8 +197,12 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag
return; // Void element return; // Void element
}, },
.br => { .br => {
try writer.writeByte('\n'); if (state.in_table) {
state.last_char_was_newline = true; try writer.writeByte(' ');
} else {
try writer.writeByte('\n');
state.last_char_was_newline = true;
}
return; // Void element return; // Void element
}, },
.img => { .img => {
@@ -263,17 +286,41 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag
.ul, .ol => { .ul, .ol => {
if (state.list_depth > 0) state.list_depth -= 1; if (state.list_depth > 0) state.list_depth -= 1;
}, },
.table => {
state.in_table = false;
},
.tr => {
try writer.writeByte('\n');
if (state.table_row_index == 0) {
try writer.writeByte('|');
var i: usize = 0;
while (i < state.table_col_count) : (i += 1) {
try writer.writeAll("---|");
}
try writer.writeByte('\n');
}
state.table_row_index += 1;
state.last_char_was_newline = true;
},
.td, .th => {
try writer.writeAll(" |");
state.table_col_count += 1;
state.last_char_was_newline = false;
},
else => {}, else => {},
} }
// Post-block newlines // Post-block newlines
switch (tag) { switch (tag) {
.p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .table, .tr => { .p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .table => {
if (!state.last_char_was_newline) { if (!state.in_table) {
try writer.writeByte('\n'); if (!state.last_char_was_newline) {
state.last_char_was_newline = true; try writer.writeByte('\n');
state.last_char_was_newline = true;
}
} }
}, },
.tr => {}, // Handled explicitly in closing tag logic
else => {}, else => {},
} }
} }
@@ -501,3 +548,56 @@ test "markdown: ordered list" {
try testing.expectString("1. First\n2. Second\n", aw.written()); try testing.expectString("1. First\n2. Second\n", aw.written());
} }
test "markdown: table" {
const testing = @import("../testing.zig");
const page = try testing.test_session.createPage();
defer testing.test_session.removePage();
const doc = page.window._document;
const div = try doc.createElement("div", null, page);
const table = try doc.createElement("table", null, page);
_ = try div.asNode().appendChild(table.asNode(), page);
const thead = try doc.createElement("thead", null, page);
_ = try table.asNode().appendChild(thead.asNode(), page);
const tr1 = try doc.createElement("tr", null, page);
_ = try thead.asNode().appendChild(tr1.asNode(), page);
const th1 = try doc.createElement("th", null, page);
try th1.asNode().setTextContent("Head 1", page);
_ = try tr1.asNode().appendChild(th1.asNode(), page);
const th2 = try doc.createElement("th", null, page);
try th2.asNode().setTextContent("Head 2", page);
_ = try tr1.asNode().appendChild(th2.asNode(), page);
const tbody = try doc.createElement("tbody", null, page);
_ = try table.asNode().appendChild(tbody.asNode(), page);
const tr2 = try doc.createElement("tr", null, page);
_ = try tbody.asNode().appendChild(tr2.asNode(), page);
const td1 = try doc.createElement("td", null, page);
try td1.asNode().setTextContent("Cell 1", page);
_ = try tr2.asNode().appendChild(td1.asNode(), page);
const td2 = try doc.createElement("td", null, page);
try td2.asNode().setTextContent("Cell 2", page);
_ = try tr2.asNode().appendChild(td2.asNode(), page);
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try dump(div.asNode(), .{}, &aw.writer, page);
const expected =
\\
\\| Head 1 | Head 2 |
\\|---|---|
\\| Cell 1 | Cell 2 |
\\
;
try testing.expectString(expected, aw.written());
}