diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index 362f3229..52e0cd18 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -33,118 +33,137 @@ const State = struct { index: usize, }; - list_depth: usize = 0, - list_stack: [32]ListState = undefined, - in_pre: bool = false, - in_code: bool = false, - in_blockquote: bool = false, - last_char_was_newline: bool = true, -}; - -pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void { - _ = opts; - var state = State{}; - try render(node, &state, writer, page); - if (!state.last_char_was_newline) { - try writer.writeByte('\n'); + list_depth: usize = 0, + list_stack: [32]ListState = undefined, + in_pre: bool = false, + in_code: bool = false, + in_blockquote: bool = false, + 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 { + _ = opts; + 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 { - 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 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 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 --- - - // 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.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; + + 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 --- + + // 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 { try writer.writeAll("- "); } - } else { - try writer.writeAll("- "); - } - state.last_char_was_newline = false; - }, - .blockquote => { - try writer.writeAll("> "); + state.last_char_was_newline = false; + }, + .table => { + state.in_table = true; + state.table_row_index = 0; + state.table_col_count = 0; + }, + .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.last_char_was_newline = false; }, @@ -178,8 +197,12 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag return; // Void element }, .br => { - try writer.writeByte('\n'); - state.last_char_was_newline = true; + if (state.in_table) { + try writer.writeByte(' '); + } else { + try writer.writeByte('\n'); + state.last_char_was_newline = true; + } return; // Void element }, .img => { @@ -263,17 +286,41 @@ fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Pag .ul, .ol => { 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 => {}, } // Post-block newlines switch (tag) { - .p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .table, .tr => { - if (!state.last_char_was_newline) { - try writer.writeByte('\n'); - state.last_char_was_newline = true; + .p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .table => { + if (!state.in_table) { + if (!state.last_char_was_newline) { + try writer.writeByte('\n'); + state.last_char_was_newline = true; + } } }, + .tr => {}, // Handled explicitly in closing tag logic else => {}, } } @@ -501,3 +548,56 @@ test "markdown: ordered list" { 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()); +}