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