| 1 | const std = @import("std"); |
| 2 | const log = std.log; |
| 3 | const assert = std.debug.assert; |
| 4 | const Ast = std.zig.Ast; |
| 5 | const Walk = @import("Walk"); |
| 6 | const markdown = @import("markdown.zig"); |
| 7 | const Decl = Walk.Decl; |
| 8 | const ArrayList = std.ArrayList; |
| 9 | const Writer = std.Io.Writer; |
| 10 | |
| 11 | const fileSourceHtml = @import("html_render.zig").fileSourceHtml; |
| 12 | const fileSourceLineNumbersHtml = @import("html_render.zig").fileSourceLineNumbersHtml; |
| 13 | const appendEscaped = @import("html_render.zig").appendEscaped; |
| 14 | const resolveDeclLink = @import("html_render.zig").resolveDeclLink; |
| 15 | const missing_feature_url_escape = @import("html_render.zig").missing_feature_url_escape; |
| 16 | |
| 17 | const gpa = std.heap.wasm_allocator; |
| 18 | |
| 19 | const 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 | |
| 31 | pub const std_options: std.Options = .{ |
| 32 | .logFn = logFn, |
| 33 | //.log_level = .debug, |
| 34 | }; |
| 35 | |
| 36 | pub 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 | |
| 43 | fn 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 | |
| 58 | export fn alloc(n: usize) [*]u8 { |
| 59 | const slice = gpa.alloc(u8, n) catch @panic("OOM"); |
| 60 | return slice.ptr; |
| 61 | } |
| 62 | |
| 63 | export 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 | |
| 72 | var query_string: ArrayList(u8) = .empty; |
| 73 | var 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. |
| 77 | export 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. |
| 87 | export 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 | |
| 97 | const max_matched_items = 1000; |
| 98 | |
| 99 | fn 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 | |
| 212 | const String = Slice(u8); |
| 213 | |
| 214 | fn 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 | |
| 228 | const 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 | |
| 266 | var string_result: ArrayList(u8) = .empty; |
| 267 | var error_set_result: std.array_hash_map.String(ErrorIdentifier) = .empty; |
| 268 | |
| 269 | export 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 | |
| 273 | export 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 | |
| 280 | export 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 | |
| 287 | fn 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 | |
| 294 | fn 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 | |
| 306 | fn 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 | |
| 317 | fn 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 | |
| 344 | fn 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 | |
| 375 | export fn type_fn_fields(decl_index: Decl.Index) Slice(Ast.Node.Index) { |
| 376 | return decl_fields(decl_index); |
| 377 | } |
| 378 | |
| 379 | export 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 | |
| 383 | export 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 | |
| 387 | fn 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 | |
| 415 | fn 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 | |
| 433 | fn 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 | |
| 447 | export 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 | |
| 453 | export 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 | |
| 459 | export 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 | |
| 465 | fn 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 | |
| 485 | fn 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 | |
| 520 | export 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 | |
| 547 | export 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 | |
| 557 | export 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 | |
| 567 | export 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 | |
| 579 | export 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 | |
| 586 | export fn decl_parent(decl_index: Decl.Index) Decl.Index { |
| 587 | const decl = decl_index.get(); |
| 588 | return decl.parent; |
| 589 | } |
| 590 | |
| 591 | export 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 | |
| 604 | export 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 | |
| 610 | export 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 | |
| 641 | export 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 | |
| 655 | export 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 | |
| 664 | fn 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 | |
| 682 | fn 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 | |
| 753 | fn 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 | |
| 766 | export 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 | |
| 789 | const Oom = error{OutOfMemory}; |
| 790 | |
| 791 | fn 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 | |
| 830 | fn ascii_lower(bytes: []u8) void { |
| 831 | for (bytes) |*b| b.* = std.ascii.toLower(b.*); |
| 832 | } |
| 833 | |
| 834 | export 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 | |
| 839 | export 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`. |
| 847 | var input_string: ArrayList(u8) = .empty; |
| 848 | |
| 849 | export 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`. |
| 856 | export 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. |
| 864 | export 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`. |
| 885 | var global_aliasee: Decl.Index = .none; |
| 886 | |
| 887 | export fn get_aliasee() Decl.Index { |
| 888 | return global_aliasee; |
| 889 | } |
| 890 | export 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 | |
| 912 | export 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 | |
| 923 | export 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 | |
| 941 | fn 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 | } |