master
  1const std = @import("std");
  2const Ast = std.zig.Ast;
  3const assert = std.debug.assert;
  4const ArrayList = std.ArrayList;
  5const Writer = std.Io.Writer;
  6
  7const Walk = @import("Walk");
  8const Decl = Walk.Decl;
  9
 10const gpa = std.heap.wasm_allocator;
 11const Oom = error{OutOfMemory};
 12
 13/// Delete this to find out where URL escaping needs to be added.
 14pub const missing_feature_url_escape = true;
 15
 16pub const RenderSourceOptions = struct {
 17    skip_doc_comments: bool = false,
 18    skip_comments: bool = false,
 19    collapse_whitespace: bool = false,
 20    fn_link: Decl.Index = .none,
 21    /// Assumed to be sorted ascending.
 22    source_location_annotations: []const Annotation = &.{},
 23    /// Concatenated with dom_id.
 24    annotation_prefix: []const u8 = "l",
 25};
 26
 27pub const Annotation = struct {
 28    file_byte_offset: u32,
 29    /// Concatenated with annotation_prefix.
 30    dom_id: u32,
 31};
 32
 33pub fn fileSourceHtml(
 34    file_index: Walk.File.Index,
 35    out: *ArrayList(u8),
 36    root_node: Ast.Node.Index,
 37    options: RenderSourceOptions,
 38) !void {
 39    const ast = file_index.get_ast();
 40    const file = file_index.get();
 41
 42    const g = struct {
 43        var field_access_buffer: ArrayList(u8) = .empty;
 44    };
 45
 46    const start_token = ast.firstToken(root_node);
 47    const end_token = ast.lastToken(root_node) + 1;
 48
 49    var cursor: usize = ast.tokenStart(start_token);
 50
 51    var indent: usize = 0;
 52    if (std.mem.lastIndexOf(u8, ast.source[0..cursor], "\n")) |newline_index| {
 53        for (ast.source[newline_index + 1 .. cursor]) |c| {
 54            if (c == ' ') {
 55                indent += 1;
 56            } else {
 57                break;
 58            }
 59        }
 60    }
 61
 62    var next_annotate_index: usize = 0;
 63
 64    for (
 65        ast.tokens.items(.tag)[start_token..end_token],
 66        ast.tokens.items(.start)[start_token..end_token],
 67        start_token..,
 68    ) |tag, start, token_index| {
 69        const between = ast.source[cursor..start];
 70        if (std.mem.trim(u8, between, " \t\r\n").len > 0) {
 71            if (!options.skip_comments) {
 72                try out.appendSlice(gpa, "<span class=\"tok-comment\">");
 73                try appendUnindented(out, between, indent);
 74                try out.appendSlice(gpa, "</span>");
 75            }
 76        } else if (between.len > 0) {
 77            if (options.collapse_whitespace) {
 78                if (out.items.len > 0 and out.items[out.items.len - 1] != ' ')
 79                    try out.append(gpa, ' ');
 80            } else {
 81                try appendUnindented(out, between, indent);
 82            }
 83        }
 84        if (tag == .eof) break;
 85        const slice = ast.tokenSlice(token_index);
 86        cursor = start + slice.len;
 87
 88        // Insert annotations.
 89        while (true) {
 90            if (next_annotate_index >= options.source_location_annotations.len) break;
 91            const next_annotation = options.source_location_annotations[next_annotate_index];
 92            if (cursor <= next_annotation.file_byte_offset) break;
 93            try out.print(gpa, "<span id=\"{s}{d}\"></span>", .{
 94                options.annotation_prefix, next_annotation.dom_id,
 95            });
 96            next_annotate_index += 1;
 97        }
 98
 99        switch (tag) {
100            .eof => unreachable,
101
102            .keyword_addrspace,
103            .keyword_align,
104            .keyword_and,
105            .keyword_asm,
106            .keyword_break,
107            .keyword_catch,
108            .keyword_comptime,
109            .keyword_const,
110            .keyword_continue,
111            .keyword_defer,
112            .keyword_else,
113            .keyword_enum,
114            .keyword_errdefer,
115            .keyword_error,
116            .keyword_export,
117            .keyword_extern,
118            .keyword_for,
119            .keyword_if,
120            .keyword_inline,
121            .keyword_noalias,
122            .keyword_noinline,
123            .keyword_nosuspend,
124            .keyword_opaque,
125            .keyword_or,
126            .keyword_orelse,
127            .keyword_packed,
128            .keyword_anyframe,
129            .keyword_pub,
130            .keyword_resume,
131            .keyword_return,
132            .keyword_linksection,
133            .keyword_callconv,
134            .keyword_struct,
135            .keyword_suspend,
136            .keyword_switch,
137            .keyword_test,
138            .keyword_threadlocal,
139            .keyword_try,
140            .keyword_union,
141            .keyword_unreachable,
142            .keyword_var,
143            .keyword_volatile,
144            .keyword_allowzero,
145            .keyword_while,
146            .keyword_anytype,
147            .keyword_fn,
148            => {
149                try out.appendSlice(gpa, "<span class=\"tok-kw\">");
150                try appendEscaped(out, slice);
151                try out.appendSlice(gpa, "</span>");
152            },
153
154            .string_literal,
155            .char_literal,
156            .multiline_string_literal_line,
157            => {
158                try out.appendSlice(gpa, "<span class=\"tok-str\">");
159                try appendEscaped(out, slice);
160                try out.appendSlice(gpa, "</span>");
161            },
162
163            .builtin => {
164                try out.appendSlice(gpa, "<span class=\"tok-builtin\">");
165                try appendEscaped(out, slice);
166                try out.appendSlice(gpa, "</span>");
167            },
168
169            .doc_comment,
170            .container_doc_comment,
171            => {
172                if (!options.skip_doc_comments) {
173                    try out.appendSlice(gpa, "<span class=\"tok-comment\">");
174                    try appendEscaped(out, slice);
175                    try out.appendSlice(gpa, "</span>");
176                }
177            },
178
179            .identifier => i: {
180                if (options.fn_link != .none) {
181                    const fn_link = options.fn_link.get();
182                    const fn_token = ast.nodeMainToken(fn_link.ast_node);
183                    if (token_index == fn_token + 1) {
184                        try out.appendSlice(gpa, "<a class=\"tok-fn\" href=\"#");
185                        _ = missing_feature_url_escape;
186                        try fn_link.fqn(out);
187                        try out.appendSlice(gpa, "\">");
188                        try appendEscaped(out, slice);
189                        try out.appendSlice(gpa, "</a>");
190                        break :i;
191                    }
192                }
193
194                if (token_index > 0 and ast.tokenTag(token_index - 1) == .keyword_fn) {
195                    try out.appendSlice(gpa, "<span class=\"tok-fn\">");
196                    try appendEscaped(out, slice);
197                    try out.appendSlice(gpa, "</span>");
198                    break :i;
199                }
200
201                if (Walk.isPrimitiveNonType(slice)) {
202                    try out.appendSlice(gpa, "<span class=\"tok-null\">");
203                    try appendEscaped(out, slice);
204                    try out.appendSlice(gpa, "</span>");
205                    break :i;
206                }
207
208                if (std.zig.primitives.isPrimitive(slice)) {
209                    try out.appendSlice(gpa, "<span class=\"tok-type\">");
210                    try appendEscaped(out, slice);
211                    try out.appendSlice(gpa, "</span>");
212                    break :i;
213                }
214
215                if (file.token_parents.get(token_index)) |field_access_node| {
216                    g.field_access_buffer.clearRetainingCapacity();
217                    try walkFieldAccesses(file_index, &g.field_access_buffer, field_access_node);
218                    if (g.field_access_buffer.items.len > 0) {
219                        try out.appendSlice(gpa, "<a href=\"#");
220                        _ = missing_feature_url_escape;
221                        try out.appendSlice(gpa, g.field_access_buffer.items);
222                        try out.appendSlice(gpa, "\">");
223                        try appendEscaped(out, slice);
224                        try out.appendSlice(gpa, "</a>");
225                    } else {
226                        try appendEscaped(out, slice);
227                    }
228                    break :i;
229                }
230
231                {
232                    g.field_access_buffer.clearRetainingCapacity();
233                    try resolveIdentLink(file_index, &g.field_access_buffer, token_index);
234                    if (g.field_access_buffer.items.len > 0) {
235                        try out.appendSlice(gpa, "<a href=\"#");
236                        _ = missing_feature_url_escape;
237                        try out.appendSlice(gpa, g.field_access_buffer.items);
238                        try out.appendSlice(gpa, "\">");
239                        try appendEscaped(out, slice);
240                        try out.appendSlice(gpa, "</a>");
241                        break :i;
242                    }
243                }
244
245                try appendEscaped(out, slice);
246            },
247
248            .number_literal => {
249                try out.appendSlice(gpa, "<span class=\"tok-number\">");
250                try appendEscaped(out, slice);
251                try out.appendSlice(gpa, "</span>");
252            },
253
254            .bang,
255            .pipe,
256            .pipe_pipe,
257            .pipe_equal,
258            .equal,
259            .equal_equal,
260            .equal_angle_bracket_right,
261            .bang_equal,
262            .l_paren,
263            .r_paren,
264            .semicolon,
265            .percent,
266            .percent_equal,
267            .l_brace,
268            .r_brace,
269            .l_bracket,
270            .r_bracket,
271            .period,
272            .period_asterisk,
273            .ellipsis2,
274            .ellipsis3,
275            .caret,
276            .caret_equal,
277            .plus,
278            .plus_plus,
279            .plus_equal,
280            .plus_percent,
281            .plus_percent_equal,
282            .plus_pipe,
283            .plus_pipe_equal,
284            .minus,
285            .minus_equal,
286            .minus_percent,
287            .minus_percent_equal,
288            .minus_pipe,
289            .minus_pipe_equal,
290            .asterisk,
291            .asterisk_equal,
292            .asterisk_asterisk,
293            .asterisk_percent,
294            .asterisk_percent_equal,
295            .asterisk_pipe,
296            .asterisk_pipe_equal,
297            .arrow,
298            .colon,
299            .slash,
300            .slash_equal,
301            .comma,
302            .ampersand,
303            .ampersand_equal,
304            .question_mark,
305            .angle_bracket_left,
306            .angle_bracket_left_equal,
307            .angle_bracket_angle_bracket_left,
308            .angle_bracket_angle_bracket_left_equal,
309            .angle_bracket_angle_bracket_left_pipe,
310            .angle_bracket_angle_bracket_left_pipe_equal,
311            .angle_bracket_right,
312            .angle_bracket_right_equal,
313            .angle_bracket_angle_bracket_right,
314            .angle_bracket_angle_bracket_right_equal,
315            .tilde,
316            => try appendEscaped(out, slice),
317
318            .invalid, .invalid_periodasterisks => return error.InvalidToken,
319        }
320    }
321}
322
323fn appendUnindented(out: *ArrayList(u8), s: []const u8, indent: usize) !void {
324    var it = std.mem.splitScalar(u8, s, '\n');
325    var is_first_line = true;
326    while (it.next()) |line| {
327        if (is_first_line) {
328            try appendEscaped(out, line);
329            is_first_line = false;
330        } else {
331            try out.appendSlice(gpa, "\n");
332            try appendEscaped(out, unindent(line, indent));
333        }
334    }
335}
336
337pub fn appendEscaped(out: *ArrayList(u8), s: []const u8) !void {
338    for (s) |c| {
339        try out.ensureUnusedCapacity(gpa, 6);
340        switch (c) {
341            '&' => out.appendSliceAssumeCapacity("&amp;"),
342            '<' => out.appendSliceAssumeCapacity("&lt;"),
343            '>' => out.appendSliceAssumeCapacity("&gt;"),
344            '"' => out.appendSliceAssumeCapacity("&quot;"),
345            else => out.appendAssumeCapacity(c),
346        }
347    }
348}
349
350fn walkFieldAccesses(
351    file_index: Walk.File.Index,
352    out: *ArrayList(u8),
353    node: Ast.Node.Index,
354) Oom!void {
355    const ast = file_index.get_ast();
356    assert(ast.nodeTag(node) == .field_access);
357    const object_node, const field_ident = ast.nodeData(node).node_and_token;
358    switch (ast.nodeTag(object_node)) {
359        .identifier => {
360            const lhs_ident = ast.nodeMainToken(object_node);
361            try resolveIdentLink(file_index, out, lhs_ident);
362        },
363        .field_access => {
364            try walkFieldAccesses(file_index, out, object_node);
365        },
366        else => {},
367    }
368    if (out.items.len > 0) {
369        try out.append(gpa, '.');
370        try out.appendSlice(gpa, ast.tokenSlice(field_ident));
371    }
372}
373
374fn resolveIdentLink(
375    file_index: Walk.File.Index,
376    out: *ArrayList(u8),
377    ident_token: Ast.TokenIndex,
378) Oom!void {
379    const decl_index = file_index.get().lookup_token(ident_token);
380    if (decl_index == .none) return;
381    try resolveDeclLink(decl_index, out);
382}
383
384fn unindent(s: []const u8, indent: usize) []const u8 {
385    var indent_idx: usize = 0;
386    for (s) |c| {
387        if (c == ' ' and indent_idx < indent) {
388            indent_idx += 1;
389        } else {
390            break;
391        }
392    }
393    return s[indent_idx..];
394}
395
396pub fn resolveDeclLink(decl_index: Decl.Index, out: *ArrayList(u8)) Oom!void {
397    const decl = decl_index.get();
398    switch (decl.categorize()) {
399        .alias => |alias_decl| try alias_decl.get().fqn(out),
400        else => try decl.fqn(out),
401    }
402}