zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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