mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 12:44:43 +00:00
Add support for table rendering in Markdown
This commit is contained in:
@@ -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,
|
||||||
|
last_char_was_newline: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
_ = opts;
|
_ = opts;
|
||||||
var state = State{};
|
var state = State{};
|
||||||
try render(node, &state, writer, page);
|
try render(node, &state, writer, page);
|
||||||
if (!state.last_char_was_newline) {
|
if (!state.last_char_was_newline) {
|
||||||
try writer.writeByte('\n');
|
try writer.writeByte('\n');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
|
|
||||||
switch (node._type) {
|
|
||||||
.document, .document_fragment => {
|
|
||||||
try renderChildren(node, state, writer, page);
|
|
||||||
},
|
|
||||||
.element => |el| {
|
|
||||||
try renderElement(el, state, writer, page);
|
|
||||||
},
|
|
||||||
.cdata => |cd| {
|
|
||||||
if (node.is(Node.CData.Text)) |_| {
|
|
||||||
try renderText(cd.getData(), state, writer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => {}, // Ignore other node types
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
try render(child, state, writer, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
|
|
||||||
const tag = el.getTag();
|
|
||||||
|
|
||||||
// Skip hidden/metadata elements
|
|
||||||
switch (tag) {
|
|
||||||
.script, .style, .noscript, .template, .head, .meta, .link, .title, .svg => return,
|
|
||||||
else => {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Opening Tag Logic ---
|
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
|
||||||
|
switch (node._type) {
|
||||||
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
.document, .document_fragment => {
|
||||||
switch (tag) {
|
try renderChildren(node, state, writer, page);
|
||||||
.p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .pre, .table, .hr => {
|
},
|
||||||
if (!state.last_char_was_newline) {
|
.element => |el| {
|
||||||
try writer.writeByte('\n');
|
try renderElement(el, state, writer, page);
|
||||||
state.last_char_was_newline = true;
|
},
|
||||||
}
|
.cdata => |cd| {
|
||||||
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 (node.is(Node.CData.Text)) |_| {
|
||||||
// Add an extra newline for spacing between blocks
|
try renderText(cd.getData(), state, writer);
|
||||||
try writer.writeByte('\n');
|
}
|
||||||
}
|
},
|
||||||
},
|
else => {}, // Ignore other node types
|
||||||
.li, .tr => {
|
}
|
||||||
if (!state.last_char_was_newline) {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefixes
|
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
|
||||||
switch (tag) {
|
var it = parent.childrenIterator();
|
||||||
.h1 => try writer.writeAll("# "),
|
while (it.next()) |child| {
|
||||||
.h2 => try writer.writeAll("## "),
|
try render(child, state, writer, page);
|
||||||
.h3 => try writer.writeAll("### "),
|
}
|
||||||
.h4 => try writer.writeAll("#### "),
|
}
|
||||||
.h5 => try writer.writeAll("##### "),
|
|
||||||
.h6 => try writer.writeAll("###### "),
|
|
||||||
.ul => {
|
|
||||||
if (state.list_depth < state.list_stack.len) {
|
|
||||||
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
|
|
||||||
state.list_depth += 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.ol => {
|
|
||||||
if (state.list_depth < state.list_stack.len) {
|
|
||||||
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;
|
|
||||||
try writeIndentation(indent, writer);
|
|
||||||
|
|
||||||
if (state.list_depth > 0) {
|
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) anyerror!void {
|
||||||
const current_list = &state.list_stack[state.list_depth - 1];
|
const tag = el.getTag();
|
||||||
if (current_list.type == .ordered) {
|
|
||||||
try writer.print("{d}. ", .{current_list.index});
|
// Skip hidden/metadata elements
|
||||||
current_list.index += 1;
|
switch (tag) {
|
||||||
|
.script, .style, .noscript, .template, .head, .meta, .link, .title, .svg => return,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Opening Tag Logic ---
|
||||||
|
|
||||||
|
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
||||||
|
switch (tag) {
|
||||||
|
.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) {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
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) {
|
||||||
|
// Add an extra newline for spacing between blocks
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.li, .tr => {
|
||||||
|
if (!state.last_char_was_newline) {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefixes
|
||||||
|
switch (tag) {
|
||||||
|
.h1 => try writer.writeAll("# "),
|
||||||
|
.h2 => try writer.writeAll("## "),
|
||||||
|
.h3 => try writer.writeAll("### "),
|
||||||
|
.h4 => try writer.writeAll("#### "),
|
||||||
|
.h5 => try writer.writeAll("##### "),
|
||||||
|
.h6 => try writer.writeAll("###### "),
|
||||||
|
.ul => {
|
||||||
|
if (state.list_depth < state.list_stack.len) {
|
||||||
|
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
|
||||||
|
state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.ol => {
|
||||||
|
if (state.list_depth < state.list_stack.len) {
|
||||||
|
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;
|
||||||
|
try writeIndentation(indent, writer);
|
||||||
|
|
||||||
|
if (state.list_depth > 0) {
|
||||||
|
const current_list = &state.list_stack[state.list_depth - 1];
|
||||||
|
if (current_list.type == .ordered) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user