╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ docgen.zig ───────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
424 else => return parseError(tokenizer, bracket_tok, "invalid header_open token", .{}), 425 } 426 } 427 428 header_stack_size += 1; 429 430 const urlized = try urlize(gpa, content); 431 try nodes.append(Node{ 432 .HeaderOpen = HeaderOpen{ 433 .name = content, 434 .url = urlized, 435 .n = header_stack_size + 1, // highest-level section headers start at h2 436 }, 437 }); 438 if (try urls.fetchPut(urlized, tag_token)) |kv| { 439 parseError(tokenizer, tag_token, "duplicate header url: #{s}", .{urlized}) catch {}; 440 parseError(tokenizer, kv.value, "other tag here", .{}) catch {}; 441 return error.ParseError; 442 } 443 if (last_action == .open) { 444 try toc.writeByte('\n'); 445 try toc.splatByteAll(' ', header_stack_size * 4); 446 if (last_columns) |n| { 447 try toc.print("<ul style=\"columns: {d}\">\n", .{n}); 448 } else { 449 try toc.writeAll("<ul>\n"); 450 } 451 } else { 452 last_action = .open; 453 } 454 last_columns = columns; 455 try toc.splatByteAll(' ', 4 + header_stack_size * 4); 456 try toc.print("<li><a id=\"toc-{s}\" href=\"#{s}\">{s}</a>", .{ urlized, urlized, content }); 457 } else if (mem.eql(u8, tag_name, "header_close")) { 458 if (header_stack_size == 0) { 459 return parseError(tokenizer, tag_token, "unbalanced close header", .{}); 460 } 461 header_stack_size -= 1; 462 _ = try eatToken(tokenizer, .bracket_close); 463 464 if (last_action == .close) { 465 try toc.splatByteAll(' ', 8 + header_stack_size * 4); 466 try toc.writeAll("</ul></li>\n"); 467 } else { 468 try toc.writeAll("</li>\n"); 469 last_action = .close; 470 } 471 } else if (mem.eql(u8, tag_name, "see_also")) { 472 var list = std.array_list.Managed(SeeAlsoItem).init(gpa); 473 errdefer list.deinit(); 474 475 while (true) { 476 const see_also_tok = tokenizer.next(); 477 switch (see_also_tok.id) { 478 .tag_content => { 479 const content = tokenizer.buffer[see_also_tok.start..see_also_tok.end]; 480 try list.append(SeeAlsoItem{ 481 .name = content, 482 .token = see_also_tok, 483 }); 484 }, 485 .separator => {}, 486 .bracket_close => { 487 try nodes.ensureUnusedCapacity(1); 488 nodes.appendAssumeCapacity(.{ .SeeAlso = try list.toOwnedSlice() }); 489 break; 490 }, 491 else => return parseError(tokenizer, see_also_tok, "invalid see_also token", .{}), 492 } 493 } 494 } else if (mem.eql(u8, tag_name, "link")) { 495 _ = try eatToken(tokenizer, .separator); 496 const name_tok = try eatToken(tokenizer, .tag_content); 497 const name = tokenizer.buffer[name_tok.start..name_tok.end]; 498 499 const url_name = blk: { 500 const tok = tokenizer.next(); 501 switch (tok.id) { 502 .bracket_close => break :blk name, 503 .separator => { 504 const explicit_text = try eatToken(tokenizer, .tag_content); 505 _ = try eatToken(tokenizer, .bracket_close); 506 break :blk tokenizer.buffer[explicit_text.start..explicit_text.end]; 507 }, 508 else => return parseError(tokenizer, tok, "invalid link token", .{}), 509 } 510 }; 511 512 try nodes.append(Node{ 513 .Link = Link{ 514 .url = try urlize(gpa, url_name), 515 .name = name, 516 .token = name_tok, 517 }, 518 }); 519 } else if (mem.eql(u8, tag_name, "code")) { 520 _ = try eatToken(tokenizer, .separator); 521 const name_tok = try eatToken(tokenizer, .tag_content); 522 _ = try eatToken(tokenizer, .bracket_close); 523 try nodes.append(.{ 524 .Code = .{ 525 .name = tokenizer.buffer[name_tok.start..name_tok.end], 526 .token = name_tok, 527 }, 528 }); 529 } else if (mem.eql(u8, tag_name, "grammar")) { 530 _ = try eatToken(tokenizer, .bracket_close); 531 try nodes.append(.Grammar); 532 } else if (mem.eql(u8, tag_name, "syntax")) { 533 _ = try eatToken(tokenizer, .bracket_close); 534 const content_tok = try eatToken(tokenizer, .content); 535 _ = try eatToken(tokenizer, .bracket_open); 536 const end_syntax_tag = try eatToken(tokenizer, .tag_content); 537 const end_tag_name = tokenizer.buffer[end_syntax_tag.start..end_syntax_tag.end]; 538 if (!mem.eql(u8, end_tag_name, "endsyntax")) { 539 return parseError( 540 tokenizer, 541 end_syntax_tag, 542 "invalid token inside syntax: {s}", 543 .{end_tag_name}, 544 ); 545 } 546 _ = try eatToken(tokenizer, .bracket_close); 547 try nodes.append(Node{ .InlineSyntax = content_tok }); 548 } else if (mem.eql(u8, tag_name, "shell_samp")) { 549 _ = try eatToken(tokenizer, .bracket_close); 550 const content_tok = try eatToken(tokenizer, .content); 551 _ = try eatToken(tokenizer, .bracket_open); 552 const end_syntax_tag = try eatToken(tokenizer, .tag_content); 553 const end_tag_name = tokenizer.buffer[end_syntax_tag.start..end_syntax_tag.end]; 554 if (!mem.eql(u8, end_tag_name, "end_shell_samp")) { 555 return parseError( 556 tokenizer, 557 end_syntax_tag, 558 "invalid token inside syntax: {s}", 559 .{end_tag_name}, 560 ); 561 } 562 _ = try eatToken(tokenizer, .bracket_close); 563 try nodes.append(Node{ .Shell = content_tok }); 564 } else if (mem.eql(u8, tag_name, "syntax_block")) { 565 _ = try eatToken(tokenizer, .separator); 566 const source_type_tok = try eatToken(tokenizer, .tag_content); 567 var name: []const u8 = "sample_code"; 568 const maybe_sep = tokenizer.next(); 569 switch (maybe_sep.id) { 570 .separator => { 571 const name_tok = try eatToken(tokenizer, .tag_content); 572 name = tokenizer.buffer[name_tok.start..name_tok.end]; 573 _ = try eatToken(tokenizer, .bracket_close); 574 }, 575 .bracket_close => {}, 576 else => return parseError(tokenizer, token, "invalid token", .{}), 577 } 578 const source_type_str = tokenizer.buffer[source_type_tok.start..source_type_tok.end]; 579 var source_type: SyntaxBlock.SourceType = undefined; 580 if (mem.eql(u8, source_type_str, "zig")) { 581 source_type = SyntaxBlock.SourceType.zig; 582 } else if (mem.eql(u8, source_type_str, "c")) { 583 source_type = SyntaxBlock.SourceType.c; 584 } else if (mem.eql(u8, source_type_str, "peg")) { 585 source_type = SyntaxBlock.SourceType.peg; 586 } else if (mem.eql(u8, source_type_str, "javascript")) { 587 source_type = SyntaxBlock.SourceType.javascript; 588 } else { 589 return parseError(tokenizer, source_type_tok, "unrecognized code kind: {s}", .{source_type_str}); 590 } 591 const source_token = while (true) { 592 const content_tok = try eatToken(tokenizer, .content); 593 _ = try eatToken(tokenizer, .bracket_open); 594 const end_code_tag = try eatToken(tokenizer, .tag_content); 595 const end_tag_name = tokenizer.buffer[end_code_tag.start..end_code_tag.end]; 596 if (mem.eql(u8, end_tag_name, "end_syntax_block")) { 597 _ = try eatToken(tokenizer, .bracket_close); 598 break content_tok; 599 } else { 600 return parseError( 601 tokenizer, 602 end_code_tag, 603 "invalid token inside code_begin: {s}", 604 .{end_tag_name}, 605 ); 606 } 607 _ = try eatToken(tokenizer, .bracket_close); 608 }; 609 try nodes.append(Node{ .SyntaxBlock = SyntaxBlock{ .source_type = source_type, .name = name, .source_token = source_token } }); 610 } else { 611 return parseError(tokenizer, tag_token, "unrecognized tag name: {s}", .{tag_name}); 612 } 613 }, 614 else => return parseError(tokenizer, token, "invalid token", .{}), 615 } 616 } 617 618 const nodes_slice = try nodes.toOwnedSlice(); 619 errdefer gpa.free(nodes_slice); 620 const toc_slice = try toc_buf.toOwnedSlice(); 621 errdefer gpa.free(toc_slice); 622 623 return .{ 624 .nodes = nodes_slice, 625 .toc = toc_slice, 626 .urls = urls, 627 }; 628 } 629 630 fn urlize(gpa: Allocator, input: []const u8) ![]u8 { 631 var buf: ArrayList(u8) = .empty; 632 defer buf.deinit(gpa); 633 634 for (input) |c| { 635 switch (c) { 636 'a'...'z', 'A'...'Z', '_', '-', '0'...'9' => { 637 try buf.append(gpa, c); 638 }, 639 ' ' => { 640 try buf.append(gpa, '-'); 641 }, 642 else => {}, 643 } 644 } 645 return try buf.toOwnedSlice(gpa); 646 } 647 648 fn escapeHtml(gpa: Allocator, input: []const u8) ![]u8 { 649 var buf: std.Io.Writer.Allocating = .init(gpa); 650 defer buf.deinit(gpa); 651 652 try writeEscaped(&buf.writer, input); 653 return try buf.toOwnedSlice(); 654 } 655 656 fn writeEscaped(out: *Writer, input: []const u8) !void { 657 for (input) |c| { 658 try switch (c) { 659 '&' => out.writeAll("&amp;"), 660 '<' => out.writeAll("&lt;"), 661 '>' => out.writeAll("&gt;"), 662 '"' => out.writeAll("&quot;"), 663 else => out.writeByte(c), 664 }; 665 } 666 } 667 668 // Returns true if number is in slice. 669 fn in(slice: []const u8, number: u8) bool { 670 for (slice) |n| { 671 if (number == n) return true; 672 } 673 return false; 674 } 675 676 const builtin_types = [_][]const u8{ 677 "f16", "f32", "f64", "f80", "f128", 678 "c_longdouble", "c_short", "c_ushort", "c_int", "c_uint", 679 "c_long", "c_ulong", "c_longlong", "c_ulonglong", "c_char", 680 "anyopaque", "void", "bool", "isize", "usize", 681 "noreturn", "type", "anyerror", "comptime_int", "comptime_float", 682 }; 683 684 fn isType(name: []const u8) bool { 685 for (builtin_types) |t| { 686 if (mem.eql(u8, t, name)) 687 return true; 688 } 689 return false; 690 } 691 692 fn writeEscapedLines(out: *Writer, text: []const u8) !void { 693 return writeEscaped(out, text); 694 } 695 696 fn tokenizeAndPrintRaw( 697 allocator: Allocator, 698 docgen_tokenizer: *Tokenizer, 699 out: *Writer, 700 source_token: Token, 701 raw_src: []const u8, 702 ) !void { 703 const src_non_terminated = mem.trim(u8, raw_src, " \r\n"); 704 const src = try allocator.dupeSentinel(u8, src_non_terminated, 0); 705 706 try out.writeAll("<code>"); 707 var tokenizer = std.zig.Tokenizer.init(src); 708 var index: usize = 0; 709 var next_tok_is_fn = false; 710 while (true) { 711 const prev_tok_was_fn = next_tok_is_fn; 712 next_tok_is_fn = false; 713 714 const token = tokenizer.next(); 715 if (mem.find(u8, src[index..token.loc.start], "//")) |comment_start_off| { 716 // render one comment 717 const comment_start = index + comment_start_off; 718 const comment_end_off = mem.find(u8, src[comment_start..token.loc.start], "\n"); 719 const comment_end = if (comment_end_off) |o| comment_start + o else token.loc.start; 720 721 try writeEscapedLines(out, src[index..comment_start]); 722 try out.writeAll("<span class=\"tok-comment\">"); 723 try writeEscaped(out, src[comment_start..comment_end]); 724 try out.writeAll("</span>"); 725 index = comment_end; 726 tokenizer.index = index; 727 continue; 728 } 729 730 try writeEscapedLines(out, src[index..token.loc.start]); 731 switch (token.tag) { 732 .eof => break, 733 734 .keyword_addrspace, 735 .keyword_align, 736 .keyword_and, 737 .keyword_asm, 738 .keyword_break, 739 .keyword_catch, 740 .keyword_comptime, 741 .keyword_const, 742 .keyword_continue, 743 .keyword_defer, 744 .keyword_else, 745 .keyword_enum, 746 .keyword_errdefer, 747 .keyword_error, 748 .keyword_export, 749 .keyword_extern, 750 .keyword_for, 751 .keyword_if, 752 .keyword_inline, 753 .keyword_noalias, 754 .keyword_noinline, 755 .keyword_nosuspend, 756 .keyword_opaque, 757 .keyword_or, 758 .keyword_orelse, 759 .keyword_packed, 760 .keyword_anyframe, 761 .keyword_pub, 762 .keyword_resume, 763 .keyword_return, 764 .keyword_linksection, 765 .keyword_callconv, 766 .keyword_struct, 767 .keyword_suspend, 768 .keyword_switch, 769 .keyword_test, 770 .keyword_threadlocal, 771 .keyword_try, 772 .keyword_union, 773 .keyword_unreachable, 774 .keyword_var, 775 .keyword_volatile, 776 .keyword_allowzero, 777 .keyword_while, 778 .keyword_anytype, 779 => { 780 try out.writeAll("<span class=\"tok-kw\">"); 781 try writeEscaped(out, src[token.loc.start..token.loc.end]); 782 try out.writeAll("</span>"); 783 }, 784 785 .keyword_fn => { 786 try out.writeAll("<span class=\"tok-kw\">"); 787 try writeEscaped(out, src[token.loc.start..token.loc.end]); 788 try out.writeAll("</span>"); 789 next_tok_is_fn = true; 790 }, 791 792 .string_literal, 793 .multiline_string_literal_line, 794 .char_literal, 795 => { 796 try out.writeAll("<span class=\"tok-str\">"); 797 try writeEscaped(out, src[token.loc.start..token.loc.end]); 798 try out.writeAll("</span>"); 799 }, 800 801 .builtin => { 802 try out.writeAll("<span class=\"tok-builtin\">"); 803 try writeEscaped(out, src[token.loc.start..token.loc.end]); 804 try out.writeAll("</span>"); 805 }, 806 807 .doc_comment, 808 .container_doc_comment, 809 => { 810 try out.writeAll("<span class=\"tok-comment\">"); 811 try writeEscaped(out, src[token.loc.start..token.loc.end]); 812 try out.writeAll("</span>"); 813 }, 814 815 .identifier => { 816 const tok_bytes = src[token.loc.start..token.loc.end]; 817 if (mem.eql(u8, tok_bytes, "undefined") or 818 mem.eql(u8, tok_bytes, "null") or 819 mem.eql(u8, tok_bytes, "true") or 820 mem.eql(u8, tok_bytes, "false")) 821 { 822 try out.writeAll("<span class=\"tok-null\">"); 823 try writeEscaped(out, tok_bytes); 824 try out.writeAll("</span>"); 825 } else if (prev_tok_was_fn) { 826 try out.writeAll("<span class=\"tok-fn\">"); 827 try writeEscaped(out, tok_bytes); 828 try out.writeAll("</span>"); 829 } else { 830 const is_int = blk: { 831 if (src[token.loc.start] != 'i' and src[token.loc.start] != 'u') 832 break :blk false; 833 var i = token.loc.start + 1; 834 if (i == token.loc.end) 835 break :blk false; 836 while (i != token.loc.end) : (i += 1) { 837 if (src[i] < '0' or src[i] > '9') 838 break :blk false; 839 } 840 break :blk true; 841 }; 842 if (is_int or isType(tok_bytes)) { 843 try out.writeAll("<span class=\"tok-type\">"); 844 try writeEscaped(out, tok_bytes); 845 try out.writeAll("</span>"); 846 } else { 847 try writeEscaped(out, tok_bytes); 848 } 849 } 850 }, 851 852 .number_literal => { 853 try out.writeAll("<span class=\"tok-number\">"); 854 try writeEscaped(out, src[token.loc.start..token.loc.end]); 855 try out.writeAll("</span>"); 856 }, 857 858 .bang, 859 .pipe, 860 .pipe_pipe, 861 .pipe_equal, 862 .equal, 863 .equal_equal, 864 .equal_angle_bracket_right, 865 .bang_equal, 866 .l_paren, 867 .r_paren, 868 .semicolon, 869 .percent, 870 .percent_equal, 871 .l_brace, 872 .r_brace, 873 .l_bracket, 874 .r_bracket, 875 .period, 876 .period_asterisk, 877 .ellipsis2, 878 .ellipsis3, 879 .caret, 880 .caret_equal, 881 .plus, 882 .plus_plus, 883 .plus_equal, 884 .plus_percent, 885 .plus_percent_equal, 886 .plus_pipe, 887 .plus_pipe_equal, 888 .minus, 889 .minus_equal, 890 .minus_percent, 891 .minus_percent_equal, 892 .minus_pipe, 893 .minus_pipe_equal, 894 .asterisk, 895 .asterisk_equal, 896 .asterisk_percent, 897 .asterisk_percent_equal, 898 .asterisk_pipe, 899 .asterisk_pipe_equal, 900 .arrow, 901 .colon, 902 .slash, 903 .slash_equal, 904 .comma, 905 .ampersand, 906 .ampersand_equal, 907 .question_mark, 908 .angle_bracket_left, 909 .angle_bracket_left_equal, 910 .angle_bracket_angle_bracket_left, 911 .angle_bracket_angle_bracket_left_equal, 912 .angle_bracket_angle_bracket_left_pipe, 913 .angle_bracket_angle_bracket_left_pipe_equal, 914 .angle_bracket_right, 915 .angle_bracket_right_equal, 916 .angle_bracket_angle_bracket_right, 917 .angle_bracket_angle_bracket_right_equal, 918 .tilde, 919 => try writeEscaped(out, src[token.loc.start..token.loc.end]), 920 921 .invalid => return parseError( 922 docgen_tokenizer, 923 source_token, 924 "syntax error", 925 .{}, 926 ), 927 } 928 index = token.loc.end; 929 } 930 try out.writeAll("</code>"); 931 } 932 933 fn tokenizeAndPrint( 934 allocator: Allocator, 935 docgen_tokenizer: *Tokenizer, 936 out: *Writer, 937 source_token: Token, 938 ) !void { 939 const raw_src = docgen_tokenizer.buffer[source_token.start..source_token.end]; 940 return tokenizeAndPrintRaw(allocator, docgen_tokenizer, out, source_token, raw_src); 941 } 942 943 fn printSourceBlock(allocator: Allocator, docgen_tokenizer: *Tokenizer, out: *Writer, syntax_block: SyntaxBlock, content: ?[]const u8) !void { 944 const source_type = @tagName(syntax_block.source_type); 945 946 try out.print("<figure><figcaption class=\"{s}-cap\"><cite class=\"file\">{s}</cite></figcaption><pre>", .{ source_type, syntax_block.name }); 947 switch (syntax_block.source_type) { 948 .zig => try tokenizeAndPrint(allocator, docgen_tokenizer, out, syntax_block.source_token), 949 else => { 950 const raw_source = content orelse docgen_tokenizer.buffer[syntax_block.source_token.start..syntax_block.source_token.end]; 951 const trimmed_raw_source = mem.trim(u8, raw_source, " \r\n"); 952 953 try out.writeAll("<code>"); 954 try writeEscapedLines(out, trimmed_raw_source); 955 try out.writeAll("</code>"); 956 }, 957 } 958 try out.writeAll("</pre></figure>"); 959 } 960 961 fn printShell(out: *Writer, shell_content: []const u8, escape: bool) !void { 962 const trimmed_shell_content = mem.trim(u8, shell_content, " \r\n"); 963 try out.writeAll("<figure><figcaption class=\"shell-cap\">Shell</figcaption><pre><samp>"); 964 var cmd_cont: bool = false; 965 var iter = std.mem.splitScalar(u8, trimmed_shell_content, '\n'); 966 while (iter.next()) |orig_line| { 967 const line = mem.trimEnd(u8, orig_line, " \r"); 968 if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] != '\\') { 969 try out.writeAll("$ <kbd>"); 970 const s = std.mem.trimStart(u8, line[1..], " "); 971 if (escape) { 972 try writeEscaped(out, s); 973 } else { 974 try out.writeAll(s); 975 } 976 try out.writeAll("</kbd>" ++ "\n"); 977 } else if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] == '\\') { 978 try out.writeAll("$ <kbd>"); 979 const s = std.mem.trimStart(u8, line[1..], " "); 980 if (escape) { 981 try writeEscaped(out, s); 982 } else { 983 try out.writeAll(s); 984 } 985 try out.writeAll("\n"); 986 cmd_cont = true; 987 } else if (line.len > 0 and line[line.len - 1] != '\\' and cmd_cont) { 988 if (escape) { 989 try writeEscaped(out, line); 990 } else { 991 try out.writeAll(line); 992 } 993 try out.writeAll("</kbd>" ++ "\n"); 994 cmd_cont = false; 995 } else { 996 if (escape) { 997 try writeEscaped(out, line); 998 } else { 999 try out.writeAll(line); 1000 } 1001 try out.writeAll("\n"); 1002 } 1003 } 1004 1005 try out.writeAll("</samp></pre></figure>"); 1006 } 1007 1008 fn genHtml( 1009 allocator: Allocator, 1010 io: Io, 1011 tokenizer: *Tokenizer, 1012 toc: *Toc, 1013 code_dir: Path, 1014 grammar: []const u8, 1015 out: *Writer, 1016 ) !void { 1017 for (toc.nodes) |node| { 1018 switch (node) { 1019 .Content => |data| { 1020 try out.writeAll(data); 1021 }, 1022 .Link => |info| { 1023 if (!toc.urls.contains(info.url)) { 1024 return parseError(tokenizer, info.token, "url not found: {s}", .{info.url}); 1025 } 1026 try out.print("<a href=\"#{s}\">{s}</a>", .{ info.url, info.name }); 1027 }, 1028 .Nav => { 1029 try out.writeAll(toc.toc); 1030 }, 1031 .Builtin => |tok| { 1032 try out.writeAll("<figure><figcaption class=\"zig-cap\"><cite>@import(\"builtin\")</cite></figcaption><pre>"); 1033 const builtin_code = @embedFile("builtin"); // 😎 1034 try tokenizeAndPrintRaw(allocator, tokenizer, out, tok, builtin_code); 1035 try out.writeAll("</pre></figure>"); 1036 }, 1037 .HeaderOpen => |info| { 1038 try out.print( 1039 "<h{d} id=\"{s}\"><a href=\"#toc-{s}\">{s}</a> <a class=\"hdr\" href=\"#{s}\">§</a></h{d}>\n", 1040 .{ info.n, info.url, info.url, info.name, info.url, info.n }, 1041 ); 1042 }, 1043 .SeeAlso => |items| { 1044 try out.writeAll("<p>See also:</p><ul>\n"); 1045 for (items) |item| { 1046 const url = try urlize(allocator, item.name); 1047 if (!toc.urls.contains(url)) { 1048 return parseError(tokenizer, item.token, "url not found: {s}", .{url}); 1049 } 1050 try out.print("<li><a href=\"#{s}\">{s}</a></li>\n", .{ url, item.name }); 1051 } 1052 try out.writeAll("</ul>\n"); 1053 }, 1054 .InlineSyntax => |content_tok| { 1055 try tokenizeAndPrint(allocator, tokenizer, out, content_tok); 1056 }, 1057 .Shell => |content_tok| { 1058 const raw_shell_content = tokenizer.buffer[content_tok.start..content_tok.end]; 1059 try printShell(out, raw_shell_content, true); 1060 }, 1061 .SyntaxBlock => |syntax_block| { 1062 try printSourceBlock(allocator, tokenizer, out, syntax_block, null); 1063 }, 1064 .Code => |code| { 1065 const out_basename = try std.fmt.allocPrint(allocator, "{s}.out", .{ 1066 Dir.path.stem(code.name), 1067 }); 1068 defer allocator.free(out_basename); 1069 1070 const out_path: Path = .{ 1071 .root_dir = code_dir.root_dir, 1072 .sub_path = out_basename, 1073 }; 1074 1075 const contents = out_path.root_dir.handle.readFileAlloc(io, out_path.sub_path, allocator, .unlimited) catch |err| { 1076 return parseError(tokenizer, code.token, "failed opening {f}: {t}", .{ out_path, err }); 1077 }; 1078 defer allocator.free(contents); 1079 1080 try out.writeAll(contents); 1081 }, 1082 .Grammar => { 1083 try printSourceBlock(allocator, tokenizer, out, .{ 1084 .source_type = .peg, 1085 .name = "grammar.peg", 1086 .source_token = undefined, 1087 }, grammar); 1088 }, 1089 } 1090 } 1091 } 1092