| 1 | const std = @import("std"); |
| 2 | const Ast = std.zig.Ast; |
| 3 | const assert = std.debug.assert; |
| 4 | const ArrayList = std.ArrayList; |
| 5 | const Writer = std.Io.Writer; |
| 6 | |
| 7 | const Walk = @import("Walk"); |
| 8 | const Decl = Walk.Decl; |
| 9 | |
| 10 | const gpa = std.heap.wasm_allocator; |
| 11 | const Oom = error{OutOfMemory}; |
| 12 | |
| 13 | /// Delete this to find out where URL escaping needs to be added. |
| 14 | pub const missing_feature_url_escape = true; |
| 15 | |
| 16 | pub 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 | |
| 27 | pub const Annotation = struct { |
| 28 | file_byte_offset: u32, |
| 29 | /// Concatenated with annotation_prefix. |
| 30 | dom_id: u32, |
| 31 | }; |
| 32 | |
| 33 | pub 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 | |
| 46 | pub 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 | |
| 335 | fn 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 | |
| 349 | pub 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 | |
| 362 | fn 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 | |
| 386 | fn 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 | |
| 396 | fn 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 | |
| 408 | pub 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 | } |