1const std = @import("std");
2const log = std.log;
3const assert = std.debug.assert;
4const Ast = std.zig.Ast;
5const Walk = @import("Walk");
6const markdown = @import("markdown.zig");
7const Decl = Walk.Decl;
8const ArrayList = std.ArrayList;
9const Writer = std.Io.Writer;
10
11const fileSourceHtml = @import("html_render.zig").fileSourceHtml;
12const fileSourceLineNumbersHtml = @import("html_render.zig").fileSourceLineNumbersHtml;
13const appendEscaped = @import("html_render.zig").appendEscaped;
14const resolveDeclLink = @import("html_render.zig").resolveDeclLink;
15const missing_feature_url_escape = @import("html_render.zig").missing_feature_url_escape;
16
17const gpa = std.heap.wasm_allocator;
18
19const js = struct {
20 /// Keep in sync with the `LOG_` constants in `main.js`.
21 const LogLevel = enum(u8) {
22 err,
23 warn,
24 info,
25 debug,
26 };
27
28 extern "js" fn log(level: LogLevel, ptr: [*]const u8, len: usize) void;
29};
30
31pub const std_options: std.Options = .{
32 .logFn = logFn,
33 //.log_level = .debug,
34};
35
36pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
37 _ = st;
38 _ = addr;
39 log.err("panic: {s}", .{msg});
40 @trap();
41}
42
43fn logFn(
44 comptime message_level: log.Level,
45 comptime scope: @EnumLiteral(),
46 comptime format: []const u8,
47 args: anytype,
48) void {
49 const prefix = if (scope == .default) "" else @tagName(scope) ++ ": ";
50 var buf: [500]u8 = undefined;
51 const line = std.mem.print(&buf, prefix ++ format, args) catch l: {
52 buf[buf.len - 3 ..][0..3].* = "...".*;
53 break :l &buf;
54 };
55 js.log(@field(js.LogLevel, @tagName(message_level)), line.ptr, line.len);
56}
57
58export fn alloc(n: usize) [*]u8 {
59 const slice = gpa.alloc(u8, n) catch @panic("OOM");
60 return slice.ptr;
61}
62
63export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
64 const tar_bytes = tar_ptr[0..tar_len];
65 //log.debug("received {d} bytes of tar file", .{tar_bytes.len});
66
67 unpackInner(tar_bytes) catch |err| {
68 std.debug.panic("unable to unpack tar: {s}", .{@errorName(err)});
69 };
70}
71
72var query_string: ArrayList(u8) = .empty;
73var query_results: ArrayList(Decl.Index) = .empty;
74
75/// Resizes the query string to be the correct length; returns the pointer to
76/// the query string.
77export fn query_begin(query_string_len: usize) [*]u8 {
78 query_string.resize(gpa, query_string_len) catch @panic("OOM");
79 return query_string.items.ptr;
80}
81
82/// Executes the query. Returns the pointer to the query results which is an
83/// array of u32.
84/// The first element is the length of the array.
85/// Subsequent elements are Decl.Index values which are all public
86/// declarations.
87export fn query_exec(ignore_case: bool) [*]Decl.Index {
88 const query = query_string.items;
89 log.debug("querying '{s}'", .{query});
90 query_exec_fallible(query, ignore_case) catch |err| switch (err) {
91 error.OutOfMemory => @panic("OOM"),
92 };
93 query_results.items[0] = @fromBackingInt(@intCast(query_results.items.len - 1));
94 return query_results.items.ptr;
95}
96
97const max_matched_items = 1000;
98
99fn query_exec_fallible(query: []const u8, ignore_case: bool) !void {
100 const Score = packed struct(u32) {
101 points: u16,
102 segments: u16,
103 };
104 const g = struct {
105 var full_path_search_text: ArrayList(u8) = .empty;
106 var full_path_search_text_lower: ArrayList(u8) = .empty;
107 var doc_search_text: ArrayList(u8) = .empty;
108 /// Each element matches a corresponding query_results element.
109 var scores: ArrayList(Score) = .empty;
110 };
111
112 // First element stores the size of the list.
113 try query_results.resize(gpa, 1);
114 // Corresponding point value is meaningless and therefore undefined.
115 try g.scores.resize(gpa, 1);
116
117 decl_loop: for (Walk.decls.items, 0..) |*decl, decl_index| {
118 const info = decl.extra_info();
119 if (!info.is_pub) continue;
120
121 try decl.reset_with_path(&g.full_path_search_text);
122 if (decl.parent != .none)
123 try Decl.append_parent_ns(&g.full_path_search_text, decl.parent);
124 try g.full_path_search_text.appendSlice(gpa, info.name);
125
126 try g.full_path_search_text_lower.resize(gpa, g.full_path_search_text.items.len);
127 @memcpy(g.full_path_search_text_lower.items, g.full_path_search_text.items);
128
129 const ast = decl.file.get_ast();
130 if (info.first_doc_comment.unwrap()) |first_doc_comment| {
131 try collect_docs(&g.doc_search_text, ast, first_doc_comment);
132 }
133
134 if (ignore_case) {
135 ascii_lower(g.full_path_search_text_lower.items);
136 ascii_lower(g.doc_search_text.items);
137 }
138
139 var it = std.mem.tokenizeScalar(u8, query, ' ');
140 var points: u16 = 0;
141 var bypass_limit = false;
142 while (it.next()) |term| {
143 // exact, case sensitive match of full decl path
144 if (std.mem.eql(u8, g.full_path_search_text.items, term)) {
145 points += 4;
146 bypass_limit = true;
147 continue;
148 }
149 // exact, case sensitive match of just decl name
150 if (std.mem.eql(u8, info.name, term)) {
151 points += 3;
152 bypass_limit = true;
153 continue;
154 }
155 // substring, case insensitive match of full decl path
156 if (std.mem.find(u8, g.full_path_search_text_lower.items, term) != null) {
157 points += 2;
158 continue;
159 }
160 if (std.mem.find(u8, g.doc_search_text.items, term) != null) {
161 points += 1;
162 continue;
163 }
164 continue :decl_loop;
165 }
166
167 if (query_results.items.len < max_matched_items or bypass_limit) {
168 try query_results.append(gpa, @fromBackingInt(@intCast(decl_index)));
169 try g.scores.append(gpa, .{
170 .points = points,
171 .segments = @intCast(count_scalar(g.full_path_search_text.items, '.')),
172 });
173 }
174 }
175
176 const sort_context: struct {
177 pub fn swap(sc: @This(), a_index: usize, b_index: usize) void {
178 _ = sc;
179 std.mem.swap(Score, &g.scores.items[a_index], &g.scores.items[b_index]);
180 std.mem.swap(Decl.Index, &query_results.items[a_index], &query_results.items[b_index]);
181 }
182
183 pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
184 _ = sc;
185 const a_score = g.scores.items[a_index];
186 const b_score = g.scores.items[b_index];
187 if (b_score.points < a_score.points) {
188 return true;
189 } else if (b_score.points > a_score.points) {
190 return false;
191 } else if (a_score.segments < b_score.segments) {
192 return true;
193 } else if (a_score.segments > b_score.segments) {
194 return false;
195 } else {
196 const a_decl = query_results.items[a_index];
197 const b_decl = query_results.items[b_index];
198 const a_file_path = a_decl.get().file.path();
199 const b_file_path = b_decl.get().file.path();
200 // This neglects to check the local namespace inside the file.
201 return std.mem.lessThan(u8, b_file_path, a_file_path);
202 }
203 }
204 } = .{};
205
206 std.mem.sortUnstableContext(1, query_results.items.len, sort_context);
207
208 if (query_results.items.len > max_matched_items)
209 query_results.shrinkRetainingCapacity(max_matched_items);
210}
211
212const String = Slice(u8);
213
214fn Slice(T: type) type {
215 return packed struct(u64) {
216 ptr: u32,
217 len: u32,
218
219 fn init(s: []const T) @This() {
220 return .{
221 .ptr = @intFromPtr(s.ptr),
222 .len = s.len,
223 };
224 }
225 };
226}
227
228const ErrorIdentifier = packed struct(u64) {
229 token_index: Ast.TokenIndex,
230 decl_index: Decl.Index,
231
232 fn hasDocs(ei: ErrorIdentifier) bool {
233 const decl_index = ei.decl_index;
234 const ast = decl_index.get().file.get_ast();
235 const token_index = ei.token_index;
236 if (token_index == 0) return false;
237 return ast.tokenTag(token_index - 1) == .doc_comment;
238 }
239
240 fn html(ei: ErrorIdentifier, base_decl: Decl.Index, out: *ArrayList(u8)) Oom!void {
241 const decl_index = ei.decl_index;
242 const ast = decl_index.get().file.get_ast();
243 const name = ast.tokenSlice(ei.token_index);
244 const has_link = base_decl != decl_index;
245
246 try out.appendSlice(gpa, "<dt>");
247 try out.appendSlice(gpa, name);
248 if (has_link) {
249 try out.appendSlice(gpa, " <a href=\"#");
250 _ = missing_feature_url_escape;
251 try decl_index.get().fqn(out);
252 try out.appendSlice(gpa, "\">");
253 try out.appendSlice(gpa, decl_index.get().extra_info().name);
254 try out.appendSlice(gpa, "</a>");
255 }
256 try out.appendSlice(gpa, "</dt>");
257
258 if (Decl.findFirstDocComment(ast, ei.token_index).unwrap()) |first_doc_comment| {
259 try out.appendSlice(gpa, "<dd>");
260 try render_docs(out, decl_index, first_doc_comment, false);
261 try out.appendSlice(gpa, "</dd>");
262 }
263 }
264};
265
266var string_result: ArrayList(u8) = .empty;
267var error_set_result: std.array_hash_map.String(ErrorIdentifier) = .empty;
268
269export fn decl_error_set(decl_index: Decl.Index) Slice(ErrorIdentifier) {
270 return Slice(ErrorIdentifier).init(decl_error_set_fallible(decl_index) catch @panic("OOM"));
271}
272
273export fn error_set_node_list(base_decl: Decl.Index, node: Ast.Node.Index) Slice(ErrorIdentifier) {
274 error_set_result.clearRetainingCapacity();
275 addErrorsFromExpr(base_decl, &error_set_result, node) catch @panic("OOM");
276 sort_error_set_result();
277 return Slice(ErrorIdentifier).init(error_set_result.values());
278}
279
280export fn fn_error_set_decl(decl_index: Decl.Index, node: Ast.Node.Index) Decl.Index {
281 return switch (decl_index.get().file.categorize_expr(node)) {
282 .alias => |aliasee| fn_error_set_decl(aliasee, aliasee.get().ast_node),
283 else => decl_index,
284 };
285}
286
287fn decl_error_set_fallible(decl_index: Decl.Index) Oom![]ErrorIdentifier {
288 error_set_result.clearRetainingCapacity();
289 try addErrorsFromDecl(decl_index, &error_set_result);
290 sort_error_set_result();
291 return error_set_result.values();
292}
293
294fn sort_error_set_result() void {
295 const sort_context: struct {
296 pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
297 _ = sc;
298 const a_name = error_set_result.keys()[a_index];
299 const b_name = error_set_result.keys()[b_index];
300 return std.mem.lessThan(u8, a_name, b_name);
301 }
302 } = .{};
303 error_set_result.sortUnstable(sort_context);
304}
305
306fn addErrorsFromDecl(
307 decl_index: Decl.Index,
308 out: *std.array_hash_map.String(ErrorIdentifier),
309) Oom!void {
310 switch (decl_index.get().categorize()) {
311 .error_set => |node| try addErrorsFromExpr(decl_index, out, node),
312 .alias => |aliasee| try addErrorsFromDecl(aliasee, out),
313 else => |cat| log.debug("unable to addErrorsFromDecl: {any}", .{cat}),
314 }
315}
316
317fn addErrorsFromExpr(
318 decl_index: Decl.Index,
319 out: *std.array_hash_map.String(ErrorIdentifier),
320 node: Ast.Node.Index,
321) Oom!void {
322 const decl = decl_index.get();
323 const ast = decl.file.get_ast();
324
325 switch (decl.file.categorize_expr(node)) {
326 .error_set => |n| switch (ast.nodeTag(n)) {
327 .error_set_decl => {
328 try addErrorsFromNode(decl_index, out, node);
329 },
330 .merge_error_sets => {
331 const lhs, const rhs = ast.nodeData(n).node_and_node;
332 try addErrorsFromExpr(decl_index, out, lhs);
333 try addErrorsFromExpr(decl_index, out, rhs);
334 },
335 else => unreachable,
336 },
337 .alias => |aliasee| {
338 try addErrorsFromDecl(aliasee, out);
339 },
340 else => return,
341 }
342}
343
344fn addErrorsFromNode(
345 decl_index: Decl.Index,
346 out: *std.array_hash_map.String(ErrorIdentifier),
347 node: Ast.Node.Index,
348) Oom!void {
349 const decl = decl_index.get();
350 const ast = decl.file.get_ast();
351 const error_token = ast.nodeMainToken(node);
352 var tok_i = error_token + 2;
353 while (true) : (tok_i += 1) switch (ast.tokenTag(tok_i)) {
354 .doc_comment, .comma => {},
355 .identifier => {
356 const name = ast.tokenSlice(tok_i);
357 const gop = try out.getOrPut(gpa, name);
358 // If there are more than one, take the one with doc comments.
359 // If they both have doc comments, prefer the existing one.
360 const new: ErrorIdentifier = .{
361 .token_index = tok_i,
362 .decl_index = decl_index,
363 };
364 if (!gop.found_existing or
365 (!gop.value_ptr.hasDocs() and new.hasDocs()))
366 {
367 gop.value_ptr.* = new;
368 }
369 },
370 .r_brace => break,
371 else => unreachable,
372 };
373}
374
375export fn type_fn_fields(decl_index: Decl.Index) Slice(Ast.Node.Index) {
376 return decl_fields(decl_index);
377}
378
379export fn decl_fields(decl_index: Decl.Index) Slice(Ast.Node.Index) {
380 return Slice(Ast.Node.Index).init(decl_fields_fallible(decl_index) catch @panic("OOM"));
381}
382
383export fn decl_params(decl_index: Decl.Index) Slice(Ast.Node.Index) {
384 return Slice(Ast.Node.Index).init(decl_params_fallible(decl_index) catch @panic("OOM"));
385}
386
387fn decl_fields_fallible(decl_index: Decl.Index) ![]Ast.Node.Index {
388 const decl = decl_index.get();
389 const ast = decl.file.get_ast();
390
391 switch (decl.categorize()) {
392 .type_function => {
393 // If the type function returns a reference to another type function, get the fields from there
394 if (decl.get_type_fn_return_type_fn()) |function_decl| {
395 return decl_fields_fallible(function_decl);
396 }
397 // If the type function returns a container, such as a `struct`, read that container's fields
398 if (decl.get_type_fn_return_expr()) |return_expr| {
399 switch (ast.nodeTag(return_expr)) {
400 .container_decl, .container_decl_trailing, .container_decl_two, .container_decl_two_trailing, .container_decl_arg, .container_decl_arg_trailing => {
401 return ast_decl_fields_fallible(ast, return_expr);
402 },
403 else => {},
404 }
405 }
406 return &.{};
407 },
408 else => {
409 const value_node = decl.value_node() orelse return &.{};
410 return ast_decl_fields_fallible(ast, value_node);
411 },
412 }
413}
414
415fn ast_decl_fields_fallible(ast: *Ast, ast_index: Ast.Node.Index) ![]Ast.Node.Index {
416 const g = struct {
417 var result: ArrayList(Ast.Node.Index) = .empty;
418 };
419 g.result.clearRetainingCapacity();
420 var buf: [2]Ast.Node.Index = undefined;
421 const container_decl = ast.fullContainerDecl(&buf, ast_index) orelse return &.{};
422 for (container_decl.ast.members) |member_node| switch (ast.nodeTag(member_node)) {
423 .container_field_init,
424 .container_field_align,
425 .container_field,
426 => try g.result.append(gpa, member_node),
427
428 else => continue,
429 };
430 return g.result.items;
431}
432
433fn decl_params_fallible(decl_index: Decl.Index) ![]Ast.Node.Index {
434 const g = struct {
435 var result: ArrayList(Ast.Node.Index) = .empty;
436 };
437 g.result.clearRetainingCapacity();
438 const decl = decl_index.get();
439 const ast = decl.file.get_ast();
440 const value_node = decl.value_node() orelse return &.{};
441 var buf: [1]Ast.Node.Index = undefined;
442 const fn_proto = ast.fullFnProto(&buf, value_node) orelse return &.{};
443 try g.result.appendSlice(gpa, fn_proto.ast.params);
444 return g.result.items;
445}
446
447export fn error_html(base_decl: Decl.Index, error_identifier: ErrorIdentifier) String {
448 string_result.clearRetainingCapacity();
449 error_identifier.html(base_decl, &string_result) catch @panic("OOM");
450 return String.init(string_result.items);
451}
452
453export fn decl_field_html(decl_index: Decl.Index, field_node: Ast.Node.Index) String {
454 string_result.clearRetainingCapacity();
455 decl_field_html_fallible(&string_result, decl_index, field_node) catch @panic("OOM");
456 return String.init(string_result.items);
457}
458
459export fn decl_param_html(decl_index: Decl.Index, param_node: Ast.Node.Index) String {
460 string_result.clearRetainingCapacity();
461 decl_param_html_fallible(&string_result, decl_index, param_node) catch @panic("OOM");
462 return String.init(string_result.items);
463}
464
465fn decl_field_html_fallible(
466 out: *ArrayList(u8),
467 decl_index: Decl.Index,
468 field_node: Ast.Node.Index,
469) !void {
470 const decl = decl_index.get();
471 const ast = decl.file.get_ast();
472 try out.appendSlice(gpa, "<pre><code>");
473 try fileSourceHtml(decl.file, out, field_node, .{});
474 try out.appendSlice(gpa, "</code></pre>");
475
476 const field = ast.fullContainerField(field_node).?;
477
478 if (Decl.findFirstDocComment(ast, field.firstToken()).unwrap()) |first_doc_comment| {
479 try out.appendSlice(gpa, "<div class=\"fieldDocs\">");
480 try render_docs(out, decl_index, first_doc_comment, false);
481 try out.appendSlice(gpa, "</div>");
482 }
483}
484
485fn decl_param_html_fallible(
486 out: *ArrayList(u8),
487 decl_index: Decl.Index,
488 param_node: Ast.Node.Index,
489) !void {
490 const decl = decl_index.get();
491 const ast = decl.file.get_ast();
492 const colon = ast.firstToken(param_node) - 1;
493 const name_token = colon - 1;
494 const first_doc_comment = f: {
495 var it = ast.firstToken(param_node);
496 while (it > 0) {
497 it -= 1;
498 switch (ast.tokenTag(it)) {
499 .doc_comment, .colon, .identifier, .keyword_comptime, .keyword_noalias => {},
500 else => break,
501 }
502 }
503 break :f it + 1;
504 };
505 const name = ast.tokenSlice(name_token);
506
507 try out.appendSlice(gpa, "<pre><code>");
508 try appendEscaped(out, name);
509 try out.appendSlice(gpa, ": ");
510 try fileSourceHtml(decl.file, out, param_node, .{});
511 try out.appendSlice(gpa, "</code></pre>");
512
513 if (ast.tokenTag(first_doc_comment) == .doc_comment) {
514 try out.appendSlice(gpa, "<div class=\"fieldDocs\">");
515 try render_docs(out, decl_index, first_doc_comment, false);
516 try out.appendSlice(gpa, "</div>");
517 }
518}
519
520export fn decl_fn_proto_html(decl_index: Decl.Index, linkify_fn_name: bool) String {
521 const decl = decl_index.get();
522 const ast = decl.file.get_ast();
523 const proto_node = switch (ast.nodeTag(decl.ast_node)) {
524 .fn_decl => ast.nodeData(decl.ast_node).node_and_node[0],
525
526 .fn_proto,
527 .fn_proto_one,
528 .fn_proto_simple,
529 .fn_proto_multi,
530 => decl.ast_node,
531
532 else => unreachable,
533 };
534
535 string_result.clearRetainingCapacity();
536 fileSourceHtml(decl.file, &string_result, proto_node, .{
537 .skip_doc_comments = true,
538 .skip_comments = true,
539 .collapse_whitespace = true,
540 .fn_link = if (linkify_fn_name) decl_index else .none,
541 }) catch |err| {
542 std.debug.panic("unable to render source: {s}", .{@errorName(err)});
543 };
544 return String.init(string_result.items);
545}
546
547export fn decl_line_numbers_html(decl_index: Decl.Index) String {
548 const decl = decl_index.get();
549
550 string_result.clearRetainingCapacity();
551 fileSourceLineNumbersHtml(decl.file, &string_result, decl.ast_node) catch |err| {
552 std.debug.panic("unable to render source line numbers: {s}", .{@errorName(err)});
553 };
554 return String.init(string_result.items);
555}
556
557export fn decl_source_html(decl_index: Decl.Index) String {
558 const decl = decl_index.get();
559
560 string_result.clearRetainingCapacity();
561 fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
562 std.debug.panic("unable to render source: {s}", .{@errorName(err)});
563 };
564 return String.init(string_result.items);
565}
566
567export fn decl_doctest_html(decl_index: Decl.Index) String {
568 const decl = decl_index.get();
569 const doctest_ast_node = decl.file.get().doctests.get(decl.ast_node) orelse
570 return String.init("");
571
572 string_result.clearRetainingCapacity();
573 fileSourceHtml(decl.file, &string_result, doctest_ast_node, .{}) catch |err| {
574 std.debug.panic("unable to render source: {s}", .{@errorName(err)});
575 };
576 return String.init(string_result.items);
577}
578
579export fn decl_fqn(decl_index: Decl.Index) String {
580 const decl = decl_index.get();
581 string_result.clearRetainingCapacity();
582 decl.fqn(&string_result) catch @panic("OOM");
583 return String.init(string_result.items);
584}
585
586export fn decl_parent(decl_index: Decl.Index) Decl.Index {
587 const decl = decl_index.get();
588 return decl.parent;
589}
590
591export fn fn_error_set(decl_index: Decl.Index) Ast.Node.OptionalIndex {
592 const decl = decl_index.get();
593 const ast = decl.file.get_ast();
594 var buf: [1]Ast.Node.Index = undefined;
595 const full = ast.fullFnProto(&buf, decl.ast_node).?;
596 const return_type = full.ast.return_type.unwrap().?;
597 return switch (ast.nodeTag(return_type)) {
598 .error_set_decl => return_type.toOptional(),
599 .error_union => ast.nodeData(return_type).node_and_node[0].toOptional(),
600 else => .none,
601 };
602}
603
604export fn decl_file_path(decl_index: Decl.Index) String {
605 string_result.clearRetainingCapacity();
606 string_result.appendSlice(gpa, decl_index.get().file.path()) catch @panic("OOM");
607 return String.init(string_result.items);
608}
609
610export fn decl_category_name(decl_index: Decl.Index) String {
611 const decl = decl_index.get();
612 const ast = decl.file.get_ast();
613 const name = switch (decl.categorize()) {
614 .namespace, .container => |node| {
615 if (ast.nodeTag(decl.ast_node) == .root)
616 return String.init("struct");
617 string_result.clearRetainingCapacity();
618 var buf: [2]Ast.Node.Index = undefined;
619 const container_decl = ast.fullContainerDecl(&buf, node).?;
620 if (container_decl.layout_token) |t| {
621 if (ast.tokenTag(t) == .keyword_extern) {
622 string_result.appendSlice(gpa, "extern ") catch @panic("OOM");
623 }
624 }
625 const main_token_tag = ast.tokenTag(container_decl.ast.main_token);
626 string_result.appendSlice(gpa, main_token_tag.lexeme().?) catch @panic("OOM");
627 return String.init(string_result.items);
628 },
629 .global_variable => "Global Variable",
630 .function => "Function",
631 .type_function => "Type Function",
632 .type, .type_type => "Type",
633 .error_set => "Error Set",
634 .global_const => "Constant",
635 .primitive => "Primitive Value",
636 .alias => "Alias",
637 };
638 return String.init(name);
639}
640
641export fn decl_name(decl_index: Decl.Index) String {
642 const decl = decl_index.get();
643 string_result.clearRetainingCapacity();
644 const name = n: {
645 if (decl.parent == .none) {
646 // Then it is the root struct of a file.
647 break :n std.fs.path.stem(decl.file.path());
648 }
649 break :n decl.extra_info().name;
650 };
651 string_result.appendSlice(gpa, name) catch @panic("OOM");
652 return String.init(string_result.items);
653}
654
655export fn decl_docs_html(decl_index: Decl.Index, short: bool) String {
656 const decl = decl_index.get();
657 string_result.clearRetainingCapacity();
658 if (decl.extra_info().first_doc_comment.unwrap()) |first_doc_comment| {
659 render_docs(&string_result, decl_index, first_doc_comment, short) catch @panic("OOM");
660 }
661 return String.init(string_result.items);
662}
663
664fn collect_docs(
665 list: *ArrayList(u8),
666 ast: *const Ast,
667 first_doc_comment: Ast.TokenIndex,
668) Oom!void {
669 list.clearRetainingCapacity();
670 var it = first_doc_comment;
671 while (true) : (it += 1) switch (ast.tokenTag(it)) {
672 .doc_comment, .container_doc_comment => {
673 // It is tempting to trim this string but think carefully about how
674 // that will affect the markdown parser.
675 const line = ast.tokenSlice(it)[3..];
676 try list.appendSlice(gpa, line);
677 },
678 else => break,
679 };
680}
681
682fn render_docs(
683 out: *ArrayList(u8),
684 decl_index: Decl.Index,
685 first_doc_comment: Ast.TokenIndex,
686 short: bool,
687) Oom!void {
688 const decl = decl_index.get();
689 const ast = decl.file.get_ast();
690
691 var parser = try markdown.Parser.init(gpa);
692 defer parser.deinit();
693 var it = first_doc_comment;
694 while (true) : (it += 1) switch (ast.tokenTag(it)) {
695 .doc_comment, .container_doc_comment => {
696 const line = ast.tokenSlice(it)[3..];
697 if (short and line.len == 0) break;
698 try parser.feedLine(line);
699 },
700 else => break,
701 };
702
703 var parsed_doc = try parser.endInput();
704 defer parsed_doc.deinit(gpa);
705
706 const g = struct {
707 var link_buffer: ArrayList(u8) = .empty;
708 };
709
710 const Renderer = markdown.Renderer(Decl.Index);
711 const renderer: Renderer = .{
712 .context = decl_index,
713 .renderFn = struct {
714 fn render(
715 r: Renderer,
716 doc: markdown.Document,
717 node: markdown.Document.Node.Index,
718 writer: *Writer,
719 ) Writer.Error!void {
720 const data = doc.nodes.items(.data)[@backingInt(node)];
721 switch (doc.nodes.items(.tag)[@backingInt(node)]) {
722 .code_span => {
723 try writer.writeAll("<code>");
724 const content = doc.string(data.text.content);
725 if (resolve_decl_path(r.context, content)) |resolved_decl_index| {
726 g.link_buffer.clearRetainingCapacity();
727 resolveDeclLink(resolved_decl_index, &g.link_buffer) catch return error.WriteFailed;
728
729 try writer.writeAll("<a href=\"#");
730 _ = missing_feature_url_escape;
731 try writer.writeAll(g.link_buffer.items);
732 try writer.print("\">{f}</a>", .{markdown.fmtHtml(content)});
733 } else {
734 try writer.print("{f}", .{markdown.fmtHtml(content)});
735 }
736
737 try writer.writeAll("</code>");
738 },
739
740 else => try Renderer.renderDefault(r, doc, node, writer),
741 }
742 }
743 }.render,
744 };
745
746 var allocating = Writer.Allocating.fromArrayList(gpa, out);
747 defer out.* = allocating.toArrayList();
748 renderer.render(parsed_doc, &allocating.writer) catch |err| switch (err) {
749 error.WriteFailed => return error.OutOfMemory,
750 };
751}
752
753fn resolve_decl_path(decl_index: Decl.Index, path: []const u8) ?Decl.Index {
754 var path_components = std.mem.splitScalar(u8, path, '.');
755 var current_decl_index = decl_index.get().lookup(path_components.first()) orelse return null;
756 while (path_components.next()) |component| {
757 switch (current_decl_index.get().categorize()) {
758 .alias => |aliasee| current_decl_index = aliasee,
759 else => {},
760 }
761 current_decl_index = current_decl_index.get().get_child(component) orelse return null;
762 }
763 return current_decl_index;
764}
765
766export fn decl_type_html(decl_index: Decl.Index) String {
767 const decl = decl_index.get();
768 const ast = decl.file.get_ast();
769 string_result.clearRetainingCapacity();
770 t: {
771 // If there is an explicit type, use it.
772 if (ast.fullVarDecl(decl.ast_node)) |var_decl| {
773 if (var_decl.ast.type_node.unwrap()) |type_node| {
774 string_result.appendSlice(gpa, "<code>") catch @panic("OOM");
775 fileSourceHtml(decl.file, &string_result, type_node, .{
776 .skip_comments = true,
777 .collapse_whitespace = true,
778 }) catch |e| {
779 std.debug.panic("unable to render html: {s}", .{@errorName(e)});
780 };
781 string_result.appendSlice(gpa, "</code>") catch @panic("OOM");
782 break :t;
783 }
784 }
785 }
786 return String.init(string_result.items);
787}
788
789const Oom = error{OutOfMemory};
790
791fn unpackInner(tar_bytes: []u8) !void {
792 var reader: std.Io.Reader = .fixed(tar_bytes);
793 var file_name_buffer: [1024]u8 = undefined;
794 var link_name_buffer: [1024]u8 = undefined;
795 var it: std.tar.Iterator = .init(&reader, .{
796 .file_name_buffer = &file_name_buffer,
797 .link_name_buffer = &link_name_buffer,
798 });
799 while (try it.next()) |tar_file| {
800 switch (tar_file.kind) {
801 .file => {
802 if (tar_file.size == 0 and tar_file.name.len == 0) break;
803 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
804 log.debug("found file: '{s}'", .{tar_file.name});
805 const file_name = try gpa.dupe(u8, tar_file.name);
806 if (std.mem.findScalar(u8, file_name, '/')) |pkg_name_end| {
807 const pkg_name = file_name[0..pkg_name_end];
808 const gop = try Walk.modules.getOrPut(gpa, pkg_name);
809 const file: Walk.File.Index = @fromBackingInt(@intCast(Walk.files.entries.len));
810 if (!gop.found_existing or
811 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
812 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
813 {
814 gop.value_ptr.* = file;
815 }
816 const file_bytes = tar_bytes[reader.seek..][0..@intCast(tar_file.size)];
817 assert(file == try Walk.add_file(file_name, file_bytes));
818 }
819 } else {
820 log.warn("skipping: '{s}' - the tar creation should have done that", .{
821 tar_file.name,
822 });
823 }
824 },
825 else => continue,
826 }
827 }
828}
829
830fn ascii_lower(bytes: []u8) void {
831 for (bytes) |*b| b.* = std.ascii.toLower(b.*);
832}
833
834export fn module_name(index: u32) String {
835 const names = Walk.modules.keys();
836 return String.init(if (index >= names.len) "" else names[index]);
837}
838
839export fn find_module_root(pkg: Walk.ModuleIndex) Decl.Index {
840 const root_file = Walk.modules.values()[@backingInt(pkg)];
841 const result = root_file.findRootDecl();
842 assert(result != .none);
843 return result;
844}
845
846/// Set by `set_input_string`.
847var input_string: ArrayList(u8) = .empty;
848
849export fn set_input_string(len: usize) [*]u8 {
850 input_string.resize(gpa, len) catch @panic("OOM");
851 return input_string.items.ptr;
852}
853
854/// Looks up the root struct decl corresponding to a file by path.
855/// Uses `input_string`.
856export fn find_file_root() Decl.Index {
857 const file: Walk.File.Index = @fromBackingInt(@intCast(Walk.files.getIndex(input_string.items) orelse return .none));
858 return file.findRootDecl();
859}
860
861/// Uses `input_string`.
862/// Tries to look up the Decl component-wise but then falls back to a file path
863/// based scan.
864export fn find_decl() Decl.Index {
865 const result = Decl.find(input_string.items);
866 if (result != .none) return result;
867
868 const g = struct {
869 var match_fqn: ArrayList(u8) = .empty;
870 };
871 for (Walk.decls.items, 0..) |*decl, decl_index| {
872 g.match_fqn.clearRetainingCapacity();
873 decl.fqn(&g.match_fqn) catch @panic("OOM");
874 if (std.mem.eql(u8, g.match_fqn.items, input_string.items)) {
875 //const path = @as(Decl.Index, @enumFromInt(decl_index)).get().file.path();
876 //log.debug("find_decl '{s}' found in {s}", .{ input_string.items, path });
877 return @fromBackingInt(@intCast(decl_index));
878 }
879 }
880 return .none;
881}
882
883/// Set only by `categorize_decl`; read only by `get_aliasee`, valid only
884/// when `categorize_decl` returns `.alias`.
885var global_aliasee: Decl.Index = .none;
886
887export fn get_aliasee() Decl.Index {
888 return global_aliasee;
889}
890export fn categorize_decl(decl_index: Decl.Index, resolve_alias_count: usize) Walk.Category.Tag {
891 global_aliasee = .none;
892 var chase_alias_n = resolve_alias_count;
893 var decl = decl_index.get();
894 while (true) {
895 const result = decl.categorize();
896 switch (result) {
897 .alias => |new_index| {
898 assert(new_index != .none);
899 global_aliasee = new_index;
900 if (chase_alias_n > 0) {
901 chase_alias_n -= 1;
902 decl = new_index.get();
903 continue;
904 }
905 },
906 else => {},
907 }
908 return result;
909 }
910}
911
912export fn type_fn_members(parent: Decl.Index, include_private: bool) Slice(Decl.Index) {
913 const decl = parent.get();
914
915 // If the type function returns another type function, get the members of that function
916 if (decl.get_type_fn_return_type_fn()) |function_decl| {
917 return namespace_members(function_decl, include_private);
918 }
919
920 return namespace_members(parent, include_private);
921}
922
923export fn namespace_members(parent: Decl.Index, include_private: bool) Slice(Decl.Index) {
924 const g = struct {
925 var members: ArrayList(Decl.Index) = .empty;
926 };
927
928 g.members.clearRetainingCapacity();
929
930 for (Walk.decls.items, 0..) |*decl, i| {
931 if (decl.parent == parent) {
932 if (include_private or decl.is_pub()) {
933 g.members.append(gpa, @fromBackingInt(@intCast(i))) catch @panic("OOM");
934 }
935 }
936 }
937
938 return Slice(Decl.Index).init(g.members.items);
939}
940
941fn count_scalar(haystack: []const u8, needle: u8) usize {
942 var total: usize = 0;
943 for (haystack) |elem| {
944 if (elem == needle)
945 total += 1;
946 }
947 return total;
948}