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("&lt;"),
241        '>' => try w.writeAll("&gt;"),
242        '&' => try w.writeAll("&amp;"),
243        '"' => try w.writeAll("&quot;"),
244        else => try w.writeByte(b),
245    };
246}