const std = @import("std"); const Document = @import("Document.zig"); const Node = Document.Node; const assert = std.debug.assert; const Writer = std.Io.Writer; /// A Markdown document renderer. /// /// Each concrete `Renderer` type has a `renderDefault` function, with the /// intention that custom `renderFn` implementations can call `renderDefault` /// for node types for which they require no special rendering. pub fn Renderer(comptime Context: type) type { return struct { renderFn: *const fn ( r: Self, doc: Document, node: Node.Index, writer: *Writer, ) Writer.Error!void = renderDefault, context: Context, const Self = @This(); pub fn render(r: Self, doc: Document, writer: *Writer) Writer.Error!void { try r.renderFn(r, doc, .root, writer); } pub fn renderDefault( r: Self, doc: Document, node: Node.Index, writer: *Writer, ) Writer.Error!void { const data = doc.nodes.items(.data)[@intFromEnum(node)]; switch (doc.nodes.items(.tag)[@intFromEnum(node)]) { .root => { for (doc.extraChildren(data.container.children)) |child| { try r.renderFn(r, doc, child, writer); } }, .list => { if (data.list.start.asNumber()) |start| { if (start == 1) { try writer.writeAll("
    \n"); } else { try writer.print("
      \n", .{start}); } } else { try writer.writeAll("
    \n"); } else { try writer.writeAll("\n"); } }, .list_item => { try writer.writeAll("
  1. "); for (doc.extraChildren(data.list_item.children)) |child| { if (data.list_item.tight and doc.nodes.items(.tag)[@intFromEnum(child)] == .paragraph) { const para_data = doc.nodes.items(.data)[@intFromEnum(child)]; for (doc.extraChildren(para_data.container.children)) |para_child| { try r.renderFn(r, doc, para_child, writer); } } else { try r.renderFn(r, doc, child, writer); } } try writer.writeAll("
  2. \n"); }, .table => { try writer.writeAll("\n"); for (doc.extraChildren(data.container.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.writeAll("
    \n"); }, .table_row => { try writer.writeAll("\n"); for (doc.extraChildren(data.container.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.writeAll("\n"); }, .table_cell => { if (data.table_cell.info.header) { try writer.writeAll(" try writer.writeAll(">"), else => |a| try writer.print(" style=\"text-align: {s}\">", .{@tagName(a)}), } for (doc.extraChildren(data.table_cell.children)) |child| { try r.renderFn(r, doc, child, writer); } if (data.table_cell.info.header) { try writer.writeAll("\n"); } else { try writer.writeAll("\n"); } }, .heading => { try writer.print("", .{data.heading.level}); for (doc.extraChildren(data.heading.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.print("\n", .{data.heading.level}); }, .code_block => { const content = doc.string(data.code_block.content); try writer.print("
    {f}
    \n", .{fmtHtml(content)}); }, .blockquote => { try writer.writeAll("
    \n"); for (doc.extraChildren(data.container.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.writeAll("
    \n"); }, .paragraph => { try writer.writeAll("

    "); for (doc.extraChildren(data.container.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.writeAll("

    \n"); }, .thematic_break => { try writer.writeAll("
    \n"); }, .link => { const target = doc.string(data.link.target); try writer.print("", .{fmtHtml(target)}); for (doc.extraChildren(data.link.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.writeAll(""); }, .autolink => { const target = doc.string(data.text.content); try writer.print("{0f}", .{fmtHtml(target)}); }, .image => { const target = doc.string(data.link.target); try writer.print("\"","); }, .strong => { try writer.writeAll(""); for (doc.extraChildren(data.container.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.writeAll(""); }, .emphasis => { try writer.writeAll(""); for (doc.extraChildren(data.container.children)) |child| { try r.renderFn(r, doc, child, writer); } try writer.writeAll(""); }, .code_span => { const content = doc.string(data.text.content); try writer.print("{f}", .{fmtHtml(content)}); }, .text => { const content = doc.string(data.text.content); try writer.print("{f}", .{fmtHtml(content)}); }, .line_break => { try writer.writeAll("
    \n"); }, } } }; } /// Renders an inline node as plain text. Asserts that the node is an inline and /// has no non-inline children. pub fn renderInlineNodeText( doc: Document, node: Node.Index, writer: *Writer, ) Writer.Error!void { const data = doc.nodes.items(.data)[@intFromEnum(node)]; switch (doc.nodes.items(.tag)[@intFromEnum(node)]) { .root, .list, .list_item, .table, .table_row, .table_cell, .heading, .code_block, .blockquote, .paragraph, .thematic_break, => unreachable, // Blocks .link, .image => { for (doc.extraChildren(data.link.children)) |child| { try renderInlineNodeText(doc, child, writer); } }, .strong => { for (doc.extraChildren(data.container.children)) |child| { try renderInlineNodeText(doc, child, writer); } }, .emphasis => { for (doc.extraChildren(data.container.children)) |child| { try renderInlineNodeText(doc, child, writer); } }, .autolink, .code_span, .text => { const content = doc.string(data.text.content); try writer.print("{f}", .{fmtHtml(content)}); }, .line_break => { try writer.writeAll("\n"); }, } } pub fn fmtHtml(bytes: []const u8) std.fmt.Formatter([]const u8, formatHtml) { return .{ .data = bytes }; } fn formatHtml(bytes: []const u8, w: *Writer) Writer.Error!void { for (bytes) |b| switch (b) { '<' => try w.writeAll("<"), '>' => try w.writeAll(">"), '&' => try w.writeAll("&"), '"' => try w.writeAll("""), else => try w.writeByte(b), }; }