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