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