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("&"),
342 '<' => out.appendSliceAssumeCapacity("<"),
343 '>' => out.appendSliceAssumeCapacity(">"),
344 '"' => out.appendSliceAssumeCapacity("""),
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}