master
1//! An abstract tree representation of a Markdown document.
2
3const std = @import("std");
4const builtin = @import("builtin");
5const assert = std.debug.assert;
6const Allocator = std.mem.Allocator;
7const Renderer = @import("renderer.zig").Renderer;
8
9nodes: Node.List.Slice,
10extra: []u32,
11string_bytes: []u8,
12
13const Document = @This();
14
15pub const Node = struct {
16 tag: Tag,
17 data: Data,
18
19 pub const Index = enum(u32) {
20 root = 0,
21 _,
22 };
23 pub const List = std.MultiArrayList(Node);
24
25 pub const Tag = enum {
26 /// Data is `container`.
27 root,
28
29 // Blocks
30 /// Data is `list`.
31 list,
32 /// Data is `list_item`.
33 list_item,
34 /// Data is `container`.
35 table,
36 /// Data is `container`.
37 table_row,
38 /// Data is `table_cell`.
39 table_cell,
40 /// Data is `heading`.
41 heading,
42 /// Data is `code_block`.
43 code_block,
44 /// Data is `container`.
45 blockquote,
46 /// Data is `container`.
47 paragraph,
48 /// Data is `none`.
49 thematic_break,
50
51 // Inlines
52 /// Data is `link`.
53 link,
54 /// Data is `text`.
55 autolink,
56 /// Data is `link`.
57 image,
58 /// Data is `container`.
59 strong,
60 /// Data is `container`.
61 emphasis,
62 /// Data is `text`.
63 code_span,
64 /// Data is `text`.
65 text,
66 /// Data is `none`.
67 line_break,
68 };
69
70 pub const Data = union {
71 none: void,
72 container: struct {
73 children: ExtraIndex,
74 },
75 text: struct {
76 content: StringIndex,
77 },
78 list: struct {
79 start: ListStart,
80 children: ExtraIndex,
81 },
82 list_item: struct {
83 tight: bool,
84 children: ExtraIndex,
85 },
86 table_cell: struct {
87 info: packed struct {
88 alignment: TableCellAlignment,
89 header: bool,
90 },
91 children: ExtraIndex,
92 },
93 heading: struct {
94 /// Between 1 and 6, inclusive.
95 level: u3,
96 children: ExtraIndex,
97 },
98 code_block: struct {
99 tag: StringIndex,
100 content: StringIndex,
101 },
102 link: struct {
103 target: StringIndex,
104 children: ExtraIndex,
105 },
106
107 comptime {
108 // In Debug and ReleaseSafe builds, there may be hidden extra fields
109 // included for safety checks. Without such safety checks enabled,
110 // we always want this union to be 8 bytes.
111 if (builtin.mode != .Debug and builtin.mode != .ReleaseSafe) {
112 assert(@sizeOf(Data) == 8);
113 }
114 }
115 };
116
117 /// The starting number of a list. This is either a number between 0 and
118 /// 999,999,999, inclusive, or `unordered` to indicate an unordered list.
119 pub const ListStart = enum(u30) {
120 // When https://github.com/ziglang/zig/issues/104 is implemented, this
121 // type can be more naturally expressed as ?u30. As it is, we want
122 // values to fit within 4 bytes, so ?u30 does not yet suffice for
123 // storage.
124 unordered = std.math.maxInt(u30),
125 _,
126
127 pub fn asNumber(start: ListStart) ?u30 {
128 if (start == .unordered) return null;
129 assert(@intFromEnum(start) <= 999_999_999);
130 return @intFromEnum(start);
131 }
132 };
133
134 pub const TableCellAlignment = enum(u2) {
135 unset,
136 left,
137 center,
138 right,
139 };
140
141 /// Trailing: `len` times `Node.Index`
142 pub const Children = struct {
143 len: u32,
144 };
145};
146
147pub const ExtraIndex = enum(u32) { _ };
148
149/// The index of a null-terminated string in `string_bytes`.
150pub const StringIndex = enum(u32) {
151 empty = 0,
152 _,
153};
154
155pub fn deinit(doc: *Document, allocator: Allocator) void {
156 doc.nodes.deinit(allocator);
157 allocator.free(doc.extra);
158 allocator.free(doc.string_bytes);
159 doc.* = undefined;
160}
161
162/// Renders a document directly to a writer using the default renderer.
163pub fn render(doc: Document, writer: anytype) @TypeOf(writer).Error!void {
164 const renderer: Renderer(@TypeOf(writer), void) = .{ .context = {} };
165 try renderer.render(doc, writer);
166}
167
168pub fn ExtraData(comptime T: type) type {
169 return struct { data: T, end: usize };
170}
171
172pub fn extraData(doc: Document, comptime T: type, index: ExtraIndex) ExtraData(T) {
173 const fields = @typeInfo(T).@"struct".fields;
174 var i: usize = @intFromEnum(index);
175 var result: T = undefined;
176 inline for (fields) |field| {
177 @field(result, field.name) = switch (field.type) {
178 u32 => doc.extra[i],
179 else => @compileError("bad field type"),
180 };
181 i += 1;
182 }
183 return .{ .data = result, .end = i };
184}
185
186pub fn extraChildren(doc: Document, index: ExtraIndex) []const Node.Index {
187 const children = doc.extraData(Node.Children, index);
188 return @ptrCast(doc.extra[children.end..][0..children.data.len]);
189}
190
191pub fn string(doc: Document, index: StringIndex) [:0]const u8 {
192 const start = @intFromEnum(index);
193 return std.mem.span(@as([*:0]u8, @ptrCast(doc.string_bytes[start..].ptr)));
194}