renderer.zig (9906B) - Raw
1 const std = @import("std"); 2 const Document = @import("Document.zig"); 3 const Node = Document.Node; 4 const assert = std.debug.assert; 5 6 /// A Markdown document renderer. 7 /// 8 /// Each concrete `Renderer` type has a `renderDefault` function, with the 9 /// intention that custom `renderFn` implementations can call `renderDefault` 10 /// for node types for which they require no special rendering. 11 pub fn Renderer(comptime Writer: type, comptime Context: type) type { 12 return struct { 13 renderFn: *const fn ( 14 r: Self, 15 doc: Document, 16 node: Node.Index, 17 writer: Writer, 18 ) Writer.Error!void = renderDefault, 19 context: Context, 20 21 const Self = @This(); 22 23 pub fn render(r: Self, doc: Document, writer: Writer) Writer.Error!void { 24 try r.renderFn(r, doc, .root, writer); 25 } 26 27 pub fn renderDefault( 28 r: Self, 29 doc: Document, 30 node: Node.Index, 31 writer: Writer, 32 ) Writer.Error!void { 33 const data = doc.nodes.items(.data)[@intFromEnum(node)]; 34 switch (doc.nodes.items(.tag)[@intFromEnum(node)]) { 35 .root => { 36 for (doc.extraChildren(data.container.children)) |child| { 37 try r.renderFn(r, doc, child, writer); 38 } 39 }, 40 .list => { 41 if (data.list.start.asNumber()) |start| { 42 if (start == 1) { 43 try writer.writeAll("<ol>\n"); 44 } else { 45 try writer.print("<ol start=\"{d}\">\n", .{start}); 46 } 47 } else { 48 try writer.writeAll("<ul>\n"); 49 } 50 for (doc.extraChildren(data.list.children)) |child| { 51 try r.renderFn(r, doc, child, writer); 52 } 53 if (data.list.start.asNumber() != null) { 54 try writer.writeAll("</ol>\n"); 55 } else { 56 try writer.writeAll("</ul>\n"); 57 } 58 }, 59 .list_item => { 60 try writer.writeAll("<li>"); 61 for (doc.extraChildren(data.list_item.children)) |child| { 62 if (data.list_item.tight and doc.nodes.items(.tag)[@intFromEnum(child)] == .paragraph) { 63 const para_data = doc.nodes.items(.data)[@intFromEnum(child)]; 64 for (doc.extraChildren(para_data.container.children)) |para_child| { 65 try r.renderFn(r, doc, para_child, writer); 66 } 67 } else { 68 try r.renderFn(r, doc, child, writer); 69 } 70 } 71 try writer.writeAll("</li>\n"); 72 }, 73 .table => { 74 try writer.writeAll("<table>\n"); 75 for (doc.extraChildren(data.container.children)) |child| { 76 try r.renderFn(r, doc, child, writer); 77 } 78 try writer.writeAll("</table>\n"); 79 }, 80 .table_row => { 81 try writer.writeAll("<tr>\n"); 82 for (doc.extraChildren(data.container.children)) |child| { 83 try r.renderFn(r, doc, child, writer); 84 } 85 try writer.writeAll("</tr>\n"); 86 }, 87 .table_cell => { 88 if (data.table_cell.info.header) { 89 try writer.writeAll("<th"); 90 } else { 91 try writer.writeAll("<td"); 92 } 93 switch (data.table_cell.info.alignment) { 94 .unset => try writer.writeAll(">"), 95 else => |a| try writer.print(" style=\"text-align: {s}\">", .{@tagName(a)}), 96 } 97 98 for (doc.extraChildren(data.table_cell.children)) |child| { 99 try r.renderFn(r, doc, child, writer); 100 } 101 102 if (data.table_cell.info.header) { 103 try writer.writeAll("</th>\n"); 104 } else { 105 try writer.writeAll("</td>\n"); 106 } 107 }, 108 .heading => { 109 try writer.print("<h{d}>", .{data.heading.level}); 110 for (doc.extraChildren(data.heading.children)) |child| { 111 try r.renderFn(r, doc, child, writer); 112 } 113 try writer.print("</h{d}>\n", .{data.heading.level}); 114 }, 115 .code_block => { 116 const content = doc.string(data.code_block.content); 117 try writer.print("<pre><code>{f}</code></pre>\n", .{fmtHtml(content)}); 118 }, 119 .blockquote => { 120 try writer.writeAll("<blockquote>\n"); 121 for (doc.extraChildren(data.container.children)) |child| { 122 try r.renderFn(r, doc, child, writer); 123 } 124 try writer.writeAll("</blockquote>\n"); 125 }, 126 .paragraph => { 127 try writer.writeAll("<p>"); 128 for (doc.extraChildren(data.container.children)) |child| { 129 try r.renderFn(r, doc, child, writer); 130 } 131 try writer.writeAll("</p>\n"); 132 }, 133 .thematic_break => { 134 try writer.writeAll("<hr />\n"); 135 }, 136 .link => { 137 const target = doc.string(data.link.target); 138 try writer.print("<a href=\"{f}\">", .{fmtHtml(target)}); 139 for (doc.extraChildren(data.link.children)) |child| { 140 try r.renderFn(r, doc, child, writer); 141 } 142 try writer.writeAll("</a>"); 143 }, 144 .autolink => { 145 const target = doc.string(data.text.content); 146 try writer.print("<a href=\"{0f}\">{0f}</a>", .{fmtHtml(target)}); 147 }, 148 .image => { 149 const target = doc.string(data.link.target); 150 try writer.print("<img src=\"{f}\" alt=\"", .{fmtHtml(target)}); 151 for (doc.extraChildren(data.link.children)) |child| { 152 try renderInlineNodeText(doc, child, writer); 153 } 154 try writer.writeAll("\" />"); 155 }, 156 .strong => { 157 try writer.writeAll("<strong>"); 158 for (doc.extraChildren(data.container.children)) |child| { 159 try r.renderFn(r, doc, child, writer); 160 } 161 try writer.writeAll("</strong>"); 162 }, 163 .emphasis => { 164 try writer.writeAll("<em>"); 165 for (doc.extraChildren(data.container.children)) |child| { 166 try r.renderFn(r, doc, child, writer); 167 } 168 try writer.writeAll("</em>"); 169 }, 170 .code_span => { 171 const content = doc.string(data.text.content); 172 try writer.print("<code>{f}</code>", .{fmtHtml(content)}); 173 }, 174 .text => { 175 const content = doc.string(data.text.content); 176 try writer.print("{f}", .{fmtHtml(content)}); 177 }, 178 .line_break => { 179 try writer.writeAll("<br />\n"); 180 }, 181 } 182 } 183 }; 184 } 185 186 /// Renders an inline node as plain text. Asserts that the node is an inline and 187 /// has no non-inline children. 188 pub fn renderInlineNodeText( 189 doc: Document, 190 node: Node.Index, 191 writer: anytype, 192 ) @TypeOf(writer).Error!void { 193 const data = doc.nodes.items(.data)[@intFromEnum(node)]; 194 switch (doc.nodes.items(.tag)[@intFromEnum(node)]) { 195 .root, 196 .list, 197 .list_item, 198 .table, 199 .table_row, 200 .table_cell, 201 .heading, 202 .code_block, 203 .blockquote, 204 .paragraph, 205 .thematic_break, 206 => unreachable, // Blocks 207 208 .link, .image => { 209 for (doc.extraChildren(data.link.children)) |child| { 210 try renderInlineNodeText(doc, child, writer); 211 } 212 }, 213 .strong => { 214 for (doc.extraChildren(data.container.children)) |child| { 215 try renderInlineNodeText(doc, child, writer); 216 } 217 }, 218 .emphasis => { 219 for (doc.extraChildren(data.container.children)) |child| { 220 try renderInlineNodeText(doc, child, writer); 221 } 222 }, 223 .autolink, .code_span, .text => { 224 const content = doc.string(data.text.content); 225 try writer.print("{f}", .{fmtHtml(content)}); 226 }, 227 .line_break => { 228 try writer.writeAll("\n"); 229 }, 230 } 231 } 232 233 pub fn fmtHtml(bytes: []const u8) std.fmt.Formatter([]const u8, formatHtml) { 234 return .{ .data = bytes }; 235 } 236 237 fn formatHtml(bytes: []const u8, writer: *std.io.Writer) std.io.Writer.Error!void { 238 for (bytes) |b| { 239 switch (b) { 240 '<' => try writer.writeAll("<"), 241 '>' => try writer.writeAll(">"), 242 '&' => try writer.writeAll("&"), 243 '"' => try writer.writeAll("""), 244 else => try writer.writeByte(b), 245 } 246 } 247 }