Commit 0b1b3f0225

Andrew Kelley <andrew@ziglang.org>
2024-03-06 22:21:22
upstream new autodocs implementation
1 parent 356e653
lib/docs/wasm/markdown/Document.zig
@@ -0,0 +1,192 @@
+//! An abstract tree representation of a Markdown document.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const Renderer = @import("renderer.zig").Renderer;
+
+nodes: Node.List.Slice,
+extra: []u32,
+string_bytes: []u8,
+
+const Document = @This();
+
+pub const Node = struct {
+    tag: Tag,
+    data: Data,
+
+    pub const Index = enum(u32) {
+        root = 0,
+        _,
+    };
+    pub const List = std.MultiArrayList(Node);
+
+    pub const Tag = enum {
+        /// Data is `container`.
+        root,
+
+        // Blocks
+        /// Data is `list`.
+        list,
+        /// Data is `list_item`.
+        list_item,
+        /// Data is `container`.
+        table,
+        /// Data is `container`.
+        table_row,
+        /// Data is `table_cell`.
+        table_cell,
+        /// Data is `heading`.
+        heading,
+        /// Data is `code_block`.
+        code_block,
+        /// Data is `container`.
+        blockquote,
+        /// Data is `container`.
+        paragraph,
+        /// Data is `none`.
+        thematic_break,
+
+        // Inlines
+        /// Data is `link`.
+        link,
+        /// Data is `link`.
+        image,
+        /// Data is `container`.
+        strong,
+        /// Data is `container`.
+        emphasis,
+        /// Data is `text`.
+        code_span,
+        /// Data is `text`.
+        text,
+        /// Data is `none`.
+        line_break,
+    };
+
+    pub const Data = union {
+        none: void,
+        container: struct {
+            children: ExtraIndex,
+        },
+        text: struct {
+            content: StringIndex,
+        },
+        list: struct {
+            start: ListStart,
+            children: ExtraIndex,
+        },
+        list_item: struct {
+            tight: bool,
+            children: ExtraIndex,
+        },
+        table_cell: struct {
+            info: packed struct {
+                alignment: TableCellAlignment,
+                header: bool,
+            },
+            children: ExtraIndex,
+        },
+        heading: struct {
+            /// Between 1 and 6, inclusive.
+            level: u3,
+            children: ExtraIndex,
+        },
+        code_block: struct {
+            tag: StringIndex,
+            content: StringIndex,
+        },
+        link: struct {
+            target: StringIndex,
+            children: ExtraIndex,
+        },
+
+        comptime {
+            // In Debug and ReleaseSafe builds, there may be hidden extra fields
+            // included for safety checks. Without such safety checks enabled,
+            // we always want this union to be 8 bytes.
+            if (builtin.mode != .Debug and builtin.mode != .ReleaseSafe) {
+                assert(@sizeOf(Data) == 8);
+            }
+        }
+    };
+
+    /// The starting number of a list. This is either a number between 0 and
+    /// 999,999,999, inclusive, or `unordered` to indicate an unordered list.
+    pub const ListStart = enum(u30) {
+        // When https://github.com/ziglang/zig/issues/104 is implemented, this
+        // type can be more naturally expressed as ?u30. As it is, we want
+        // values to fit within 4 bytes, so ?u30 does not yet suffice for
+        // storage.
+        unordered = std.math.maxInt(u30),
+        _,
+
+        pub fn asNumber(start: ListStart) ?u30 {
+            if (start == .unordered) return null;
+            assert(@intFromEnum(start) <= 999_999_999);
+            return @intFromEnum(start);
+        }
+    };
+
+    pub const TableCellAlignment = enum {
+        unset,
+        left,
+        center,
+        right,
+    };
+
+    /// Trailing: `len` times `Node.Index`
+    pub const Children = struct {
+        len: u32,
+    };
+};
+
+pub const ExtraIndex = enum(u32) { _ };
+
+/// The index of a null-terminated string in `string_bytes`.
+pub const StringIndex = enum(u32) {
+    empty = 0,
+    _,
+};
+
+pub fn deinit(doc: *Document, allocator: Allocator) void {
+    doc.nodes.deinit(allocator);
+    allocator.free(doc.extra);
+    allocator.free(doc.string_bytes);
+    doc.* = undefined;
+}
+
+/// Renders a document directly to a writer using the default renderer.
+pub fn render(doc: Document, writer: anytype) @TypeOf(writer).Error!void {
+    const renderer: Renderer(@TypeOf(writer), void) = .{ .context = {} };
+    try renderer.render(doc, writer);
+}
+
+pub fn ExtraData(comptime T: type) type {
+    return struct { data: T, end: usize };
+}
+
+pub fn extraData(doc: Document, comptime T: type, index: ExtraIndex) ExtraData(T) {
+    const fields = @typeInfo(T).Struct.fields;
+    var i: usize = @intFromEnum(index);
+    var result: T = undefined;
+    inline for (fields) |field| {
+        @field(result, field.name) = switch (field.type) {
+            u32 => doc.extra[i],
+            else => @compileError("bad field type"),
+        };
+        i += 1;
+    }
+    return .{ .data = result, .end = i };
+}
+
+pub fn extraChildren(doc: Document, index: ExtraIndex) []const Node.Index {
+    const children = doc.extraData(Node.Children, index);
+    return @ptrCast(doc.extra[children.end..][0..children.data.len]);
+}
+
+pub fn string(doc: Document, index: StringIndex) [:0]const u8 {
+    const start = @intFromEnum(index);
+    return std.mem.span(@as([*:0]u8, @ptrCast(doc.string_bytes[start..].ptr)));
+}
lib/docs/wasm/markdown/Parser.zig
@@ -0,0 +1,1500 @@
+//! A Markdown parser producing `Document`s.
+//!
+//! The parser operates at two levels: at the outer level, the parser accepts
+//! the content of an input document line by line and begins building the _block
+//! structure_ of the document. This creates a stack of currently open blocks.
+//!
+//! When the parser detects the end of a block, it closes the block, popping it
+//! from the open block stack and completing any additional parsing of the
+//! block's content. For blocks which contain parseable inline content, this
+//! invokes the inner level of the parser, handling the _inline structure_ of
+//! the block.
+//!
+//! Inline parsing scans through the collected inline content of a block. When
+//! it encounters a character that could indicate the beginning of an inline, it
+//! either handles the inline right away (if possible) or adds it to a pending
+//! inlines stack. When an inline is completed, it is added to a list of
+//! completed inlines, which (along with any surrounding text nodes) will become
+//! the children of the parent inline or the block whose inline content is being
+//! parsed.
+
+const std = @import("std");
+const mem = std.mem;
+const assert = std.debug.assert;
+const isWhitespace = std.ascii.isWhitespace;
+const Allocator = mem.Allocator;
+const expectEqual = std.testing.expectEqual;
+const Document = @import("Document.zig");
+const Node = Document.Node;
+const ExtraIndex = Document.ExtraIndex;
+const ExtraData = Document.ExtraData;
+const StringIndex = Document.StringIndex;
+
+nodes: Node.List = .{},
+extra: std.ArrayListUnmanaged(u32) = .{},
+scratch_extra: std.ArrayListUnmanaged(u32) = .{},
+string_bytes: std.ArrayListUnmanaged(u8) = .{},
+scratch_string: std.ArrayListUnmanaged(u8) = .{},
+pending_blocks: std.ArrayListUnmanaged(Block) = .{},
+allocator: Allocator,
+
+const Parser = @This();
+
+/// An arbitrary limit on the maximum number of columns in a table so that
+/// table-related metadata maintained by the parser does not require dynamic
+/// memory allocation.
+const max_table_columns = 128;
+
+/// A block element which is still receiving children.
+const Block = struct {
+    tag: Tag,
+    data: Data,
+    extra_start: usize,
+    string_start: usize,
+
+    const Tag = enum {
+        /// Data is `list`.
+        list,
+        /// Data is `list_item`.
+        list_item,
+        /// Data is `table`.
+        table,
+        /// Data is `none`.
+        table_row,
+        /// Data is `heading`.
+        heading,
+        /// Data is `code_block`.
+        code_block,
+        /// Data is `none`.
+        blockquote,
+        /// Data is `none`.
+        paragraph,
+        /// Data is `none`.
+        thematic_break,
+    };
+
+    const Data = union {
+        none: void,
+        list: struct {
+            marker: ListMarker,
+            /// Between 0 and 999,999,999, inclusive.
+            start: u30,
+            tight: bool,
+            last_line_blank: bool = false,
+        },
+        list_item: struct {
+            continuation_indent: usize,
+        },
+        table: struct {
+            column_alignments: std.BoundedArray(Node.TableCellAlignment, max_table_columns) = .{},
+        },
+        heading: struct {
+            /// Between 1 and 6, inclusive.
+            level: u3,
+        },
+        code_block: struct {
+            tag: StringIndex,
+            fence_len: usize,
+            indent: usize,
+        },
+
+        const ListMarker = enum {
+            @"-",
+            @"*",
+            @"+",
+            number_dot,
+            number_paren,
+        };
+    };
+
+    const ContentType = enum {
+        blocks,
+        inlines,
+        raw_inlines,
+        nothing,
+    };
+
+    fn canAccept(b: Block) ContentType {
+        return switch (b.tag) {
+            .list,
+            .list_item,
+            .table,
+            .blockquote,
+            => .blocks,
+
+            .heading,
+            .paragraph,
+            => .inlines,
+
+            .code_block,
+            => .raw_inlines,
+
+            .table_row,
+            .thematic_break,
+            => .nothing,
+        };
+    }
+
+    /// Attempts to continue `b` using the contents of `line`. If successful,
+    /// returns the remaining portion of `line` to be considered part of `b`
+    /// (e.g. for a blockquote, this would be everything except the leading
+    /// `>`). If unsuccessful, returns null.
+    fn match(b: Block, line: []const u8) ?[]const u8 {
+        const unindented = mem.trimLeft(u8, line, " \t");
+        const indent = line.len - unindented.len;
+        return switch (b.tag) {
+            .list => line,
+            .list_item => if (indent >= b.data.list_item.continuation_indent)
+                line[b.data.list_item.continuation_indent..]
+            else if (unindented.len == 0)
+                // Blank lines should not close list items, since there may be
+                // more indented contents to follow after the blank line.
+                ""
+            else
+                null,
+            .table => if (unindented.len > 0) unindented else null,
+            .table_row => null,
+            .heading => null,
+            .code_block => code_block: {
+                const trimmed = mem.trimRight(u8, unindented, " \t");
+                if (mem.indexOfNone(u8, trimmed, "`") != null or trimmed.len != b.data.code_block.fence_len) {
+                    const effective_indent = @min(indent, b.data.code_block.indent);
+                    break :code_block line[effective_indent..];
+                } else {
+                    break :code_block null;
+                }
+            },
+            .blockquote => if (mem.startsWith(u8, unindented, ">"))
+                unindented[1..]
+            else
+                null,
+            .paragraph => if (unindented.len > 0) unindented else null,
+            .thematic_break => null,
+        };
+    }
+};
+
+pub fn init(allocator: Allocator) Allocator.Error!Parser {
+    var p: Parser = .{ .allocator = allocator };
+    try p.nodes.append(allocator, .{
+        .tag = .root,
+        .data = undefined,
+    });
+    try p.string_bytes.append(allocator, 0);
+    return p;
+}
+
+pub fn deinit(p: *Parser) void {
+    p.nodes.deinit(p.allocator);
+    p.extra.deinit(p.allocator);
+    p.scratch_extra.deinit(p.allocator);
+    p.string_bytes.deinit(p.allocator);
+    p.scratch_string.deinit(p.allocator);
+    p.pending_blocks.deinit(p.allocator);
+    p.* = undefined;
+}
+
+/// Accepts a single line of content. `line` should not have a trailing line
+/// ending character.
+pub fn feedLine(p: *Parser, line: []const u8) Allocator.Error!void {
+    var rest_line = line;
+    const first_unmatched = for (p.pending_blocks.items, 0..) |b, i| {
+        if (b.match(rest_line)) |rest| {
+            rest_line = rest;
+        } else {
+            break i;
+        }
+    } else p.pending_blocks.items.len;
+
+    const in_code_block = p.pending_blocks.items.len > 0 and
+        p.pending_blocks.getLast().tag == .code_block;
+    const code_block_end = in_code_block and
+        first_unmatched + 1 == p.pending_blocks.items.len;
+    // New blocks cannot be started if we are actively inside a code block or
+    // are just closing one (to avoid interpreting the closing ``` as a new code
+    // block start).
+    var maybe_block_start = if (!in_code_block or first_unmatched + 2 <= p.pending_blocks.items.len)
+        try p.startBlock(rest_line)
+    else
+        null;
+
+    // This is a lazy continuation line if there are no new blocks to open and
+    // the last open block is a paragraph.
+    if (maybe_block_start == null and
+        !isBlank(rest_line) and
+        p.pending_blocks.items.len > 0 and
+        p.pending_blocks.getLast().tag == .paragraph)
+    {
+        try p.addScratchStringLine(rest_line);
+        return;
+    }
+
+    // If a new block needs to be started, any paragraph needs to be closed,
+    // even though this isn't detected as part of the closing condition for
+    // paragraphs.
+    if (maybe_block_start != null and
+        p.pending_blocks.items.len > 0 and
+        p.pending_blocks.getLast().tag == .paragraph)
+    {
+        try p.closeLastBlock();
+    }
+
+    while (p.pending_blocks.items.len > first_unmatched) {
+        try p.closeLastBlock();
+    }
+
+    while (maybe_block_start) |block_start| : (maybe_block_start = try p.startBlock(rest_line)) {
+        try p.appendBlockStart(block_start);
+        // There may be more blocks to start within the same line.
+        rest_line = block_start.rest;
+        // Headings may only contain inline content.
+        if (block_start.tag == .heading) break;
+        // An opening code fence does not contain any additional block or inline
+        // content to process.
+        if (block_start.tag == .code_block) return;
+    }
+
+    // Do not append the end of a code block (```) as textual content.
+    if (code_block_end) return;
+
+    const can_accept = if (p.pending_blocks.getLastOrNull()) |last_pending_block|
+        last_pending_block.canAccept()
+    else
+        .blocks;
+    const rest_line_trimmed = mem.trimLeft(u8, rest_line, " \t");
+    switch (can_accept) {
+        .blocks => {
+            // If we're inside a list item and the rest of the line is blank, it
+            // means that any subsequent child of the list item (or subsequent
+            // item in the list) will cause the containing list to be considered
+            // loose. However, we can't immediately declare that the list is
+            // loose, since we might just be looking at a blank line after the
+            // end of the last item in the list. The final determination will be
+            // made when appending the next child of the list or list item.
+            const maybe_containing_list = if (p.pending_blocks.items.len > 0 and p.pending_blocks.getLast().tag == .list_item)
+                &p.pending_blocks.items[p.pending_blocks.items.len - 2]
+            else
+                null;
+
+            if (rest_line_trimmed.len > 0) {
+                try p.appendBlockStart(.{
+                    .tag = .paragraph,
+                    .data = .{ .none = {} },
+                    .rest = undefined,
+                });
+                try p.addScratchStringLine(rest_line_trimmed);
+            }
+
+            if (maybe_containing_list) |containing_list| {
+                containing_list.data.list.last_line_blank = rest_line_trimmed.len == 0;
+            }
+        },
+        .inlines => try p.addScratchStringLine(rest_line_trimmed),
+        .raw_inlines => try p.addScratchStringLine(rest_line),
+        .nothing => {},
+    }
+}
+
+/// Completes processing of the input and returns the parsed document.
+pub fn endInput(p: *Parser) Allocator.Error!Document {
+    while (p.pending_blocks.items.len > 0) {
+        try p.closeLastBlock();
+    }
+    // There should be no inline content pending after closing the last open
+    // block.
+    assert(p.scratch_string.items.len == 0);
+
+    const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items));
+    p.nodes.items(.data)[0] = .{ .container = .{ .children = children } };
+    p.scratch_string.items.len = 0;
+    p.scratch_extra.items.len = 0;
+
+    var nodes = p.nodes.toOwnedSlice();
+    errdefer nodes.deinit(p.allocator);
+    const extra = try p.extra.toOwnedSlice(p.allocator);
+    errdefer p.allocator.free(extra);
+    const string_bytes = try p.string_bytes.toOwnedSlice(p.allocator);
+    errdefer p.allocator.free(string_bytes);
+
+    return .{
+        .nodes = nodes,
+        .extra = extra,
+        .string_bytes = string_bytes,
+    };
+}
+
+/// Data describing the start of a new block element.
+const BlockStart = struct {
+    tag: Tag,
+    data: Data,
+    rest: []const u8,
+
+    const Tag = enum {
+        /// Data is `list_item`.
+        list_item,
+        /// Data is `table_row`.
+        table_row,
+        /// Data is `heading`.
+        heading,
+        /// Data is `code_block`.
+        code_block,
+        /// Data is `none`.
+        blockquote,
+        /// Data is `none`.
+        paragraph,
+        /// Data is `none`.
+        thematic_break,
+    };
+
+    const Data = union {
+        none: void,
+        list_item: struct {
+            marker: Block.Data.ListMarker,
+            number: u30,
+            continuation_indent: usize,
+        },
+        table_row: struct {
+            cells: std.BoundedArray([]const u8, max_table_columns),
+        },
+        heading: struct {
+            /// Between 1 and 6, inclusive.
+            level: u3,
+        },
+        code_block: struct {
+            tag: StringIndex,
+            fence_len: usize,
+            indent: usize,
+        },
+    };
+};
+
+fn appendBlockStart(p: *Parser, block_start: BlockStart) !void {
+    if (p.pending_blocks.getLastOrNull()) |last_pending_block| {
+        // Close the last block if it is a list and the new block is not a list item
+        // or not of the same marker type.
+        const should_close_list = last_pending_block.tag == .list and
+            (block_start.tag != .list_item or
+            block_start.data.list_item.marker != last_pending_block.data.list.marker);
+        // The last block should also be closed if the new block is not a table
+        // row, which is the only allowed child of a table.
+        const should_close_table = last_pending_block.tag == .table and
+            block_start.tag != .table_row;
+        if (should_close_list or should_close_table) {
+            try p.closeLastBlock();
+        }
+    }
+
+    if (p.pending_blocks.getLastOrNull()) |last_pending_block| {
+        // If the last block is a list or list item, check for tightness based
+        // on the last line.
+        const maybe_containing_list = switch (last_pending_block.tag) {
+            .list => &p.pending_blocks.items[p.pending_blocks.items.len - 1],
+            .list_item => &p.pending_blocks.items[p.pending_blocks.items.len - 2],
+            else => null,
+        };
+        if (maybe_containing_list) |containing_list| {
+            if (containing_list.data.list.last_line_blank) {
+                containing_list.data.list.tight = false;
+            }
+        }
+    }
+
+    // Start a new list if the new block is a list item and there is no
+    // containing list yet.
+    if (block_start.tag == .list_item and
+        (p.pending_blocks.items.len == 0 or p.pending_blocks.getLast().tag != .list))
+    {
+        try p.pending_blocks.append(p.allocator, .{
+            .tag = .list,
+            .data = .{ .list = .{
+                .marker = block_start.data.list_item.marker,
+                .start = block_start.data.list_item.number,
+                .tight = true,
+            } },
+            .string_start = p.scratch_string.items.len,
+            .extra_start = p.scratch_extra.items.len,
+        });
+    }
+
+    if (block_start.tag == .table_row) {
+        // Likewise, table rows start a table implicitly.
+        if (p.pending_blocks.items.len == 0 or p.pending_blocks.getLast().tag != .table) {
+            try p.pending_blocks.append(p.allocator, .{
+                .tag = .table,
+                .data = .{ .table = .{
+                    .column_alignments = .{},
+                } },
+                .string_start = p.scratch_string.items.len,
+                .extra_start = p.scratch_extra.items.len,
+            });
+        }
+
+        const current_row = p.scratch_extra.items.len - p.pending_blocks.getLast().extra_start;
+        if (current_row <= 1) {
+            if (parseTableHeaderDelimiter(block_start.data.table_row.cells)) |alignments| {
+                p.pending_blocks.items[p.pending_blocks.items.len - 1].data.table.column_alignments = alignments;
+                if (current_row == 1) {
+                    // We need to go back and mark the header row and its column
+                    // alignments.
+                    const datas = p.nodes.items(.data);
+                    const header_data = datas[p.scratch_extra.getLast()];
+                    for (p.extraChildren(header_data.container.children), 0..) |header_cell, i| {
+                        const alignment = if (i < alignments.len) alignments.buffer[i] else .unset;
+                        const cell_data = &datas[@intFromEnum(header_cell)].table_cell;
+                        cell_data.info.alignment = alignment;
+                        cell_data.info.header = true;
+                    }
+                }
+                return;
+            }
+        }
+    }
+
+    const tag: Block.Tag, const data: Block.Data = switch (block_start.tag) {
+        .list_item => .{ .list_item, .{ .list_item = .{
+            .continuation_indent = block_start.data.list_item.continuation_indent,
+        } } },
+        .table_row => .{ .table_row, .{ .none = {} } },
+        .heading => .{ .heading, .{ .heading = .{
+            .level = block_start.data.heading.level,
+        } } },
+        .code_block => .{ .code_block, .{ .code_block = .{
+            .tag = block_start.data.code_block.tag,
+            .fence_len = block_start.data.code_block.fence_len,
+            .indent = block_start.data.code_block.indent,
+        } } },
+        .blockquote => .{ .blockquote, .{ .none = {} } },
+        .paragraph => .{ .paragraph, .{ .none = {} } },
+        .thematic_break => .{ .thematic_break, .{ .none = {} } },
+    };
+
+    try p.pending_blocks.append(p.allocator, .{
+        .tag = tag,
+        .data = data,
+        .string_start = p.scratch_string.items.len,
+        .extra_start = p.scratch_extra.items.len,
+    });
+
+    if (tag == .table_row) {
+        // Table rows are unique, since we already have all the children
+        // available in the BlockStart. We can immediately parse and append
+        // these children now.
+        const containing_table = p.pending_blocks.items[p.pending_blocks.items.len - 2];
+        const column_alignments = containing_table.data.table.column_alignments.slice();
+        for (block_start.data.table_row.cells.slice(), 0..) |cell_content, i| {
+            const cell_children = try p.parseInlines(cell_content);
+            const alignment = if (i < column_alignments.len) column_alignments[i] else .unset;
+            const cell = try p.addNode(.{
+                .tag = .table_cell,
+                .data = .{ .table_cell = .{
+                    .info = .{
+                        .alignment = alignment,
+                        .header = false,
+                    },
+                    .children = cell_children,
+                } },
+            });
+            try p.addScratchExtraNode(cell);
+        }
+    }
+}
+
+fn startBlock(p: *Parser, line: []const u8) !?BlockStart {
+    const unindented = mem.trimLeft(u8, line, " \t");
+    const indent = line.len - unindented.len;
+    if (isThematicBreak(line)) {
+        // Thematic breaks take precedence over list items.
+        return .{
+            .tag = .thematic_break,
+            .data = .{ .none = {} },
+            .rest = "",
+        };
+    } else if (startListItem(unindented)) |list_item| {
+        return .{
+            .tag = .list_item,
+            .data = .{ .list_item = .{
+                .marker = list_item.marker,
+                .number = list_item.number,
+                .continuation_indent = list_item.continuation_indent,
+            } },
+            .rest = list_item.rest,
+        };
+    } else if (startTableRow(unindented)) |table_row| {
+        return .{
+            .tag = .table_row,
+            .data = .{ .table_row = .{
+                .cells = table_row.cells,
+            } },
+            .rest = "",
+        };
+    } else if (startHeading(unindented)) |heading| {
+        return .{
+            .tag = .heading,
+            .data = .{ .heading = .{
+                .level = heading.level,
+            } },
+            .rest = heading.rest,
+        };
+    } else if (try p.startCodeBlock(unindented)) |code_block| {
+        return .{
+            .tag = .code_block,
+            .data = .{ .code_block = .{
+                .tag = code_block.tag,
+                .fence_len = code_block.fence_len,
+                .indent = indent,
+            } },
+            .rest = "",
+        };
+    } else if (startBlockquote(unindented)) |rest| {
+        return .{
+            .tag = .blockquote,
+            .data = .{ .none = {} },
+            .rest = rest,
+        };
+    } else {
+        return null;
+    }
+}
+
+const ListItemStart = struct {
+    marker: Block.Data.ListMarker,
+    number: u30,
+    continuation_indent: usize,
+    rest: []const u8,
+};
+
+fn startListItem(unindented_line: []const u8) ?ListItemStart {
+    if (mem.startsWith(u8, unindented_line, "- ")) {
+        return .{
+            .marker = .@"-",
+            .number = undefined,
+            .continuation_indent = 2,
+            .rest = unindented_line[2..],
+        };
+    } else if (mem.startsWith(u8, unindented_line, "* ")) {
+        return .{
+            .marker = .@"*",
+            .number = undefined,
+            .continuation_indent = 2,
+            .rest = unindented_line[2..],
+        };
+    } else if (mem.startsWith(u8, unindented_line, "+ ")) {
+        return .{
+            .marker = .@"+",
+            .number = undefined,
+            .continuation_indent = 2,
+            .rest = unindented_line[2..],
+        };
+    }
+
+    const number_end = mem.indexOfNone(u8, unindented_line, "0123456789") orelse return null;
+    const after_number = unindented_line[number_end..];
+    const marker: Block.Data.ListMarker = if (mem.startsWith(u8, after_number, ". "))
+        .number_dot
+    else if (mem.startsWith(u8, after_number, ") "))
+        .number_paren
+    else
+        return null;
+    const number = std.fmt.parseInt(u30, unindented_line[0..number_end], 10) catch return null;
+    if (number > 999_999_999) return null;
+    return .{
+        .marker = marker,
+        .number = number,
+        .continuation_indent = number_end + 2,
+        .rest = after_number[2..],
+    };
+}
+
+const TableRowStart = struct {
+    cells: std.BoundedArray([]const u8, max_table_columns),
+};
+
+fn startTableRow(unindented_line: []const u8) ?TableRowStart {
+    if (!mem.startsWith(u8, unindented_line, "|") or
+        mem.endsWith(u8, unindented_line, "\\|") or
+        !mem.endsWith(u8, unindented_line, "|")) return null;
+
+    var cells: std.BoundedArray([]const u8, max_table_columns) = .{};
+    const table_row_content = unindented_line[1 .. unindented_line.len - 1];
+    var cell_start: usize = 0;
+    var i: usize = 0;
+    while (i < table_row_content.len) : (i += 1) {
+        switch (table_row_content[i]) {
+            '\\' => i += 1,
+            '|' => {
+                cells.append(table_row_content[cell_start..i]) catch return null;
+                cell_start = i + 1;
+            },
+            '`' => {
+                // Ignoring pipes in code spans allows table cells to contain
+                // code using ||, for example.
+                const open_start = i;
+                i = mem.indexOfNonePos(u8, table_row_content, i, "`") orelse return null;
+                const open_len = i - open_start;
+                while (mem.indexOfScalarPos(u8, table_row_content, i, '`')) |close_start| {
+                    i = mem.indexOfNonePos(u8, table_row_content, close_start, "`") orelse return null;
+                    const close_len = i - close_start;
+                    if (close_len == open_len) break;
+                } else return null;
+            },
+            else => {},
+        }
+    }
+    cells.append(table_row_content[cell_start..]) catch return null;
+
+    return .{ .cells = cells };
+}
+
+fn parseTableHeaderDelimiter(
+    row_cells: std.BoundedArray([]const u8, max_table_columns),
+) ?std.BoundedArray(Node.TableCellAlignment, max_table_columns) {
+    var alignments: std.BoundedArray(Node.TableCellAlignment, max_table_columns) = .{};
+    for (row_cells.slice()) |content| {
+        const alignment = parseTableHeaderDelimiterCell(content) orelse return null;
+        alignments.appendAssumeCapacity(alignment);
+    }
+    return alignments;
+}
+
+fn parseTableHeaderDelimiterCell(content: []const u8) ?Node.TableCellAlignment {
+    var state: enum {
+        before_rule,
+        after_left_anchor,
+        in_rule,
+        after_right_anchor,
+        after_rule,
+    } = .before_rule;
+    var left_anchor = false;
+    var right_anchor = false;
+    for (content) |c| {
+        switch (state) {
+            .before_rule => switch (c) {
+                ' ' => {},
+                ':' => {
+                    left_anchor = true;
+                    state = .after_left_anchor;
+                },
+                '-' => state = .in_rule,
+                else => return null,
+            },
+            .after_left_anchor => switch (c) {
+                '-' => state = .in_rule,
+                else => return null,
+            },
+            .in_rule => switch (c) {
+                '-' => {},
+                ':' => {
+                    right_anchor = true;
+                    state = .after_right_anchor;
+                },
+                ' ' => state = .after_rule,
+                else => return null,
+            },
+            .after_right_anchor => switch (c) {
+                ' ' => state = .after_rule,
+                else => return null,
+            },
+            .after_rule => switch (c) {
+                ' ' => {},
+                else => return null,
+            },
+        }
+    }
+
+    switch (state) {
+        .before_rule,
+        .after_left_anchor,
+        => return null,
+
+        .in_rule,
+        .after_right_anchor,
+        .after_rule,
+        => {},
+    }
+
+    return if (left_anchor and right_anchor)
+        .center
+    else if (left_anchor)
+        .left
+    else if (right_anchor)
+        .right
+    else
+        .unset;
+}
+
+test parseTableHeaderDelimiterCell {
+    try expectEqual(null, parseTableHeaderDelimiterCell(""));
+    try expectEqual(null, parseTableHeaderDelimiterCell("   "));
+    try expectEqual(.unset, parseTableHeaderDelimiterCell("-"));
+    try expectEqual(.unset, parseTableHeaderDelimiterCell(" - "));
+    try expectEqual(.unset, parseTableHeaderDelimiterCell("----"));
+    try expectEqual(.unset, parseTableHeaderDelimiterCell(" ---- "));
+    try expectEqual(null, parseTableHeaderDelimiterCell(":"));
+    try expectEqual(null, parseTableHeaderDelimiterCell("::"));
+    try expectEqual(.left, parseTableHeaderDelimiterCell(":-"));
+    try expectEqual(.left, parseTableHeaderDelimiterCell(" :----"));
+    try expectEqual(.center, parseTableHeaderDelimiterCell(":-:"));
+    try expectEqual(.center, parseTableHeaderDelimiterCell(":----:"));
+    try expectEqual(.center, parseTableHeaderDelimiterCell("   :----:   "));
+    try expectEqual(.right, parseTableHeaderDelimiterCell("-:"));
+    try expectEqual(.right, parseTableHeaderDelimiterCell("----:"));
+    try expectEqual(.right, parseTableHeaderDelimiterCell("  ----:  "));
+}
+
+const HeadingStart = struct {
+    level: u3,
+    rest: []const u8,
+};
+
+fn startHeading(unindented_line: []const u8) ?HeadingStart {
+    var level: u3 = 0;
+    return for (unindented_line, 0..) |c, i| {
+        switch (c) {
+            '#' => {
+                if (level == 6) break null;
+                level += 1;
+            },
+            ' ' => {
+                // We must have seen at least one # by this point, since
+                // unindented_line has no leading spaces.
+                assert(level > 0);
+                break .{
+                    .level = level,
+                    .rest = unindented_line[i + 1 ..],
+                };
+            },
+            else => break null,
+        }
+    } else null;
+}
+
+const CodeBlockStart = struct {
+    tag: StringIndex,
+    fence_len: usize,
+};
+
+fn startCodeBlock(p: *Parser, unindented_line: []const u8) !?CodeBlockStart {
+    var fence_len: usize = 0;
+    const tag_bytes = for (unindented_line, 0..) |c, i| {
+        switch (c) {
+            '`' => fence_len += 1,
+            else => break unindented_line[i..],
+        }
+    } else "";
+    // Code block tags may not contain backticks, since that would create
+    // potential confusion with inline code spans.
+    if (fence_len < 3 or mem.indexOfScalar(u8, tag_bytes, '`') != null) return null;
+    return .{
+        .tag = try p.addString(mem.trim(u8, tag_bytes, " ")),
+        .fence_len = fence_len,
+    };
+}
+
+fn startBlockquote(unindented_line: []const u8) ?[]const u8 {
+    return if (mem.startsWith(u8, unindented_line, ">"))
+        unindented_line[1..]
+    else
+        null;
+}
+
+fn isThematicBreak(line: []const u8) bool {
+    var char: ?u8 = null;
+    var count: usize = 0;
+    for (line) |c| {
+        switch (c) {
+            ' ' => {},
+            '-', '_', '*' => {
+                if (char != null and c != char.?) return false;
+                char = c;
+                count += 1;
+            },
+            else => return false,
+        }
+    }
+    return count >= 3;
+}
+
+fn closeLastBlock(p: *Parser) !void {
+    const b = p.pending_blocks.pop();
+    const node = switch (b.tag) {
+        .list => list: {
+            assert(b.string_start == p.scratch_string.items.len);
+
+            // Although tightness is parsed as a property of the list, it is
+            // stored at the list item level to make it possible to render each
+            // node without any context from its parents.
+            const list_items = p.scratch_extra.items[b.extra_start..];
+            const node_datas = p.nodes.items(.data);
+            if (!b.data.list.tight) {
+                for (list_items) |list_item| {
+                    node_datas[list_item].list_item.tight = false;
+                }
+            }
+
+            const children = try p.addExtraChildren(@ptrCast(list_items));
+            break :list try p.addNode(.{
+                .tag = .list,
+                .data = .{ .list = .{
+                    .start = switch (b.data.list.marker) {
+                        .number_dot, .number_paren => @enumFromInt(b.data.list.start),
+                        .@"-", .@"*", .@"+" => .unordered,
+                    },
+                    .children = children,
+                } },
+            });
+        },
+        .list_item => list_item: {
+            assert(b.string_start == p.scratch_string.items.len);
+            const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+            break :list_item try p.addNode(.{
+                .tag = .list_item,
+                .data = .{ .list_item = .{
+                    .tight = true,
+                    .children = children,
+                } },
+            });
+        },
+        .table => table: {
+            assert(b.string_start == p.scratch_string.items.len);
+            const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+            break :table try p.addNode(.{
+                .tag = .table,
+                .data = .{ .container = .{
+                    .children = children,
+                } },
+            });
+        },
+        .table_row => table_row: {
+            assert(b.string_start == p.scratch_string.items.len);
+            const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+            break :table_row try p.addNode(.{
+                .tag = .table_row,
+                .data = .{ .container = .{
+                    .children = children,
+                } },
+            });
+        },
+        .heading => heading: {
+            const children = try p.parseInlines(p.scratch_string.items[b.string_start..]);
+            break :heading try p.addNode(.{
+                .tag = .heading,
+                .data = .{ .heading = .{
+                    .level = b.data.heading.level,
+                    .children = children,
+                } },
+            });
+        },
+        .code_block => code_block: {
+            const content = try p.addString(p.scratch_string.items[b.string_start..]);
+            break :code_block try p.addNode(.{
+                .tag = .code_block,
+                .data = .{ .code_block = .{
+                    .tag = b.data.code_block.tag,
+                    .content = content,
+                } },
+            });
+        },
+        .blockquote => blockquote: {
+            assert(b.string_start == p.scratch_string.items.len);
+            const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+            break :blockquote try p.addNode(.{
+                .tag = .blockquote,
+                .data = .{ .container = .{
+                    .children = children,
+                } },
+            });
+        },
+        .paragraph => paragraph: {
+            const children = try p.parseInlines(p.scratch_string.items[b.string_start..]);
+            break :paragraph try p.addNode(.{
+                .tag = .paragraph,
+                .data = .{ .container = .{
+                    .children = children,
+                } },
+            });
+        },
+        .thematic_break => try p.addNode(.{
+            .tag = .thematic_break,
+            .data = .{ .none = {} },
+        }),
+    };
+    p.scratch_string.items.len = b.string_start;
+    p.scratch_extra.items.len = b.extra_start;
+    try p.addScratchExtraNode(node);
+}
+
+const InlineParser = struct {
+    parent: *Parser,
+    content: []const u8,
+    pos: usize = 0,
+    pending_inlines: std.ArrayListUnmanaged(PendingInline) = .{},
+    completed_inlines: std.ArrayListUnmanaged(CompletedInline) = .{},
+
+    const PendingInline = struct {
+        tag: Tag,
+        data: Data,
+        start: usize,
+
+        const Tag = enum {
+            /// Data is `emphasis`.
+            emphasis,
+            /// Data is `none`.
+            link,
+            /// Data is `none`.
+            image,
+        };
+
+        const Data = union {
+            none: void,
+            emphasis: struct {
+                underscore: bool,
+                run_len: usize,
+            },
+        };
+    };
+
+    const CompletedInline = struct {
+        node: Node.Index,
+        start: usize,
+        len: usize,
+    };
+
+    fn deinit(ip: *InlineParser) void {
+        ip.pending_inlines.deinit(ip.parent.allocator);
+        ip.completed_inlines.deinit(ip.parent.allocator);
+    }
+
+    /// Parses all of `ip.content`, returning the children of the node
+    /// containing the inline content.
+    fn parse(ip: *InlineParser) Allocator.Error!ExtraIndex {
+        while (ip.pos < ip.content.len) : (ip.pos += 1) {
+            switch (ip.content[ip.pos]) {
+                '\\' => ip.pos += 1,
+                '[' => try ip.pending_inlines.append(ip.parent.allocator, .{
+                    .tag = .link,
+                    .data = .{ .none = {} },
+                    .start = ip.pos,
+                }),
+                '!' => if (ip.pos + 1 < ip.content.len and ip.content[ip.pos + 1] == '[') {
+                    try ip.pending_inlines.append(ip.parent.allocator, .{
+                        .tag = .image,
+                        .data = .{ .none = {} },
+                        .start = ip.pos,
+                    });
+                    ip.pos += 1;
+                },
+                ']' => try ip.parseLink(),
+                '*', '_' => try ip.parseEmphasis(),
+                '`' => try ip.parseCodeSpan(),
+                else => {},
+            }
+        }
+
+        const children = try ip.encodeChildren(0, ip.content.len);
+        // There may be pending inlines after parsing (e.g. unclosed emphasis
+        // runs), but there must not be any completed inlines, since those
+        // should all be part of `children`.
+        assert(ip.completed_inlines.items.len == 0);
+        return children;
+    }
+
+    /// Parses a link, starting at the `]` at the end of the link text. `ip.pos`
+    /// is left at the closing `)` of the link target or at the closing `]` if
+    /// there is none.
+    fn parseLink(ip: *InlineParser) !void {
+        var i = ip.pending_inlines.items.len;
+        while (i > 0) {
+            i -= 1;
+            if (ip.pending_inlines.items[i].tag == .link or
+                ip.pending_inlines.items[i].tag == .image) break;
+        } else return;
+        const opener = ip.pending_inlines.items[i];
+        ip.pending_inlines.shrinkRetainingCapacity(i);
+        const text_start = switch (opener.tag) {
+            .link => opener.start + 1,
+            .image => opener.start + 2,
+            else => unreachable,
+        };
+
+        if (ip.pos + 1 >= ip.content.len or ip.content[ip.pos + 1] != '(') return;
+        const text_end = ip.pos;
+
+        const target_start = text_end + 2;
+        var target_end = target_start;
+        var nesting_level: usize = 1;
+        while (target_end < ip.content.len) : (target_end += 1) {
+            switch (ip.content[target_end]) {
+                '\\' => target_end += 1,
+                '(' => nesting_level += 1,
+                ')' => {
+                    if (nesting_level == 1) break;
+                    nesting_level -= 1;
+                },
+                else => {},
+            }
+        } else return;
+        ip.pos = target_end;
+
+        const children = try ip.encodeChildren(text_start, text_end);
+        const target = try ip.encodeLinkTarget(target_start, target_end);
+
+        const link = try ip.parent.addNode(.{
+            .tag = switch (opener.tag) {
+                .link => .link,
+                .image => .image,
+                else => unreachable,
+            },
+            .data = .{ .link = .{
+                .target = target,
+                .children = children,
+            } },
+        });
+        try ip.completed_inlines.append(ip.parent.allocator, .{
+            .node = link,
+            .start = opener.start,
+            .len = ip.pos - opener.start + 1,
+        });
+    }
+
+    fn encodeLinkTarget(ip: *InlineParser, start: usize, end: usize) !StringIndex {
+        // For efficiency, we can encode directly into string_bytes rather than
+        // creating a temporary string and then encoding it, since this process
+        // is entirely linear.
+        const string_top = ip.parent.string_bytes.items.len;
+        errdefer ip.parent.string_bytes.shrinkRetainingCapacity(string_top);
+
+        var text_iter: TextIterator = .{ .content = ip.content[start..end] };
+        while (text_iter.next()) |content| {
+            switch (content) {
+                .char => |c| try ip.parent.string_bytes.append(ip.parent.allocator, c),
+                .text => |s| try ip.parent.string_bytes.appendSlice(ip.parent.allocator, s),
+                .line_break => try ip.parent.string_bytes.appendSlice(ip.parent.allocator, "\\\n"),
+            }
+        }
+        try ip.parent.string_bytes.append(ip.parent.allocator, 0);
+        return @enumFromInt(string_top);
+    }
+
+    /// Parses emphasis, starting at the beginning of a run of `*` or `_`
+    /// characters. `ip.pos` is left at the last character in the run after
+    /// parsing.
+    fn parseEmphasis(ip: *InlineParser) !void {
+        const char = ip.content[ip.pos];
+        var start = ip.pos;
+        while (ip.pos + 1 < ip.content.len and ip.content[ip.pos + 1] == char) {
+            ip.pos += 1;
+        }
+        var len = ip.pos - start + 1;
+        const underscore = char == '_';
+        const space_before = start == 0 or isWhitespace(ip.content[start - 1]);
+        const space_after = start + len == ip.content.len or isWhitespace(ip.content[start + len]);
+        const punct_before = start == 0 or isPunctuation(ip.content[start - 1]);
+        const punct_after = start + len == ip.content.len or isPunctuation(ip.content[start + len]);
+        // The rules for when emphasis may be closed or opened are stricter for
+        // underscores to avoid inappropriately interpreting snake_case words as
+        // containing emphasis markers.
+        const can_open = if (underscore)
+            !space_after and (space_before or punct_before)
+        else
+            !space_after;
+        const can_close = if (underscore)
+            !space_before and (space_after or punct_after)
+        else
+            !space_before;
+
+        if (can_close and ip.pending_inlines.items.len > 0) {
+            var i = ip.pending_inlines.items.len;
+            while (i > 0 and len > 0) {
+                i -= 1;
+                const opener = &ip.pending_inlines.items[i];
+                if (opener.tag != .emphasis or
+                    opener.data.emphasis.underscore != underscore) continue;
+
+                const close_len = @min(opener.data.emphasis.run_len, len);
+                const opener_end = opener.start + opener.data.emphasis.run_len;
+
+                const emphasis = try ip.encodeEmphasis(opener_end, start, close_len);
+                const emphasis_start = opener_end - close_len;
+                const emphasis_len = start - emphasis_start + close_len;
+                try ip.completed_inlines.append(ip.parent.allocator, .{
+                    .node = emphasis,
+                    .start = emphasis_start,
+                    .len = emphasis_len,
+                });
+
+                // There may still be other openers further down in the
+                // stack to close, or part of this run might serve as an
+                // opener itself.
+                start += close_len;
+                len -= close_len;
+
+                // Remove any pending inlines above this on the stack, since
+                // closing this emphasis will prevent them from being closed.
+                // Additionally, if this opener is completely consumed by
+                // being closed, it can be removed.
+                opener.data.emphasis.run_len -= close_len;
+                if (opener.data.emphasis.run_len == 0) {
+                    ip.pending_inlines.shrinkRetainingCapacity(i);
+                } else {
+                    ip.pending_inlines.shrinkRetainingCapacity(i + 1);
+                }
+            }
+        }
+
+        if (can_open and len > 0) {
+            try ip.pending_inlines.append(ip.parent.allocator, .{
+                .tag = .emphasis,
+                .data = .{ .emphasis = .{
+                    .underscore = underscore,
+                    .run_len = len,
+                } },
+                .start = start,
+            });
+        }
+    }
+
+    /// Encodes emphasis specified by a run of `run_len` emphasis characters,
+    /// with `start..end` being the range of content contained within the
+    /// emphasis.
+    fn encodeEmphasis(ip: *InlineParser, start: usize, end: usize, run_len: usize) !Node.Index {
+        const children = try ip.encodeChildren(start, end);
+        var inner = switch (run_len % 3) {
+            1 => try ip.parent.addNode(.{
+                .tag = .emphasis,
+                .data = .{ .container = .{
+                    .children = children,
+                } },
+            }),
+            2 => try ip.parent.addNode(.{
+                .tag = .strong,
+                .data = .{ .container = .{
+                    .children = children,
+                } },
+            }),
+            0 => strong_emphasis: {
+                const strong = try ip.parent.addNode(.{
+                    .tag = .strong,
+                    .data = .{ .container = .{
+                        .children = children,
+                    } },
+                });
+                break :strong_emphasis try ip.parent.addNode(.{
+                    .tag = .emphasis,
+                    .data = .{ .container = .{
+                        .children = try ip.parent.addExtraChildren(&.{strong}),
+                    } },
+                });
+            },
+            else => unreachable,
+        };
+
+        var run_left = run_len;
+        while (run_left > 3) : (run_left -= 3) {
+            const strong = try ip.parent.addNode(.{
+                .tag = .strong,
+                .data = .{ .container = .{
+                    .children = try ip.parent.addExtraChildren(&.{inner}),
+                } },
+            });
+            inner = try ip.parent.addNode(.{
+                .tag = .emphasis,
+                .data = .{ .container = .{
+                    .children = try ip.parent.addExtraChildren(&.{strong}),
+                } },
+            });
+        }
+
+        return inner;
+    }
+
+    /// Parses a code span, starting at the beginning of the opening backtick
+    /// run. `ip.pos` is left at the last character in the closing run after
+    /// parsing.
+    fn parseCodeSpan(ip: *InlineParser) !void {
+        const opener_start = ip.pos;
+        ip.pos = mem.indexOfNonePos(u8, ip.content, ip.pos, "`") orelse ip.content.len;
+        const opener_len = ip.pos - opener_start;
+
+        const start = ip.pos;
+        const end = while (mem.indexOfScalarPos(u8, ip.content, ip.pos, '`')) |closer_start| {
+            ip.pos = mem.indexOfNonePos(u8, ip.content, closer_start, "`") orelse ip.content.len;
+            const closer_len = ip.pos - closer_start;
+
+            if (closer_len == opener_len) break closer_start;
+        } else unterminated: {
+            ip.pos = ip.content.len;
+            break :unterminated ip.content.len;
+        };
+
+        var content = if (start < ip.content.len)
+            ip.content[start..end]
+        else
+            "";
+        // This single space removal rule allows code spans to be written which
+        // start or end with backticks.
+        if (mem.startsWith(u8, content, " `")) content = content[1..];
+        if (mem.endsWith(u8, content, "` ")) content = content[0 .. content.len - 1];
+
+        const text = try ip.parent.addNode(.{
+            .tag = .code_span,
+            .data = .{ .text = .{
+                .content = try ip.parent.addString(content),
+            } },
+        });
+        try ip.completed_inlines.append(ip.parent.allocator, .{
+            .node = text,
+            .start = opener_start,
+            .len = ip.pos - opener_start,
+        });
+        // Ensure ip.pos is pointing at the last character of the
+        // closer, not after it.
+        ip.pos -= 1;
+    }
+
+    /// Encodes children parsed in the content range `start..end`. The children
+    /// will be text nodes and any completed inlines within the range.
+    fn encodeChildren(ip: *InlineParser, start: usize, end: usize) !ExtraIndex {
+        const scratch_extra_top = ip.parent.scratch_extra.items.len;
+        defer ip.parent.scratch_extra.shrinkRetainingCapacity(scratch_extra_top);
+
+        var child_index = ip.completed_inlines.items.len;
+        while (child_index > 0 and ip.completed_inlines.items[child_index - 1].start >= start) {
+            child_index -= 1;
+        }
+        const start_child_index = child_index;
+
+        var pos = start;
+        while (child_index < ip.completed_inlines.items.len) : (child_index += 1) {
+            const child_inline = ip.completed_inlines.items[child_index];
+            // Completed inlines must be strictly nested within the encodable
+            // content.
+            assert(child_inline.start >= pos and child_inline.start + child_inline.len <= end);
+
+            if (child_inline.start > pos) {
+                try ip.encodeTextNode(pos, child_inline.start);
+            }
+            try ip.parent.addScratchExtraNode(child_inline.node);
+
+            pos = child_inline.start + child_inline.len;
+        }
+        ip.completed_inlines.shrinkRetainingCapacity(start_child_index);
+
+        if (pos < end) {
+            try ip.encodeTextNode(pos, end);
+        }
+
+        const children = ip.parent.scratch_extra.items[scratch_extra_top..];
+        return try ip.parent.addExtraChildren(@ptrCast(children));
+    }
+
+    /// Encodes textual content `ip.content[start..end]` to `scratch_extra`. The
+    /// encoded content may include both `text` and `line_break` nodes.
+    fn encodeTextNode(ip: *InlineParser, start: usize, end: usize) !void {
+        // For efficiency, we can encode directly into string_bytes rather than
+        // creating a temporary string and then encoding it, since this process
+        // is entirely linear.
+        const string_top = ip.parent.string_bytes.items.len;
+        errdefer ip.parent.string_bytes.shrinkRetainingCapacity(string_top);
+
+        var string_start = string_top;
+        var text_iter: TextIterator = .{ .content = ip.content[start..end] };
+        while (text_iter.next()) |content| {
+            switch (content) {
+                .char => |c| try ip.parent.string_bytes.append(ip.parent.allocator, c),
+                .text => |s| try ip.parent.string_bytes.appendSlice(ip.parent.allocator, s),
+                .line_break => {
+                    if (ip.parent.string_bytes.items.len > string_start) {
+                        try ip.parent.string_bytes.append(ip.parent.allocator, 0);
+                        try ip.parent.addScratchExtraNode(try ip.parent.addNode(.{
+                            .tag = .text,
+                            .data = .{ .text = .{
+                                .content = @enumFromInt(string_start),
+                            } },
+                        }));
+                        string_start = ip.parent.string_bytes.items.len;
+                    }
+                    try ip.parent.addScratchExtraNode(try ip.parent.addNode(.{
+                        .tag = .line_break,
+                        .data = .{ .none = {} },
+                    }));
+                },
+            }
+        }
+        if (ip.parent.string_bytes.items.len > string_start) {
+            try ip.parent.string_bytes.append(ip.parent.allocator, 0);
+            try ip.parent.addScratchExtraNode(try ip.parent.addNode(.{
+                .tag = .text,
+                .data = .{ .text = .{
+                    .content = @enumFromInt(string_start),
+                } },
+            }));
+        }
+    }
+
+    /// An iterator over parts of textual content, handling unescaping of
+    /// escaped characters and line breaks.
+    const TextIterator = struct {
+        content: []const u8,
+        pos: usize = 0,
+
+        const Content = union(enum) {
+            char: u8,
+            text: []const u8,
+            line_break,
+        };
+
+        const replacement = "\u{FFFD}";
+
+        fn next(iter: *TextIterator) ?Content {
+            if (iter.pos >= iter.content.len) return null;
+            if (iter.content[iter.pos] == '\\') {
+                iter.pos += 1;
+                if (iter.pos == iter.content.len) {
+                    return .{ .char = '\\' };
+                } else if (iter.content[iter.pos] == '\n') {
+                    iter.pos += 1;
+                    return .line_break;
+                } else if (isPunctuation(iter.content[iter.pos])) {
+                    const c = iter.content[iter.pos];
+                    iter.pos += 1;
+                    return .{ .char = c };
+                } else {
+                    return .{ .char = '\\' };
+                }
+            }
+            return iter.nextCodepoint();
+        }
+
+        fn nextCodepoint(iter: *TextIterator) ?Content {
+            switch (iter.content[iter.pos]) {
+                0 => {
+                    iter.pos += 1;
+                    return .{ .text = replacement };
+                },
+                1...127 => |c| {
+                    iter.pos += 1;
+                    return .{ .char = c };
+                },
+                else => |b| {
+                    const cp_len = std.unicode.utf8ByteSequenceLength(b) catch {
+                        iter.pos += 1;
+                        return .{ .text = replacement };
+                    };
+                    const is_valid = iter.pos + cp_len < iter.content.len and
+                        std.unicode.utf8ValidateSlice(iter.content[iter.pos..][0..cp_len]);
+                    const cp_encoded = if (is_valid)
+                        iter.content[iter.pos..][0..cp_len]
+                    else
+                        replacement;
+                    iter.pos += cp_len;
+                    return .{ .text = cp_encoded };
+                },
+            }
+        }
+    };
+};
+
+fn parseInlines(p: *Parser, content: []const u8) !ExtraIndex {
+    var ip: InlineParser = .{
+        .parent = p,
+        .content = mem.trim(u8, content, " \t\n"),
+    };
+    defer ip.deinit();
+    return try ip.parse();
+}
+
+pub fn extraData(p: Parser, comptime T: type, index: ExtraIndex) ExtraData(T) {
+    const fields = @typeInfo(T).Struct.fields;
+    var i: usize = @intFromEnum(index);
+    var result: T = undefined;
+    inline for (fields) |field| {
+        @field(result, field.name) = switch (field.type) {
+            u32 => p.extra.items[i],
+            else => @compileError("bad field type"),
+        };
+        i += 1;
+    }
+    return .{ .data = result, .end = i };
+}
+
+pub fn extraChildren(p: Parser, index: ExtraIndex) []const Node.Index {
+    const children = p.extraData(Node.Children, index);
+    return @ptrCast(p.extra.items[children.end..][0..children.data.len]);
+}
+
+fn addNode(p: *Parser, node: Node) !Node.Index {
+    const index: Node.Index = @enumFromInt(@as(u32, @intCast(p.nodes.len)));
+    try p.nodes.append(p.allocator, node);
+    return index;
+}
+
+fn addString(p: *Parser, s: []const u8) !StringIndex {
+    if (s.len == 0) return .empty;
+
+    const index: StringIndex = @enumFromInt(@as(u32, @intCast(p.string_bytes.items.len)));
+    try p.string_bytes.ensureUnusedCapacity(p.allocator, s.len + 1);
+    p.string_bytes.appendSliceAssumeCapacity(s);
+    p.string_bytes.appendAssumeCapacity(0);
+    return index;
+}
+
+fn addExtraChildren(p: *Parser, nodes: []const Node.Index) !ExtraIndex {
+    const index: ExtraIndex = @enumFromInt(@as(u32, @intCast(p.extra.items.len)));
+    try p.extra.ensureUnusedCapacity(p.allocator, nodes.len + 1);
+    p.extra.appendAssumeCapacity(@intCast(nodes.len));
+    p.extra.appendSliceAssumeCapacity(@ptrCast(nodes));
+    return index;
+}
+
+fn addScratchExtraNode(p: *Parser, node: Node.Index) !void {
+    try p.scratch_extra.append(p.allocator, @intFromEnum(node));
+}
+
+fn addScratchStringLine(p: *Parser, line: []const u8) !void {
+    try p.scratch_string.ensureUnusedCapacity(p.allocator, line.len + 1);
+    p.scratch_string.appendSliceAssumeCapacity(line);
+    p.scratch_string.appendAssumeCapacity('\n');
+}
+
+fn isBlank(line: []const u8) bool {
+    return mem.indexOfNone(u8, line, " \t") == null;
+}
+
+fn isPunctuation(c: u8) bool {
+    return switch (c) {
+        '!',
+        '"',
+        '#',
+        '$',
+        '%',
+        '&',
+        '\'',
+        '(',
+        ')',
+        '*',
+        '+',
+        ',',
+        '-',
+        '.',
+        '/',
+        ':',
+        ';',
+        '<',
+        '=',
+        '>',
+        '?',
+        '@',
+        '[',
+        '\\',
+        ']',
+        '^',
+        '_',
+        '`',
+        '{',
+        '|',
+        '}',
+        '~',
+        => true,
+        else => false,
+    };
+}
lib/docs/wasm/markdown/renderer.zig
@@ -0,0 +1,254 @@
+const std = @import("std");
+const Document = @import("Document.zig");
+const Node = Document.Node;
+
+/// A Markdown document renderer.
+///
+/// Each concrete `Renderer` type has a `renderDefault` function, with the
+/// intention that custom `renderFn` implementations can call `renderDefault`
+/// for node types for which they require no special rendering.
+pub fn Renderer(comptime Writer: type, comptime Context: type) type {
+    return struct {
+        renderFn: *const fn (
+            r: Self,
+            doc: Document,
+            node: Node.Index,
+            writer: Writer,
+        ) Writer.Error!void = renderDefault,
+        context: Context,
+
+        const Self = @This();
+
+        pub fn render(r: Self, doc: Document, writer: Writer) Writer.Error!void {
+            try r.renderFn(r, doc, .root, writer);
+        }
+
+        pub fn renderDefault(
+            r: Self,
+            doc: Document,
+            node: Node.Index,
+            writer: Writer,
+        ) Writer.Error!void {
+            const data = doc.nodes.items(.data)[@intFromEnum(node)];
+            switch (doc.nodes.items(.tag)[@intFromEnum(node)]) {
+                .root => {
+                    for (doc.extraChildren(data.container.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                },
+                .list => {
+                    if (data.list.start.asNumber()) |start| {
+                        if (start == 1) {
+                            try writer.writeAll("<ol>\n");
+                        } else {
+                            try writer.print("<ol start=\"{}\">\n", .{start});
+                        }
+                    } else {
+                        try writer.writeAll("<ul>\n");
+                    }
+                    for (doc.extraChildren(data.list.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    if (data.list.start.asNumber() != null) {
+                        try writer.writeAll("</ol>\n");
+                    } else {
+                        try writer.writeAll("</ul>\n");
+                    }
+                },
+                .list_item => {
+                    try writer.writeAll("<li>");
+                    for (doc.extraChildren(data.list_item.children)) |child| {
+                        if (data.list_item.tight and doc.nodes.items(.tag)[@intFromEnum(child)] == .paragraph) {
+                            const para_data = doc.nodes.items(.data)[@intFromEnum(child)];
+                            for (doc.extraChildren(para_data.container.children)) |para_child| {
+                                try r.renderFn(r, doc, para_child, writer);
+                            }
+                        } else {
+                            try r.renderFn(r, doc, child, writer);
+                        }
+                    }
+                    try writer.writeAll("</li>\n");
+                },
+                .table => {
+                    try writer.writeAll("<table>\n");
+                    for (doc.extraChildren(data.container.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.writeAll("</table>\n");
+                },
+                .table_row => {
+                    try writer.writeAll("<tr>\n");
+                    for (doc.extraChildren(data.container.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.writeAll("</tr>\n");
+                },
+                .table_cell => {
+                    if (data.table_cell.info.header) {
+                        try writer.writeAll("<th");
+                    } else {
+                        try writer.writeAll("<td");
+                    }
+                    switch (data.table_cell.info.alignment) {
+                        .unset => try writer.writeAll(">"),
+                        else => |a| try writer.print(" style=\"text-align: {s}\">", .{@tagName(a)}),
+                    }
+
+                    for (doc.extraChildren(data.table_cell.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+
+                    if (data.table_cell.info.header) {
+                        try writer.writeAll("</th>\n");
+                    } else {
+                        try writer.writeAll("</td>\n");
+                    }
+                },
+                .heading => {
+                    try writer.print("<h{}>", .{data.heading.level});
+                    for (doc.extraChildren(data.heading.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.print("</h{}>\n", .{data.heading.level});
+                },
+                .code_block => {
+                    const tag = doc.string(data.code_block.tag);
+                    const content = doc.string(data.code_block.content);
+                    if (tag.len > 0) {
+                        try writer.print("<pre><code class=\"{}\">{}</code></pre>\n", .{ fmtHtml(tag), fmtHtml(content) });
+                    } else {
+                        try writer.print("<pre><code>{}</code></pre>\n", .{fmtHtml(content)});
+                    }
+                },
+                .blockquote => {
+                    try writer.writeAll("<blockquote>\n");
+                    for (doc.extraChildren(data.container.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.writeAll("</blockquote>\n");
+                },
+                .paragraph => {
+                    try writer.writeAll("<p>");
+                    for (doc.extraChildren(data.container.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.writeAll("</p>\n");
+                },
+                .thematic_break => {
+                    try writer.writeAll("<hr />\n");
+                },
+                .link => {
+                    const target = doc.string(data.link.target);
+                    try writer.print("<a href=\"{}\">", .{fmtHtml(target)});
+                    for (doc.extraChildren(data.link.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.writeAll("</a>");
+                },
+                .image => {
+                    const target = doc.string(data.link.target);
+                    try writer.print("<img src=\"{}\" alt=\"", .{fmtHtml(target)});
+                    for (doc.extraChildren(data.link.children)) |child| {
+                        try renderInlineNodeText(doc, child, writer);
+                    }
+                    try writer.writeAll("\" />");
+                },
+                .strong => {
+                    try writer.writeAll("<strong>");
+                    for (doc.extraChildren(data.container.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.writeAll("</strong>");
+                },
+                .emphasis => {
+                    try writer.writeAll("<em>");
+                    for (doc.extraChildren(data.container.children)) |child| {
+                        try r.renderFn(r, doc, child, writer);
+                    }
+                    try writer.writeAll("</em>");
+                },
+                .code_span => {
+                    const content = doc.string(data.text.content);
+                    try writer.print("<code>{}</code>", .{fmtHtml(content)});
+                },
+                .text => {
+                    const content = doc.string(data.text.content);
+                    try writer.print("{}", .{fmtHtml(content)});
+                },
+                .line_break => {
+                    try writer.writeAll("<br />\n");
+                },
+            }
+        }
+    };
+}
+
+/// Renders an inline node as plain text. Asserts that the node is an inline and
+/// has no non-inline children.
+pub fn renderInlineNodeText(
+    doc: Document,
+    node: Node.Index,
+    writer: anytype,
+) @TypeOf(writer).Error!void {
+    const data = doc.nodes.items(.data)[@intFromEnum(node)];
+    switch (doc.nodes.items(.tag)[@intFromEnum(node)]) {
+        .root,
+        .list,
+        .list_item,
+        .table,
+        .table_row,
+        .table_cell,
+        .heading,
+        .code_block,
+        .blockquote,
+        .paragraph,
+        .thematic_break,
+        => unreachable, // Blocks
+
+        .link, .image => {
+            for (doc.extraChildren(data.link.children)) |child| {
+                try renderInlineNodeText(doc, child, writer);
+            }
+        },
+        .strong => {
+            for (doc.extraChildren(data.container.children)) |child| {
+                try renderInlineNodeText(doc, child, writer);
+            }
+        },
+        .emphasis => {
+            for (doc.extraChildren(data.container.children)) |child| {
+                try renderInlineNodeText(doc, child, writer);
+            }
+        },
+        .code_span, .text => {
+            const content = doc.string(data.text.content);
+            try writer.print("{}", .{fmtHtml(content)});
+        },
+        .line_break => {
+            try writer.writeAll("\n");
+        },
+    }
+}
+
+pub fn fmtHtml(bytes: []const u8) std.fmt.Formatter(formatHtml) {
+    return .{ .data = bytes };
+}
+
+fn formatHtml(
+    bytes: []const u8,
+    comptime fmt: []const u8,
+    options: std.fmt.FormatOptions,
+    writer: anytype,
+) !void {
+    _ = fmt;
+    _ = options;
+    for (bytes) |b| {
+        switch (b) {
+            '<' => try writer.writeAll("&lt;"),
+            '>' => try writer.writeAll("&gt;"),
+            '&' => try writer.writeAll("&amp;"),
+            '"' => try writer.writeAll("&quot;"),
+            else => try writer.writeByte(b),
+        }
+    }
+}
lib/docs/wasm/Decl.zig
@@ -0,0 +1,180 @@
+ast_node: Ast.Node.Index,
+file: Walk.File.Index,
+/// The decl whose namespace this is in.
+parent: Index,
+
+pub const ExtraInfo = struct {
+    is_pub: bool,
+    name: []const u8,
+    /// This might not be a doc_comment token in which case there are no doc comments.
+    first_doc_comment: Ast.TokenIndex,
+};
+
+pub const Index = enum(u32) {
+    none = std.math.maxInt(u32),
+    _,
+
+    pub fn get(i: Index) *Decl {
+        return &Walk.decls.items[@intFromEnum(i)];
+    }
+};
+
+pub fn is_pub(d: *const Decl) bool {
+    return d.extra_info().is_pub;
+}
+
+pub fn extra_info(d: *const Decl) ExtraInfo {
+    const ast = d.file.get_ast();
+    const token_tags = ast.tokens.items(.tag);
+    const node_tags = ast.nodes.items(.tag);
+    switch (node_tags[d.ast_node]) {
+        .root => return .{
+            .name = "",
+            .is_pub = true,
+            .first_doc_comment = if (token_tags[0] == .container_doc_comment)
+                0
+            else
+                token_tags.len - 1,
+        },
+
+        .global_var_decl,
+        .local_var_decl,
+        .simple_var_decl,
+        .aligned_var_decl,
+        => {
+            const var_decl = ast.fullVarDecl(d.ast_node).?;
+            const name_token = var_decl.ast.mut_token + 1;
+            assert(token_tags[name_token] == .identifier);
+            const ident_name = ast.tokenSlice(name_token);
+            return .{
+                .name = ident_name,
+                .is_pub = var_decl.visib_token != null,
+                .first_doc_comment = findFirstDocComment(ast, var_decl.firstToken()),
+            };
+        },
+
+        .fn_proto,
+        .fn_proto_multi,
+        .fn_proto_one,
+        .fn_proto_simple,
+        .fn_decl,
+        => {
+            var buf: [1]Ast.Node.Index = undefined;
+            const fn_proto = ast.fullFnProto(&buf, d.ast_node).?;
+            const name_token = fn_proto.name_token.?;
+            assert(token_tags[name_token] == .identifier);
+            const ident_name = ast.tokenSlice(name_token);
+            return .{
+                .name = ident_name,
+                .is_pub = fn_proto.visib_token != null,
+                .first_doc_comment = findFirstDocComment(ast, fn_proto.firstToken()),
+            };
+        },
+
+        else => |t| {
+            log.debug("hit '{s}'", .{@tagName(t)});
+            unreachable;
+        },
+    }
+}
+
+pub fn value_node(d: *const Decl) ?Ast.Node.Index {
+    const ast = d.file.get_ast();
+    const node_tags = ast.nodes.items(.tag);
+    const token_tags = ast.tokens.items(.tag);
+    return switch (node_tags[d.ast_node]) {
+        .fn_proto,
+        .fn_proto_multi,
+        .fn_proto_one,
+        .fn_proto_simple,
+        .fn_decl,
+        .root,
+        => d.ast_node,
+
+        .global_var_decl,
+        .local_var_decl,
+        .simple_var_decl,
+        .aligned_var_decl,
+        => {
+            const var_decl = ast.fullVarDecl(d.ast_node).?;
+            if (token_tags[var_decl.ast.mut_token] == .keyword_const)
+                return var_decl.ast.init_node;
+
+            return null;
+        },
+
+        else => null,
+    };
+}
+
+pub fn categorize(decl: *const Decl) Walk.Category {
+    return decl.file.categorize_decl(decl.ast_node);
+}
+
+pub fn fqn(decl: *const Decl, out: *std.ArrayListUnmanaged(u8)) Oom!void {
+    try decl.reset_with_path(out);
+    if (decl.parent != .none) {
+        try append_parent_ns(out, decl.parent);
+        try out.appendSlice(gpa, decl.extra_info().name);
+    } else {
+        out.items.len -= 1; // remove the trailing '.'
+    }
+}
+
+pub fn reset_with_path(decl: *const Decl, list: *std.ArrayListUnmanaged(u8)) Oom!void {
+    list.clearRetainingCapacity();
+
+    // Prefer the package name alias.
+    for (Walk.packages.keys(), Walk.packages.values()) |pkg_name, pkg_file| {
+        if (pkg_file == decl.file) {
+            try list.ensureUnusedCapacity(gpa, pkg_name.len + 1);
+            list.appendSliceAssumeCapacity(pkg_name);
+            list.appendAssumeCapacity('.');
+            return;
+        }
+    }
+
+    const file_path = decl.file.path();
+    try list.ensureUnusedCapacity(gpa, file_path.len + 1);
+    list.appendSliceAssumeCapacity(file_path);
+    for (list.items) |*byte| switch (byte.*) {
+        '/' => byte.* = '.',
+        else => continue,
+    };
+    if (std.mem.endsWith(u8, list.items, ".zig")) {
+        list.items.len -= 3;
+    } else {
+        list.appendAssumeCapacity('.');
+    }
+}
+
+pub fn append_parent_ns(list: *std.ArrayListUnmanaged(u8), parent: Decl.Index) Oom!void {
+    assert(parent != .none);
+    const decl = parent.get();
+    if (decl.parent != .none) {
+        try append_parent_ns(list, decl.parent);
+        try list.appendSlice(gpa, decl.extra_info().name);
+        try list.append(gpa, '.');
+    }
+}
+
+pub fn findFirstDocComment(ast: *const Ast, token: Ast.TokenIndex) Ast.TokenIndex {
+    const token_tags = ast.tokens.items(.tag);
+    var it = token;
+    while (it > 0) {
+        it -= 1;
+        if (token_tags[it] != .doc_comment) {
+            return it + 1;
+        }
+    }
+    return it;
+}
+
+const Decl = @This();
+const std = @import("std");
+const Ast = std.zig.Ast;
+const Walk = @import("Walk.zig");
+const gpa = std.heap.wasm_allocator;
+const assert = std.debug.assert;
+const log = std.log;
+const Oom = error{OutOfMemory};
lib/docs/wasm/main.zig
@@ -0,0 +1,920 @@
+const gpa = std.heap.wasm_allocator;
+
+const std = @import("std");
+const log = std.log;
+const assert = std.debug.assert;
+const Ast = std.zig.Ast;
+const Walk = @import("Walk.zig");
+const markdown = @import("markdown.zig");
+const Decl = @import("Decl.zig");
+
+const js = struct {
+    extern "js" fn log(ptr: [*]const u8, len: usize) void;
+    extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
+};
+
+pub const std_options: std.Options = .{
+    .logFn = logFn,
+    //.log_level = .debug,
+};
+
+pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
+    _ = st;
+    _ = addr;
+    log.err("panic: {s}", .{msg});
+    @trap();
+}
+
+fn logFn(
+    comptime message_level: log.Level,
+    comptime scope: @TypeOf(.enum_literal),
+    comptime format: []const u8,
+    args: anytype,
+) void {
+    const level_txt = comptime message_level.asText();
+    const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
+    var buf: [500]u8 = undefined;
+    const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
+        buf[buf.len - 3 ..][0..3].* = "...".*;
+        break :l &buf;
+    };
+    js.log(line.ptr, line.len);
+}
+
+export fn alloc(n: usize) [*]u8 {
+    const slice = gpa.alloc(u8, n) catch @panic("OOM");
+    return slice.ptr;
+}
+
+export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
+    const tar_bytes = tar_ptr[0..tar_len];
+    //log.debug("received {d} bytes of tar file", .{tar_bytes.len});
+
+    unpack_inner(tar_bytes) catch |err| {
+        fatal("unable to unpack tar: {s}", .{@errorName(err)});
+    };
+}
+
+var query_string: std.ArrayListUnmanaged(u8) = .{};
+var query_results: std.ArrayListUnmanaged(Decl.Index) = .{};
+
+/// Resizes the query string to be the correct length; returns the pointer to
+/// the query string.
+export fn query_begin(query_string_len: usize) [*]u8 {
+    query_string.resize(gpa, query_string_len) catch @panic("OOM");
+    return query_string.items.ptr;
+}
+
+/// Executes the query. Returns the pointer to the query results which is an
+/// array of u32.
+/// The first element is the length of the array.
+/// Subsequent elements are Decl.Index values which are all public
+/// declarations.
+export fn query_exec(ignore_case: bool) [*]Decl.Index {
+    const query = query_string.items;
+    log.debug("querying '{s}'", .{query});
+    query_exec_fallible(query, ignore_case) catch |err| switch (err) {
+        error.OutOfMemory => @panic("OOM"),
+    };
+    query_results.items[0] = @enumFromInt(query_results.items.len - 1);
+    return query_results.items.ptr;
+}
+
+const max_matched_items = 2000;
+
+fn query_exec_fallible(query: []const u8, ignore_case: bool) !void {
+    const Score = packed struct(u32) {
+        points: u16,
+        segments: u16,
+    };
+    const g = struct {
+        var full_path_search_text: std.ArrayListUnmanaged(u8) = .{};
+        var full_path_search_text_lower: std.ArrayListUnmanaged(u8) = .{};
+        var doc_search_text: std.ArrayListUnmanaged(u8) = .{};
+        /// Each element matches a corresponding query_results element.
+        var scores: std.ArrayListUnmanaged(Score) = .{};
+    };
+
+    // First element stores the size of the list.
+    try query_results.resize(gpa, 1);
+    // Corresponding point value is meaningless and therefore undefined.
+    try g.scores.resize(gpa, 1);
+
+    decl_loop: for (Walk.decls.items, 0..) |*decl, decl_index| {
+        const info = decl.extra_info();
+        if (!info.is_pub) continue;
+
+        try decl.reset_with_path(&g.full_path_search_text);
+        if (decl.parent != .none)
+            try Decl.append_parent_ns(&g.full_path_search_text, decl.parent);
+        try g.full_path_search_text.appendSlice(gpa, info.name);
+
+        try g.full_path_search_text_lower.resize(gpa, g.full_path_search_text.items.len);
+        @memcpy(g.full_path_search_text_lower.items, g.full_path_search_text.items);
+
+        const ast = decl.file.get_ast();
+        try collect_docs(&g.doc_search_text, ast, info.first_doc_comment);
+
+        if (ignore_case) {
+            ascii_lower(g.full_path_search_text_lower.items);
+            ascii_lower(g.doc_search_text.items);
+        }
+
+        var it = std.mem.tokenizeScalar(u8, query, ' ');
+        var points: u16 = 0;
+        var bypass_limit = false;
+        while (it.next()) |term| {
+            // exact, case sensitive match of full decl path
+            if (std.mem.eql(u8, g.full_path_search_text.items, term)) {
+                points += 4;
+                bypass_limit = true;
+                continue;
+            }
+            // exact, case sensitive match of just decl name
+            if (std.mem.eql(u8, info.name, term)) {
+                points += 3;
+                bypass_limit = true;
+                continue;
+            }
+            // substring, case insensitive match of full decl path
+            if (std.mem.indexOf(u8, g.full_path_search_text_lower.items, term) != null) {
+                points += 2;
+                continue;
+            }
+            if (std.mem.indexOf(u8, g.doc_search_text.items, term) != null) {
+                points += 1;
+                continue;
+            }
+            continue :decl_loop;
+        }
+
+        if (query_results.items.len < max_matched_items or bypass_limit) {
+            try query_results.append(gpa, @enumFromInt(decl_index));
+            try g.scores.append(gpa, .{
+                .points = points,
+                .segments = @intCast(count_scalar(g.full_path_search_text.items, '.')),
+            });
+        }
+    }
+
+    const sort_context: struct {
+        pub fn swap(sc: @This(), a_index: usize, b_index: usize) void {
+            _ = sc;
+            std.mem.swap(Score, &g.scores.items[a_index], &g.scores.items[b_index]);
+            std.mem.swap(Decl.Index, &query_results.items[a_index], &query_results.items[b_index]);
+        }
+
+        pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
+            _ = sc;
+            const a_score = g.scores.items[a_index];
+            const b_score = g.scores.items[b_index];
+            if (b_score.points < a_score.points) {
+                return true;
+            } else if (b_score.points > a_score.points) {
+                return false;
+            } else if (a_score.segments < b_score.segments) {
+                return true;
+            } else if (a_score.segments > b_score.segments) {
+                return false;
+            } else {
+                const a_decl = query_results.items[a_index];
+                const b_decl = query_results.items[b_index];
+                const a_file_path = a_decl.get().file.path();
+                const b_file_path = b_decl.get().file.path();
+                // TODO Also check the local namespace  inside the file
+                return std.mem.lessThan(u8, b_file_path, a_file_path);
+            }
+        }
+    } = .{};
+
+    std.mem.sortUnstableContext(1, query_results.items.len, sort_context);
+
+    if (query_results.items.len > max_matched_items)
+        query_results.shrinkRetainingCapacity(max_matched_items);
+}
+
+const String = Slice(u8);
+
+fn Slice(T: type) type {
+    return packed struct(u64) {
+        ptr: u32,
+        len: u32,
+
+        fn init(s: []const T) @This() {
+            return .{
+                .ptr = @intFromPtr(s.ptr),
+                .len = s.len,
+            };
+        }
+    };
+}
+
+var string_result: std.ArrayListUnmanaged(u8) = .{};
+
+export fn decl_fields(decl_index: Decl.Index) Slice(Ast.Node.Index) {
+    return Slice(Ast.Node.Index).init(decl_fields_fallible(decl_index) catch @panic("OOM"));
+}
+
+fn decl_fields_fallible(decl_index: Decl.Index) ![]Ast.Node.Index {
+    const g = struct {
+        var result: std.ArrayListUnmanaged(Ast.Node.Index) = .{};
+    };
+    g.result.clearRetainingCapacity();
+    const decl = decl_index.get();
+    const ast = decl.file.get_ast();
+    const node_tags = ast.nodes.items(.tag);
+    const value_node = decl.value_node() orelse return &.{};
+    var buf: [2]Ast.Node.Index = undefined;
+    const container_decl = ast.fullContainerDecl(&buf, value_node) orelse return &.{};
+    for (container_decl.ast.members) |member_node| switch (node_tags[member_node]) {
+        .container_field_init,
+        .container_field_align,
+        .container_field,
+        => try g.result.append(gpa, member_node),
+
+        else => continue,
+    };
+    return g.result.items;
+}
+
+export fn decl_field_html(decl_index: Decl.Index, field_node: Ast.Node.Index) String {
+    string_result.clearRetainingCapacity();
+    decl_field_html_fallible(&string_result, decl_index, field_node) catch @panic("OOM");
+    return String.init(string_result.items);
+}
+
+fn decl_field_html_fallible(
+    out: *std.ArrayListUnmanaged(u8),
+    decl_index: Decl.Index,
+    field_node: Ast.Node.Index,
+) !void {
+    const decl = decl_index.get();
+    const ast = decl.file.get_ast();
+    try out.appendSlice(gpa, "<pre><code>");
+    try file_source_html(decl.file, out, field_node);
+    try out.appendSlice(gpa, "</code></pre>");
+
+    const field = ast.fullContainerField(field_node).?;
+    const first_doc_comment = Decl.findFirstDocComment(ast, field.firstToken());
+
+    if (ast.tokens.items(.tag)[first_doc_comment] == .doc_comment) {
+        try out.appendSlice(gpa, "<div class=\"fieldDocs\">");
+        try render_docs(out, ast, first_doc_comment, false);
+        try out.appendSlice(gpa, "</div>");
+    }
+}
+
+export fn decl_fn_proto_html(decl_index: Decl.Index) String {
+    const decl = decl_index.get();
+    const ast = decl.file.get_ast();
+    const node_tags = ast.nodes.items(.tag);
+    const node_datas = ast.nodes.items(.data);
+    const proto_node = switch (node_tags[decl.ast_node]) {
+        .fn_decl => node_datas[decl.ast_node].lhs,
+
+        .fn_proto,
+        .fn_proto_one,
+        .fn_proto_simple,
+        .fn_proto_multi,
+        => decl.ast_node,
+
+        else => unreachable,
+    };
+
+    string_result.clearRetainingCapacity();
+    file_source_html(decl.file, &string_result, proto_node) catch |err| {
+        fatal("unable to render source: {s}", .{@errorName(err)});
+    };
+    return String.init(string_result.items);
+}
+
+export fn decl_source_html(decl_index: Decl.Index) String {
+    const decl = decl_index.get();
+
+    string_result.clearRetainingCapacity();
+    file_source_html(decl.file, &string_result, decl.ast_node) catch |err| {
+        fatal("unable to render source: {s}", .{@errorName(err)});
+    };
+    return String.init(string_result.items);
+}
+
+export fn decl_doctest_html(decl_index: Decl.Index) String {
+    const decl = decl_index.get();
+    const doctest_ast_node = decl.file.get().doctests.get(decl.ast_node) orelse
+        return String.init("");
+
+    string_result.clearRetainingCapacity();
+    file_source_html(decl.file, &string_result, doctest_ast_node) catch |err| {
+        fatal("unable to render source: {s}", .{@errorName(err)});
+    };
+    return String.init(string_result.items);
+}
+
+export fn decl_fqn(decl_index: Decl.Index) String {
+    const decl = decl_index.get();
+    decl.fqn(&string_result) catch @panic("OOM");
+    return String.init(string_result.items);
+}
+
+export fn decl_parent(decl_index: Decl.Index) Decl.Index {
+    const decl = decl_index.get();
+    return decl.parent;
+}
+
+export fn decl_file_path(decl_index: Decl.Index) String {
+    string_result.clearRetainingCapacity();
+    string_result.appendSlice(gpa, decl_index.get().file.path()) catch @panic("OOM");
+    return String.init(string_result.items);
+}
+
+export fn decl_category_name(decl_index: Decl.Index) String {
+    const decl = decl_index.get();
+    const ast = decl.file.get_ast();
+    const token_tags = ast.tokens.items(.tag);
+    const name = switch (decl.categorize()) {
+        .namespace => |node| {
+            const node_tags = ast.nodes.items(.tag);
+            if (node_tags[decl.ast_node] == .root)
+                return String.init("struct");
+            string_result.clearRetainingCapacity();
+            var buf: [2]Ast.Node.Index = undefined;
+            const container_decl = ast.fullContainerDecl(&buf, node).?;
+            if (container_decl.layout_token) |t| {
+                if (token_tags[t] == .keyword_extern) {
+                    string_result.appendSlice(gpa, "extern ") catch @panic("OOM");
+                }
+            }
+            const main_token_tag = token_tags[container_decl.ast.main_token];
+            string_result.appendSlice(gpa, main_token_tag.lexeme().?) catch @panic("OOM");
+            return String.init(string_result.items);
+        },
+        .global_variable => "Global Variable",
+        .function => "Function",
+        .type => "Type",
+        .error_set => "Error Set",
+        .global_const => "Constant",
+        .primitive => "Primitive Value",
+        .alias => "Alias",
+    };
+    return String.init(name);
+}
+
+export fn decl_name(decl_index: Decl.Index) String {
+    const decl = decl_index.get();
+    string_result.clearRetainingCapacity();
+    const name = n: {
+        if (decl.parent == .none) {
+            // Then it is the root struct of a file.
+            break :n std.fs.path.stem(decl.file.path());
+        }
+        break :n decl.extra_info().name;
+    };
+    string_result.appendSlice(gpa, name) catch @panic("OOM");
+    return String.init(string_result.items);
+}
+
+export fn decl_docs_html(decl_index: Decl.Index, short: bool) String {
+    const decl = decl_index.get();
+    const ast = decl.file.get_ast();
+    string_result.clearRetainingCapacity();
+    render_docs(&string_result, ast, decl.extra_info().first_doc_comment, short) catch @panic("OOM");
+    return String.init(string_result.items);
+}
+
+fn collect_docs(
+    list: *std.ArrayListUnmanaged(u8),
+    ast: *const Ast,
+    first_doc_comment: Ast.TokenIndex,
+) Oom!void {
+    const token_tags = ast.tokens.items(.tag);
+    list.clearRetainingCapacity();
+    var it = first_doc_comment;
+    while (true) : (it += 1) switch (token_tags[it]) {
+        .doc_comment, .container_doc_comment => {
+            // It is tempting to trim this string but think carefully about how
+            // that will affect the markdown parser.
+            const line = ast.tokenSlice(it)[3..];
+            try list.appendSlice(gpa, line);
+        },
+        else => break,
+    };
+}
+
+fn render_docs(
+    out: *std.ArrayListUnmanaged(u8),
+    ast: *const Ast,
+    first_doc_comment: Ast.TokenIndex,
+    short: bool,
+) Oom!void {
+    const token_tags = ast.tokens.items(.tag);
+
+    var parser = try markdown.Parser.init(gpa);
+    defer parser.deinit();
+    var it = first_doc_comment;
+    while (true) : (it += 1) switch (token_tags[it]) {
+        .doc_comment, .container_doc_comment => {
+            const line = ast.tokenSlice(it)[3..];
+            if (short and line.len == 0) break;
+            try parser.feedLine(line);
+        },
+        else => break,
+    };
+
+    var parsed_doc = try parser.endInput();
+    defer parsed_doc.deinit(gpa);
+
+    const Writer = std.ArrayListUnmanaged(u8).Writer;
+    const Renderer = markdown.Renderer(Writer, void);
+    const renderer: Renderer = .{
+        .context = {},
+        .renderFn = struct {
+            fn render(
+                r: Renderer,
+                doc: markdown.Document,
+                node: markdown.Document.Node.Index,
+                writer: Writer,
+            ) !void {
+                const data = doc.nodes.items(.data)[@intFromEnum(node)];
+                switch (doc.nodes.items(.tag)[@intFromEnum(node)]) {
+                    // TODO: detect identifier references (dotted paths) in
+                    // these three node types and render them appropriately.
+                    // Also, syntax highlighting can be applied in code blocks
+                    // unless the tag says otherwise.
+                    .code_block => {
+                        const tag = doc.string(data.code_block.tag);
+                        _ = tag;
+                        const content = doc.string(data.code_block.content);
+                        try writer.print("<pre><code>{}</code></pre>\n", .{markdown.fmtHtml(content)});
+                    },
+                    .code_span => {
+                        const content = doc.string(data.text.content);
+                        try writer.print("<code>{}</code>", .{markdown.fmtHtml(content)});
+                    },
+                    .text => {
+                        const content = doc.string(data.text.content);
+                        try writer.print("{}", .{markdown.fmtHtml(content)});
+                    },
+
+                    else => try Renderer.renderDefault(r, doc, node, writer),
+                }
+            }
+        }.render,
+    };
+    try renderer.render(parsed_doc, out.writer(gpa));
+}
+
+export fn decl_type_html(decl_index: Decl.Index) String {
+    const decl = decl_index.get();
+    const ast = decl.file.get_ast();
+    string_result.clearRetainingCapacity();
+    _ = ast; // TODO
+    string_result.appendSlice(gpa, "TODO_type_here") catch @panic("OOM");
+    return String.init(string_result.items);
+}
+
+const Oom = error{OutOfMemory};
+
+fn unpack_inner(tar_bytes: []u8) !void {
+    var fbs = std.io.fixedBufferStream(tar_bytes);
+    var file_name_buffer: [1024]u8 = undefined;
+    var link_name_buffer: [1024]u8 = undefined;
+    var it = std.tar.iterator(fbs.reader(), .{
+        .file_name_buffer = &file_name_buffer,
+        .link_name_buffer = &link_name_buffer,
+    });
+    while (try it.next()) |tar_file| {
+        switch (tar_file.kind) {
+            .normal => {
+                if (tar_file.size == 0 and tar_file.name.len == 0) break;
+                if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
+                    log.debug("found file: '{s}'", .{tar_file.name});
+                    const file_name = try gpa.dupe(u8, tar_file.name);
+                    if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
+                        const pkg_name = file_name[0..pkg_name_end];
+                        const gop = try Walk.packages.getOrPut(gpa, pkg_name);
+                        const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
+                        if (!gop.found_existing or
+                            std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
+                            std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
+                        {
+                            gop.value_ptr.* = file;
+                        }
+                        const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
+                        assert(file == try Walk.add_file(file_name, file_bytes));
+                    }
+                } else {
+                    log.warn("skipping: '{s}' - the tar creation should have done that", .{
+                        tar_file.name,
+                    });
+                }
+                try tar_file.skip();
+            },
+            else => continue,
+        }
+    }
+}
+
+fn fatal(comptime format: []const u8, args: anytype) noreturn {
+    var buf: [500]u8 = undefined;
+    const line = std.fmt.bufPrint(&buf, format, args) catch l: {
+        buf[buf.len - 3 ..][0..3].* = "...".*;
+        break :l &buf;
+    };
+    js.panic(line.ptr, line.len);
+}
+
+fn ascii_lower(bytes: []u8) void {
+    for (bytes) |*b| b.* = std.ascii.toLower(b.*);
+}
+
+export fn package_name(index: u32) String {
+    const names = Walk.packages.keys();
+    return String.init(if (index >= names.len) "" else names[index]);
+}
+
+export fn find_package_root(pkg: Walk.PackageIndex) Decl.Index {
+    const root_file = Walk.packages.values()[@intFromEnum(pkg)];
+    const result = root_file.findRootDecl();
+    assert(result != .none);
+    return result;
+}
+
+/// Set by `set_input_string`.
+var input_string: std.ArrayListUnmanaged(u8) = .{};
+
+export fn set_input_string(len: usize) [*]u8 {
+    input_string.resize(gpa, len) catch @panic("OOM");
+    return input_string.items.ptr;
+}
+
+/// Looks up the root struct decl corresponding to a file by path.
+/// Uses `input_string`.
+export fn find_file_root() Decl.Index {
+    const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
+    return file.findRootDecl();
+}
+
+/// Uses `input_string`.
+export fn find_decl() Decl.Index {
+    const g = struct {
+        var match_fqn: std.ArrayListUnmanaged(u8) = .{};
+    };
+    for (Walk.decls.items, 0..) |*decl, decl_index| {
+        decl.fqn(&g.match_fqn) catch @panic("OOM");
+        if (std.mem.eql(u8, g.match_fqn.items, input_string.items)) {
+            //const path = @as(Decl.Index, @enumFromInt(decl_index)).get().file.path();
+            //log.debug("find_decl '{s}' found in {s}", .{ input_string.items, path });
+            return @enumFromInt(decl_index);
+        }
+    }
+    return .none;
+}
+
+/// Set only by `categorize_decl`; read only by `get_aliasee`, valid only
+/// when `categorize_decl` returns `.alias`.
+var global_aliasee: Decl.Index = .none;
+
+export fn get_aliasee() Decl.Index {
+    return global_aliasee;
+}
+export fn categorize_decl(decl_index: Decl.Index, resolve_alias_count: usize) Walk.Category.Tag {
+    global_aliasee = .none;
+    var chase_alias_n = resolve_alias_count;
+    var decl = decl_index.get();
+    while (true) {
+        const result = decl.categorize();
+        switch (decl.categorize()) {
+            .alias => |new_index| {
+                assert(new_index != .none);
+                global_aliasee = new_index;
+                if (chase_alias_n > 0) {
+                    chase_alias_n -= 1;
+                    decl = new_index.get();
+                    continue;
+                }
+            },
+            else => {},
+        }
+        return result;
+    }
+}
+
+export fn namespace_members(parent: Decl.Index, include_private: bool) Slice(Decl.Index) {
+    const g = struct {
+        var members: std.ArrayListUnmanaged(Decl.Index) = .{};
+    };
+
+    g.members.clearRetainingCapacity();
+
+    for (Walk.decls.items, 0..) |*decl, i| {
+        if (decl.parent == parent) {
+            if (include_private or decl.is_pub()) {
+                g.members.append(gpa, @enumFromInt(i)) catch @panic("OOM");
+            }
+        }
+    }
+
+    return Slice(Decl.Index).init(g.members.items);
+}
+
+fn file_source_html(
+    file_index: Walk.File.Index,
+    out: *std.ArrayListUnmanaged(u8),
+    root_node: Ast.Node.Index,
+) !void {
+    const ast = file_index.get_ast();
+    const file = file_index.get();
+
+    const g = struct {
+        var field_access_buffer: std.ArrayListUnmanaged(u8) = .{};
+    };
+
+    const token_tags = ast.tokens.items(.tag);
+    const token_starts = ast.tokens.items(.start);
+
+    const start_token = ast.firstToken(root_node);
+    const end_token = ast.lastToken(root_node) + 1;
+
+    var cursor: usize = token_starts[start_token];
+
+    for (
+        token_tags[start_token..end_token],
+        token_starts[start_token..end_token],
+        start_token..,
+    ) |tag, start, token_index| {
+        const between = ast.source[cursor..start];
+        try appendEscaped(out, between);
+        if (tag == .eof) break;
+        const slice = ast.tokenSlice(token_index);
+        cursor = start + slice.len;
+        switch (tag) {
+            .eof => unreachable,
+
+            .keyword_addrspace,
+            .keyword_align,
+            .keyword_and,
+            .keyword_asm,
+            .keyword_async,
+            .keyword_await,
+            .keyword_break,
+            .keyword_catch,
+            .keyword_comptime,
+            .keyword_const,
+            .keyword_continue,
+            .keyword_defer,
+            .keyword_else,
+            .keyword_enum,
+            .keyword_errdefer,
+            .keyword_error,
+            .keyword_export,
+            .keyword_extern,
+            .keyword_for,
+            .keyword_if,
+            .keyword_inline,
+            .keyword_noalias,
+            .keyword_noinline,
+            .keyword_nosuspend,
+            .keyword_opaque,
+            .keyword_or,
+            .keyword_orelse,
+            .keyword_packed,
+            .keyword_anyframe,
+            .keyword_pub,
+            .keyword_resume,
+            .keyword_return,
+            .keyword_linksection,
+            .keyword_callconv,
+            .keyword_struct,
+            .keyword_suspend,
+            .keyword_switch,
+            .keyword_test,
+            .keyword_threadlocal,
+            .keyword_try,
+            .keyword_union,
+            .keyword_unreachable,
+            .keyword_usingnamespace,
+            .keyword_var,
+            .keyword_volatile,
+            .keyword_allowzero,
+            .keyword_while,
+            .keyword_anytype,
+            .keyword_fn,
+            => {
+                try out.appendSlice(gpa, "<span class=\"tok-kw\">");
+                try appendEscaped(out, slice);
+                try out.appendSlice(gpa, "</span>");
+            },
+
+            .string_literal,
+            .char_literal,
+            .multiline_string_literal_line,
+            => {
+                try out.appendSlice(gpa, "<span class=\"tok-str\">");
+                try appendEscaped(out, slice);
+                try out.appendSlice(gpa, "</span>");
+            },
+
+            .builtin => {
+                try out.appendSlice(gpa, "<span class=\"tok-builtin\">");
+                try appendEscaped(out, slice);
+                try out.appendSlice(gpa, "</span>");
+            },
+
+            .doc_comment,
+            .container_doc_comment,
+            => {
+                try out.appendSlice(gpa, "<span class=\"tok-comment\">");
+                try appendEscaped(out, slice);
+                try out.appendSlice(gpa, "</span>");
+            },
+
+            .identifier => i: {
+                if (std.mem.eql(u8, slice, "undefined") or
+                    std.mem.eql(u8, slice, "null") or
+                    std.mem.eql(u8, slice, "true") or
+                    std.mem.eql(u8, slice, "false"))
+                {
+                    try out.appendSlice(gpa, "<span class=\"tok-null\">");
+                    try appendEscaped(out, slice);
+                    try out.appendSlice(gpa, "</span>");
+                    break :i;
+                }
+
+                if (std.zig.primitives.isPrimitive(slice)) {
+                    try out.appendSlice(gpa, "<span class=\"tok-type\">");
+                    try appendEscaped(out, slice);
+                    try out.appendSlice(gpa, "</span>");
+                    break :i;
+                }
+
+                if (file.token_parents.get(token_index)) |field_access_node| {
+                    g.field_access_buffer.clearRetainingCapacity();
+                    try walk_field_accesses(file_index, &g.field_access_buffer, field_access_node);
+                    if (g.field_access_buffer.items.len > 0) {
+                        try out.appendSlice(gpa, "<a href=\"#");
+                        try out.appendSlice(gpa, g.field_access_buffer.items); // TODO url escape
+                        try out.appendSlice(gpa, "\">");
+                        try appendEscaped(out, slice);
+                        try out.appendSlice(gpa, "</a>");
+                    } else {
+                        try appendEscaped(out, slice);
+                    }
+                    break :i;
+                }
+
+                {
+                    g.field_access_buffer.clearRetainingCapacity();
+                    try resolve_ident_link(file_index, &g.field_access_buffer, token_index);
+                    if (g.field_access_buffer.items.len > 0) {
+                        try out.appendSlice(gpa, "<a href=\"#");
+                        try out.appendSlice(gpa, g.field_access_buffer.items); // TODO url escape
+                        try out.appendSlice(gpa, "\">");
+                        try appendEscaped(out, slice);
+                        try out.appendSlice(gpa, "</a>");
+                        break :i;
+                    }
+                }
+
+                try appendEscaped(out, slice);
+            },
+
+            .number_literal => {
+                try out.appendSlice(gpa, "<span class=\"tok-number\">");
+                try appendEscaped(out, slice);
+                try out.appendSlice(gpa, "</span>");
+            },
+
+            .bang,
+            .pipe,
+            .pipe_pipe,
+            .pipe_equal,
+            .equal,
+            .equal_equal,
+            .equal_angle_bracket_right,
+            .bang_equal,
+            .l_paren,
+            .r_paren,
+            .semicolon,
+            .percent,
+            .percent_equal,
+            .l_brace,
+            .r_brace,
+            .l_bracket,
+            .r_bracket,
+            .period,
+            .period_asterisk,
+            .ellipsis2,
+            .ellipsis3,
+            .caret,
+            .caret_equal,
+            .plus,
+            .plus_plus,
+            .plus_equal,
+            .plus_percent,
+            .plus_percent_equal,
+            .plus_pipe,
+            .plus_pipe_equal,
+            .minus,
+            .minus_equal,
+            .minus_percent,
+            .minus_percent_equal,
+            .minus_pipe,
+            .minus_pipe_equal,
+            .asterisk,
+            .asterisk_equal,
+            .asterisk_asterisk,
+            .asterisk_percent,
+            .asterisk_percent_equal,
+            .asterisk_pipe,
+            .asterisk_pipe_equal,
+            .arrow,
+            .colon,
+            .slash,
+            .slash_equal,
+            .comma,
+            .ampersand,
+            .ampersand_equal,
+            .question_mark,
+            .angle_bracket_left,
+            .angle_bracket_left_equal,
+            .angle_bracket_angle_bracket_left,
+            .angle_bracket_angle_bracket_left_equal,
+            .angle_bracket_angle_bracket_left_pipe,
+            .angle_bracket_angle_bracket_left_pipe_equal,
+            .angle_bracket_right,
+            .angle_bracket_right_equal,
+            .angle_bracket_angle_bracket_right,
+            .angle_bracket_angle_bracket_right_equal,
+            .tilde,
+            => try appendEscaped(out, slice),
+
+            .invalid, .invalid_periodasterisks => return error.InvalidToken,
+        }
+    }
+}
+
+fn resolve_ident_link(
+    file_index: Walk.File.Index,
+    out: *std.ArrayListUnmanaged(u8),
+    ident_token: Ast.TokenIndex,
+) Oom!void {
+    const decl_index = file_index.get().lookup_token(ident_token);
+    if (decl_index == .none) return;
+
+    const decl = decl_index.get();
+    switch (decl.categorize()) {
+        .alias => |alias_decl| try alias_decl.get().fqn(out),
+        else => try decl.fqn(out),
+    }
+}
+
+fn walk_field_accesses(
+    file_index: Walk.File.Index,
+    out: *std.ArrayListUnmanaged(u8),
+    node: Ast.Node.Index,
+) Oom!void {
+    const ast = file_index.get_ast();
+    const node_tags = ast.nodes.items(.tag);
+    assert(node_tags[node] == .field_access);
+    const node_datas = ast.nodes.items(.data);
+    const main_tokens = ast.nodes.items(.main_token);
+    const object_node = node_datas[node].lhs;
+    const dot_token = main_tokens[node];
+    const field_ident = dot_token + 1;
+    switch (node_tags[object_node]) {
+        .identifier => {
+            const lhs_ident = main_tokens[object_node];
+            try resolve_ident_link(file_index, out, lhs_ident);
+        },
+        .field_access => {
+            try walk_field_accesses(file_index, out, object_node);
+        },
+        else => {},
+    }
+    if (out.items.len > 0) {
+        try out.append(gpa, '.');
+        try out.appendSlice(gpa, ast.tokenSlice(field_ident));
+    }
+}
+
+fn appendEscaped(out: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
+    for (s) |c| {
+        try out.ensureUnusedCapacity(gpa, 6);
+        switch (c) {
+            '&' => out.appendSliceAssumeCapacity("&amp;"),
+            '<' => out.appendSliceAssumeCapacity("&lt;"),
+            '>' => out.appendSliceAssumeCapacity("&gt;"),
+            '"' => out.appendSliceAssumeCapacity("&quot;"),
+            else => out.appendAssumeCapacity(c),
+        }
+    }
+}
+
+fn count_scalar(haystack: []const u8, needle: u8) usize {
+    var total: usize = 0;
+    for (haystack) |elem| {
+        if (elem == needle)
+            total += 1;
+    }
+    return total;
+}
lib/docs/wasm/markdown.zig
@@ -0,0 +1,933 @@
+//! Markdown parsing and rendering support.
+//!
+//! A Markdown document consists of a series of blocks. Depending on its type,
+//! each block may contain other blocks, inline content, or nothing. The
+//! supported blocks are as follows:
+//!
+//! - **List** - a sequence of list items of the same type.
+//!
+//! - **List item** - unordered list items start with `-`, `*`, or `+` followed
+//!   by a space. Ordered list items start with a number between 0 and
+//!   999,999,999, followed by a `.` or `)` and a space. The number of an
+//!   ordered list item only matters for the first item in the list (to
+//!   determine the starting number of the list). All subsequent ordered list
+//!   items will have sequentially increasing numbers.
+//!
+//!   All list items may contain block content. Any content indented at least as
+//!   far as the end of the list item marker (including the space after it) is
+//!   considered part of the list item.
+//!
+//!   Lists which have no blank lines between items or between direct children
+//!   of items are considered _tight_, and direct child paragraphs of tight list
+//!   items are rendered without `<p>` tags.
+//!
+//! - **Table** - a sequence of adjacent table row lines, where each line starts
+//!   and ends with a `|`, and cells within the row are delimited by `|`s.
+//!
+//!   The first or second row of a table may be a _header delimiter row_, which
+//!   is a row consisting of cells of the pattern `---` (for unset column
+//!   alignment), `:--` (for left alignment), `:-:` (for center alignment), or
+//!   `--:` (for right alignment). The number of `-`s must be at least one, but
+//!   is otherwise arbitrary. If there is a row just before the header delimiter
+//!   row, it becomes the header row for the table (a table need not have a
+//!   header row at all).
+//!
+//! - **Heading** - a sequence of between 1 and 6 `#` characters, followed by a
+//!   space and further inline content on the same line.
+//!
+//! - **Code block** - a sequence of at least 3 `` ` `` characters (a _fence_),
+//!   optionally followed by a "tag" on the same line, and continuing until a
+//!   line consisting only of a closing fence whose length matches the opening
+//!   fence, or until the end of the containing block.
+//!
+//!   The content of a code block is not parsed as inline content. It is
+//!   included verbatim in the output document (minus leading indentation up to
+//!   the position of the opening fence).
+//!
+//! - **Blockquote** - a sequence of lines preceded by `>` characters.
+//!
+//! - **Paragraph** - ordinary text, parsed as inline content, ending with a
+//!   blank line or the end of the containing block.
+//!
+//!   Paragraphs which are part of another block may be "lazily" continued by
+//!   subsequent paragraph lines even if those lines would not ordinarily be
+//!   considered part of the containing block. For example, this is a single
+//!   list item, not a list item followed by a paragraph:
+//!
+//!   ```markdown
+//!   - First line of content.
+//!   This content is still part of the paragraph,
+//!   even though it isn't indented far enough.
+//!   ```
+//!
+//! - **Thematic break** - a line consisting of at least three matching `-`,
+//!   `_`, or `*` characters and, optionally, spaces.
+//!
+//! Indentation may consist of spaces and tabs. The use of tabs is not
+//! recommended: a tab is treated the same as a single space for the purpose of
+//! determining the indentation level, and is not recognized as a space for
+//! block starters which require one (for example, `-` followed by a tab is not
+//! a valid list item).
+//!
+//! The supported inlines are as follows:
+//!
+//! - **Link** - of the format `[text](target)`. `text` may contain inline
+//!   content. `target` may contain `\`-escaped characters and balanced
+//!   parentheses.
+//!
+//! - **Image** - a link directly preceded by a `!`. The link text is
+//!   interpreted as the alt text of the image.
+//!
+//! - **Emphasis** - a run of `*` or `_` characters may be an emphasis opener,
+//!   closer, or both. For `*` characters, the run may be an opener as long as
+//!   it is not directly followed by a whitespace character (or the end of the
+//!   inline content) and a closer as long as it is not directly preceded by
+//!   one. For `_` characters, this rule is strengthened by requiring that the
+//!   run also be preceded by a whitespace or punctuation character (for
+//!   openers) or followed by one (for closers), to avoid mangling `snake_case`
+//!   words.
+//!
+//!   The rule for emphasis handling is greedy: any run that can close existing
+//!   emphasis will do so, otherwise it will open emphasis. A single run may
+//!   serve both functions: the middle `**` in the following example both closes
+//!   the initial emphasis and opens a new one:
+//!
+//!   ```markdown
+//!   *one**two*
+//!   ```
+//!
+//!   A single `*` or `_` is used for normal emphasis (HTML `<em>`), and a
+//!   double `**` or `__` is used for strong emphasis (HTML `<strong>`). Even
+//!   longer runs may be used to produce further nested emphasis (though only
+//!   `***` and `___` to produce `<em><strong>` is really useful).
+//!
+//! - **Code span** - a run of `` ` `` characters, terminated by a matching run
+//!   or the end of inline content. The content of a code span is not parsed
+//!   further.
+//!
+//! - **Text** - normal text is interpreted as-is, except that `\` may be used
+//!   to escape any punctuation character, preventing it from being interpreted
+//!   according to other syntax rules. A `\` followed by a line break within a
+//!   paragraph is interpreted as a hard line break.
+//!
+//!   Any null bytes or invalid UTF-8 bytes within text are replaced with Unicode
+//!   replacement characters, `U+FFFD`.
+
+const std = @import("std");
+const testing = std.testing;
+
+pub const Document = @import("markdown/Document.zig");
+pub const Parser = @import("markdown/Parser.zig");
+pub const Renderer = @import("markdown/renderer.zig").Renderer;
+pub const renderNodeInlineText = @import("markdown/renderer.zig").renderNodeInlineText;
+pub const fmtHtml = @import("markdown/renderer.zig").fmtHtml;
+
+// Avoid exposing main to other files merely importing this one.
+pub const main = if (@import("root") == @This())
+    mainImpl
+else
+    @compileError("only available as root source file");
+
+fn mainImpl() !void {
+    const gpa = std.heap.c_allocator;
+
+    var parser = try Parser.init(gpa);
+    defer parser.deinit();
+
+    var stdin_buf = std.io.bufferedReader(std.io.getStdIn().reader());
+    var line_buf = std.ArrayList(u8).init(gpa);
+    defer line_buf.deinit();
+    while (stdin_buf.reader().streamUntilDelimiter(line_buf.writer(), '\n', null)) {
+        if (line_buf.getLastOrNull() == '\r') _ = line_buf.pop();
+        try parser.feedLine(line_buf.items);
+        line_buf.clearRetainingCapacity();
+    } else |err| switch (err) {
+        error.EndOfStream => {},
+        else => |e| return e,
+    }
+
+    var doc = try parser.endInput();
+    defer doc.deinit(gpa);
+
+    var stdout_buf = std.io.bufferedWriter(std.io.getStdOut().writer());
+    try doc.render(stdout_buf.writer());
+    try stdout_buf.flush();
+}
+
+test "empty document" {
+    try testRender("", "");
+    try testRender("   ", "");
+    try testRender("\n \n\t\n   \n", "");
+}
+
+test "unordered lists" {
+    try testRender(
+        \\- Spam
+        \\- Spam
+        \\- Spam
+        \\- Eggs
+        \\- Bacon
+        \\- Spam
+        \\
+        \\* Spam
+        \\* Spam
+        \\* Spam
+        \\* Eggs
+        \\* Bacon
+        \\* Spam
+        \\
+        \\+ Spam
+        \\+ Spam
+        \\+ Spam
+        \\+ Eggs
+        \\+ Bacon
+        \\+ Spam
+        \\
+    ,
+        \\<ul>
+        \\<li>Spam</li>
+        \\<li>Spam</li>
+        \\<li>Spam</li>
+        \\<li>Eggs</li>
+        \\<li>Bacon</li>
+        \\<li>Spam</li>
+        \\</ul>
+        \\<ul>
+        \\<li>Spam</li>
+        \\<li>Spam</li>
+        \\<li>Spam</li>
+        \\<li>Eggs</li>
+        \\<li>Bacon</li>
+        \\<li>Spam</li>
+        \\</ul>
+        \\<ul>
+        \\<li>Spam</li>
+        \\<li>Spam</li>
+        \\<li>Spam</li>
+        \\<li>Eggs</li>
+        \\<li>Bacon</li>
+        \\<li>Spam</li>
+        \\</ul>
+        \\
+    );
+}
+
+test "ordered lists" {
+    try testRender(
+        \\1. Breakfast
+        \\2. Second breakfast
+        \\3. Lunch
+        \\2. Afternoon snack
+        \\1. Dinner
+        \\6. Dessert
+        \\7. Midnight snack
+        \\
+        \\1) Breakfast
+        \\2) Second breakfast
+        \\3) Lunch
+        \\2) Afternoon snack
+        \\1) Dinner
+        \\6) Dessert
+        \\7) Midnight snack
+        \\
+        \\1001. Breakfast
+        \\2. Second breakfast
+        \\3. Lunch
+        \\2. Afternoon snack
+        \\1. Dinner
+        \\6. Dessert
+        \\7. Midnight snack
+        \\
+        \\1001) Breakfast
+        \\2) Second breakfast
+        \\3) Lunch
+        \\2) Afternoon snack
+        \\1) Dinner
+        \\6) Dessert
+        \\7) Midnight snack
+        \\
+    ,
+        \\<ol>
+        \\<li>Breakfast</li>
+        \\<li>Second breakfast</li>
+        \\<li>Lunch</li>
+        \\<li>Afternoon snack</li>
+        \\<li>Dinner</li>
+        \\<li>Dessert</li>
+        \\<li>Midnight snack</li>
+        \\</ol>
+        \\<ol>
+        \\<li>Breakfast</li>
+        \\<li>Second breakfast</li>
+        \\<li>Lunch</li>
+        \\<li>Afternoon snack</li>
+        \\<li>Dinner</li>
+        \\<li>Dessert</li>
+        \\<li>Midnight snack</li>
+        \\</ol>
+        \\<ol start="1001">
+        \\<li>Breakfast</li>
+        \\<li>Second breakfast</li>
+        \\<li>Lunch</li>
+        \\<li>Afternoon snack</li>
+        \\<li>Dinner</li>
+        \\<li>Dessert</li>
+        \\<li>Midnight snack</li>
+        \\</ol>
+        \\<ol start="1001">
+        \\<li>Breakfast</li>
+        \\<li>Second breakfast</li>
+        \\<li>Lunch</li>
+        \\<li>Afternoon snack</li>
+        \\<li>Dinner</li>
+        \\<li>Dessert</li>
+        \\<li>Midnight snack</li>
+        \\</ol>
+        \\
+    );
+}
+
+test "nested lists" {
+    try testRender(
+        \\- - Item 1.
+        \\  - Item 2.
+        \\Item 2 continued.
+        \\  * New list.
+        \\
+    ,
+        \\<ul>
+        \\<li><ul>
+        \\<li>Item 1.</li>
+        \\<li>Item 2.
+        \\Item 2 continued.</li>
+        \\</ul>
+        \\<ul>
+        \\<li>New list.</li>
+        \\</ul>
+        \\</li>
+        \\</ul>
+        \\
+    );
+}
+
+test "lists with block content" {
+    try testRender(
+        \\1. Item 1.
+        \\2. Item 2.
+        \\
+        \\   This one has another paragraph.
+        \\3. Item 3.
+        \\
+        \\- > Blockquote.
+        \\- - Sub-list.
+        \\  - Sub-list continued.
+        \\  * Different sub-list.
+        \\- ## Heading.
+        \\
+        \\  Some contents below the heading.
+        \\  1. Item 1.
+        \\  2. Item 2.
+        \\  3. Item 3.
+        \\
+    ,
+        \\<ol>
+        \\<li><p>Item 1.</p>
+        \\</li>
+        \\<li><p>Item 2.</p>
+        \\<p>This one has another paragraph.</p>
+        \\</li>
+        \\<li><p>Item 3.</p>
+        \\</li>
+        \\</ol>
+        \\<ul>
+        \\<li><blockquote>
+        \\<p>Blockquote.</p>
+        \\</blockquote>
+        \\</li>
+        \\<li><ul>
+        \\<li>Sub-list.</li>
+        \\<li>Sub-list continued.</li>
+        \\</ul>
+        \\<ul>
+        \\<li>Different sub-list.</li>
+        \\</ul>
+        \\</li>
+        \\<li><h2>Heading.</h2>
+        \\<p>Some contents below the heading.</p>
+        \\<ol>
+        \\<li>Item 1.</li>
+        \\<li>Item 2.</li>
+        \\<li>Item 3.</li>
+        \\</ol>
+        \\</li>
+        \\</ul>
+        \\
+    );
+}
+
+test "tables" {
+    try testRender(
+        \\| Operator | Meaning          |
+        \\| :------: | ---------------- |
+        \\| `+`      | Add              |
+        \\| `-`      | Subtract         |
+        \\| `*`      | Multiply         |
+        \\| `/`      | Divide           |
+        \\| `??`     | **Not sure yet** |
+        \\
+        \\| Item 1 | Value 1 |
+        \\| Item 2 | Value 2 |
+        \\| Item 3 | Value 3 |
+        \\| Item 4 | Value 4 |
+        \\
+        \\| :--- | :----: | ----: |
+        \\| Left | Center | Right |
+        \\
+    ,
+        \\<table>
+        \\<tr>
+        \\<th style="text-align: center">Operator</th>
+        \\<th>Meaning</th>
+        \\</tr>
+        \\<tr>
+        \\<td style="text-align: center"><code>+</code></td>
+        \\<td>Add</td>
+        \\</tr>
+        \\<tr>
+        \\<td style="text-align: center"><code>-</code></td>
+        \\<td>Subtract</td>
+        \\</tr>
+        \\<tr>
+        \\<td style="text-align: center"><code>*</code></td>
+        \\<td>Multiply</td>
+        \\</tr>
+        \\<tr>
+        \\<td style="text-align: center"><code>/</code></td>
+        \\<td>Divide</td>
+        \\</tr>
+        \\<tr>
+        \\<td style="text-align: center"><code>??</code></td>
+        \\<td><strong>Not sure yet</strong></td>
+        \\</tr>
+        \\</table>
+        \\<table>
+        \\<tr>
+        \\<td>Item 1</td>
+        \\<td>Value 1</td>
+        \\</tr>
+        \\<tr>
+        \\<td>Item 2</td>
+        \\<td>Value 2</td>
+        \\</tr>
+        \\<tr>
+        \\<td>Item 3</td>
+        \\<td>Value 3</td>
+        \\</tr>
+        \\<tr>
+        \\<td>Item 4</td>
+        \\<td>Value 4</td>
+        \\</tr>
+        \\</table>
+        \\<table>
+        \\<tr>
+        \\<td style="text-align: left">Left</td>
+        \\<td style="text-align: center">Center</td>
+        \\<td style="text-align: right">Right</td>
+        \\</tr>
+        \\</table>
+        \\
+    );
+}
+
+test "table with uneven number of columns" {
+    try testRender(
+        \\| One |
+        \\| :-- | :--: |
+        \\| One | Two | Three |
+        \\
+    ,
+        \\<table>
+        \\<tr>
+        \\<th style="text-align: left">One</th>
+        \\</tr>
+        \\<tr>
+        \\<td style="text-align: left">One</td>
+        \\<td style="text-align: center">Two</td>
+        \\<td>Three</td>
+        \\</tr>
+        \\</table>
+        \\
+    );
+}
+
+test "table with escaped pipes" {
+    try testRender(
+        \\| One \| Two |
+        \\| --- | --- |
+        \\| One \| Two |
+        \\
+    ,
+        \\<table>
+        \\<tr>
+        \\<th>One | Two</th>
+        \\</tr>
+        \\<tr>
+        \\<td>One | Two</td>
+        \\</tr>
+        \\</table>
+        \\
+    );
+}
+
+test "table with pipes in code spans" {
+    try testRender(
+        \\| `|` | Bitwise _OR_ |
+        \\| `||` | Combines error sets |
+        \\| `` `||` `` | Escaped version |
+        \\| ` ``||`` ` | Another escaped version |
+        \\| `Oops unterminated code span |
+        \\
+    ,
+        \\<table>
+        \\<tr>
+        \\<td><code>|</code></td>
+        \\<td>Bitwise <em>OR</em></td>
+        \\</tr>
+        \\<tr>
+        \\<td><code>||</code></td>
+        \\<td>Combines error sets</td>
+        \\</tr>
+        \\<tr>
+        \\<td><code>`||`</code></td>
+        \\<td>Escaped version</td>
+        \\</tr>
+        \\<tr>
+        \\<td><code>``||``</code></td>
+        \\<td>Another escaped version</td>
+        \\</tr>
+        \\</table>
+        \\<p>| <code>Oops unterminated code span |</code></p>
+        \\
+    );
+}
+
+test "tables require leading and trailing pipes" {
+    try testRender(
+        \\Not | a | table
+        \\
+        \\| But | this | is |
+        \\
+    ,
+        \\<p>Not | a | table</p>
+        \\<table>
+        \\<tr>
+        \\<td>But</td>
+        \\<td>this</td>
+        \\<td>is</td>
+        \\</tr>
+        \\</table>
+        \\
+    );
+}
+
+test "headings" {
+    try testRender(
+        \\# Level one
+        \\## Level two
+        \\### Level three
+        \\#### Level four
+        \\##### Level five
+        \\###### Level six
+        \\####### Not a heading
+        \\
+    ,
+        \\<h1>Level one</h1>
+        \\<h2>Level two</h2>
+        \\<h3>Level three</h3>
+        \\<h4>Level four</h4>
+        \\<h5>Level five</h5>
+        \\<h6>Level six</h6>
+        \\<p>####### Not a heading</p>
+        \\
+    );
+}
+
+test "headings with inline content" {
+    try testRender(
+        \\# Outline of `std.zig`
+        \\## **Important** notes
+        \\### ***Nested* inline content**
+        \\
+    ,
+        \\<h1>Outline of <code>std.zig</code></h1>
+        \\<h2><strong>Important</strong> notes</h2>
+        \\<h3><strong><em>Nested</em> inline content</strong></h3>
+        \\
+    );
+}
+
+test "code blocks" {
+    try testRender(
+        \\```
+        \\Hello, world!
+        \\This is some code.
+        \\```
+        \\``` zig test
+        \\const std = @import("std");
+        \\
+        \\test {
+        \\    try std.testing.expect(2 + 2 == 4);
+        \\}
+        \\```
+        \\
+    ,
+        \\<pre><code>Hello, world!
+        \\This is some code.
+        \\</code></pre>
+        \\<pre><code class="zig test">const std = @import(&quot;std&quot;);
+        \\
+        \\test {
+        \\    try std.testing.expect(2 + 2 == 4);
+        \\}
+        \\</code></pre>
+        \\
+    );
+}
+
+test "blockquotes" {
+    try testRender(
+        \\> > You miss 100% of the shots you don't take.
+        \\> >
+        \\> > ~ Wayne Gretzky
+        \\>
+        \\> ~ Michael Scott
+        \\
+    ,
+        \\<blockquote>
+        \\<blockquote>
+        \\<p>You miss 100% of the shots you don't take.</p>
+        \\<p>~ Wayne Gretzky</p>
+        \\</blockquote>
+        \\<p>~ Michael Scott</p>
+        \\</blockquote>
+        \\
+    );
+}
+
+test "blockquote lazy continuation lines" {
+    try testRender(
+        \\>>>>Deeply nested blockquote
+        \\>>which continues on another line
+        \\and then yet another one.
+        \\>>
+        \\>> But now two of them have been closed.
+        \\
+        \\And then there were none.
+        \\
+    ,
+        \\<blockquote>
+        \\<blockquote>
+        \\<blockquote>
+        \\<blockquote>
+        \\<p>Deeply nested blockquote
+        \\which continues on another line
+        \\and then yet another one.</p>
+        \\</blockquote>
+        \\</blockquote>
+        \\<p>But now two of them have been closed.</p>
+        \\</blockquote>
+        \\</blockquote>
+        \\<p>And then there were none.</p>
+        \\
+    );
+}
+
+test "paragraphs" {
+    try testRender(
+        \\Paragraph one.
+        \\
+        \\Paragraph two.
+        \\Still in the paragraph.
+        \\    So is this.
+        \\
+        \\
+        \\
+        \\
+        \\ Last paragraph.
+        \\
+    ,
+        \\<p>Paragraph one.</p>
+        \\<p>Paragraph two.
+        \\Still in the paragraph.
+        \\So is this.</p>
+        \\<p>Last paragraph.</p>
+        \\
+    );
+}
+
+test "thematic breaks" {
+    try testRender(
+        \\---
+        \\***
+        \\___
+        \\          ---
+        \\ - - - - - - - - - - -
+        \\
+    ,
+        \\<hr />
+        \\<hr />
+        \\<hr />
+        \\<hr />
+        \\<hr />
+        \\
+    );
+}
+
+test "links" {
+    try testRender(
+        \\[Link](https://example.com)
+        \\[Link *with inlines*](https://example.com)
+        \\[Nested parens](https://example.com/nested(parens(inside)))
+        \\[Escaped parens](https://example.com/\)escaped\()
+        \\[Line break in target](test\
+        \\target)
+        \\
+    ,
+        \\<p><a href="https://example.com">Link</a>
+        \\<a href="https://example.com">Link <em>with inlines</em></a>
+        \\<a href="https://example.com/nested(parens(inside))">Nested parens</a>
+        \\<a href="https://example.com/)escaped(">Escaped parens</a>
+        \\<a href="test\
+        \\target">Line break in target</a></p>
+        \\
+    );
+}
+
+test "images" {
+    try testRender(
+        \\![Alt text](https://example.com/image.png)
+        \\![Alt text *with inlines*](https://example.com/image.png)
+        \\![Nested parens](https://example.com/nested(parens(inside)).png)
+        \\![Escaped parens](https://example.com/\)escaped\(.png)
+        \\![Line break in target](test\
+        \\target)
+        \\
+    ,
+        \\<p><img src="https://example.com/image.png" alt="Alt text" />
+        \\<img src="https://example.com/image.png" alt="Alt text with inlines" />
+        \\<img src="https://example.com/nested(parens(inside)).png" alt="Nested parens" />
+        \\<img src="https://example.com/)escaped(.png" alt="Escaped parens" />
+        \\<img src="test\
+        \\target" alt="Line break in target" /></p>
+        \\
+    );
+}
+
+test "emphasis" {
+    try testRender(
+        \\*Emphasis.*
+        \\**Strong.**
+        \\***Strong emphasis.***
+        \\****More...****
+        \\*****MORE...*****
+        \\******Even more...******
+        \\*******OK, this is enough.*******
+        \\
+    ,
+        \\<p><em>Emphasis.</em>
+        \\<strong>Strong.</strong>
+        \\<em><strong>Strong emphasis.</strong></em>
+        \\<em><strong><em>More...</em></strong></em>
+        \\<em><strong><strong>MORE...</strong></strong></em>
+        \\<em><strong><em><strong>Even more...</strong></em></strong></em>
+        \\<em><strong><em><strong><em>OK, this is enough.</em></strong></em></strong></em></p>
+        \\
+    );
+    try testRender(
+        \\_Emphasis._
+        \\__Strong.__
+        \\___Strong emphasis.___
+        \\____More...____
+        \\_____MORE..._____
+        \\______Even more...______
+        \\_______OK, this is enough._______
+        \\
+    ,
+        \\<p><em>Emphasis.</em>
+        \\<strong>Strong.</strong>
+        \\<em><strong>Strong emphasis.</strong></em>
+        \\<em><strong><em>More...</em></strong></em>
+        \\<em><strong><strong>MORE...</strong></strong></em>
+        \\<em><strong><em><strong>Even more...</strong></em></strong></em>
+        \\<em><strong><em><strong><em>OK, this is enough.</em></strong></em></strong></em></p>
+        \\
+    );
+}
+
+test "nested emphasis" {
+    try testRender(
+        \\**Hello, *world!***
+        \\*Hello, **world!***
+        \\**Hello, _world!_**
+        \\_Hello, **world!**_
+        \\*Hello, **nested** *world!**
+        \\***Hello,* world!**
+        \\__**Hello, world!**__
+        \\****Hello,** world!**
+        \\__Hello,_ world!_
+        \\*Test**123*
+        \\__Test____123__
+        \\
+    ,
+        \\<p><strong>Hello, <em>world!</em></strong>
+        \\<em>Hello, <strong>world!</strong></em>
+        \\<strong>Hello, <em>world!</em></strong>
+        \\<em>Hello, <strong>world!</strong></em>
+        \\<em>Hello, <strong>nested</strong> <em>world!</em></em>
+        \\<strong><em>Hello,</em> world!</strong>
+        \\<strong><strong>Hello, world!</strong></strong>
+        \\<strong><strong>Hello,</strong> world!</strong>
+        \\<em><em>Hello,</em> world!</em>
+        \\<em>Test</em><em>123</em>
+        \\<strong>Test____123</strong></p>
+        \\
+    );
+}
+
+test "emphasis precedence" {
+    try testRender(
+        \\*First one _wins*_.
+        \\_*No other __rule matters.*_
+        \\
+    ,
+        \\<p><em>First one _wins</em>_.
+        \\<em><em>No other __rule matters.</em></em></p>
+        \\
+    );
+}
+
+test "emphasis open and close" {
+    try testRender(
+        \\Cannot open: *
+        \\Cannot open: _
+        \\*Cannot close: *
+        \\_Cannot close: _
+        \\
+        \\foo*bar*baz
+        \\foo_bar_baz
+        \\foo**bar**baz
+        \\foo__bar__baz
+        \\
+    ,
+        \\<p>Cannot open: *
+        \\Cannot open: _
+        \\*Cannot close: *
+        \\_Cannot close: _</p>
+        \\<p>foo<em>bar</em>baz
+        \\foo_bar_baz
+        \\foo<strong>bar</strong>baz
+        \\foo__bar__baz</p>
+        \\
+    );
+}
+
+test "code spans" {
+    try testRender(
+        \\`Hello, world!`
+        \\```Multiple `backticks` can be used.```
+        \\`**This** does not produce emphasis.`
+        \\`` `Backtick enclosed string.` ``
+        \\`Delimiter lengths ```must``` match.`
+        \\
+        \\Unterminated ``code...
+        \\
+        \\Weird empty code span: `
+        \\
+        \\**Very important code: `hi`**
+        \\
+    ,
+        \\<p><code>Hello, world!</code>
+        \\<code>Multiple `backticks` can be used.</code>
+        \\<code>**This** does not produce emphasis.</code>
+        \\<code>`Backtick enclosed string.`</code>
+        \\<code>Delimiter lengths ```must``` match.</code></p>
+        \\<p>Unterminated <code>code...</code></p>
+        \\<p>Weird empty code span: <code></code></p>
+        \\<p><strong>Very important code: <code>hi</code></strong></p>
+        \\
+    );
+}
+
+test "backslash escapes" {
+    try testRender(
+        \\Not \*emphasized\*.
+        \\Literal \\backslashes\\.
+        \\Not code: \`hi\`.
+        \\\# Not a title.
+        \\#\# Also not a title.
+        \\\> Not a blockquote.
+        \\\- Not a list item.
+        \\\| Not a table. |
+        \\| Also not a table. \|
+        \\Any \punctuation\ characte\r can be escaped:
+        \\\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~
+        \\
+    ,
+        \\<p>Not *emphasized*.
+        \\Literal \backslashes\.
+        \\Not code: `hi`.
+        \\# Not a title.
+        \\## Also not a title.
+        \\&gt; Not a blockquote.
+        \\- Not a list item.
+        \\| Not a table. |
+        \\| Also not a table. |
+        \\Any \punctuation\ characte\r can be escaped:
+        \\!&quot;#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</p>
+        \\
+    );
+}
+
+test "hard line breaks" {
+    try testRender(
+        \\The iguana sits\
+        \\Perched atop a short desk chair\
+        \\Writing code in Zig
+        \\
+    ,
+        \\<p>The iguana sits<br />
+        \\Perched atop a short desk chair<br />
+        \\Writing code in Zig</p>
+        \\
+    );
+}
+
+test "Unicode handling" {
+    // Null bytes must be replaced.
+    try testRender("\x00\x00\x00", "<p>\u{FFFD}\u{FFFD}\u{FFFD}</p>\n");
+
+    // Invalid UTF-8 must be replaced.
+    try testRender("\xC0\x80\xE0\x80\x80\xF0\x80\x80\x80", "<p>\u{FFFD}\u{FFFD}\u{FFFD}</p>\n");
+    try testRender("\xED\xA0\x80\xED\xBF\xBF", "<p>\u{FFFD}\u{FFFD}</p>\n");
+
+    // Incomplete UTF-8 must be replaced.
+    try testRender("\xE2\x82", "<p>\u{FFFD}</p>\n");
+}
+
+fn testRender(input: []const u8, expected: []const u8) !void {
+    var parser = try Parser.init(testing.allocator);
+    defer parser.deinit();
+
+    var lines = std.mem.split(u8, input, "\n");
+    while (lines.next()) |line| {
+        try parser.feedLine(line);
+    }
+    var doc = try parser.endInput();
+    defer doc.deinit(testing.allocator);
+
+    var actual = std.ArrayList(u8).init(testing.allocator);
+    defer actual.deinit();
+    try doc.render(actual.writer());
+
+    try testing.expectEqualStrings(expected, actual.items);
+}
lib/docs/wasm/Walk.zig
@@ -0,0 +1,928 @@
+//! Find and annotate identifiers with links to their declarations.
+pub var files: std.StringArrayHashMapUnmanaged(File) = .{};
+pub var decls: std.ArrayListUnmanaged(Decl) = .{};
+pub var packages: std.StringArrayHashMapUnmanaged(File.Index) = .{};
+
+arena: std.mem.Allocator,
+file: File.Index,
+
+/// keep in sync with "CAT_" constants in main.js
+pub const Category = union(enum(u8)) {
+    namespace: Ast.Node.Index,
+    global_variable: Ast.Node.Index,
+    function: Ast.Node.Index,
+    primitive: Ast.Node.Index,
+    error_set: Ast.Node.Index,
+    global_const: Ast.Node.Index,
+    alias: Decl.Index,
+    type,
+
+    pub const Tag = @typeInfo(Category).Union.tag_type.?;
+};
+
+pub const File = struct {
+    ast: Ast,
+    /// Maps identifiers to the declarations they point to.
+    ident_decls: std.AutoArrayHashMapUnmanaged(Ast.TokenIndex, Ast.Node.Index) = .{},
+    /// Maps field access identifiers to the containing field access node.
+    token_parents: std.AutoArrayHashMapUnmanaged(Ast.TokenIndex, Ast.Node.Index) = .{},
+    /// Maps declarations to their global index.
+    node_decls: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, Decl.Index) = .{},
+    /// Maps function declarations to doctests.
+    doctests: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, Ast.Node.Index) = .{},
+
+    pub fn lookup_token(file: *File, token: Ast.TokenIndex) Decl.Index {
+        const decl_node = file.ident_decls.get(token) orelse return .none;
+        return file.node_decls.get(decl_node) orelse return .none;
+    }
+
+    pub const Index = enum(u32) {
+        _,
+
+        fn add_decl(i: Index, node: Ast.Node.Index, parent_decl: Decl.Index) Oom!Decl.Index {
+            try decls.append(gpa, .{
+                .ast_node = node,
+                .file = i,
+                .parent = parent_decl,
+            });
+            const decl_index: Decl.Index = @enumFromInt(decls.items.len - 1);
+            try i.get().node_decls.put(gpa, node, decl_index);
+            return decl_index;
+        }
+
+        pub fn get(i: File.Index) *File {
+            return &files.values()[@intFromEnum(i)];
+        }
+
+        pub fn get_ast(i: File.Index) *Ast {
+            return &i.get().ast;
+        }
+
+        pub fn path(i: File.Index) []const u8 {
+            return files.keys()[@intFromEnum(i)];
+        }
+
+        pub fn findRootDecl(file_index: File.Index) Decl.Index {
+            return file_index.get().node_decls.values()[0];
+        }
+
+        pub fn categorize_decl(file_index: File.Index, node: Ast.Node.Index) Category {
+            const ast = file_index.get_ast();
+            const node_tags = ast.nodes.items(.tag);
+            const token_tags = ast.tokens.items(.tag);
+            switch (node_tags[node]) {
+                .root => return .{ .namespace = node },
+
+                .global_var_decl,
+                .local_var_decl,
+                .simple_var_decl,
+                .aligned_var_decl,
+                => {
+                    const var_decl = ast.fullVarDecl(node).?;
+                    if (token_tags[var_decl.ast.mut_token] == .keyword_var)
+                        return .{ .global_variable = node };
+
+                    return categorize_expr(file_index, var_decl.ast.init_node);
+                },
+
+                .fn_proto,
+                .fn_proto_multi,
+                .fn_proto_one,
+                .fn_proto_simple,
+                .fn_decl,
+                => return .{ .function = node },
+
+                else => unreachable,
+            }
+        }
+
+        pub fn categorize_expr(file_index: File.Index, node: Ast.Node.Index) Category {
+            const ast = file_index.get_ast();
+            const node_tags = ast.nodes.items(.tag);
+            const node_datas = ast.nodes.items(.data);
+            return switch (node_tags[node]) {
+                .container_decl,
+                .container_decl_trailing,
+                .container_decl_arg,
+                .container_decl_arg_trailing,
+                .container_decl_two,
+                .container_decl_two_trailing,
+                .tagged_union,
+                .tagged_union_trailing,
+                .tagged_union_enum_tag,
+                .tagged_union_enum_tag_trailing,
+                .tagged_union_two,
+                .tagged_union_two_trailing,
+                => .{ .namespace = node },
+
+                .error_set_decl,
+                => .{ .error_set = node },
+
+                .identifier => {
+                    const name_token = ast.nodes.items(.main_token)[node];
+                    const ident_name = ast.tokenSlice(name_token);
+                    if (std.zig.primitives.isPrimitive(ident_name)) {
+                        return .{ .primitive = node };
+                    }
+
+                    const decl_index = file_index.get().lookup_token(name_token);
+                    if (decl_index != .none) return .{ .alias = decl_index };
+
+                    return .{ .global_const = node };
+                },
+
+                .field_access => {
+                    // TODO:
+                    //return .alias;
+                    return .{ .global_const = node };
+                },
+
+                .builtin_call_two, .builtin_call_two_comma => {
+                    if (node_datas[node].lhs == 0) {
+                        const params = [_]Ast.Node.Index{};
+                        return categorize_builtin_call(file_index, node, &params);
+                    } else if (node_datas[node].rhs == 0) {
+                        const params = [_]Ast.Node.Index{node_datas[node].lhs};
+                        return categorize_builtin_call(file_index, node, &params);
+                    } else {
+                        const params = [_]Ast.Node.Index{ node_datas[node].lhs, node_datas[node].rhs };
+                        return categorize_builtin_call(file_index, node, &params);
+                    }
+                },
+                .builtin_call, .builtin_call_comma => {
+                    const params = ast.extra_data[node_datas[node].lhs..node_datas[node].rhs];
+                    return categorize_builtin_call(file_index, node, params);
+                },
+
+                else => .{ .global_const = node },
+            };
+        }
+
+        fn categorize_builtin_call(
+            file_index: File.Index,
+            node: Ast.Node.Index,
+            params: []const Ast.Node.Index,
+        ) Category {
+            const ast = file_index.get_ast();
+            const main_tokens = ast.nodes.items(.main_token);
+            const builtin_token = main_tokens[node];
+            const builtin_name = ast.tokenSlice(builtin_token);
+            if (std.mem.eql(u8, builtin_name, "@import")) {
+                const str_lit_token = main_tokens[params[0]];
+                const str_bytes = ast.tokenSlice(str_lit_token);
+                const file_path = std.zig.string_literal.parseAlloc(gpa, str_bytes) catch @panic("OOM");
+                defer gpa.free(file_path);
+                const base_path = file_index.path();
+                const resolved_path = std.fs.path.resolvePosix(gpa, &.{
+                    base_path, "..", file_path,
+                }) catch @panic("OOM");
+                defer gpa.free(resolved_path);
+                log.debug("from '{s}' @import '{s}' resolved='{s}'", .{
+                    base_path, file_path, resolved_path,
+                });
+                if (Walk.files.getIndex(resolved_path)) |imported_file_index| {
+                    return .{ .alias = Walk.File.Index.findRootDecl(@enumFromInt(imported_file_index)) };
+                } else {
+                    log.warn("import target '{s}' did not resolve to any file", .{resolved_path});
+                }
+            } else if (std.mem.eql(u8, builtin_name, "@This")) {
+                if (file_index.get().node_decls.get(node)) |decl_index| {
+                    return .{ .alias = decl_index };
+                } else {
+                    log.warn("@This() is missing link to Decl.Index", .{});
+                }
+            }
+
+            return .{ .global_const = node };
+        }
+    };
+};
+
+pub const PackageIndex = enum(u32) {
+    _,
+};
+
+pub fn add_file(file_name: []const u8, bytes: []u8) !File.Index {
+    const ast = try parse(bytes);
+    const file_index: File.Index = @enumFromInt(files.entries.len);
+    try files.put(gpa, file_name, .{ .ast = ast });
+
+    if (ast.errors.len > 0) {
+        // TODO: expose this in the UI
+        log.err("can't index '{s}' because it has syntax errors", .{file_index.path()});
+        return file_index;
+    }
+
+    var arena_instance = std.heap.ArenaAllocator.init(gpa);
+    defer arena_instance.deinit();
+
+    var w: Walk = .{
+        .arena = arena_instance.allocator(),
+        .file = file_index,
+    };
+    var scope: Scope = .{ .tag = .top };
+
+    const decl_index = try file_index.add_decl(0, .none);
+    try struct_decl(&w, &scope, decl_index, ast.containerDeclRoot());
+
+    const file = file_index.get();
+    shrinkToFit(&file.ident_decls);
+    shrinkToFit(&file.token_parents);
+    shrinkToFit(&file.node_decls);
+    shrinkToFit(&file.doctests);
+
+    return file_index;
+}
+
+fn parse(source: []u8) Oom!Ast {
+    // Require every source file to end with a newline so that Zig's tokenizer
+    // can continue to require null termination and Autodoc implementation can
+    // avoid copying source bytes from the decompressed tar file buffer.
+    const adjusted_source: [:0]const u8 = s: {
+        if (source.len == 0)
+            break :s "";
+
+        assert(source[source.len - 1] == '\n');
+        source[source.len - 1] = 0;
+        break :s source[0 .. source.len - 1 :0];
+    };
+
+    return Ast.parse(gpa, adjusted_source, .zig);
+}
+
+const Scope = struct {
+    tag: Tag,
+
+    const Tag = enum { top, local, namespace };
+
+    const Local = struct {
+        base: Scope = .{ .tag = .local },
+        parent: *Scope,
+        var_node: Ast.Node.Index,
+    };
+
+    const Namespace = struct {
+        base: Scope = .{ .tag = .namespace },
+        parent: *Scope,
+        names: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .{},
+        doctests: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .{},
+        decl_index: Decl.Index,
+    };
+
+    fn getNamespaceDecl(start_scope: *Scope) Decl.Index {
+        var it: *Scope = start_scope;
+        while (true) switch (it.tag) {
+            .top => unreachable,
+            .local => {
+                const local = @fieldParentPtr(Local, "base", it);
+                it = local.parent;
+            },
+            .namespace => {
+                const namespace = @fieldParentPtr(Namespace, "base", it);
+                return namespace.decl_index;
+            },
+        };
+    }
+
+    fn lookup(start_scope: *Scope, ast: *const Ast, name: []const u8) ?Ast.Node.Index {
+        const main_tokens = ast.nodes.items(.main_token);
+        var it: *Scope = start_scope;
+        while (true) switch (it.tag) {
+            .top => break,
+            .local => {
+                const local = @fieldParentPtr(Local, "base", it);
+                const name_token = main_tokens[local.var_node] + 1;
+                const ident_name = ast.tokenSlice(name_token);
+                if (std.mem.eql(u8, ident_name, name)) {
+                    return local.var_node;
+                }
+                it = local.parent;
+            },
+            .namespace => {
+                const namespace = @fieldParentPtr(Namespace, "base", it);
+                if (namespace.names.get(name)) |node| {
+                    return node;
+                }
+                it = namespace.parent;
+            },
+        };
+        return null;
+    }
+};
+
+fn struct_decl(
+    w: *Walk,
+    scope: *Scope,
+    parent_decl: Decl.Index,
+    container_decl: Ast.full.ContainerDecl,
+) Oom!void {
+    const ast = w.file.get_ast();
+    const node_tags = ast.nodes.items(.tag);
+    const node_datas = ast.nodes.items(.data);
+
+    var namespace: Scope.Namespace = .{
+        .parent = scope,
+        .decl_index = parent_decl,
+    };
+    try w.scanDecls(&namespace, container_decl.ast.members);
+
+    for (container_decl.ast.members) |member| switch (node_tags[member]) {
+        .container_field_init,
+        .container_field_align,
+        .container_field,
+        => try w.container_field(&namespace.base, parent_decl, ast.fullContainerField(member).?),
+
+        .fn_proto,
+        .fn_proto_multi,
+        .fn_proto_one,
+        .fn_proto_simple,
+        .fn_decl,
+        => {
+            var buf: [1]Ast.Node.Index = undefined;
+            const full = ast.fullFnProto(&buf, member).?;
+            const fn_name_token = full.ast.fn_token + 1;
+            const fn_name = ast.tokenSlice(fn_name_token);
+            if (namespace.doctests.get(fn_name)) |doctest_node| {
+                try w.file.get().doctests.put(gpa, member, doctest_node);
+            }
+            const decl_index = try w.file.add_decl(member, parent_decl);
+            const body = if (node_tags[member] == .fn_decl) node_datas[member].rhs else 0;
+            try w.fn_decl(&namespace.base, decl_index, body, full);
+        },
+
+        .global_var_decl,
+        .local_var_decl,
+        .simple_var_decl,
+        .aligned_var_decl,
+        => {
+            const decl_index = try w.file.add_decl(member, parent_decl);
+            try w.global_var_decl(&namespace.base, decl_index, ast.fullVarDecl(member).?);
+        },
+
+        .@"comptime",
+        .@"usingnamespace",
+        => try w.expr(&namespace.base, parent_decl, node_datas[member].lhs),
+
+        .test_decl => try w.expr(&namespace.base, parent_decl, node_datas[member].rhs),
+
+        else => unreachable,
+    };
+}
+
+fn comptime_decl(
+    w: *Walk,
+    scope: *Scope,
+    parent_decl: Decl.Index,
+    full: Ast.full.VarDecl,
+) Oom!void {
+    try w.expr(scope, parent_decl, full.ast.type_node);
+    try w.maybe_expr(scope, parent_decl, full.ast.align_node);
+    try w.maybe_expr(scope, parent_decl, full.ast.addrspace_node);
+    try w.maybe_expr(scope, parent_decl, full.ast.section_node);
+    try w.expr(scope, parent_decl, full.ast.init_node);
+}
+
+fn global_var_decl(
+    w: *Walk,
+    scope: *Scope,
+    parent_decl: Decl.Index,
+    full: Ast.full.VarDecl,
+) Oom!void {
+    try w.maybe_expr(scope, parent_decl, full.ast.type_node);
+    try w.maybe_expr(scope, parent_decl, full.ast.align_node);
+    try w.maybe_expr(scope, parent_decl, full.ast.addrspace_node);
+    try w.maybe_expr(scope, parent_decl, full.ast.section_node);
+    try w.maybe_expr(scope, parent_decl, full.ast.init_node);
+}
+
+fn container_field(
+    w: *Walk,
+    scope: *Scope,
+    parent_decl: Decl.Index,
+    full: Ast.full.ContainerField,
+) Oom!void {
+    try w.maybe_expr(scope, parent_decl, full.ast.type_expr);
+    try w.maybe_expr(scope, parent_decl, full.ast.align_expr);
+    try w.maybe_expr(scope, parent_decl, full.ast.value_expr);
+}
+
+fn fn_decl(
+    w: *Walk,
+    scope: *Scope,
+    parent_decl: Decl.Index,
+    body: Ast.Node.Index,
+    full: Ast.full.FnProto,
+) Oom!void {
+    for (full.ast.params) |param| {
+        try expr(w, scope, parent_decl, param);
+    }
+    try expr(w, scope, parent_decl, full.ast.return_type);
+    try maybe_expr(w, scope, parent_decl, full.ast.align_expr);
+    try maybe_expr(w, scope, parent_decl, full.ast.addrspace_expr);
+    try maybe_expr(w, scope, parent_decl, full.ast.section_expr);
+    try maybe_expr(w, scope, parent_decl, full.ast.callconv_expr);
+    try maybe_expr(w, scope, parent_decl, body);
+}
+
+fn maybe_expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.Index) Oom!void {
+    if (node != 0) return expr(w, scope, parent_decl, node);
+}
+
+fn expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.Index) Oom!void {
+    assert(node != 0);
+    const ast = w.file.get_ast();
+    const node_tags = ast.nodes.items(.tag);
+    const node_datas = ast.nodes.items(.data);
+    const main_tokens = ast.nodes.items(.main_token);
+    switch (node_tags[node]) {
+        .root => unreachable, // Top-level declaration.
+        .@"usingnamespace" => unreachable, // Top-level declaration.
+        .test_decl => unreachable, // Top-level declaration.
+        .container_field_init => unreachable, // Top-level declaration.
+        .container_field_align => unreachable, // Top-level declaration.
+        .container_field => unreachable, // Top-level declaration.
+        .fn_decl => unreachable, // Top-level declaration.
+
+        .global_var_decl => unreachable, // Handled in `block`.
+        .local_var_decl => unreachable, // Handled in `block`.
+        .simple_var_decl => unreachable, // Handled in `block`.
+        .aligned_var_decl => unreachable, // Handled in `block`.
+        .@"defer" => unreachable, // Handled in `block`.
+        .@"errdefer" => unreachable, // Handled in `block`.
+
+        .switch_case => unreachable, // Handled in `switchExpr`.
+        .switch_case_inline => unreachable, // Handled in `switchExpr`.
+        .switch_case_one => unreachable, // Handled in `switchExpr`.
+        .switch_case_inline_one => unreachable, // Handled in `switchExpr`.
+
+        .asm_output => unreachable, // Handled in `asmExpr`.
+        .asm_input => unreachable, // Handled in `asmExpr`.
+
+        .for_range => unreachable, // Handled in `forExpr`.
+
+        .assign,
+        .assign_shl,
+        .assign_shl_sat,
+        .assign_shr,
+        .assign_bit_and,
+        .assign_bit_or,
+        .assign_bit_xor,
+        .assign_div,
+        .assign_sub,
+        .assign_sub_wrap,
+        .assign_sub_sat,
+        .assign_mod,
+        .assign_add,
+        .assign_add_wrap,
+        .assign_add_sat,
+        .assign_mul,
+        .assign_mul_wrap,
+        .assign_mul_sat,
+        .shl,
+        .shr,
+        .add,
+        .add_wrap,
+        .add_sat,
+        .sub,
+        .sub_wrap,
+        .sub_sat,
+        .mul,
+        .mul_wrap,
+        .mul_sat,
+        .div,
+        .mod,
+        .shl_sat,
+
+        .bit_and,
+        .bit_or,
+        .bit_xor,
+        .bang_equal,
+        .equal_equal,
+        .greater_than,
+        .greater_or_equal,
+        .less_than,
+        .less_or_equal,
+        .array_cat,
+
+        .array_mult,
+        .error_union,
+        .merge_error_sets,
+        .bool_and,
+        .bool_or,
+        .@"catch",
+        .@"orelse",
+        .array_type,
+        .array_access,
+        .switch_range,
+        => {
+            try expr(w, scope, parent_decl, node_datas[node].lhs);
+            try expr(w, scope, parent_decl, node_datas[node].rhs);
+        },
+
+        .assign_destructure => {
+            const extra_index = node_datas[node].lhs;
+            const lhs_count = ast.extra_data[extra_index];
+            const lhs_nodes: []const Ast.Node.Index = @ptrCast(ast.extra_data[extra_index + 1 ..][0..lhs_count]);
+            const rhs = node_datas[node].rhs;
+            for (lhs_nodes) |lhs_node| try expr(w, scope, parent_decl, lhs_node);
+            _ = try expr(w, scope, parent_decl, rhs);
+        },
+
+        .bool_not,
+        .bit_not,
+        .negation,
+        .negation_wrap,
+        .@"return",
+        .deref,
+        .address_of,
+        .optional_type,
+        .unwrap_optional,
+        .grouped_expression,
+        .@"comptime",
+        .@"nosuspend",
+        .@"suspend",
+        .@"await",
+        .@"resume",
+        .@"try",
+        => try maybe_expr(w, scope, parent_decl, node_datas[node].lhs),
+
+        .anyframe_type,
+        .@"break",
+        => try maybe_expr(w, scope, parent_decl, node_datas[node].rhs),
+
+        .identifier => {
+            const ident_token = main_tokens[node];
+            const ident_name = ast.tokenSlice(ident_token);
+            if (scope.lookup(ast, ident_name)) |var_node| {
+                try w.file.get().ident_decls.put(gpa, ident_token, var_node);
+            }
+        },
+        .field_access => {
+            const object_node = node_datas[node].lhs;
+            const dot_token = main_tokens[node];
+            const field_ident = dot_token + 1;
+            try w.file.get().token_parents.put(gpa, field_ident, node);
+            // This will populate the left-most field object if it is an
+            // identifier, allowing rendering code to piece together the link.
+            try expr(w, scope, parent_decl, object_node);
+        },
+
+        .string_literal,
+        .multiline_string_literal,
+        .number_literal,
+        .unreachable_literal,
+        .enum_literal,
+        .error_value,
+        .anyframe_literal,
+        .@"continue",
+        .char_literal,
+        .error_set_decl,
+        => {},
+
+        .asm_simple,
+        .@"asm",
+        => {
+            const full = ast.fullAsm(node).?;
+            for (full.ast.items) |n| {
+                // TODO handle .asm_input, .asm_output
+                _ = n;
+            }
+            try expr(w, scope, parent_decl, full.ast.template);
+        },
+
+        .builtin_call_two, .builtin_call_two_comma => {
+            if (node_datas[node].lhs == 0) {
+                const params = [_]Ast.Node.Index{};
+                return builtin_call(w, scope, parent_decl, node, &params);
+            } else if (node_datas[node].rhs == 0) {
+                const params = [_]Ast.Node.Index{node_datas[node].lhs};
+                return builtin_call(w, scope, parent_decl, node, &params);
+            } else {
+                const params = [_]Ast.Node.Index{ node_datas[node].lhs, node_datas[node].rhs };
+                return builtin_call(w, scope, parent_decl, node, &params);
+            }
+        },
+        .builtin_call, .builtin_call_comma => {
+            const params = ast.extra_data[node_datas[node].lhs..node_datas[node].rhs];
+            return builtin_call(w, scope, parent_decl, node, params);
+        },
+
+        .call_one,
+        .call_one_comma,
+        .async_call_one,
+        .async_call_one_comma,
+        .call,
+        .call_comma,
+        .async_call,
+        .async_call_comma,
+        => {
+            var buf: [1]Ast.Node.Index = undefined;
+            const full = ast.fullCall(&buf, node).?;
+            try expr(w, scope, parent_decl, full.ast.fn_expr);
+            for (full.ast.params) |param| {
+                try expr(w, scope, parent_decl, param);
+            }
+        },
+
+        .if_simple,
+        .@"if",
+        => {
+            const full = ast.fullIf(node).?;
+            try expr(w, scope, parent_decl, full.ast.cond_expr);
+            try expr(w, scope, parent_decl, full.ast.then_expr);
+            try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
+        },
+
+        .while_simple,
+        .while_cont,
+        .@"while",
+        => {
+            try while_expr(w, scope, parent_decl, ast.fullWhile(node).?);
+        },
+
+        .for_simple, .@"for" => {
+            const full = ast.fullFor(node).?;
+            for (full.ast.inputs) |input| {
+                if (node_tags[input] == .for_range) {
+                    try expr(w, scope, parent_decl, node_datas[input].lhs);
+                    try maybe_expr(w, scope, parent_decl, node_datas[input].rhs);
+                } else {
+                    try expr(w, scope, parent_decl, input);
+                }
+            }
+            try expr(w, scope, parent_decl, full.ast.then_expr);
+            try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
+        },
+
+        .slice => return slice(w, scope, parent_decl, ast.slice(node)),
+        .slice_open => return slice(w, scope, parent_decl, ast.sliceOpen(node)),
+        .slice_sentinel => return slice(w, scope, parent_decl, ast.sliceSentinel(node)),
+
+        .block_two, .block_two_semicolon => {
+            const statements = [2]Ast.Node.Index{ node_datas[node].lhs, node_datas[node].rhs };
+            if (node_datas[node].lhs == 0) {
+                return block(w, scope, parent_decl, statements[0..0]);
+            } else if (node_datas[node].rhs == 0) {
+                return block(w, scope, parent_decl, statements[0..1]);
+            } else {
+                return block(w, scope, parent_decl, statements[0..2]);
+            }
+        },
+        .block, .block_semicolon => {
+            const statements = ast.extra_data[node_datas[node].lhs..node_datas[node].rhs];
+            return block(w, scope, parent_decl, statements);
+        },
+
+        .ptr_type_aligned,
+        .ptr_type_sentinel,
+        .ptr_type,
+        .ptr_type_bit_range,
+        => {
+            const full = ast.fullPtrType(node).?;
+            try maybe_expr(w, scope, parent_decl, full.ast.align_node);
+            try maybe_expr(w, scope, parent_decl, full.ast.addrspace_node);
+            try maybe_expr(w, scope, parent_decl, full.ast.sentinel);
+            try maybe_expr(w, scope, parent_decl, full.ast.bit_range_start);
+            try maybe_expr(w, scope, parent_decl, full.ast.bit_range_end);
+            try expr(w, scope, parent_decl, full.ast.child_type);
+        },
+
+        .container_decl,
+        .container_decl_trailing,
+        .container_decl_arg,
+        .container_decl_arg_trailing,
+        .container_decl_two,
+        .container_decl_two_trailing,
+        .tagged_union,
+        .tagged_union_trailing,
+        .tagged_union_enum_tag,
+        .tagged_union_enum_tag_trailing,
+        .tagged_union_two,
+        .tagged_union_two_trailing,
+        => {
+            var buf: [2]Ast.Node.Index = undefined;
+            return struct_decl(w, scope, parent_decl, ast.fullContainerDecl(&buf, node).?);
+        },
+
+        .array_type_sentinel => {
+            const extra = ast.extraData(node_datas[node].rhs, Ast.Node.ArrayTypeSentinel);
+            try expr(w, scope, parent_decl, node_datas[node].lhs);
+            try expr(w, scope, parent_decl, extra.elem_type);
+            try expr(w, scope, parent_decl, extra.sentinel);
+        },
+        .@"switch", .switch_comma => {
+            const operand_node = node_datas[node].lhs;
+            try expr(w, scope, parent_decl, operand_node);
+            const extra = ast.extraData(node_datas[node].rhs, Ast.Node.SubRange);
+            const case_nodes = ast.extra_data[extra.start..extra.end];
+            for (case_nodes) |case_node| {
+                const case = ast.fullSwitchCase(case_node).?;
+                for (case.ast.values) |value_node| {
+                    try expr(w, scope, parent_decl, value_node);
+                }
+                try expr(w, scope, parent_decl, case.ast.target_expr);
+            }
+        },
+
+        .array_init_one,
+        .array_init_one_comma,
+        .array_init_dot_two,
+        .array_init_dot_two_comma,
+        .array_init_dot,
+        .array_init_dot_comma,
+        .array_init,
+        .array_init_comma,
+        => {
+            var buf: [2]Ast.Node.Index = undefined;
+            const full = ast.fullArrayInit(&buf, node).?;
+            try maybe_expr(w, scope, parent_decl, full.ast.type_expr);
+            for (full.ast.elements) |elem| {
+                try expr(w, scope, parent_decl, elem);
+            }
+        },
+
+        .struct_init_one,
+        .struct_init_one_comma,
+        .struct_init_dot_two,
+        .struct_init_dot_two_comma,
+        .struct_init_dot,
+        .struct_init_dot_comma,
+        .struct_init,
+        .struct_init_comma,
+        => {
+            var buf: [2]Ast.Node.Index = undefined;
+            const full = ast.fullStructInit(&buf, node).?;
+            try maybe_expr(w, scope, parent_decl, full.ast.type_expr);
+            for (full.ast.fields) |field| {
+                try expr(w, scope, parent_decl, field);
+            }
+        },
+
+        .fn_proto_simple,
+        .fn_proto_multi,
+        .fn_proto_one,
+        .fn_proto,
+        => {
+            var buf: [1]Ast.Node.Index = undefined;
+            return fn_decl(w, scope, parent_decl, 0, ast.fullFnProto(&buf, node).?);
+        },
+    }
+}
+
+fn slice(w: *Walk, scope: *Scope, parent_decl: Decl.Index, full: Ast.full.Slice) Oom!void {
+    try expr(w, scope, parent_decl, full.ast.sliced);
+    try expr(w, scope, parent_decl, full.ast.start);
+    try maybe_expr(w, scope, parent_decl, full.ast.end);
+    try maybe_expr(w, scope, parent_decl, full.ast.sentinel);
+}
+
+fn builtin_call(
+    w: *Walk,
+    scope: *Scope,
+    parent_decl: Decl.Index,
+    node: Ast.Node.Index,
+    params: []const Ast.Node.Index,
+) Oom!void {
+    const ast = w.file.get_ast();
+    const main_tokens = ast.nodes.items(.main_token);
+    const builtin_token = main_tokens[node];
+    const builtin_name = ast.tokenSlice(builtin_token);
+    if (std.mem.eql(u8, builtin_name, "@This")) {
+        try w.file.get().node_decls.put(gpa, node, scope.getNamespaceDecl());
+    }
+
+    for (params) |param| {
+        try expr(w, scope, parent_decl, param);
+    }
+}
+
+fn block(
+    w: *Walk,
+    parent_scope: *Scope,
+    parent_decl: Decl.Index,
+    statements: []const Ast.Node.Index,
+) Oom!void {
+    const ast = w.file.get_ast();
+    const arena = w.arena;
+    const node_tags = ast.nodes.items(.tag);
+    const node_datas = ast.nodes.items(.data);
+
+    var scope = parent_scope;
+
+    for (statements) |node| {
+        switch (node_tags[node]) {
+            .global_var_decl,
+            .local_var_decl,
+            .simple_var_decl,
+            .aligned_var_decl,
+            => {
+                const full = ast.fullVarDecl(node).?;
+                try global_var_decl(w, scope, parent_decl, full);
+                const local = try arena.create(Scope.Local);
+                local.* = .{
+                    .parent = scope,
+                    .var_node = node,
+                };
+                scope = &local.base;
+            },
+
+            .assign_destructure => {
+                // TODO
+            },
+
+            .grouped_expression => try expr(w, scope, parent_decl, node_datas[node].lhs),
+
+            .@"defer",
+            .@"errdefer",
+            => try expr(w, scope, parent_decl, node_datas[node].rhs),
+
+            else => try expr(w, scope, parent_decl, node),
+        }
+    }
+}
+
+fn while_expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, full: Ast.full.While) Oom!void {
+    try expr(w, scope, parent_decl, full.ast.cond_expr);
+    try maybe_expr(w, scope, parent_decl, full.ast.cont_expr);
+    try expr(w, scope, parent_decl, full.ast.then_expr);
+    try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
+}
+
+fn scanDecls(w: *Walk, namespace: *Scope.Namespace, members: []const Ast.Node.Index) Oom!void {
+    const arena = w.arena;
+    const ast = w.file.get_ast();
+    const node_tags = ast.nodes.items(.tag);
+    const main_tokens = ast.nodes.items(.main_token);
+    const token_tags = ast.tokens.items(.tag);
+    const node_datas = ast.nodes.items(.data);
+
+    for (members) |member_node| {
+        const name_token = switch (node_tags[member_node]) {
+            .global_var_decl,
+            .local_var_decl,
+            .simple_var_decl,
+            .aligned_var_decl,
+            => main_tokens[member_node] + 1,
+
+            .fn_proto_simple,
+            .fn_proto_multi,
+            .fn_proto_one,
+            .fn_proto,
+            .fn_decl,
+            => blk: {
+                const ident = main_tokens[member_node] + 1;
+                if (token_tags[ident] != .identifier) continue;
+                break :blk ident;
+            },
+
+            .test_decl => {
+                const ident_token = node_datas[member_node].lhs;
+                const is_doctest = token_tags[ident_token] == .identifier;
+                if (is_doctest) {
+                    const token_bytes = ast.tokenSlice(ident_token);
+                    try namespace.doctests.put(arena, token_bytes, member_node);
+                }
+                continue;
+            },
+
+            else => continue,
+        };
+
+        const token_bytes = ast.tokenSlice(name_token);
+        try namespace.names.put(arena, token_bytes, member_node);
+    }
+}
+
+//test {
+//    const gpa = std.testing.allocator;
+//
+//    var arena_instance = std.heap.ArenaAllocator.init(gpa);
+//    defer arena_instance.deinit();
+//    const arena = arena_instance.allocator();
+//
+//    // example test command:
+//    // zig test --dep input.zig -Mroot=src/Walk.zig -Minput.zig=/home/andy/dev/zig/lib/std/fs/File/zig
+//    var ast = try Ast.parse(gpa, @embedFile("input.zig"), .zig);
+//    defer ast.deinit(gpa);
+//
+//    var w: Walk = .{
+//        .arena = arena,
+//        .token_links = .{},
+//        .ast = &ast,
+//    };
+//
+//    try w.root();
+//}
+
+const Walk = @This();
+const std = @import("std");
+const Ast = std.zig.Ast;
+const assert = std.debug.assert;
+const Decl = @import("Decl.zig");
+const log = std.log;
+const gpa = std.heap.wasm_allocator;
+const Oom = error{OutOfMemory};
+
+fn shrinkToFit(m: anytype) void {
+    m.shrinkAndFree(gpa, m.entries.len);
+}
lib/docs/index.html
@@ -1,634 +1,168 @@
 <!doctype html>
-<html lang="en">
+<html>
   <head>
     <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <title>Documentation - Zig</title>
-    <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAgklEQVR4AWMYWuD7EllJIM4G4g4g5oIJ/odhOJ8wToOxSTXgNxDHoeiBMfA4+wGShjyYOCkG/IGqWQziEzYAoUAeiF9D5U+DxEg14DRU7jWIT5IBIOdCxf+A+CQZAAoopEB7QJwBCBwHiip8UYmRdrAlDpIMgApwQZNnNii5Dq0MBgCxxycBnwEd+wAAAABJRU5ErkJggg==">
+    <title>Zig Documentation</title>
     <link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNTMgMTQwIj48ZyBmaWxsPSIjRjdBNDFEIj48Zz48cG9seWdvbiBwb2ludHM9IjQ2LDIyIDI4LDQ0IDE5LDMwIi8+PHBvbHlnb24gcG9pbnRzPSI0NiwyMiAzMywzMyAyOCw0NCAyMiw0NCAyMiw5NSAzMSw5NSAyMCwxMDAgMTIsMTE3IDAsMTE3IDAsMjIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxwb2x5Z29uIHBvaW50cz0iMzEsOTUgMTIsMTE3IDQsMTA2Ii8+PC9nPjxnPjxwb2x5Z29uIHBvaW50cz0iNTYsMjIgNjIsMzYgMzcsNDQiLz48cG9seWdvbiBwb2ludHM9IjU2LDIyIDExMSwyMiAxMTEsNDQgMzcsNDQgNTYsMzIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxwb2x5Z29uIHBvaW50cz0iMTE2LDk1IDk3LDExNyA5MCwxMDQiLz48cG9seWdvbiBwb2ludHM9IjExNiw5NSAxMDAsMTA0IDk3LDExNyA0MiwxMTcgNDIsOTUiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxwb2x5Z29uIHBvaW50cz0iMTUwLDAgNTIsMTE3IDMsMTQwIDEwMSwyMiIvPjwvZz48Zz48cG9seWdvbiBwb2ludHM9IjE0MSwyMiAxNDAsNDAgMTIyLDQ1Ii8+PHBvbHlnb24gcG9pbnRzPSIxNTMsMjIgMTUzLDExNyAxMDYsMTE3IDEyMCwxMDUgMTI1LDk1IDEzMSw5NSAxMzEsNDUgMTIyLDQ1IDEzMiwzNiAxNDEsMjIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxwb2x5Z29uIHBvaW50cz0iMTI1LDk1IDEzMCwxMTAgMTA2LDExNyIvPjwvZz48L2c+PC9zdmc+">
-    <style>
-      :root {
-        font-size: 1em;
-        --ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
-        --mono: "Source Code Pro", monospace;
-        --tx-color: #141414;
-        --bg-color: #ffffff;
-        --link-color: #2A6286;
-        --sidebar-sh-color: rgba(0, 0, 0, 0.09);
-        --sidebar-mod-bg-color: #f1f1f1;
-        --sidebar-modlnk-tx-color: #141414;
-        --sidebar-modlnk-tx-color-hover: #fff;
-        --sidebar-modlnk-tx-color-active: #000;
-        --sidebar-modlnk-bg-color: transparent;
-        --sidebar-modlnk-bg-color-hover: #555;
-        --sidebar-modlnk-bg-color-active: #FFBB4D;
-        --search-bg-color: #f3f3f3;
-        --search-bg-color-focus: #ffffff;
-        --search-sh-color: rgba(0, 0, 0, 0.18);
-        --search-other-results-color: rgb(100, 100, 100);
-        --modal-sh-color: rgba(0, 0, 0, 0.75);
-        --modal-bg-color: #aaa;
-        --warning-popover-bg-color: #ff4747;
+    <style type="text/css">
+      body {
+        font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
+        color: #000000;
       }
-
-      html, body { margin: 0; padding: 0; height: 100%; }
-
-      a {
-        text-decoration: none;
-      }
-
-      pre a {
-        text-decoration: underline;
-        color: unset;
-      }
-
-      a:hover {
-        text-decoration: underline;
-      }
-
-      a[href^="src/"] {
-        border-bottom: 2px dotted var(--tx-color);
-      }
-
       .hidden {
-        display: none !important;
-      }
-
-      /* layout */
-      .canvas {
-        display:flex;
-        flex-direction: column;
-        width: 100vw;
-        height: 100vh;
-        margin: 0;
-        padding: 0;
-        font-family: var(--ui);
-        color: var(--tx-color);
-        background-color: var(--bg-color);
-      }
-
-      .flex-main {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-
-        height: 100%;
-        overflow: hidden;
-        
-
-        z-index: 100;
-      }
-
-      .flex-horizontal {
-        display: flex;
-        flex-direction: row;
-        align-items: center;
-      }
-
-      .flex-filler {
-        flex-grow: 1;
-        flex-shrink: 1;
-      }
-
-      .flex-left {
-        overflow: auto;
-        -webkit-overflow-scrolling: touch;
-        overflow-wrap: break-word;
-        flex-shrink: 0;
-        flex-grow: 0;
-        margin-right: 0.5rem;
-
-        z-index: 300;
-      }
-
-      .flex-right {
-        display: flex;
-        flex-direction: column;
-        overflow: auto;
-        -webkit-overflow-scrolling: touch;
-        flex-grow: 1;
-        flex-shrink: 1;
-
-        z-index: 200;
-      }
-
-      .flex-right > .wrap {
-        width: 60rem;
-        max-width: 85vw;
-        flex-shrink: 1;
-      }
-
-      .modal-container {
-        z-index: 400;
-      }
-
-      .understated {
-        color: var(--search-other-results-color);
-      }
-
-      .sidebar {
-        background-color: var(--bg-color);
-        box-shadow: 0 0 1rem var(--sidebar-sh-color);
-        clip-path: inset(0px -15px 0px 0px);
-      }
-
-      .logo {
-        margin: 0.5rem;
-        width: 130px; 
+        display: none;
       }
-
-      .logo > svg {
-        display: block;
+      a {
+        color: #2A6286;
       }
-
-      ul.guides-api-switch {
-        display: flex;
-        flex-direction: row;
-        justify-content: center;
-        text-align: center;
-        list-style-type: none;
+      pre{
+        font-family:"Source Code Pro",monospace;
+        font-size:1em;
+        background-color:#F5F5F5;
+        padding: 1em;
         margin: 0;
-        padding: 0;
-      }
-
-      .guides-api-switch a {
-        display: block;
-        padding: 0.5rem 1rem;      
-        color: var(--sidebar-modlnk-tx-color);
-        background-color: var(--sidebar-modlnk-bg-color);
-        border: 1px solid var(--tx-color);
+        overflow-x: auto;
       }
-
-
-      #ApiSwitch {
-        border-radius: 10px 0 0 10px;        
+      code {
+        font-family:"Source Code Pro",monospace;
+        font-size:1em;
       }
-
-      #guideSwitch {
-        border-radius: 0 10px 10px 0;
+      code a {
+        color: #000000;
       }
-
-      
-      #ApiSwitch:hover, #guideSwitch:hover {
-        text-decoration: none;      
+      #listFields > div {
+        margin-bottom: 1em;
       }
-
-      #ApiSwitch:hover:not(.active), #guideSwitch:hover:not(.active) {
-        color: var(--sidebar-modlnk-tx-color-hover);
-        background-color: var(--sidebar-modlnk-bg-color-hover);
+      .fieldDocs {
+        border: 1px solid #F5F5F5;
+        border-top: 0px;
+        padding: 1px 1em;
       }
 
-      .guides-api-switch .active {
-        color: var(--sidebar-modlnk-tx-color-active);
-        background-color: var(--sidebar-modlnk-bg-color-active);
-      }
 
-      #guidesMenu {
-        height: 100%;
-        overflow: hidden;
-        width: 30%;
-        margin-right: 2rem;
-      }
-      
-      #activeGuide {
-        overflow-y: scroll;
-        height: 100%;
-        width: 70%;
-        padding-right: 1rem;
-      }
-      .sidebar h2 {
-        margin: 0.5rem;
-        padding: 0;
-        font-size: 1.2rem;
+      #navWrap {
+        float: left;
+        width: 47em;
+        margin-left: 1em;
       }
 
-      .sidebar h2 > span {
-        border-bottom: 0.125rem dotted var(--tx-color);
+      nav {
+        width: 10em;
+        float: left;
       }
-
-      .sidebar .modules {
-        list-style-type: none;
+      nav h2 {
+        font-size: 1.2em;
+        text-decoration: underline;
         margin: 0;
-        padding: 0;
-        background-color: var(--sidebar-mod-bg-color);
-      }
-
-      .sidebar .modules > li > a {
-        display: block;
-        padding: 0.5rem 1rem;
-        color: var(--sidebar-modlnk-tx-color);
-        background-color: var(--sidebar-modlnk-bg-color);
-        text-decoration: none;
-      }
-
-      .sidebar .modules > li > a:hover {
-        color: var(--sidebar-modlnk-tx-color-hover);
-        background-color: var(--sidebar-modlnk-bg-color-hover);
-      }
-
-      .sidebar .modules > li > a.active {
-        color: var(--sidebar-modlnk-tx-color-active);
-        background-color: var(--sidebar-modlnk-bg-color-active);
-      }
-
-      .sidebar p.str {
-        margin: 0.5rem;
-        font-family: var(--mono);
-      }
-
-      #guideTocList {
-        padding: 0 1rem;
+        padding: 0.5em 0;
+        text-align: center;
       }
-      
-      #guideTocList ul {
-        padding-left: 1rem;
+      nav p {
         margin: 0;
-      }
-
-      #guides {
-        box-sizing: border-box;
-        font-size: 1rem;
-        background-color: var(--bg-color);
-        overflow-wrap: break-word;
-      }
-
-      /* docs section */
-      .docs {
-        flex-grow: 2;
-        padding: 0rem 0.7rem 0rem 1.4rem;
-        font-size: 1rem;
-        background-color: var(--bg-color);
-        overflow-wrap: break-word;
-        height: 100%;
-        overflow-y: scroll;
-      }
-
-      #noDocsNamespaces {
-      	margin: 1rem;
-      	border: 1px solid var(--search-other-results-color);
-      	padding: 0.5rem 1rem;
-      	background-color: var(--help-bg-color);
-      }
-
-      .column {
-        flex-basis: 0;
-        flex-grow: 1;
-        min-width: min(24rem, 90%);
-      }
-
-      
-      .search-container {
-        flex-grow: 2;
-      }
-
-      .search {
-        width: 100%;
-        padding: 0.5rem;
-        font-family: var(--ui);
-        font-size: 1rem;
-        color: var(--tx-color);
-        background-color: var(--search-bg-color);
-        border-top: 0;
-        border-left: 0;
-        border-right: 0;
-        border-bottom-width: 0.125rem;
-        border-bottom-style: solid;
-        border-bottom-color: var(--tx-color);
-        outline: none;
-        transition: border-bottom-color 0.35s, background 0.35s, box-shadow 0.35s;
-        border-radius: 0;
-        -webkit-appearance: none;
-      }
-
-      .search:focus {
-        background-color: var(--search-bg-color-focus);
-        border-bottom-color: #ffbb4d;
-        box-shadow: 0 0.3em 1em 0.125em var(--search-sh-color);
-      }
-
-      #searchPlaceholder {
-        position: absolute;
-        pointer-events: none;
-        height: 100%;
-        display: flex;
-        align-items: center;
-        padding-left: 5px;
-      }
-
-      #searchPlaceholderTextMobile {
-        display: none;
-      }
-
-      #dotsPopover:before {
-        position: absolute;
-        content: "";
-        left: 20px;
-        top: -8px;
-        border-style: solid;
-        border-width: 0 10px 10px 10px;
-        border-color: transparent transparent var(--warning-popover-bg-color) transparent;
-        transition-duration: 0.3s;
-        transition-property: transform;
-        z-index: 10;
-      }      
-      
-      #dotsPopover {
-        position: absolute;
-        opacity: 0;
-        visibility: hidden;
-        background-color: var(--warning-popover-bg-color);
-        border-radius: 10px;
-        left: 10px;
-        transform: translate(0, -20px);
-        padding: 0.5rem 1rem;
-        box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
-        transition: all 0.5s cubic-bezier(0.75, -0.02, 0.2, 0.97);
-        z-index: 20;
-      }
-
-      #dotsPopover.active {
-        opacity: 1;
-        visibility: visible;
-        transform: translate(0, 0);
-      }
-
-      #sectSearchResults {
-        box-sizing: border-box;
-      }
-
-      #searchHelp summary {
-        color: red;
-        list-style-position: outside;
-      }
-
-      #searchHelp summary.normal {
-        color: var(--search-other-results-color);
-        transition: all 0.5s cubic-bezier(0.75, -0.02, 0.2, 0.97);
-      }
-
-      #searchHelp div {
-        background-color: var(--modal-bg-color);
-        padding: 0.5rem 1rem;
-      }
-      
-      .other-results {
-        line-height: 1em;
-        position: relative;
-        outline: 0;
-        border: 0;
-        color: var(--search-other-results-color);
+        padding: 0;
         text-align: center;
-        height: 1.5em;
-        opacity: .5;
       }
-      .other-results:before {
-        content: '';
-        background: var(--search-other-results-color);
-        position: absolute;
-        left: 0;
-        top: 50%;
-        width: 100%;
-        height: 1px;
+      section {
+        clear: both;
+        padding-top: 1em;
       }
-        
-      .other-results:after {
-        content: "other results";
-        position: relative;
-        display: inline-block;
-        padding: 0 .5em;
-        line-height: 1.5em;
-        color: var(--search-other-results-color);
-        background-color: var(--bg-color);
-      }
-      
-      
-      a {
-        color: var(--link-color);
+      section h1 {
+        border-bottom: 1px dashed;
+        margin: 0 0;
       }
-
-      p {
-        margin: 0.8rem 0;
-      }
-
-      pre {
-        font-family: var(--mono);
-        font-size: 1em;
-        background-color: #F5F5F5;
-        padding: 1em;
-        overflow-x: auto;
-      }
-
-      pre.inline {
-        background-color: var(--bg-color);
-        padding: 0;
-        display: inline;
-      }
-
-
-      code {
-        font-family: var(--mono);
-        font-size: 1em;
-      }
-
-      h1 {
-        font-size: 1.4em;
-        margin: 0.8em 0;
-        padding: 0;
-        border-bottom: 0.0625rem dashed;
-      }
-
-      h2 {
+      section h2 {
         font-size: 1.3em;
         margin: 0.5em 0;
         padding: 0;
-        border-bottom: 0.0625rem solid;
+        border-bottom: 1px solid;
       }
-      .listNav {
+      #listNav {
         list-style-type: none;
-        margin: 0;
+        margin: 0.5em 0 0 0;
         padding: 0;
         overflow: hidden;
         background-color: #f1f1f1;
-        display: flex;
-        flex-direction: row;
       }
-      .listNav li {
-      
+      #listNav li {
+        float:left;
       }
-      .listNav li a {
+      #listNav li a {
         display: block;
         color: #000;
         text-align: center;
         padding: .5em .8em;
         text-decoration: none;
       }
-      .listNav li a:hover {
+      #listNav li a:hover {
         background-color: #555;
         color: #fff;
       }
-      .listNav li a.active {
+      #listNav li a.active {
         background-color: #FFBB4D;
         color: #000;
       }
 
-      #listSearchResults li.selected {
-        background-color: #93e196;
-      }
-
-      #tableFnErrors dt {
-        font-weight: bold;
-      }
-
-      .expand[open] .sum-less {
-        display: none;
-      }
-
-      .expand[open] .sum-more {
-        display: block;
-      }
-
-      .expand .sum-more {
-        display: none;
-      }
-
-      .expand {
-        position: relative;
-      }
-
-      .expand .button:before {
-        content: "[+] ";
-        font-family: var(--mono);
-        color: var(--link-color);
-        position: sticky;
-        float: left;
-        top: 0.5em;
-        right: -16px;
-        z-index: 1;
-        margin-left: -2em;
-        pointer-events: all;
-        cursor: pointer;
+      #logo {
+        width: 8em;
+        padding: 0.5em 1em;
       }
 
-      .expand[open] .button:before {
-        content: "[-] ";
-      }
-
-      .examples {
-        list-style-type: none;
-        margin: 0;
-        padding: 0;
-      }
-      .examples li {
-        padding: 0.5em 0;
-        white-space: nowrap;
-        overflow-x: auto;
-      }
-
-      .docs td {
-        margin: 0;
-        padding: 0.5em;
-        max-width: 27em;
-        text-overflow: ellipsis;
-        overflow-x: hidden;
-      }
-
-      .fieldHasDocs {
-        margin-bottom: 0;
-      }
-
-      .fieldDocs {
-        border: 1px solid #F5F5F5;
-        border-top: 0px;
-        padding: 1px 1em;
+      #search {
+        width: 100%;
       }
 
-      /* modals */
-      .modal-container {
-        display: flex;
-        width: 100%;
-        height: 100%;
+      #helpDialog {
+        width: 21em;
+        height: 21em;
         position: fixed;
         top: 0;
         left: 0;
-        justify-content: center;
-        align-items: center;
-        background-color: rgba(0, 0, 0, 0.15);
-        backdrop-filter: blur(0.3em);
-      }
-
-      .modal-container > .modal {
-        max-width: 97vw;
-        max-height: 97vh;
-        overflow: auto;
-        font-size: 1rem;
+        background-color: #333;
         color: #fff;
-        background-color: var(--modal-bg-color);
-        border: 0.125rem solid #000;
-        box-shadow: 0 0.5rem 2.5rem 0.3rem var(--modal-sh-color);
+        border: 1px solid #fff;
       }
-
-      .modal-container h1 {
-        margin: 0.75em 2.5em 1em 2.5em;
-        font-size: 1.5em;
+      #helpDialog h1 {
         text-align: center;
+        font-size: 1.5em;
       }
-
-      .modal-container dt, .modal-container dd {
+      #helpDialog dt, #helpDialog dd {
         display: inline;
         margin: 0 0.2em;
       }
-
-      .modal-container dl {
-        margin-left: 0.5em;
-        margin-right: 0.5em;
-      }
-
-      .prefs-list {
-        list-style: none;
-        padding: 0;
-        margin-left: 0.5em;
-        margin-right: 0.5em;
-      }
-
       kbd {
-        display: inline-block;
-        padding: 0.3em 0.2em;
-        font-family: var(--mono);
-        font-size: 1em;
-        line-height: 0.8em;
-        vertical-align: middle;
         color: #000;
         background-color: #fafbfc;
         border-color: #d1d5da;
         border-bottom-color: #c6cbd1;
-        border: solid 0.0625em;
-        border-radius: 0.1875em;
-        box-shadow: inset 0 -0.2em 0 #c6cbd1;
+        box-shadow-color: #c6cbd1;
+        display: inline-block;
+        padding: 0.3em 0.2em;
+        font: 1.2em monospace;
+        line-height: 0.8em;
+        vertical-align: middle;
+        border: solid 1px;
+        border-radius: 3px;
+        box-shadow: inset 0 -1px 0;
         cursor: default;
       }
-      
-      #listFns > div {
-        padding-bottom: 10px;
+
+      #listSearchResults li.selected {
+        background-color: #93e196;
       }
 
-      #listFns dt {
-        font-family: var(--mono);
-        display: flex;
-        flex-direction: colunm;
-        justify-content: space-between;
+      #tableFnErrors dt {
+        font-weight: bold;
       }
-      
-      #listFns dt .fnSignature {
-        overflow-x: hidden;
-        white-space: nowrap;
+
+      td {
+        vertical-align: top;
+        margin: 0;
+        padding: 0.5em;
+        max-width: 20em;
         text-overflow: ellipsis;
-      }
-      
-      .argBreaker {
-        display: none;
+        overflow-x: hidden;
       }
 
-      /* tokens */
       .tok-kw {
           color: #333;
           font-weight: bold;
@@ -657,51 +191,35 @@
           color: #458;
           font-weight: bold;
       }
-      .tok-decl-ref {
-          color: #0086b3;
-          font-weight: bold;
-      }
 
-      /* dark mode */
       @media (prefers-color-scheme: dark) {
-        :root {
-          --tx-color: #bbb;
-          --bg-color: #111;
-          --link-color: #88f;
-          --sidebar-sh-color: rgba(128, 128, 128, 0.5);
-          --sidebar-mod-bg-color: #333;
-          --sidebar-modlnk-tx-color: #fff;
-          --sidebar-modlnk-tx-color-hover: #fff;
-          --sidebar-modlnk-tx-color-active: #000;
-          --sidebar-modlnk-bg-color: transparent;
-          --sidebar-modlnk-bg-color-hover: #555;
-          --sidebar-modlnk-bg-color-active: #FFBB4D;
-          --search-bg-color: #3c3c3c;
-          --search-bg-color-focus: #000;
-          --search-sh-color: rgba(255, 255, 255, 0.28);
-          --search-other-results-color: rgba(255, 255, 255, 0.28);
-          --modal-sh-color: rgba(142, 142, 142, 0.5);
-          --modal-bg-color: #333;
-        --warning-popover-bg-color: #600000;
+        body{
+          background-color: #111;
+          color: #bbb;
         }
-
-        pre {
+        a {
+          color: #88f;
+        }
+        code a {
+          color: #bbb;
+        }
+        pre{
           background-color:#2A2A2A;
         }
         .fieldDocs {
           border-color:#2A2A2A;
         }
-        .listNav {
+        #listNav {
           background-color: #333;
         }
-        .listNav li a {
+        #listNav li a {
           color: #fff;
         }
-        .listNav li a:hover {
+        #listNav li a:hover {
           background-color: #555;
           color: #fff;
         }
-        .listNav li a.active {
+        #listNav li a.active {
           background-color: #FFBB4D;
           color: #000;
         }
@@ -735,511 +253,137 @@
         .tok-type {
             color: #68f;
         }
-        .tok-decl-ref {
-            color: lightblue;
-        }
-      }
-
-      @media only screen and (max-width: 750px) {
-        .canvas {
-          overflow: auto;
-        }
-        .flex-main {
-          flex-direction: column;
-        }
-        .sidebar {
-          min-width: calc(100vw - 2.8rem);
-          padding-left: 1.4rem;
-          padding-right: 1.4rem;
-        }
-        .flex-main > .flex-filler {
-          display: none;
-        }
-        .flex-main > .flex-right > .flex-filler {
-          display: none;
-        }
-        .flex-main > .flex-right > .wrap {
-          max-width: 100vw;
-        }
-        .flex-main > .flex-right > .wrap > .docs {
-          padding-right: 1.4rem;
-          background: transparent;
-        }
-        .modules {
-          display: flex;
-          flex-wrap: wrap;
-        }
-        .table-container table {
-          display: flex;
-          flex-direction: column;
-        }
-        .table-container tr {
-          display: flex;
-          flex-direction: column;
-        }
-        .examples {
-          overflow-x: scroll;
-          -webkit-overflow-scrolling: touch;
-          max-width: 100vw;
-          margin-left: -1.4rem;
-          margin-right: -1.4rem;
-        }
-        .examples li {
-          width: max-content;
-          padding-left: 1.4rem;
-          padding-right: 1.4rem;
-        }
-        .mobile-scroll-container {
-          overflow-x: scroll;
-          -webkit-overflow-scrolling: touch;
-          margin-left: -1.4rem;
-          margin-right: -1.4rem;
-          max-width: 100vw;
-        }
-        .mobile-scroll-container > .scroll-item {
-          margin-left: 1.4rem;
-          margin-right: 1.4rem;
-          box-sizing: border-box;
-          width: max-content;
-          display: inline-block;
-          min-width: calc(100% - 2.8rem);
-        }
-        #searchPlaceholderText {
-          display: none;
-        }
-        #searchPlaceholderTextMobile {
-          display: inline;
-        }
-      }
-      .banner {
-        background-color: orange;
-        text-align: center;
-        color: black;
-        padding: 5px 5px;
-      }
-      .banner a {
-        color: black;
-        text-decoration: underline;
-    }
-
-  </style>
-
-  <style>
-      pre {
-        --zig-keyword: #333;
-        --zig-builtin: #0086b3;
-        --zig-identifier: black;
-        --zig-decl-identifier: #0086b3;
-        --zig-string-literal: #d14;
-        --zig-type: #458;
-        --zig-fn: #900;
-      }
-      
-      @media (prefers-color-scheme: dark) { 
-        pre {
-          --zig-keyword: #eee;
-          --zig-builtin: #ff894c;
-          --zig-identifier: #bbbbbb;
-          --zig-decl-identifier: lightblue;
-          --zig-string-literal: #2e5;
-          --zig-type: #68f;
-          --zig-fn: #e33;
-        }
       }
-
-    .zig_keyword_addrspace,
-    .zig_keyword_align,
-    .zig_keyword_and,
-    .zig_keyword_asm,
-    .zig_keyword_async,
-    .zig_keyword_await,
-    .zig_keyword_break,
-    .zig_keyword_catch,
-    .zig_keyword_comptime,
-    .zig_keyword_const,
-    .zig_keyword_continue,
-    .zig_keyword_defer,
-    .zig_keyword_else,
-    .zig_keyword_enum,
-    .zig_keyword_errdefer,
-    .zig_keyword_error,
-    .zig_keyword_export,
-    .zig_keyword_extern,
-    .zig_keyword_for,
-    .zig_keyword_if,
-    .zig_keyword_inline,
-    .zig_keyword_noalias,
-    .zig_keyword_noinline,
-    .zig_keyword_nosuspend,
-    .zig_keyword_opaque,
-    .zig_keyword_or,
-    .zig_keyword_orelse,
-    .zig_keyword_packed,
-    .zig_keyword_anyframe,
-    .zig_keyword_pub,
-    .zig_keyword_resume,
-    .zig_keyword_return,
-    .zig_keyword_linksection,
-    .zig_keyword_callconv,
-    .zig_keyword_struct,
-    .zig_keyword_suspend,
-    .zig_keyword_switch,
-    .zig_keyword_test,
-    .zig_keyword_threadlocal,
-    .zig_keyword_try,
-    .zig_keyword_union,
-    .zig_keyword_unreachable,
-    .zig_keyword_usingnamespace,
-    .zig_keyword_var,
-    .zig_keyword_volatile,
-    .zig_keyword_allowzero,
-    .zig_keyword_while,
-    .zig_keyword_anytype,
-    .zig_keyword_fn
-    {
-      color: var(--zig-keyword);
-      font-weight: bold;
-    }
-
-
-    .zig_string_literal,
-    .zig_multiline_string_literal_line,
-    .zig_char_literal
-    {
-      color: var(--zig-string-literal);
-    }
-
-    .zig_builtin
-    {
-      color: var(--zig-builtin);
-    }
-
-    .zig_doc_comment,
-    .zig_container_doc_comment,
-    .zig_line_comment {
-      color: #545454;
-      font-style: italic;
-    }
-
-    .zig_identifier {
-      color: var(--zig-identifier);
-      font-weight: bold;
-    }
-    
-    .zig_decl_identifier {
-      color: var(--zig-decl-identifier);
-      font-weight: bold;
-    }
-
-    .zig_number_literal,
-    .zig_special {
-      color: #ff8080;
-    }
-
-    .zig_type {
-      color: var(--zig-type);
-      font-weight: bold;
-    }
-
-    .zig_fn {
-      color: var(--zig-fn);
-      font-weight: bold;
-    }
-   
     </style>
   </head>
-  <body class="canvas">
-    <div id="banner" class="banner">
-      This is a beta autodoc build; expect bugs and missing information.
-      <a href="https://github.com/ziglang/zig/wiki/How-to-contribute-to-Autodoc">Report an Issue</a>,
-      <a href="https://github.com/ziglang/zig/wiki/How-to-contribute-to-Autodoc">Contribute</a>,
-      <a href="https://github.com/ziglang/zig/wiki/How-to-read-the-standard-library-source-code">Learn more about stdlib source code</a>.
-    </div>
-    <div id="main" class="flex-main">
-      <div class="flex-horizontal" style="justify-content: center; padding: 0 0.5rem;">
-        <div class="flex-left">
-          <div class="logo">
-            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 140">
-            <g fill="#F7A41D">
-              <g>
-                <polygon points="46,22 28,44 19,30"/>
-                <polygon points="46,22 33,33 28,44 22,44 22,95 31,95 20,100 12,117 0,117 0,22" shape-rendering="crispEdges"/>
-                <polygon points="31,95 12,117 4,106"/>
-              </g>
-              <g>
-                <polygon points="56,22 62,36 37,44"/>
-                <polygon points="56,22 111,22 111,44 37,44 56,32" shape-rendering="crispEdges"/>
-                <polygon points="116,95 97,117 90,104"/>
-                <polygon points="116,95 100,104 97,117 42,117 42,95" shape-rendering="crispEdges"/>
-                <polygon points="150,0 52,117 3,140 101,22"/>
-              </g>
-              <g>
-                <polygon points="141,22 140,40 122,45"/>
-                <polygon points="153,22 153,117 106,117 120,105 125,95 131,95 131,45 122,45 132,36 141,22" shape-rendering="crispEdges"/>
-                <polygon points="125,95 130,110 106,117"/>
-              </g>
-            </g>
-            <style>
-            #text { fill: #121212 }
-            @media (prefers-color-scheme: dark) { #text { fill: #f2f2f2 } }
-            </style>
-            <g id="text">
-              <g>
-                <polygon points="260,22 260,37 229,40 177,40 177,22" shape-rendering="crispEdges"/>
-                <polygon points="260,37 207,99 207,103 176,103 229,40 229,37"/>
-                <polygon points="261,99 261,117 176,117 176,103 206,99" shape-rendering="crispEdges"/>
-              </g>
-              <rect x="272" y="22" shape-rendering="crispEdges" width="22" height="95"/>
-              <g>
-                <polygon points="394,67 394,106 376,106 376,81 360,70 346,67" shape-rendering="crispEdges"/>
-                <polygon points="360,68 376,81 346,67"/>
-                <path d="M394,106c-10.2,7.3-24,12-37.7,12c-29,0-51.1-20.8-51.1-48.3c0-27.3,22.5-48.1,52-48.1    c14.3,0,29.2,5.5,38.9,14l-13,15c-7.1-6.3-16.8-10-25.9-10c-17,0-30.2,12.9-30.2,29.5c0,16.8,13.3,29.6,30.3,29.6    c5.7,0,12.8-2.3,19-5.5L394,106z"/>
-              </g>
-            </g>
-            </svg>
-          </div>
-          <div id="sectGuideApiSwitch">
-            <ul class="guides-api-switch">
-              <li><a id="ApiSwitch" class="active" href="#A;">API</a></li>
-              <li><a id="guideSwitch" class="" href="#G;">Guides</a></li>
-            </ul>
-          </div>
-            </div>
-        <div class="flex-right" style="padding-top: 0.5rem;overflow:visible;">
-          <div class="search-container" style="position:relative;">
-            <div id="searchPlaceholder">
-              <span id="searchPlaceholderText"><!-- populated by setPrefSlashSearch --></span>
-              <span id="searchPlaceholderTextMobile">Search</span>
-            </div>
-            <input type="search" class="search" id="search" autocomplete="off" spellcheck="false" disabled>
-            <div id="dotsPopover">
-              Use spaces instead of dots. See $resource for more info.
-            </div>
-          </div>
-          <div id="sectNavAPI" style="margin-top: 0.5rem;"><ul id="listNavAPI" class="listNav"></ul></div>
-          <div id="sectNavGuides" class="hidden" style="margin-top: 0.5rem">
-            <ul id="listNavGuides" class="listNav">
-              <li>
-                <a href="#G;" class="active">Index</a>
-              </li>
-              <li style="flex-grow:1;">
-                <a href="#G;" class="active" onclick="scrollGuidesTop(event);"></a>
-              </li>
-            </ul>
-        </div>
-        </div>
-      </div>
-      <div style="height:100%; overflow:hidden;">
-        <div id="sectSearchResults" class="docs hidden">
-          <details id="searchHelp">
-            <summary id="searchHelpSummary" class="normal">How to search effectively</summary>
-            <div>
-              <h2>How To Search Effectively</h2>
-              <h3>Matching</h3>
-              <ul>
-                <li>Search is case-insensitive by default.</li>
-                <li>Using uppercase letters in your query will make the search
-                  case-sensitive.</li>
-                <li>Given <code>ArrayListUnmanaged</code>:
-                  <ul>
-                    <li>the following search terms (and their prefixes) will match:
-                      <ul>
-                        <li><code>array</code></li>
-                        <li><code>list</code></li>
-                        <li><code>unmanaged</code></li>
-                      </ul>
-                    </li>
-                    <li>the following search terms will <b>NOT</b> match:
-                      <ul>
-                        <li><code>stun</code></li>
-                        <li><code>ray</code></li>
-                        <li><code>managed</code></li>
-                      </ul>
-                    </li>
-                  </ul>
-                </li>
-                <li>More precisely, the search system is based on a Radix Tree. The Radix Tree contains full decl names plus some suffixes, split by following the official style guide (e.g. <code>HashMapUnmanaged</code> also produces <code>MapUnmanaged</code> and <code>Unmanaged</code>, same with snake_case and camelCase names). </li>
-              </ul> 
-
-              <h3>Multiple terms</h3>
-            
-              <ul>
-                <li>When a search query contains multiple terms, order doesn't matter when 
-                    all terms match within a single decl name (e.g. "map auto" will match <code>AutoHashMap</code>).</li>
-                <li>Query term order does matter when matching different decls alognside
-                    a path (e.g. "js parse" matching <code>std.json.parse</code>), in which
-                    case the order of the terms will determine whether the match goes above or 
-                    below the "other results" line.</li>
-                <li>As an example, "fs create" will put above the line all things related to the creation of files and directories inside of `std.fs`, while still showing (but below the line) matches from `std.Bulild`.</li>
-                <li>As another example, "fs windows" will prioritize windows-related results in `std.fs`, while "windows fs" will prioritize "fs"-related results in `std.windows`.</li>
-                <li>This means that if you're searching inside a target namespace, you never have to read below the "other results" line.</li>
-                <li>Since matching doesn't have to be perfect, you can also target a group of namespaces to search into. For example "array orderedremove" will show you all "Array-" namespaces that support <code>orderedRemove</code>.</li>
-                <li>Periods are replaced by spaces because the Radix Tree doesn't index full paths, and in practice you should expect the match scoring system to consistently give you what you're looking for even when your query path is split into multiple terms.</li>
-              </ul>
-            </div>
-          </details>
-          <h2>Search Results</h2>
-          <ul id="listSearchResults"></ul>
-          <p id="sectSearchAllResultsLink" class="hidden"><a href="">show all results</a></p>
-        </div>
-        <div id="sectSearchNoResults" class="docs hidden">
-          <h2>No Results Found</h2>
-          <p>Here are some things you can try:</p>
-          <ul>
-            <li>Check out the <a id="langRefLink">Language Reference</a> for the language itself.</li>
-            <li>Check out the <a href="https://ziglang.org/learn/">Learn page</a> for other helpful resources for learning Zig.</li>
-            <li>Use your search engine.</li>
-          </ul>
-          <p>Press <kbd>?</kbd> to see keyboard shortcuts and <kbd>Esc</kbd> to return.</p>
-        </div>
-        <div id="guides" class="flex-horizontal hidden" style="align-items:flex-start;height:100%;overflow:hidden;">
-          <div id="guidesMenu" class="sidebar">
-            <h2 id="guidesMenuTitle">Table of Contents</h2>
-            <div id="guideTocListEmpty" style="margin:0 1rem;"><i>No content to display.</i></div>
-            <div id="guideTocList" style="height:100%;overflow-y:scroll;"></div>
-          </div>
-          <div id="activeGuide" class="hidden"></div>
-        </div> 
-        <div id="docs" class="hidden" style="align-items:flex-start;height:100%;overflow:hidden;">
-          <section id="docs-scroll" class="docs">
-            <p id="status">Loading...</p>
-            <div id="fnProto" class="hidden">
-              <div class="mobile-scroll-container"><pre id="fnProtoCode" class="scroll-item"></pre></div>
-              <div id="fnSourceLink" style="display:flex;flex-direction:row;justify-content:flex-end;"></div>
-            </div>
-            <h1 id="hdrName" class="hidden"></h1>
-            <div id="fnNoExamples" class="hidden">
-              <p>This function is not tested or referenced.</p>
-            </div>
-            <div id="declNoRef" class="hidden">
-              <p>
-              This declaration is not tested or referenced, and it has therefore not been included in
-              semantic analysis, which means the only documentation available is whatever is in the
-              doc comments.
-              </p>
-            </div>
-            <div id="tldDocs" class="hidden"></div>
-            <div id="sectParams" class="hidden">
-              <h2>Parameters</h2>
-              <div id="listParams"></div>
-            </div>
-            <div id="sectFnErrors" class="hidden">
-              <h2>Errors</h2>
-              <div id="fnErrorsAnyError">
-                <p><span class="tok-type">anyerror</span> means the error set is known only at runtime.</p>
-              </div>
-              <div id="tableFnErrors"><dl id="listFnErrors"></dl></div>
-            </div>
-            <div id="sectFields" class="hidden">
-              <h2>Fields</h2>
-              <div id="listFields"></div>
-            </div>
-            <div id="sectNamespaces" class="hidden">
-              <div style="position:relative;">
-                <h2 style="position:sticky; top:0; background-color:var(--bg-color)">Namespaces</h2>
-                <div class="flex-horizontal" style="justify-content:space-around;align-items:flex-start;flex-wrap:wrap;">
-                  <ul id="listNamespacesLeft" class="column"></ul>
-                  <ul id="listNamespacesRight" class="column"></ul>
-                </div>
-              </div>
-              <h3>Other Namespaces <span style="font-size:1.1rem; cursor:pointer;" title="This box contains namespaces that are exported without a doc comment.">&#9432;</span></h3>
-              <div id="noDocsNamespaces"></div>
-            </div>
-            <div id="sectTypes" class="hidden">
-              <div style="position:relative;">
-                <h2 style="position:sticky; top:0; background-color:var(--bg-color)">Types</h2>
-                <div class="flex-horizontal" style="justify-content:space-around;align-items:flex-start;flex-wrap:wrap;">
-                  <ul id="listTypesLeft" class="column"></ul>
-                  <ul id="listTypesRight" class="column"></ul>
-                </div>
-              </div>
-            </div>
-            <div id="sectGlobalVars" class="hidden">
-              <h2>Global Variables</h2>
-              <div class="table-container">
-                <table>
-                  <tbody id="listGlobalVars"></tbody>
-                </table>
-              </div>
-            </div>
-            <div id="sectFns" class="hidden">
-              <h2>Functions</h2>
-              <div class="table-container">
-                <dl id="listFns"></dl>
-              </div>
-            </div>
-            <div id="sectValues" class="hidden">
-              <h2>Values</h2>
-              <div class="table-container">
-                <table>
-                  <tbody id="listValues"></tbody>
-                </table>
-              </div>
-            </div>
-            <div id="sectErrSets" class="hidden">
-              <h2>Error Sets</h2>
-              <ul id="listErrSets"></ul>
-            </div>
-            <div id="fnExamples" class="hidden">
-              <h2>Examples</h2>
-              <ul id="listFnExamples" class="examples"></ul>
-            </div>
-            <div id="sectDocTests" class="hidden">
-              <h2>Usage Examples <span style="font-size:1.1rem; cursor:pointer;" title="See `doctests` in the language reference to learn more.">&#9432;</span></h2>
-              <pre id="docTestsCode"></pre>
-            </div>
-            <div id="sectTests" class="hidden">
-              <h2>Tests</h2>
-              <div class="table-container">
-                <table>
-                  <tbody id="listTests"></tbody>
-                </table>
-              </div>
-            </div>
-          </section>
-        </div>
-        <div class="flex-filler"></div>
+  <body>
+    <nav>
+      <div class="logo">
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 140">
+        <g fill="#F7A41D">
+          <g>
+            <polygon points="46,22 28,44 19,30"/>
+            <polygon points="46,22 33,33 28,44 22,44 22,95 31,95 20,100 12,117 0,117 0,22" shape-rendering="crispEdges"/>
+            <polygon points="31,95 12,117 4,106"/>
+          </g>
+          <g>
+            <polygon points="56,22 62,36 37,44"/>
+            <polygon points="56,22 111,22 111,44 37,44 56,32" shape-rendering="crispEdges"/>
+            <polygon points="116,95 97,117 90,104"/>
+            <polygon points="116,95 100,104 97,117 42,117 42,95" shape-rendering="crispEdges"/>
+            <polygon points="150,0 52,117 3,140 101,22"/>
+          </g>
+          <g>
+            <polygon points="141,22 140,40 122,45"/>
+            <polygon points="153,22 153,117 106,117 120,105 125,95 131,95 131,45 122,45 132,36 141,22" shape-rendering="crispEdges"/>
+            <polygon points="125,95 130,110 106,117"/>
+          </g>
+        </g>
+        <style>
+        #text { fill: #121212 }
+        @media (prefers-color-scheme: dark) { #text { fill: #f2f2f2 } }
+        </style>
+        <g id="text">
+          <g>
+            <polygon points="260,22 260,37 229,40 177,40 177,22" shape-rendering="crispEdges"/>
+            <polygon points="260,37 207,99 207,103 176,103 229,40 229,37"/>
+            <polygon points="261,99 261,117 176,117 176,103 206,99" shape-rendering="crispEdges"/>
+          </g>
+          <rect x="272" y="22" shape-rendering="crispEdges" width="22" height="95"/>
+          <g>
+            <polygon points="394,67 394,106 376,106 376,81 360,70 346,67" shape-rendering="crispEdges"/>
+            <polygon points="360,68 376,81 346,67"/>
+            <path d="M394,106c-10.2,7.3-24,12-37.7,12c-29,0-51.1-20.8-51.1-48.3c0-27.3,22.5-48.1,52-48.1    c14.3,0,29.2,5.5,38.9,14l-13,15c-7.1-6.3-16.8-10-25.9-10c-17,0-30.2,12.9-30.2,29.5c0,16.8,13.3,29.6,30.3,29.6    c5.7,0,12.8-2.3,19-5.5L394,106z"/>
+          </g>
+        </g>
+        </svg>
       </div>
+    </nav>
+    <div id="navWrap">
+      <input type="search" id="search" autocomplete="off" spellcheck="false" placeholder="`s` to search, `?` to see more options">
+      <div id="sectNav" class="hidden"><ul id="listNav"></ul></div>
+    </div>
+    <section>
+    <p id="status">Loading...</p>
+    <div id="fnProto" class="hidden">
+      <pre><code id="fnProtoCode"></code></pre>
     </div>
-    <div id="helpModal" class="hidden">
-      <div class="modal-container">
-        <div class="modal">
-          <h1>Keyboard Shortcuts</h1>
-          <dl><dt><kbd>?</kbd></dt><dd>Toggle this help modal</dd></dl>
-          <dl><dt id="searchKeys"><!-- populated by setPrefSlashSearch --></dt><dd>Focus the search field</dd></dl>
-          <div style="margin-left: 1em">
-            <dl><dt><kbd>โ†‘</kbd></dt><dd>Move up in search results</dd></dl>
-            <dl><dt><kbd>โ†“</kbd></dt><dd>Move down in search results</dd></dl>
-            <dl><dt><kbd>โŽ</kbd></dt><dd>Go to active search result</dd></dl>
-          </div>
-          <dl><dt><kbd>p</kbd></dt><dd>Open preferences</dd></dl>
-          <dl><dt><kbd>Esc</kbd></dt><dd>Clear focus; close this modal</dd></dl>
-        </div>
+    <h1 id="hdrName" class="hidden"></h1>
+    <div id="tldDocs" class="hidden"></div>
+    <div id="sectFnErrors" class="hidden">
+      <h2>Errors</h2>
+      <div id="fnErrorsAnyError">
+        <p><span class="tok-type">anyerror</span> means the error set is known only at runtime.</p>
       </div>
+      <div id="tableFnErrors"><dl id="listFnErrors"></dl></div>
     </div>
-    <div id="prefsModal" class="hidden">
-      <div class="modal-container">
-        <div class="modal">
-          <h1>Preferences</h1>
-          <ul class="prefs-list">
-            <li><input id="prefSlashSearch" type="checkbox"><label for="prefSlashSearch">Enable <kbd>/</kbd> for search</label></li>
-          </ul>
-        </div>
+    <div id="sectSearchResults" class="hidden">
+      <h2>Search Results</h2>
+      <ul id="listSearchResults"></ul>
+    </div>
+    <div id="sectSearchNoResults" class="hidden">
+      <h2>No Results Found</h2>
+      <p>Press escape to exit search and then '?' to see more options.</p>
+    </div>
+    <div id="sectFields" class="hidden">
+      <h2>Fields</h2>
+      <div id="listFields">
       </div>
     </div>
-    <script src="data-typeKinds.js"></script>
-    <script src="data-rootMod.js"></script>
-    <script src="data-modules.js"></script>
-    <script src="data-files.js"></script>
-    <script src="data-calls.js"></script>
-    <script src="data-types.js"></script>
-    <script src="data-decls.js"></script>
-    <script src="data-exprs.js"></script>
-    <script src="data-astNodes.js"></script>
-    <script src="data-comptimeExprs.js"></script>
-    <script src="data-guideSections.js"></script>
-    <script src="commonmark.js"></script>
-    <script src="ziglexer.js"></script>
+    <div id="sectTypes" class="hidden">
+      <h2>Types</h2>
+      <ul id="listTypes">
+      </ul>
+    </div>
+    <div id="sectNamespaces" class="hidden">
+      <h2>Namespaces</h2>
+      <ul id="listNamespaces">
+      </ul>
+    </div>
+    <div id="sectGlobalVars" class="hidden">
+      <h2>Global Variables</h2>
+      <table>
+        <tbody id="listGlobalVars">
+        </tbody>
+      </table>
+    </div>
+    <div id="sectFns" class="hidden">
+      <h2>Functions</h2>
+      <dl id="listFns">
+      </dl>
+    </div>
+    <div id="sectValues" class="hidden">
+      <h2>Values</h2>
+      <table>
+        <tbody id="listValues">
+        </tbody>
+      </table>
+    </div>
+    <div id="sectErrSets" class="hidden">
+      <h2>Error Sets</h2>
+      <ul id="listErrSets">
+      </ul>
+    </div>
+    <div id="sectDocTests" class="hidden">
+      <h2>Example Usage</h2>
+      <pre><code id="docTestsCode"></code></pre>
+    </div>
+    <div id="sectSource" class="hidden">
+      <h2>Source Code</h2>
+      <pre><code id="sourceText"></code></pre>
+    </div>
+    </section>
+    <div id="helpDialog" class="hidden">
+      <h1>Keyboard Shortcuts</h1>
+      <dl><dt><kbd>?</kbd></dt><dd>Show this help dialog</dd></dl>
+      <dl><dt><kbd>Esc</kbd></dt><dd>Clear focus; close this dialog</dd></dl>
+      <dl><dt><kbd>s</kbd></dt><dd>Focus the search field</dd></dl>
+      <dl><dt><kbd>u</kbd></dt><dd>Go to source code</dd></dl>
+      <dl><dt><kbd>โ†‘</kbd></dt><dd>Move up in search results</dd></dl>
+      <dl><dt><kbd>โ†“</kbd></dt><dd>Move down in search results</dd></dl>
+      <dl><dt><kbd>โŽ</kbd></dt><dd>Go to active search result</dd></dl>
+    </div>
     <script src="main.js"></script>
   </body>
 </html>
+
lib/docs/main.js
@@ -1,5242 +1,851 @@
-"use strict";
-
-var zigAnalysis = {
-  typeKinds, 
-  rootMod, 
-  modules, 
-  astNodes, 
-  calls, 
-  files, 
-  decls, 
-  exprs, 
-  types,
-  comptimeExprs, 
-  guideSections
-};
-
-let skipNextHashChange = null;
-
-const NAV_MODES = {
-  API: "#A;",
-  GUIDES: "#G;",
-};
-
-
-var scrollHistory = {};
-
 (function() {
-  const domBanner = document.getElementById("banner");
-  const domMain = document.getElementById("main");
-  const domStatus = document.getElementById("status");
-  const domSectNavAPI = document.getElementById("sectNavAPI");
-  const domListNavAPI = document.getElementById("listNavAPI");
-  const domSectNavGuides = document.getElementById("sectNavGuides");
-  const domListNavGuides = document.getElementById("listNavGuides");
-  const domApiSwitch = document.getElementById("ApiSwitch");
-  const domGuideSwitch = document.getElementById("guideSwitch");
-  const domGuidesMenu = document.getElementById("guidesMenu");
-  const domGuidesMenuTitle = document.getElementById("guidesMenuTitle");
-  const domGuideTocList = document.getElementById("guideTocList");
-  const domGuideTocListEmtpy = document.getElementById("guideTocListEmpty");
-  const domListMods = document.getElementById("listMods");
-  const domSectTypes = document.getElementById("sectTypes");
-  const domListTypesLeft = document.getElementById("listTypesLeft");
-  const domListTypesRight = document.getElementById("listTypesRight");
-  const domSectTests = document.getElementById("sectTests");
-  const domListTests = document.getElementById("listTests");
-  const domSectDocTests = document.getElementById("sectDocTests");
-  const domDocTestsCode = document.getElementById("docTestsCode");
-  const domSectNamespaces = document.getElementById("sectNamespaces");
-  const domListNamespacesLeft = document.getElementById("listNamespacesLeft");
-  const domListNamespacesRight = document.getElementById("listNamespacesRight");
-  const domNoDocsNamespaces = document.getElementById("noDocsNamespaces");
-  const domSectErrSets = document.getElementById("sectErrSets");
-  const domListErrSets = document.getElementById("listErrSets");
-  const domSectFns = document.getElementById("sectFns");
-  const domListFns = document.getElementById("listFns");
-  const domSectFields = document.getElementById("sectFields");
-  const domListFields = document.getElementById("listFields");
-  const domSectGlobalVars = document.getElementById("sectGlobalVars");
-  const domListGlobalVars = document.getElementById("listGlobalVars");
-  const domSectValues = document.getElementById("sectValues");
-  const domListValues = document.getElementById("listValues");
-  const domFnProto = document.getElementById("fnProto");
-  const domFnProtoCode = document.getElementById("fnProtoCode");
-  const domFnSourceLink = document.getElementById("fnSourceLink");
-  const domSectParams = document.getElementById("sectParams");
-  const domListParams = document.getElementById("listParams");
-  const domTldDocs = document.getElementById("tldDocs");
-  const domSectFnErrors = document.getElementById("sectFnErrors");
-  const domListFnErrors = document.getElementById("listFnErrors");
-  const domTableFnErrors = document.getElementById("tableFnErrors");
-  const domFnErrorsAnyError = document.getElementById("fnErrorsAnyError");
-  const domFnExamples = document.getElementById("fnExamples");
-  // const domListFnExamples = (document.getElementById("listFnExamples"));
-  const domFnNoExamples = document.getElementById("fnNoExamples");
-  const domDeclNoRef = document.getElementById("declNoRef");
-  const domSearch = document.getElementById("search");
-  const domSearchHelp = document.getElementById("searchHelp");
-  const domSearchHelpSummary = document.getElementById("searchHelpSummary");
-  const domSectSearchResults = document.getElementById("sectSearchResults");
-  const domSectSearchAllResultsLink = document.getElementById("sectSearchAllResultsLink");
-  const domDocs = document.getElementById("docs");
-  const domDocsScroll = document.getElementById("docs-scroll");
-  const domGuidesSection = document.getElementById("guides");
-  const domActiveGuide = document.getElementById("activeGuide");
-
-  const domListSearchResults = document.getElementById("listSearchResults");
-  const domSectSearchNoResults = document.getElementById("sectSearchNoResults");
-  // const domTdTarget = (document.getElementById("tdTarget"));
-  const domTdZigVer = document.getElementById("tdZigVer");
-  const domHdrName = document.getElementById("hdrName");
-  const domHelpModal = document.getElementById("helpModal");
-  const domSearchKeys = document.getElementById("searchKeys");
-  const domPrefsModal = document.getElementById("prefsModal");
-  const domSearchPlaceholder = document.getElementById("searchPlaceholder");
-  const domSearchPlaceholderText = document.getElementById("searchPlaceholderText");
-  const sourceFileUrlTemplate = "src/{{mod}}/{{file}}.html#L{{line}}"
-  const domLangRefLink = document.getElementById("langRefLink");
-
-  const domPrefSlashSearch = document.getElementById("prefSlashSearch");
-  const prefs = getLocalStorage();
-  loadPrefs();
-
-  domPrefSlashSearch.addEventListener("change", () => setPrefSlashSearch(domPrefSlashSearch.checked));
-
-  const scrollMonitor = [
-    domActiveGuide,
-    domGuideTocList,
-    domDocsScroll,
-    domSectSearchResults,
-  ];
-
-  computeGuideHashes();
-
-  let searchTimer = null;
-  let searchTrimResults = true;
-
-  let escapeHtmlReplacements = {
-    "&": "&amp;",
-    '"': "&quot;",
-    "<": "&lt;",
-    ">": "&gt;",
-  };
-
-  let typeKinds = indexTypeKinds();
-  let typeTypeId = findTypeTypeId();
-  let pointerSizeEnum = { One: 0, Many: 1, Slice: 2, C: 3 };
-
-  let declSearchIndex = new RadixTree();
-  window.search = declSearchIndex;
-
-  // for each module, is an array with modules to get to this one
-  let canonModPaths = computeCanonicalModulePaths();
-
-  // for each decl, is an array with {declNames, modNames} to get to this one
-  let canonDeclPaths = null; // lazy; use getCanonDeclPath
-
-  // for each type, is an array with {declNames, modNames} to get to this one
-  let canonTypeDecls = null; // lazy; use getCanonTypeDecl
-
-  let curNav = {
-    hash: "",
-    mode: NAV_MODES.API,
-    activeGuide: "",
-    activeGuideScrollTo: null,
-    // each element is a module name, e.g. @import("a") then within there @import("b")
-    // starting implicitly from root module
-    modNames: [],
-    // same as above except actual modules, not names
-    modObjs: [],
-    // Each element is a decl name, `a.b.c`, a is 0, b is 1, c is 2, etc.
-    // empty array means refers to the module itself
-    declNames: [],
-    // these will be all types, except the last one may be a type or a decl
-    declObjs: [],
-    // (a, b, c, d) comptime call; result is the value the docs refer to
-    callName: null,
-  };
-
-  let curNavSearch = "";
-  let curSearchIndex = -1;
-  let imFeelingLucky = false;
-
-  let rootIsStd = detectRootIsStd();
-
-  // map of decl index to list of non-generic fn indexes
-  // let nodesToFnsMap = indexNodesToFns();
-  // map of decl index to list of comptime fn calls
-  // let nodesToCallsMap = indexNodesToCalls();
-
-  let guidesSearchIndex = {};
-  window.guideSearch = guidesSearchIndex;
-  parseGuides();
-
-  // identifiers can contain modal trigger characters so we want to allow typing
-  // such characters when the search is focused instead of toggling the modal
-  let canToggleModal = true;
-
-  domSearch.disabled = false;
-  domSearch.addEventListener("keydown", onSearchKeyDown, false);
-  domSearch.addEventListener("input", onSearchInput, false);
-  domSearch.addEventListener("focus", ev => {
-    domSearchPlaceholder.classList.add("hidden");
-    canToggleModal = false;
-  });
-  domSearch.addEventListener("blur", ev => {
-    if (domSearch.value.length == 0)
-      domSearchPlaceholder.classList.remove("hidden");
-    canToggleModal = true;
-  });
-  domSectSearchAllResultsLink.addEventListener('click', onClickSearchShowAllResults, false);
-  function onClickSearchShowAllResults(ev) {
-    ev.preventDefault();
-    ev.stopPropagation();
-    searchTrimResults = false;
-    onHashChange();
-  }
+    const CAT_namespace = 0;
+    const CAT_global_variable = 1;
+    const CAT_function = 2;
+    const CAT_primitive = 3;
+    const CAT_error_set = 4;
+    const CAT_global_const = 5;
+    const CAT_alias = 6;
+    const CAT_type = 7;
+
+    const domDocTestsCode = document.getElementById("docTestsCode");
+    const domFnErrorsAnyError = document.getElementById("fnErrorsAnyError");
+    const domFnProto = document.getElementById("fnProto");
+    const domFnProtoCode = document.getElementById("fnProtoCode");
+    const domHdrName = document.getElementById("hdrName");
+    const domHelpModal = document.getElementById("helpDialog");
+    const domListErrSets = document.getElementById("listErrSets");
+    const domListFields = document.getElementById("listFields");
+    const domListFnErrors = document.getElementById("listFnErrors");
+    const domListFns = document.getElementById("listFns");
+    const domListGlobalVars = document.getElementById("listGlobalVars");
+    const domListInfo = document.getElementById("listInfo");
+    const domListNamespaces = document.getElementById("listNamespaces");
+    const domListNav = document.getElementById("listNav");
+    const domListSearchResults = document.getElementById("listSearchResults");
+    const domListTypes = document.getElementById("listTypes");
+    const domListValues = document.getElementById("listValues");
+    const domSearch = document.getElementById("search");
+    const domSectDocTests = document.getElementById("sectDocTests");
+    const domSectErrSets = document.getElementById("sectErrSets");
+    const domSectFields = document.getElementById("sectFields");
+    const domSectFnErrors = document.getElementById("sectFnErrors");
+    const domSectFns = document.getElementById("sectFns");
+    const domSectGlobalVars = document.getElementById("sectGlobalVars");
+    const domSectNamespaces = document.getElementById("sectNamespaces");
+    const domSectNav = document.getElementById("sectNav");
+    const domSectSearchNoResults = document.getElementById("sectSearchNoResults");
+    const domSectSearchResults = document.getElementById("sectSearchResults");
+    const domSectSource = document.getElementById("sectSource");
+    const domSectTypes = document.getElementById("sectTypes");
+    const domSectValues = document.getElementById("sectValues");
+    const domSourceText = document.getElementById("sourceText");
+    const domStatus = document.getElementById("status");
+    const domTableFnErrors = document.getElementById("tableFnErrors");
+    const domTldDocs = document.getElementById("tldDocs");
+
+    var searchTimer = null;
+
+    const curNav = {
+      // 0 = home
+      // 1 = decl (decl)
+      // 2 = source (path)
+      tag: 0,
+      // unsigned int: decl index
+      decl: null,
+      // string file name matching tarball path
+      path: null,
+
+      // when this is populated, pressing the "view source" command will
+      // navigate to this hash.
+      viewSourceHash: null,
+    };
+    var curNavSearch = "";
+    var curSearchIndex = -1;
+    var imFeelingLucky = false;
 
-  if (location.hash == "") {
-    location.hash = "#A;";
-  }
+    // names of packages in the same order as wasm
+    const packageList = [];
 
-  // make the modal disappear if you click outside it
-  function handleModalClick(ev) {
-    if (ev.target.classList.contains("modal-container")) {
-      hideModal(this);
-    }
-  }
-  domHelpModal.addEventListener("click", handleModalClick);
-  domPrefsModal.addEventListener("click", handleModalClick);
+    let wasm_promise = fetch("main.wasm");
+    let sources_promise = fetch("sources.tar").then(function(response) {
+      if (!response.ok) throw new Error("unable to download sources");
+      return response.arrayBuffer();
+    });
+    var wasm_exports = null;
 
-  window.addEventListener("hashchange", onHashChange, false);
-  window.addEventListener("keydown", onWindowKeyDown, false);
-  onHashChange();
+    const text_decoder = new TextDecoder();
+    const text_encoder = new TextEncoder();
 
-  // TODO: fix this once langref becomes part of autodoc
-  let langRefVersion = "master";
-  domLangRefLink.href = `https://ziglang.org/documentation/${langRefVersion}/`;
+    WebAssembly.instantiateStreaming(wasm_promise, {
+      js: {
+        log: function(ptr, len) {
+          const msg = decodeString(ptr, len);
+          console.log(msg);
+        },
+        panic: function (ptr, len) {
+            const msg = decodeString(ptr, len);
+            throw new Error("panic: " + msg);
+        },
+      },
+    }).then(function(obj) {
+      wasm_exports = obj.instance.exports;
+      window.wasm = obj; // for debugging
+
+      sources_promise.then(function(buffer) {
+        const js_array = new Uint8Array(buffer);
+        const ptr = wasm_exports.alloc(js_array.length);
+        const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
+        wasm_array.set(js_array);
+        wasm_exports.unpack(ptr, js_array.length);
+
+        updatePackageList();
+
+        window.addEventListener('hashchange', onHashChange, false);
+        domSearch.addEventListener('keydown', onSearchKeyDown, false);
+        domSearch.addEventListener('input', onSearchChange, false);
+        window.addEventListener('keydown', onWindowKeyDown, false);
+        onHashChange();
+      });
+    });
 
-  function renderTitle() {
-    let suffix = " - Zig";
-    switch (curNav.mode) {
-      case NAV_MODES.API:
-        let list = curNav.modNames.concat(curNav.declNames);
-        if (list.length === 0) {
-          document.title = zigAnalysis.modules[zigAnalysis.rootMod].name + suffix;
-        } else {
-          document.title = list.join(".") + suffix;
+    function renderTitle() {
+      const suffix = " - Zig Documentation";
+      if (curNavSearch.length > 0) {
+        document.title = curNavSearch + " - Search" + suffix;
+      } else if (curNav.decl != null) {
+        document.title = fullyQualifiedName(curNav.decl) + suffix;
+      } else if (curNav.path != null) {
+        document.title = curNav.path + suffix;
+      } else {
+        document.title = packageList[0] + suffix; // Home
+      }
+    }
+
+    function render() {
+        domFnErrorsAnyError.classList.add("hidden");
+        domFnProto.classList.add("hidden");
+        domHdrName.classList.add("hidden");
+        domHelpModal.classList.add("hidden");
+        domSectErrSets.classList.add("hidden");
+        domSectDocTests.classList.add("hidden");
+        domSectFields.classList.add("hidden");
+        domSectFnErrors.classList.add("hidden");
+        domSectFns.classList.add("hidden");
+        domSectGlobalVars.classList.add("hidden");
+        domSectNamespaces.classList.add("hidden");
+        domSectNav.classList.add("hidden");
+        domSectSearchNoResults.classList.add("hidden");
+        domSectSearchResults.classList.add("hidden");
+        domSectSource.classList.add("hidden");
+        domSectTypes.classList.add("hidden");
+        domSectValues.classList.add("hidden");
+        domStatus.classList.add("hidden");
+        domTableFnErrors.classList.add("hidden");
+        domTldDocs.classList.add("hidden");
+
+        renderTitle();
+
+        if (curNavSearch !== "") return renderSearch();
+
+        switch (curNav.tag) {
+          case 0: return renderHome();
+          case 1:
+            if (curNav.decl == null) {
+              return render404();
+            } else {
+              return renderDecl(curNav.decl);
+            }
+          case 2: return renderSource(curNav.path);
+          default: throw new Error("invalid navigation state");
         }
-        return;
-      case NAV_MODES.GUIDES:
-        document.title = "[G] " + curNav.activeGuide + suffix;
-        return;
     }
-  }
 
-  function isDecl(x) {
-    return "value" in x;
-  }
+    function renderHome() {
+      if (packageList.length == 1) return renderPackage(0);
 
-  function isType(x) {
-    return "kind" in x && !("value" in x);
-  }
-
-  function isContainerType(x) {
-    return isType(x) && typeKindIsContainer(x.kind);
-  }
+      domStatus.textContent = "TODO implement renderHome for multiple packages";
+      domStatus.classList.remove("hidden");
+    }
 
-  function typeShorthandName(expr) {
-    let resolvedExpr = resolveValue({ expr: expr });
-    if (!("type" in resolvedExpr)) {
-      return null;
+    function renderPackage(pkg_index) {
+      const root_decl = wasm_exports.find_package_root(pkg_index);
+      return renderDecl(root_decl);
     }
-    let type = getType(resolvedExpr.type);
 
-    outer: for (let i = 0; i < 10000; i += 1) {
-      switch (type.kind) {
-        case typeKinds.Optional:
-        case typeKinds.Pointer:
-          let child = type.child;
-          let resolvedChild = resolveValue(child);
-          if ("type" in resolvedChild) {
-            type = getType(resolvedChild.type);
-            continue;
-          } else {
-            return null;
-          }
-        default:
-          break outer;
+    function renderDecl(decl_index) {
+      const category = wasm_exports.categorize_decl(decl_index, 0);
+      switch (category) {
+        case CAT_namespace: return renderNamespace(decl_index);
+        case CAT_global_variable: throw new Error("TODO: CAT_GLOBAL_VARIABLE");
+        case CAT_function: return renderFunction(decl_index);
+        case CAT_primitive: throw new Error("TODO CAT_primitive");
+        case CAT_error_set: throw new Error("TODO CAT_error_set");
+        case CAT_global_const: return renderGlobalConst(decl_index);
+        case CAT_alias: return renderDecl(wasm_exports.get_aliasee());
+        case CAT_type: throw new Error("TODO CAT_type");
+        default: throw new Error("unrecognized category " + category);
       }
-
-      if (i == 9999) throw "Exhausted typeShorthandName quota";
     }
 
-    let name = undefined;
-    if (type.kind === typeKinds.Struct) {
-      name = "struct";
-    } else if (type.kind === typeKinds.Enum) {
-      name = "enum";
-    } else if (type.kind === typeKinds.Union) {
-      name = "union";
-    } else {
-      console.log("TODO: unhandled case in typeShortName");
-      return null;
-    }
+    function renderSource(path) {
+      const decl_index = findFileRoot(path);
+      if (decl_index == null) return render404();
 
-    return escapeHtml(name);
-  }
+      renderNav(decl_index);
 
-  function typeKindIsContainer(typeKind) {
-    return (
-      typeKind === typeKinds.Struct ||
-      typeKind === typeKinds.Union ||
-      typeKind === typeKinds.Enum ||
-      typeKind === typeKinds.Opaque
-    );
-  }
+      domSourceText.innerHTML = declSourceHtml(decl_index);
 
-  function declCanRepresentTypeKind(typeKind) {
-    return typeKind === typeKinds.ErrorSet || typeKindIsContainer(typeKind);
-  }
+      domSectSource.classList.remove("hidden");
+    }
 
-  //
-  // function findCteInRefPath(path) {
-  //     for (let i = path.length - 1; i >= 0; i -= 1) {
-  //         const ref = path[i];
-  //         if ("string" in ref) continue;
-  //         if ("comptimeExpr" in ref) return ref;
-  //         if ("refPath" in ref) return findCteInRefPath(ref.refPath);
-  //         return null;
-  //     }
+    function renderDeclHeading(decl_index) {
+      domHdrName.innerText = unwrapString(wasm_exports.decl_category_name(decl_index));
+      domHdrName.classList.remove("hidden");
 
-  //     return null;
-  // }
+      renderTopLevelDocs(decl_index);
+    }
 
-  function resolveValue(value, trackDecls) {
-    let seenDecls = [];
-    let i = 0;
-    while (true) {
-      i += 1;
-      if (i >= 10000) {
-        throw "resolveValue quota exceeded"
+    function renderTopLevelDocs(decl_index) {
+      const tld_docs_html = unwrapString(wasm_exports.decl_docs_html(decl_index, false));
+      if (tld_docs_html.length > 0) {
+        domTldDocs.innerHTML = tld_docs_html;
+        domTldDocs.classList.remove("hidden");
       }
+    }
 
-      if ("refPath" in value.expr) {
-        value = { expr: value.expr.refPath[value.expr.refPath.length - 1] };
-        continue;
-      }
+    function renderNav(cur_nav_decl) {
+      const list = [];
+      {
+        // First, walk backwards the decl parents within a file.
+        let decl_it = cur_nav_decl;
+        let prev_decl_it = null;
+        while (decl_it != null) {
+          list.push({
+            name: declIndexName(decl_it),
+            href: navLinkDeclIndex(decl_it),
+          });
+          prev_decl_it = decl_it;
+          decl_it = declParent(decl_it);
+        }
+
+        // Next, walk backwards the file path segments.
+        if (prev_decl_it != null) {
+          const file_path = fullyQualifiedName(prev_decl_it);
+          const parts = file_path.split(".");
+          parts.pop(); // skip last
+          for (;;) {
+            let part = parts.pop();
+            if (!part) break;
+            list.push({
+              name: part,
+              href: navLinkFqn(parts.join(".")),
+            });
+          }
+        }
 
-      if ("declRef" in value.expr) {
-        seenDecls.push(value.expr.declRef);
-        value = getDecl(value.expr.declRef).value;
-        continue;
+        list.reverse();
       }
+      resizeDomList(domListNav, list.length, '<li><a href="#"></a></li>');
 
-      if ("as" in value.expr) {
-        value = {
-          typeRef: zigAnalysis.exprs[value.expr.as.typeRefArg],
-          expr: zigAnalysis.exprs[value.expr.as.exprArg],
-        };
-        continue;
+      for (let i = 0; i < list.length; i += 1) {
+          const liDom = domListNav.children[i];
+          const aDom = liDom.children[0];
+          aDom.textContent = list[i].name;
+          aDom.setAttribute('href', list[i].href);
+          if (i + 1 == list.length) {
+              aDom.classList.add("active");
+          } else {
+              aDom.classList.remove("active");
+          }
       }
 
-      if (trackDecls) return { value, seenDecls };
-      return value;
+      domSectNav.classList.remove("hidden");
     }
-  }
-
-  function resolveGenericRet(genericFunc) {
-    if (genericFunc.generic_ret == null) return null;
-    let result = resolveValue({ expr: genericFunc.generic_ret });
 
-    let i = 0;
-    while (true) {
-      i += 1;
-      if (i >= 10000) {
-        throw "resolveGenericRet quota exceeded"
-      }
+    function render404() {
+        domStatus.textContent = "404 Not Found";
+        domStatus.classList.remove("hidden");
+    }
 
-      if ("call" in result.expr) {
-        let call = zigAnalysis.calls[result.expr.call];
-        let resolvedFunc = resolveValue({ expr: call.func });
-        if (!("type" in resolvedFunc.expr)) return null;
-        let callee = getType(resolvedFunc.expr.type);
-        if (!callee.generic_ret) return null;
-        result = resolveValue({ expr: callee.generic_ret });
-        continue;
-      }
+    function navLinkFqn(full_name) {
+      return '#' + full_name;
+    }
 
-      return result;
+    function navLinkDeclIndex(decl_index) {
+      return navLinkFqn(fullyQualifiedName(decl_index));
     }
-  }
 
-  //    function typeOfDecl(decl){
-  //        return decl.value.typeRef;
-  //
-  //        let i = 0;
-  //        while(i < 1000) {
-  //            i += 1;
-  //            console.assert(isDecl(decl));
-  //            if ("type" in decl.value) {
-  //                return ({ type: typeTypeId });
-  //            }
-  //
-  ////            if ("string" in decl.value) {
-  ////                return ({ type: {
-  ////                  kind: typeKinds.Pointer,
-  ////                  size: pointerSizeEnum.One,
-  ////                  child: });
-  ////            }
-  //
-  //            if ("refPath" in decl.value) {
-  //                decl =  ({
-  //                  value: decl.value.refPath[decl.value.refPath.length -1]
-  //                });
-  //                continue;
-  //            }
-  //
-  //            if ("declRef" in decl.value) {
-  //                decl = zigAnalysis.decls[decl.value.declRef];
-  //                continue;
-  //            }
-  //
-  //            if ("int" in decl.value) {
-  //                return decl.value.int.typeRef;
-  //            }
-  //
-  //            if ("float" in decl.value) {
-  //                return decl.value.float.typeRef;
-  //            }
-  //
-  //            if ("array" in decl.value) {
-  //                return decl.value.array.typeRef;
-  //            }
-  //
-  //            if ("struct" in decl.value) {
-  //                return decl.value.struct.typeRef;
-  //            }
-  //
-  //            if ("comptimeExpr" in decl.value) {
-  //                const cte = zigAnalysis.comptimeExprs[decl.value.comptimeExpr];
-  //                return cte.typeRef;
-  //            }
-  //
-  //            if ("call" in decl.value) {
-  //                const fn_call = zigAnalysis.calls[decl.value.call];
-  //                let fn_decl = undefined;
-  //                if ("declRef" in fn_call.func) {
-  //                    fn_decl = zigAnalysis.decls[fn_call.func.declRef];
-  //                } else if ("refPath" in fn_call.func) {
-  //                    console.assert("declRef" in fn_call.func.refPath[fn_call.func.refPath.length -1]);
-  //                    fn_decl = zigAnalysis.decls[fn_call.func.refPath[fn_call.func.refPath.length -1].declRef];
-  //                } else throw {};
-  //
-  //                const fn_decl_value = resolveValue(fn_decl.value);
-  //                console.assert("type" in fn_decl_value); //TODO handle comptimeExpr
-  //                const fn_type = (zigAnalysis.types[fn_decl_value.type]);
-  //                console.assert(fn_type.kind === typeKinds.Fn);
-  //                return fn_type.ret;
-  //            }
-  //
-  //            if ("void" in decl.value) {
-  //                return ({ type: typeTypeId });
-  //            }
-  //
-  //            if ("bool" in decl.value) {
-  //                return ({ type: typeKinds.Bool });
-  //            }
-  //
-  //            console.log("TODO: handle in `typeOfDecl` more cases: ", decl);
-  //            console.assert(false);
-  //            throw {};
-  //        }
-  //        console.assert(false);
-  //        return ({});
-  //    }
-    function detectDeclPath(text, context) {
-      let result = "";
-      let separator = ":";
-      const components = text.split(".");
-      let curDeclOrType = undefined;
-      
-      let curContext = context;
-      let limit = 10000;
-      while (curContext) {
-        limit -= 1;
-        
-        if (limit == 0) {
-          throw "too many iterations";
+    function resizeDomList(listDom, desiredLen, templateHtml) {
+        // add the missing dom entries
+        var i, ev;
+        for (i = listDom.childElementCount; i < desiredLen; i += 1) {
+            listDom.insertAdjacentHTML('beforeend', templateHtml);
         }
-        
-        curDeclOrType = findSubDecl(curContext, components[0]);
-        
-        if (!curDeclOrType) {
-          if (curContext.parent_container == null) break;
-          curContext = getType(curContext.parent_container);
-          continue;
+        // remove extra dom entries
+        while (desiredLen < listDom.childElementCount) {
+            listDom.removeChild(listDom.lastChild);
         }
+    }
 
-        if (curContext == context) {
-          separator = '.';
-          result = location.hash + separator + components[0];
-        } else {
-          // We had to go up, which means we need a new path!
-          const canonPath = getCanonDeclPath(curDeclOrType.find_subdecl_idx);
-          if (!canonPath) return;
-          
-          let lastModName = canonPath.modNames[canonPath.modNames.length - 1];
-          let fullPath = lastModName + ":" + canonPath.declNames.join(".");
-        
-          separator = '.';
-          result = "#A;" + fullPath;
-        }
+    function setViewSourceDecl(decl_index) {
+        curNav.viewSourceHash = "#src/" + unwrapString(wasm_exports.decl_file_path(decl_index));
+    }
 
-        break;
-      } 
+    function renderFunction(decl_index) {
+      renderNav(decl_index);
+      setViewSourceDecl(decl_index);
 
-      if (!curDeclOrType) {
-        for (let i = 0; i < zigAnalysis.modules.length; i += 1){
-          const p = zigAnalysis.modules[i];
-          if (p.name == components[0]) {
-            curDeclOrType = getType(p.main);
-            result += "#A;" + components[0];
-            break;
-          }
-        }
-      }
+      domFnProtoCode.innerHTML = fnProtoHtml(decl_index);
+      renderTopLevelDocs(decl_index);
+      domSourceText.innerHTML = declSourceHtml(decl_index);
 
-      if (!curDeclOrType) return null;
-      
-      for (let i = 1; i < components.length; i += 1) {
-        curDeclOrType = findSubDecl(curDeclOrType, components[i]);
-        if (!curDeclOrType) return null;
-        result += separator + components[i];
-        separator = '.';
+      const doctest_html = declDoctestHtml(decl_index);
+      if (doctest_html.length > 0) {
+        domDocTestsCode.innerHTML = doctest_html;
+        domSectDocTests.classList.remove("hidden");
       }
 
-      return result;
-      
+      domSectSource.classList.remove("hidden");
+      domFnProto.classList.remove("hidden");
     }
-  
-  function renderGuides() {
-    renderTitle();
 
-    // set guide mode
-    domGuideSwitch.classList.add("active");
-    domApiSwitch.classList.remove("active");
-    domDocs.classList.add("hidden");
-    domSectNavAPI.classList.add("hidden");
-    domSectNavGuides.classList.remove("hidden");
-    domGuidesSection.classList.remove("hidden");
-    domActiveGuide.classList.add("hidden");
-    domSectSearchResults.classList.add("hidden");
-    domSectSearchAllResultsLink.classList.add("hidden");
-    domSectSearchNoResults.classList.add("hidden");
-    if (curNavSearch !== "") {
-      return renderSearchGuides();
-    }
+    function renderGlobalConst(decl_index) {
+      renderNav(decl_index);
+      setViewSourceDecl(decl_index);
 
-    let activeGuide = undefined;
-    outer: for (let i = 0; i < zigAnalysis.guideSections.length; i += 1) {
-      const section = zigAnalysis.guideSections[i];
-      for (let j = 0; j < section.guides.length; j += 1) {
-        const guide = section.guides[j];
-        if (guide.name == curNav.activeGuide) {
-          activeGuide = guide;
-          break outer;
-        }
+      const docs_html = declDocsHtmlShort(decl_index);
+      if (docs_html.length > 0) {
+        domTldDocs.innerHTML = docs_html;
+        domTldDocs.classList.remove("hidden");
       }
-    }
-
 
-    // navigation bar 
-    
-    const guideIndexDom = domListNavGuides.children[0].children[0];
-    const guideDom = domListNavGuides.children[1].children[0];
-    if (activeGuide){
-      guideDom.textContent = activeGuide.title;
-      guideDom.setAttribute("href", location.hash);
-      guideDom.classList.remove("hidden");
-      guideIndexDom.classList.remove("active");
-    } else {
-      guideDom.classList.add("hidden");
-      guideIndexDom.classList.add("active");
-    } 
-
-    // main content
-    domGuidesMenuTitle.textContent = "Table of Contents";
-    if (activeGuide) {
-      if (activeGuide.toc != "") {
-        domGuideTocList.innerHTML = activeGuide.toc;
-        // add js callbacks to all links
-        function onLinkClick(ev) {
-          const link = ev.target.getAttribute("href");
-          skipNextHashChange = link;
-          location.replace(link);
-          scrollToHeading(":" + link.split(":")[1], true);
-          ev.preventDefault();
-          ev.stopPropagation();
-        }
-        for (let a of domGuideTocList.querySelectorAll("a")) {
-          a.addEventListener('click', onLinkClick, false); 
-        }
-        domGuideTocList.classList.remove("hidden");
-        domGuideTocListEmtpy.classList.add("hidden");
-      } else {
-        domGuideTocListEmtpy.classList.remove("hidden");
-        domGuideTocList.classList.add("hidden");
-      }
-      
-      let reader = new commonmark.Parser({
-        smart: true,
-        autoDoc: {
-          detectDeclPath: detectDeclPath,
+      domSourceText.innerHTML = declSourceHtml(decl_index);
+      domSectSource.classList.remove("hidden");
+    }
+
+    function renderNamespace(decl_index) {
+        renderNav(decl_index);
+        renderDeclHeading(decl_index);
+        setViewSourceDecl(decl_index);
+
+        const typesList = [];
+        const namespacesList = [];
+        const errSetsList = [];
+        const fnsList = [];
+        const varsList = [];
+        const valsList = [];
+        const members = namespaceMembers(decl_index, false);
+
+        member_loop: for (let i = 0; i < members.length; i += 1) {
+          let member = members[i];
+          while (true) {
+            const member_category = wasm_exports.categorize_decl(member, 0);
+            switch (member_category) {
+              case CAT_namespace:
+                namespacesList.push(member);
+                continue member_loop;
+              case CAT_global_variable:
+                varsList.push(member);
+                continue member_loop;
+              case CAT_function:
+                fnsList.push(member);
+                continue member_loop;
+              case CAT_type:
+                typesList.push(member);
+                continue member_loop;
+              case CAT_error_set:
+                errSetsList.push(member);
+                continue member_loop;
+              case CAT_global_const:
+              case CAT_primitive:
+                valsList.push(member);
+                continue member_loop;
+              case CAT_alias:
+                // TODO: handle aliasing loop
+                member = wasm_exports.get_aliasee();
+                continue;
+              default:
+                throw new Error("uknown category: " + member_category);
+            }
+          }
         }
-      });
-      let ast = reader.parse(activeGuide.body);        
-      let writer = new commonmark.HtmlRenderer();              
-      let result = writer.render(ast);      
-      domActiveGuide.innerHTML = result;
-      if (curNav.activeGuideScrollTo !== null) {
-        scrollToHeading(curNav.activeGuideScrollTo, false);
-      }
-    } else {
-      domGuideTocList.classList.add("hidden");
-      domGuideTocListEmtpy.classList.remove("hidden");
-      
-      if (zigAnalysis.guideSections.length > 1 || (zigAnalysis.guideSections[0].guides.length > 0)) {
-        renderGuidesIndex();
-      } else {
-        noGuidesAtAll();
-      }
-    }
 
-    domGuidesMenu.classList.remove("hidden");
-    domActiveGuide.classList.remove("hidden");
-  }
-
-  // TODO: ensure unique hashes
-  // TODO: hash also guides and their headings
-  function computeGuideHashes() {
-      for (let i = 1; i < zigAnalysis.guideSections.length; i += 1) {
-        const section = zigAnalysis.guideSections[i];
-        section.hash = "section-" + slugify(section.name || i);
-      }
-  }
-
-  function renderGuidesIndex() {
-    // main content 
-    {
-    let html = "";
-      for (let i = 0; i < zigAnalysis.guideSections.length; i += 1) {
-        const section = zigAnalysis.guideSections[i];
-        if (i != 0) { // first section is the default section
-          html += "<h2 id='"+ section.hash +"'>" + section.name + "</h2>";
-      }
-      for (let guide of section.guides) {
-        html += "<ol><li><a href='"+ NAV_MODES.GUIDES + guide.name +"'>" + (guide.title || guide.name) + "</a></li>";
-        html += guide.toc + "</ol>";
-      }
-    }
-    domActiveGuide.innerHTML = html;
-  }
+        typesList.sort(byDeclIndexName);
+        namespacesList.sort(byDeclIndexName);
+        errSetsList.sort(byDeclIndexName);
+        fnsList.sort(byDeclIndexName);
+        varsList.sort(byDeclIndexName);
+        valsList.sort(byDeclIndexName);
 
-    // sidebar / fast navigation
-    {
-      domGuidesMenuTitle.textContent = "Sections";
-      if (zigAnalysis.guideSections.length > 1) {
-        let html = "";
-        for (let i = 1; i < zigAnalysis.guideSections.length; i += 1) {
-          const section = zigAnalysis.guideSections[i];
-          html += "<li><a href='"+ NAV_MODES.GUIDES + ":" + section.hash +"'>" + section.name + "</a></li>";
+        if (typesList.length !== 0) {
+            resizeDomList(domListTypes, typesList.length, '<li><a href="#"></a></li>');
+            for (let i = 0; i < typesList.length; i += 1) {
+                const liDom = domListTypes.children[i];
+                const aDom = liDom.children[0];
+                const decl = typesList[i];
+                aDom.textContent = declIndexName(decl);
+                aDom.setAttribute('href', navLinkDeclIndex(decl));
+            }
+            domSectTypes.classList.remove("hidden");
         }
-        domGuideTocList.innerHTML = "<ul>"+html+"</ul>";
-
-        function onLinkClick(ev) {
-          const link = ev.target.getAttribute("href");
-          skipNextHashChange = link;
-          location.replace(link);
-          scrollToHeading(link.split(":")[1], true);
-          ev.preventDefault();
-          ev.stopPropagation();
+        if (namespacesList.length !== 0) {
+            resizeDomList(domListNamespaces, namespacesList.length, '<li><a href="#"></a></li>');
+            for (let i = 0; i < namespacesList.length; i += 1) {
+                const liDom = domListNamespaces.children[i];
+                const aDom = liDom.children[0];
+                const decl = namespacesList[i];
+                aDom.textContent = declIndexName(decl);
+                aDom.setAttribute('href', navLinkDeclIndex(decl));
+            }
+            domSectNamespaces.classList.remove("hidden");
         }
-        for (let a of domGuideTocList.querySelectorAll("a")) {
-          a.addEventListener('click', onLinkClick, false); 
+
+        if (errSetsList.length !== 0) {
+            resizeDomList(domListErrSets, errSetsList.length, '<li><a href="#"></a></li>');
+            for (let i = 0; i < errSetsList.length; i += 1) {
+                const liDom = domListErrSets.children[i];
+                const aDom = liDom.children[0];
+                const decl = errSetsList[i];
+                aDom.textContent = declIndexName(decl);
+                aDom.setAttribute('href', navLinkDeclIndex(decl));
+            }
+            domSectErrSets.classList.remove("hidden");
         }
-        
-        domGuideTocList.classList.remove("hidden");
-        domGuideTocListEmtpy.classList.add("hidden");
-      } else {
-        domGuideTocList.classList.add("hidden");
-        domGuideTocListEmtpy.classList.remove("hidden");
-      }
-    }    
-  }
 
-  function noGuidesAtAll() {
-      const root_file_idx = zigAnalysis.modules[zigAnalysis.rootMod].file;
-      const root_file_name = getFile(root_file_idx).name;
-    let reader = new commonmark.Parser({smart: true});
-    let ast = reader.parse(`
-# No Guides
-These autodocs don't contain any guide.
+        if (fnsList.length !== 0) {
+            resizeDomList(domListFns, fnsList.length,
+                '<div><dt><a href="#"></a></dt><dd></dd><details><summary>source</summary><pre><code></code></pre></details></div>');
+            for (let i = 0; i < fnsList.length; i += 1) {
+                const decl = fnsList[i];
+                const divDom = domListFns.children[i];
 
-While the API section is a reference guide autogenerated from Zig source code,
-guides are meant to be handwritten explanations that provide for example:
+                const dtName = divDom.children[0];
+                const ddDocs = divDom.children[1];
+                const codeDom = divDom.children[2].children[1].children[0];
 
-- how-to explanations for common use-cases 
-- technical documentation 
-- information about advanced usage patterns
+                const nameLinkDom = dtName.children[0];
+                const expandSourceDom = dtName.children[1];
 
-You can add guides by specifying which markdown files to include
-in the top level doc comment of your root file, like so:
+                nameLinkDom.setAttribute('href', navLinkDeclIndex(decl));
+                nameLinkDom.textContent = declIndexName(decl);
 
-(At the top of *${root_file_name}*)
-\`\`\`
-//!zig-autodoc-guide: intro.md
-//!zig-autodoc-guide: quickstart.md
-//!zig-autodoc-guide: advanced-docs/advanced-stuff.md
-\`\`\`
+                ddDocs.innerHTML = declDocsHtmlShort(decl);
 
-You can also create sections to group guides together:
+                codeDom.innerHTML = declSourceHtml(decl);
+            }
+            domSectFns.classList.remove("hidden");
+        }
 
-\`\`\`
-//!zig-autodoc-section: CLI Usage
-//!zig-autodoc-guide: cli-basics.md
-//!zig-autodoc-guide: cli-advanced.md
-\`\`\`
-  
+        // Prevent fields from being emptied next time wasm calls memory.grow.
+        const fields = declFields(decl_index).slice();
+        if (fields.length !== 0) {
+            resizeDomList(domListFields, fields.length, '<div></div>');
+            for (let i = 0; i < fields.length; i += 1) {
+                const divDom = domListFields.children[i];
+                divDom.innerHTML = unwrapString(wasm_exports.decl_field_html(decl_index, fields[i]));
+            }
+            domSectFields.classList.remove("hidden");
+        }
 
-**Note that this feature is still under heavy development so expect bugs**
-**and missing features!**
+        if (varsList.length !== 0) {
+            resizeDomList(domListGlobalVars, varsList.length,
+                '<tr><td><a href="#"></a></td><td></td><td></td></tr>');
+            for (let i = 0; i < varsList.length; i += 1) {
+                const decl = varsList[i];
+                const trDom = domListGlobalVars.children[i];
 
-Happy writing!
-`);
+                const tdName = trDom.children[0];
+                const tdNameA = tdName.children[0];
+                const tdType = trDom.children[1];
+                const tdDesc = trDom.children[2];
 
-    let writer = new commonmark.HtmlRenderer();              
-    let result = writer.render(ast);      
-    domActiveGuide.innerHTML = result;
+                tdNameA.setAttribute('href', navLinkDeclIndex(decl));
+                tdNameA.textContent = declIndexName(decl);
 
-  }
+                tdType.innerHTML = declTypeHtml(decl);
+                tdDesc.innerHTML = declDocsHtmlShort(decl);
+            }
+            domSectGlobalVars.classList.remove("hidden");
+        }
 
-  function renderApi() {
-    // set Api mode
-    domApiSwitch.classList.add("active");
-    domGuideSwitch.classList.remove("active");
-    domGuidesSection.classList.add("hidden");
-    domSectNavAPI.classList.remove("hidden");
-    domSectNavGuides.classList.add("hidden");
-    domDocs.classList.remove("hidden");
-    domGuidesMenu.classList.add("hidden");
-    domStatus.classList.add("hidden");
-    domFnProto.classList.add("hidden");
-    domSectParams.classList.add("hidden");
-    domTldDocs.classList.add("hidden");
-    domSectTypes.classList.add("hidden");
-    domSectTests.classList.add("hidden");
-    domSectDocTests.classList.add("hidden");
-    domSectNamespaces.classList.add("hidden");
-    domListNamespacesLeft.classList.add("hidden");
-    domListNamespacesRight.classList.add("hidden");
-    domNoDocsNamespaces.classList.add("hidden");
-    domSectErrSets.classList.add("hidden");
-    domSectFns.classList.add("hidden");
-    domSectFields.classList.add("hidden");
-    domSectSearchResults.classList.add("hidden");
-    domSectSearchAllResultsLink.classList.add("hidden");
-    domSectSearchNoResults.classList.add("hidden");
-    domHdrName.classList.add("hidden");
-    domSectFnErrors.classList.add("hidden");
-    domFnExamples.classList.add("hidden");
-    domFnNoExamples.classList.add("hidden");
-    domFnSourceLink.classList.add("hidden");
-    domDeclNoRef.classList.add("hidden");
-    domFnErrorsAnyError.classList.add("hidden");
-    domTableFnErrors.classList.add("hidden");
-    domSectGlobalVars.classList.add("hidden");
-    domSectValues.classList.add("hidden");
+        if (valsList.length !== 0) {
+            resizeDomList(domListValues, valsList.length,
+                '<tr><td><a href="#"></a></td><td></td><td></td></tr>');
+            for (let i = 0; i < valsList.length; i += 1) {
+                const decl = valsList[i];
+                const trDom = domListValues.children[i];
 
-    renderTitle();
+                const tdName = trDom.children[0];
+                const tdNameA = tdName.children[0];
+                const tdType = trDom.children[1];
+                const tdDesc = trDom.children[2];
 
-    if (curNavSearch !== "") {
-      return renderSearchAPI();
-    }
+                tdNameA.setAttribute('href', navLinkDeclIndex(decl));
+                tdNameA.textContent = declIndexName(decl);
 
-    let rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
-    let mod = rootMod;
-    curNav.modObjs = [mod];
-    for (let i = 0; i < curNav.modNames.length; i += 1) {
-      let childMod = zigAnalysis.modules[mod.table[curNav.modNames[i]]];
-      if (childMod == null) {
-        return render404();
-      }
-      mod = childMod;
-      curNav.modObjs.push(mod);
+                tdType.innerHTML = declTypeHtml(decl);
+                tdDesc.innerHTML = declDocsHtmlShort(decl);
+            }
+            domSectValues.classList.remove("hidden");
+        }
     }
 
-    let currentType = getType(mod.main);
-    curNav.declObjs = [currentType];
-    let lastDecl = mod.main;
-    for (let i = 0; i < curNav.declNames.length; i += 1) {
-      let childDecl = findSubDecl(currentType, curNav.declNames[i]);
-      window.last_decl = childDecl;
-      if (childDecl == null || childDecl.is_private === true) {
-        return render404();
-      }
-      lastDecl = childDecl;
-
-      let childDeclValue = resolveValue(childDecl.value).expr;
-      if ("type" in childDeclValue) {
-        const t = getType(childDeclValue.type);
-        if (t.kind != typeKinds.Fn) {
-          childDecl = t;
+    function operatorCompare(a, b) {
+        if (a === b) {
+            return 0;
+        } else if (a < b) {
+            return -1;
+        } else {
+            return 1;
         }
-      }
-
-      currentType = childDecl;
-      curNav.declObjs.push(currentType);
     }
 
+    function updateCurNav(location_hash) {
+        curNav.tag = 0;
+        curNav.decl = null;
+        curNav.path = null;
+        curNav.viewSourceHash = null;
+        curNavSearch = "";
 
+        if (location_hash[0] === '#' && location_hash.length > 1) {
+            const query = location_hash.substring(1);
+            const qpos = query.indexOf("?");
+            let nonSearchPart;
+            if (qpos === -1) {
+                nonSearchPart = query;
+            } else {
+                nonSearchPart = query.substring(0, qpos);
+                curNavSearch = decodeURIComponent(query.substring(qpos + 1));
+            }
 
-    window.x = currentType;
-
-    renderNav();
-
-    let last = curNav.declObjs[curNav.declObjs.length - 1];
-    let lastIsDecl = isDecl(last);
-    let lastIsType = isType(last);
-    let lastIsContainerType = isContainerType(last);
-
-    renderDocTest(lastDecl);
-
-    if (lastIsContainerType) {
-      return renderContainer(last);
+            if (nonSearchPart.length > 0) {
+              const source_mode = nonSearchPart.startsWith("src/");
+              if (source_mode) {
+                curNav.tag = 2;
+                curNav.path = nonSearchPart.substring(4);
+              } else {
+                curNav.tag = 1;
+                curNav.decl = findDecl(nonSearchPart);
+              }
+            }
+        }
     }
 
-    if (!lastIsDecl && !lastIsType) {
-      return renderUnknownDecl(last);
+    function onHashChange() {
+      navigate(location.hash);
     }
 
-    if (lastIsType) {
-      return renderType(last);
+    function navigate(location_hash) {
+      updateCurNav(location_hash);
+      if (domSearch.value !== curNavSearch) {
+          domSearch.value = curNavSearch;
+      }
+      render();
+      if (imFeelingLucky) {
+          imFeelingLucky = false;
+          activateSelectedResult();
+      }
     }
 
-    if (lastIsDecl && last.kind === "var") {
-      return renderVar(last);
-    }
+    function activateSelectedResult() {
+        if (domSectSearchResults.classList.contains("hidden")) {
+            return;
+        }
 
-    if (lastIsDecl && last.kind === "const") {
-      const value = resolveValue(last.value);
-      if ("type" in value.expr) {
-        let typeObj = getType(value.expr.type);
-        if (typeObj.kind === typeKinds.Fn) {
-          return renderFn(last);
+        var liDom = domListSearchResults.children[curSearchIndex];
+        if (liDom == null && domListSearchResults.children.length !== 0) {
+            liDom = domListSearchResults.children[0];
         }
-      }
-      return renderValue(last);
+        if (liDom != null) {
+            var aDom = liDom.children[0];
+            location.href = aDom.getAttribute("href");
+            curSearchIndex = -1;
+        }
+        domSearch.blur();
     }
 
-  }
+    function onSearchKeyDown(ev) {
+      switch (ev.which) {
+        case 13:
+          if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
 
-  function render() {
-    switch (curNav.mode) {
-      case NAV_MODES.API:
-        return renderApi();
-      case NAV_MODES.GUIDES:
-        return renderGuides();
-      default:
-        throw "?";
-    }
-  }
+          clearAsyncSearch();
+          imFeelingLucky = true;
+          location.hash = computeSearchHash();
 
+          ev.preventDefault();
+          ev.stopPropagation();
+          return;
+        case 27:
+          if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
 
-  function renderDocTest(decl) {
-    if (!decl.decltest) return;
-    const astNode = getAstNode(decl.decltest);
-    domSectDocTests.classList.remove("hidden");
-    domDocTestsCode.innerHTML = renderTokens(
-      DecoratedTokenizer(astNode.code, decl));
-  }
+          domSearch.value = "";
+          domSearch.blur();
+          curSearchIndex = -1;
+          ev.preventDefault();
+          ev.stopPropagation();
+          startSearch();
+          return;
+        case 38:
+          if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
 
-  function renderUnknownDecl(decl) {
-    domDeclNoRef.classList.remove("hidden");
+          moveSearchCursor(-1);
+          ev.preventDefault();
+          ev.stopPropagation();
+          return;
+        case 40:
+          if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
 
-    let docs = getAstNode(decl.src).docs;
-    if (docs != null) {
-      domTldDocs.innerHTML = markdown(docs);
-    } else {
-      domTldDocs.innerHTML =
-        "<p>There are no doc comments for this declaration.</p>";
+          moveSearchCursor(1);
+          ev.preventDefault();
+          ev.stopPropagation();
+          return;
+        default:
+          ev.stopPropagation(); // prevent keyboard shortcuts
+          return;
+      }
     }
-    domTldDocs.classList.remove("hidden");
-  }
-
-  function typeIsErrSet(typeIndex) {
-    let typeObj = getType(typeIndex);
-    return typeObj.kind === typeKinds.ErrorSet;
-  }
 
-  function typeIsStructWithNoFields(typeIndex) {
-    let typeObj = getType(typeIndex);
-    if (typeObj.kind !== typeKinds.Struct) return false;
-    return typeObj.field_types.length == 0;
-  }
-
-  function typeIsGenericFn(typeIndex) {
-    let typeObj = getType(typeIndex);
-    if (typeObj.kind !== typeKinds.Fn) {
-      return false;
+    function onSearchChange(ev) {
+      curSearchIndex = -1;
+      startAsyncSearch();
     }
-    return typeObj.generic_ret != null;
-  }
 
-  function renderFn(fnDecl) {
-    if ("refPath" in fnDecl.value.expr) {
-      let last = fnDecl.value.expr.refPath.length - 1;
-      let lastExpr = fnDecl.value.expr.refPath[last];
-      console.assert("declRef" in lastExpr);
-      fnDecl = getDecl(lastExpr.declRef);
+    function moveSearchCursor(dir) {
+        if (curSearchIndex < 0 || curSearchIndex >= domListSearchResults.children.length) {
+            if (dir > 0) {
+                curSearchIndex = -1 + dir;
+            } else if (dir < 0) {
+                curSearchIndex = domListSearchResults.children.length + dir;
+            }
+        } else {
+            curSearchIndex += dir;
+        }
+        if (curSearchIndex < 0) {
+            curSearchIndex = 0;
+        }
+        if (curSearchIndex >= domListSearchResults.children.length) {
+            curSearchIndex = domListSearchResults.children.length - 1;
+        }
+        renderSearchCursor();
     }
 
-    let value = resolveValue(fnDecl.value);
-    console.assert("type" in value.expr);
-    let typeObj = getType(value.expr.type);
-
-    domFnProtoCode.innerHTML = renderTokens(ex(value.expr, { fnDecl: fnDecl }));
-    domFnSourceLink.classList.remove("hidden");
-    domFnSourceLink.innerHTML = "[<a target=\"_blank\" href=\"" + sourceFileLink(fnDecl) + "\">src</a>]";
-
-    let docsSource = null;
-    let srcNode = getAstNode(fnDecl.src);
-    if (srcNode.docs != null) {
-      docsSource = srcNode.docs;
+    function onWindowKeyDown(ev) {
+        switch (ev.which) {
+            case 27:
+                if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+                if (!domHelpModal.classList.contains("hidden")) {
+                    domHelpModal.classList.add("hidden");
+                    ev.preventDefault();
+                    ev.stopPropagation();
+                }
+                break;
+            case 83:
+                if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+                domSearch.focus();
+                domSearch.select();
+                ev.preventDefault();
+                ev.stopPropagation();
+                startAsyncSearch();
+                break;
+            case 85:
+                if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+                ev.preventDefault();
+                ev.stopPropagation();
+                navigateToSource();
+                break;
+            case 191:
+                if (!ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+                ev.preventDefault();
+                ev.stopPropagation();
+                showHelpModal();
+                break;
+        }
     }
 
-    renderFnParamDocs(fnDecl, typeObj);
-
-    let retExpr = resolveValue({ expr: typeObj.ret }).expr;
-    if ("type" in retExpr) {
-      let retIndex = retExpr.type;
-      let errSetTypeIndex = null;
-      let retType = getType(retIndex);
-      if (retType.kind === typeKinds.ErrorSet) {
-        errSetTypeIndex = retIndex;
-      } else if (retType.kind === typeKinds.ErrorUnion) {
-        errSetTypeIndex = retType.err.type;
-      }
-      if (errSetTypeIndex != null) {
-        let errSetType = getType(errSetTypeIndex);
-        renderErrorSet(errSetType);
-      }
+    function showHelpModal() {
+        domHelpModal.classList.remove("hidden");
+        domHelpModal.style.left = (window.innerWidth / 2 - domHelpModal.clientWidth / 2) + "px";
+        domHelpModal.style.top = (window.innerHeight / 2 - domHelpModal.clientHeight / 2) + "px";
+        domHelpModal.focus();
     }
 
-    let protoSrcIndex = fnDecl.src;
-    if (typeIsGenericFn(value.expr.type)) {
-      // does the generic_ret contain a container?
-      var resolvedGenericRet = resolveValue({ expr: typeObj.generic_ret });
-
-      if ("call" in resolvedGenericRet.expr) {
-        let call = zigAnalysis.calls[resolvedGenericRet.expr.call];
-        let resolvedFunc = resolveValue({ expr: call.func });
-        if (!("type" in resolvedFunc.expr)) return;
-        let callee = getType(resolvedFunc.expr.type);
-        if (!callee.generic_ret) return;
-        resolvedGenericRet = resolveValue({ expr: callee.generic_ret });
-      }
-
-      // TODO: see if unwrapping the `as` here is a good idea or not.
-      if ("as" in resolvedGenericRet.expr) {
-        resolvedGenericRet = {
-          expr: zigAnalysis.exprs[resolvedGenericRet.expr.as.exprArg],
-        };
-      }
-
-      if (!("type" in resolvedGenericRet.expr)) return;
-      const genericType = getType(resolvedGenericRet.expr.type);
-      if (isContainerType(genericType)) {
-        renderContainer(genericType);
+    function navigateToSource() {
+      if (curNav.viewSourceHash != null) {
+        location.hash = curNav.viewSourceHash;
       }
-
-      // old code
-      // let instantiations = nodesToFnsMap[protoSrcIndex];
-      // let calls = nodesToCallsMap[protoSrcIndex];
-      // if (instantiations == null && calls == null) {
-      //     domFnNoExamples.classList.remove("hidden");
-      // } else if (calls != null) {
-      //     // if (fnObj.combined === undefined) fnObj.combined = allCompTimeFnCallsResult(calls);
-      //     if (fnObj.combined != null) renderContainer(fnObj.combined);
-
-      //     resizeDomList(domListFnExamples, calls.length, '<li></li>');
-
-      //     for (let callI = 0; callI < calls.length; callI += 1) {
-      //         let liDom = domListFnExamples.children[callI];
-      //         liDom.innerHTML = getCallHtml(fnDecl, calls[callI]);
-      //     }
-
-      //     domFnExamples.classList.remove("hidden");
-      // } else if (instantiations != null) {
-      //     // TODO
-      // }
-    } else {
-      domFnExamples.classList.add("hidden");
-      domFnNoExamples.classList.add("hidden");
     }
 
-    let protoSrcNode = getAstNode(protoSrcIndex);
-    if (
-      docsSource == null &&
-      protoSrcNode != null &&
-      protoSrcNode.docs != null
-    ) {
-      docsSource = protoSrcNode.docs;
-    }
-    if (docsSource != null) {
-      domTldDocs.innerHTML = markdown(docsSource, fnDecl);
-      domTldDocs.classList.remove("hidden");
+    function clearAsyncSearch() {
+        if (searchTimer != null) {
+            clearTimeout(searchTimer);
+            searchTimer = null;
+        }
     }
-    domFnProto.classList.remove("hidden");
-  }
-
-  function renderFnParamDocs(fnDecl, typeObj) {
-    let docCount = 0;
 
-    let fnNode = getAstNode(fnDecl.src);
-    let fields = fnNode.fields;
-    if (fields === null) {
-      fields = getAstNode(typeObj.src).fields;
+    function startAsyncSearch() {
+      clearAsyncSearch();
+      searchTimer = setTimeout(startSearch, 100);
     }
-    let isVarArgs = typeObj.is_var_args;
-
-    for (let i = 0; i < fields.length; i += 1) {
-      let field = fields[i];
-      let fieldNode = getAstNode(field);
-      if (fieldNode.docs != null) {
-        docCount += 1;
-      }
+    function computeSearchHash() {
+      const oldHash = location.hash;
+      const parts = oldHash.split("?");
+      const newPart2 = (domSearch.value === "") ? "" : ("?" + domSearch.value);
+      return (parts.length === 1) ? (oldHash + newPart2) : ("#" + parts[0] + newPart2);
     }
-    if (docCount == 0) {
-      return;
+    function startSearch() {
+      clearAsyncSearch();
+      navigate(computeSearchHash());
     }
+    function renderSearch() {
+        renderNav(curNav.decl);
 
-    resizeDomList(domListParams, docCount, "<div></div>");
-    let domIndex = 0;
+        const ignoreCase = (curNavSearch.toLowerCase() === curNavSearch);
+        const results = executeQuery(curNavSearch, ignoreCase);
 
-    for (let i = 0; i < fields.length; i += 1) {
-      let field = fields[i];
-      let fieldNode = getAstNode(field);
-      let docs = fieldNode.docs;
-      if (fieldNode.docs == null) {
-        continue;
-      }
-      let docsNonEmpty = docs !== "";
-      let divDom = domListParams.children[domIndex];
-      domIndex += 1;
+        if (results.length !== 0) {
+            resizeDomList(domListSearchResults, results.length, '<li><a href="#"></a></li>');
+
+            for (let i = 0; i < results.length; i += 1) {
+                const liDom = domListSearchResults.children[i];
+                const aDom = liDom.children[0];
+                const match = results[i];
+                const full_name = fullyQualifiedName(match);
+                aDom.textContent = full_name;
+                aDom.setAttribute('href', navLinkFqn(full_name));
+            }
+            renderSearchCursor();
 
-      let value = typeObj.params[i];
-      let preClass = docsNonEmpty ? ' class="fieldHasDocs"' : "";
-      let html = "<pre" + preClass + ">" + renderTokens((function*() {
-        yield Tok.identifier(fieldNode.name);
-        yield Tok.colon;
-        yield Tok.space;
-        if (isVarArgs && i === typeObj.params.length - 1) {
-          yield Tok.period;
-          yield Tok.period;
-          yield Tok.period;
+            domSectSearchResults.classList.remove("hidden");
         } else {
-          yield* ex(value, {});
+            domSectSearchNoResults.classList.remove("hidden");
         }
-        yield Tok.comma;
-      }()));
-
-      html += "</pre>";
-
-      if (docsNonEmpty) {
-        html += '<div class="fieldDocs">' + markdown(docs) + "</div>";
-      }
-      divDom.innerHTML = html;
     }
-    domSectParams.classList.remove("hidden");
-  }
 
-  function renderNav() {
-    let len = curNav.modNames.length + curNav.declNames.length;
-    resizeDomList(domListNavAPI, len, '<li><a href="#"></a></li>');
-    let list = [];
-    let hrefModNames = [];
-    let hrefDeclNames = [];
-    for (let i = 0; i < curNav.modNames.length; i += 1) {
-      hrefModNames.push(curNav.modNames[i]);
-      let name = curNav.modNames[i];
-      list.push({
-        name: name,
-        link: navLink(hrefModNames, hrefDeclNames),
-      });
-    }
-    for (let i = 0; i < curNav.declNames.length; i += 1) {
-      hrefDeclNames.push(curNav.declNames[i]);
-      list.push({
-        name: curNav.declNames[i],
-        link: navLink(hrefModNames, hrefDeclNames),
-      });
+    function renderSearchCursor() {
+        for (let i = 0; i < domListSearchResults.children.length; i += 1) {
+            var liDom = domListSearchResults.children[i];
+            if (curSearchIndex === i) {
+                liDom.classList.add("selected");
+            } else {
+                liDom.classList.remove("selected");
+            }
+        }
     }
 
-    for (let i = 0; i < list.length; i += 1) {
-      let liDom = domListNavAPI.children[i];
-      let aDom = liDom.children[0];
-      aDom.textContent = list[i].name;
-      aDom.setAttribute("href", list[i].link);
-      if (i + 1 == list.length) {
-        aDom.classList.add("active");
-      } else {
-        aDom.classList.remove("active");
+    function updatePackageList() {
+      packageList.length = 0;
+      for (let i = 0;; i += 1) {
+        const name = unwrapString(wasm_exports.package_name(i));
+        if (name.length == 0) break;
+        packageList.push(name);
       }
     }
 
-  }
-
-
-  function render404() {
-    domStatus.textContent = "404 Not Found";
-    domStatus.classList.remove("hidden");
-  }
-
-  // function renderModList() {
-  //   const rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
-  //   let list = [];
-  //   for (let key in rootMod.table) {
-  //     let modIndex = rootMod.table[key];
-  //     if (zigAnalysis.modules[modIndex] == null) continue;
-  //     if (key == rootMod.name) continue;
-  //     list.push({
-  //       name: key,
-  //       mod: modIndex,
-  //     });
-  //   }
-
-  //   {
-  //     let aDom = domSectMainMod.children[1].children[0].children[0];
-  //     aDom.textContent = rootMod.name;
-  //     aDom.setAttribute("href", navLinkMod(zigAnalysis.rootMod));
-  //     if (rootMod.name === curNav.modNames[0]) {
-  //       aDom.classList.add("active");
-  //     } else {
-  //       aDom.classList.remove("active");
-  //     }
-  //     domSectMainMod.classList.remove("hidden");
-  //   }
-
-  //   list.sort(function (a, b) {
-  //     return operatorCompare(a.name.toLowerCase(), b.name.toLowerCase());
-  //   });
-
-  //   if (list.length !== 0) {
-  //     resizeDomList(domListMods, list.length, '<li><a href="#"></a></li>');
-  //     for (let i = 0; i < list.length; i += 1) {
-  //       let liDom = domListMods.children[i];
-  //       let aDom = liDom.children[0];
-  //       aDom.textContent = list[i].name;
-  //       aDom.setAttribute("href", navLinkMod(list[i].mod));
-  //       if (list[i].name === curNav.modNames[0]) {
-  //         aDom.classList.add("active");
-  //       } else {
-  //         aDom.classList.remove("active");
-  //       }
-  //     }
-
-  //     domSectMods.classList.remove("hidden");
-  //   }
-  // }
-
-  function navLink(modNames, declNames, callName) {
-    let base = curNav.mode;
-
-    if (modNames.length === 0 && declNames.length === 0) {
-      return base;
-    } else if (declNames.length === 0 && callName == null) {
-      return base + modNames.join(".");
-    } else if (callName == null) {
-      return base + modNames.join(".") + ":" + declNames.join(".");
-    } else {
-      return (
-        base + modNames.join(".") + ":" + declNames.join(".") + ";" + callName
-      );
+    function byDeclIndexName(a, b) {
+      const a_name = declIndexName(a);
+      const b_name = declIndexName(b);
+      return operatorCompare(a_name, b_name);
     }
-  }
-
-  function navLinkMod(modIndex) {
-    return navLink(canonModPaths[modIndex], []);
-  }
-
-  function navLinkDecl(childName) {
-    return navLink(curNav.modNames, curNav.declNames.concat([childName]));
-  }
-
-  function findDeclNavLink(declName) {
-    if (curNav.declObjs.length == 0) return null;
-    const curFile = getAstNode(curNav.declObjs[curNav.declObjs.length - 1].src).file;
-
-    for (let i = curNav.declObjs.length - 1; i >= 0; i--) {
-      const curDecl = curNav.declObjs[i];
-      const curDeclName = curNav.declNames[i - 1];
-      if (curDeclName == declName) {
-        const declPath = curNav.declNames.slice(0, i);
-        return navLink(curNav.modNames, declPath);
-      }
-
-      const subDecl = findSubDecl(curDecl, declName);
 
-      if (subDecl != null) {
-        if (subDecl.is_private === true) {
-          return sourceFileLink(subDecl);
-        } else {
-          const declPath = curNav.declNames.slice(0, i).concat([declName]);
-          return navLink(curNav.modNames, declPath);
-        }
-      }
+    function decodeString(ptr, len) {
+      if (len === 0) return "";
+      return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
     }
 
-    //throw("could not resolve links for '" + declName + "'");
-  }
-
-  //
-  //  function navLinkCall(callObj) {
-  //      let declNamesCopy = curNav.declNames.concat([]);
-  //      let callName = (declNamesCopy.pop());
-
-  //      callName += '(';
-  //          for (let arg_i = 0; arg_i < callObj.args.length; arg_i += 1) {
-  //              if (arg_i !== 0) callName += ',';
-  //              let argObj = callObj.args[arg_i];
-  //              callName += getValueText(argObj, argObj, false, false);
-  //          }
-  //          callName += ')';
-
-  //      declNamesCopy.push(callName);
-  //      return navLink(curNav.modNames, declNamesCopy);
-  //  }
-
-  function resizeDomListDl(dlDom, desiredLen) {
-    // add the missing dom entries
-    for (let i = dlDom.childElementCount / 2; i < desiredLen; i += 1) {
-      dlDom.insertAdjacentHTML("beforeend", "<dt></dt><dd></dd>");
-    }
-    // remove extra dom entries
-    while (desiredLen < dlDom.childElementCount / 2) {
-      dlDom.removeChild(dlDom.lastChild);
-      dlDom.removeChild(dlDom.lastChild);
+    function unwrapString(bigint) {
+      const ptr = Number(bigint & 0xffffffffn);
+      const len = Number(bigint >> 32n);
+      return decodeString(ptr, len);
     }
-  }
 
-  function resizeDomList(listDom, desiredLen, templateHtml) {
-    // add the missing dom entries
-    for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
-      listDom.insertAdjacentHTML("beforeend", templateHtml);
+    function declTypeHtml(decl_index) {
+      return unwrapString(wasm_exports.decl_type_html(decl_index));
     }
-    // remove extra dom entries
-    while (desiredLen < listDom.childElementCount) {
-      listDom.removeChild(listDom.lastChild);
-    }
-  }
 
-  function walkResultTypeRef(wr) {
-    if (wr.typeRef) return wr.typeRef;
-    let resolved = resolveValue(wr);
-    if (wr === resolved) {
-      return { "undefined": {} };
+    function declDocsHtmlShort(decl_index) {
+      return unwrapString(wasm_exports.decl_docs_html(decl_index, true));
     }
-    return walkResultTypeRef(resolved);
-  }
-
-  function* DecoratedTokenizer(src, context) {
-    let tok_it = Tokenizer(src);
-    for (let t of tok_it) {
-      if (t.tag == Tag.identifier) {
-        const link = detectDeclPath(t.src, context);
-        if (link) {
-          t.link = link;
-        }
-      }
 
-      yield t;
+    function fullyQualifiedName(decl_index) {
+      return unwrapString(wasm_exports.decl_fqn(decl_index));
     }
-  }
 
-
-  function renderSingleToken(t) {
-
-    if (t.tag == Tag.whitespace) {
-      return t.src;
+    function declIndexName(decl_index) {
+      return unwrapString(wasm_exports.decl_name(decl_index));
     }
 
-    let src = t.src;
-    // if (t.tag == Tag.identifier) {
-    //     src = escapeHtml(src);
-    // }
-    let result = "";
-    if (t.tag == Tag.identifier && isSimpleType(t.src)) {
-      result = `<span class="zig_type">${src}</span>`;
-    } else if (t.tag == Tag.identifier && isSpecialIndentifier(t.src)) {
-      result = `<span class="zig_special">${src}</span>`;
-    } else if (t.tag == Tag.identifier && t.fnDecl) {
-      result = `<span class="zig_fn">${src}</span>`;
-    } else if (t.tag == Tag.identifier && t.isDecl) {
-      result = `<span class="zig_decl_identifier">${src}</span>`;
-    } else {
-      result = `<span class="zig_${t.tag}">${src}</span>`;
+    function declSourceHtml(decl_index) {
+      return unwrapString(wasm_exports.decl_source_html(decl_index));
     }
 
-    if (t.link) {
-      result = `<a href="${t.link}">` + result + "</a>";
+    function declDoctestHtml(decl_index) {
+      return unwrapString(wasm_exports.decl_doctest_html(decl_index));
     }
 
-    return result;
-  }
-
-  function renderTokens(tok_it) {
-    var html = [];
-
-    const max_iter = 100000;
-    let i = 0;
-    for (const t of tok_it) {
-      i += 1;
-      if (i > max_iter)
-        throw "too many iterations";
+    function fnProtoHtml(decl_index) {
+      return unwrapString(wasm_exports.decl_fn_proto_html(decl_index));
+    }
 
-      if (t.tag == Tag.eof)
-        break;
+    function setQueryString(s) {
+      const jsArray = text_encoder.encode(s);
+      const len = jsArray.length;
+      const ptr = wasm_exports.query_begin(len);
+      const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
+      wasmArray.set(jsArray);
+    }
 
-      html.push(renderSingleToken(t));
+    function executeQuery(query_string, ignore_case) {
+      setQueryString(query_string);
+      const ptr = wasm_exports.query_exec(ignore_case);
+      const head = new Uint32Array(wasm_exports.memory.buffer, ptr, 1);
+      const len = head[0];
+      return new Uint32Array(wasm_exports.memory.buffer, ptr + 4, len);
     }
 
-    return html.join("");
-  }
+    function namespaceMembers(decl_index, include_private) {
+      const bigint = wasm_exports.namespace_members(decl_index, include_private);
+      const ptr = Number(bigint & 0xffffffffn);
+      const len = Number(bigint >> 32n);
+      if (len == 0) return [];
+      return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
+    }
 
-  function* ex(expr, opts) {
-    switch (Object.keys(expr)[0]) {
-      default:
-        throw "this expression is not implemented yet: " + Object.keys(expr)[0];
-      case "comptimeExpr": {
-        const src = zigAnalysis.comptimeExprs[expr.comptimeExpr].code;
-        yield* DecoratedTokenizer(src);
-        return;
-      }
-      case "declName": {
-        yield { src: expr.declName, tag: Tag.identifier };
-        return;
-      }
-      case "declRef": {
-        const name = getDecl(expr.declRef).name;
-        const link = declLinkOrSrcLink(expr.declRef);
-        if (link) {
-          yield { src: name, tag: Tag.identifier, isDecl: true, link };
-        } else {
-          yield { src: name, tag: Tag.identifier, isDecl: true };
-        }
-        return;
-      }
-      case "refPath": {
-        for (let i = 0; i < expr.refPath.length; i += 1) {
-          if (i > 0) yield Tok.period;
-          yield* ex(expr.refPath[i], opts);
-        }
-        return;
-      }
-      case "fieldRef": {
-        const field_idx = expr.fieldRef.index;
-        const type = getType(expr.fieldRef.type);
-        const field = getAstNode(type.src).fields[field_idx];
-        const name = getAstNode(field).name;
-        yield { src: name, tag: Tag.identifier };
-        return;
-      }
-      case "bool": {
-        if (expr.bool) {
-          yield { src: "true", tag: Tag.identifier };
-          return;
-        }
-        yield { src: "false", tag: Tag.identifier };
-        return;
-      }
+    function declFields(decl_index) {
+      const bigint = wasm_exports.decl_fields(decl_index);
+      const ptr = Number(bigint & 0xffffffffn);
+      const len = Number(bigint >> 32n);
+      if (len === 0) return [];
+      return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
+    }
 
-      case "unreachable": {
-        yield { src: "unreachable", tag: Tag.identifier };
-        return;
-      }
+    function findDecl(fqn) {
+      setInputString(fqn);
+      const result = wasm_exports.find_decl();
+      if (result === -1) return null;
+      return result;
+    }
 
-      case "&": {
-        yield { src: "&", tag: Tag.ampersand };
-        yield* ex(zigAnalysis.exprs[expr["&"]], opts);
-        return;
-      }
+    function findFileRoot(path) {
+      setInputString(path);
+      const result = wasm_exports.find_file_root();
+      if (result === -1) return null;
+      return result;
+    }
 
-      case "load": {
-        yield* ex(zigAnalysis.exprs[expr.load], opts);
-        yield Tok.period;
-        yield Tok.asterisk;
-        return;
-      }
+    function declParent(decl_index) {
+      const result = wasm_exports.decl_parent(decl_index);
+      if (result === -1) return null;
+      return result;
+    }
 
-      case "call": {
-
-        let call = zigAnalysis.calls[expr.call];
-
-        switch (Object.keys(call.func)[0]) {
-          default:
-            throw "TODO";
-          case "declRef":
-          case "refPath": {
-            yield* ex(call.func, opts);
-            break;
-          }
-        }
-        yield Tok.l_paren;
-
-        for (let i = 0; i < call.args.length; i++) {
-          if (i != 0) {
-            yield Tok.comma;
-            yield Tok.space;
-          }
-          yield* ex(call.args[i], opts);
-        }
-
-        yield Tok.r_paren;
-        return;
-      }
-      case "typeOf_peer": {
-        yield { src: "@TypeOf", tag: Tag.builtin };
-        yield { src: "(", tag: Tag.l_paren };
-        for (let i = 0; i < expr.typeOf_peer.length; i+=1) {
-          const elem = zigAnalysis.exprs[expr.typeOf_peer[i]];
-          yield* ex(elem, opts);
-          if (i != expr.typeOf_peer.length - 1) {
-            yield Tok.comma;
-            yield Tok.space;
-          }
-        }
-        yield { src: ")", tag: Tag.r_paren };
-        return;
-      } 
-      case "sizeOf": {
-        const sizeOf = zigAnalysis.exprs[expr.sizeOf];
-        yield { src: "@sizeOf", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(sizeOf, opts);
-        yield Tok.r_paren;
-        return;
-      }
-      case "bitSizeOf": {
-        const bitSizeOf = zigAnalysis.exprs[expr.bitSizeOf];
-        yield { src: "@bitSizeOf", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(bitSizeOf, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "as": {
-        const exprArg = zigAnalysis.exprs[expr.as.exprArg];
-        yield* ex(exprArg, opts);
-        return;
-      }
-
-      case "int": {
-        yield { src: expr.int, tag: Tag.number_literal };
-        return;
-      }
-
-      case "int_big": {
-        if (expr.int_big.negated) {
-          yield { src: "-", tag: Tag.minus };
-        }
-        yield { src: expr.int_big.value, tag: Tag.number_literal };
-        return;
-      }
-
-      case "float": {
-        let float = expr.float;
-        if (Number.isSafeInteger(float)) float = float.toFixed(1);
-        yield { src: float, tag: Tag.number_literal };
-        return;
-      }
-
-      case "float128": {
-        yield { src: expr.float128, tag: Tag.number_literal };
-        return;
-      }
-
-      case "array": {
-        yield Tok.period;
-        yield Tok.l_brace;
-        for (let i = 0; i < expr.array.length; i++) {
-          if (i != 0) {
-            yield Tok.comma;
-            yield Tok.space;
-          }
-          let elem = zigAnalysis.exprs[expr.array[i]];
-          yield* ex(elem, opts);
-        }
-        yield Tok.r_brace;
-        return;
-      }
-
-      case "compileError": {
-        yield { src: "@compileError", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(zigAnalysis.exprs[expr.compileError], opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "optionalPayload": {
-        const opt = zigAnalysis.exprs[expr.optionalPayload];
-        yield* ex(opt, opts);
-        yield Tok.period;
-        yield Tok.question_mark;
-        return;
-      }
-
-      case "elemVal": {
-        const lhs = zigAnalysis.exprs[expr.elemVal.lhs];
-        const rhs = zigAnalysis.exprs[expr.elemVal.rhs];
-        yield* ex(lhs);
-        yield Tok.l_bracket;
-        yield* ex(rhs);
-        yield Tok.r_bracket;
-        return;
-      }
-      
-      case "sliceIndex": {
-        const slice = zigAnalysis.exprs[expr.sliceIndex];
-        yield* ex(slice, opts);
-        return;
-      }
-
-      case "slice": {
-        const slice = expr.slice;
-        const lhs = zigAnalysis.exprs[slice.lhs];
-        const start = zigAnalysis.exprs[slice.start];
-        yield* ex(lhs, opts);
-        yield Tok.l_bracket;
-        yield* ex(start, opts);
-        yield Tok.period;
-        yield Tok.period;
-        if (slice.end !== null) {
-          const end = zigAnalysis.exprs[slice.end];
-          yield* ex(end, opts);
-        }
-        if (slice.sentinel !== null) {
-          yield Tok.colon;
-          const sent = zigAnalysis.exprs[slice.sentinel];
-          yield* ex(sent, opts);
-        }
-        yield Tok.r_brace;
-        return;
-      }
-
-      case "sliceLength": {
-        const slice = expr.sliceLength;
-        const lhs = zigAnalysis.exprs[slice.lhs];
-        const start = zigAnalysis.exprs[slice.start];
-        const len = zigAnalysis.exprs[slice.len];
-        yield* ex(lhs, opts);
-        yield Tok.l_bracket;
-        yield* ex(start, opts);
-        yield Tok.period;
-        yield Tok.period;
-        yield Tok.r_bracket;
-        yield Tok.l_bracket;
-        yield { src: "0", tag: Tag.number_literal };
-        yield Tok.period;
-        yield Tok.period;
-        yield* ex(len, opts);
-        if (slice.sentinel !== null) {
-          yield Tok.colon;
-          const sent = zigAnalysis.exprs[slice.sentinel];
-          yield* ex(sent, opts);
-        }
-        yield Tok.r_brace;
-        return;
-      }
-
-      case "string": {
-        yield { src: '"' + expr.string + '"', tag: Tag.string_literal };
-        return;
-      }
-
-      case "struct": {
-        yield Tok.period;
-        yield Tok.l_brace;
-        if (expr.struct.length > 0) yield Tok.space;
-
-        for (let i = 0; i < expr.struct.length; i++) {
-          const fv = expr.struct[i];
-          const field_name = fv.name;
-          const field_expr = zigAnalysis.exprs[fv.val.expr];
-          const field_value = ex(field_expr, opts);
-          yield Tok.period;
-          yield { src: field_name, tag: Tag.identifier };
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* field_value;
-          if (i !== expr.struct.length - 1) {
-            yield Tok.comma;
-            yield Tok.space;
-          } else {
-            yield Tok.space;
-          }
-        }
-        yield Tok.r_brace;
-        return;
-      }
-
-      case "unOpIndex": {
-        const unOp = zigAnalysis.exprs[expr.unOpIndex];
-        yield* ex(unOp, opts);
-        return;
-      }
-
-      case "unOp": {
-        const param = zigAnalysis.exprs[expr.unOp.param];
-
-        switch (expr.unOp.name) {
-          case "bit_not": {
-            yield { src: "~", tag: Tag.tilde };
-            break;
-          }
-          case "bool_not": {
-            yield { src: "!", tag: Tag.bang };
-            break;
-          }
-          case "negate_wrap": {
-            yield { src: "-%", tag: Tag.minus_percent };
-            break;
-          }
-          case "negate": {
-            yield { src: "-", tag: Tag.minus };
-            break;
-          }
-          default:
-            throw "unOp: `" + expr.unOp.name + "` not implemented yet!"
-        }
-
-        if (param["binOpIndex"] !== undefined) {
-          yield Tok.l_paren;
-          yield* ex(param, opts);
-          yield Tok.r_paren;
-        } else {
-          yield* ex(param, opts);
-        }
-        return;
-      }
-        
-      case "fieldVal": {
-        const fv = expr.fieldVal;
-        const field_name = fv.name;
-        yield { src: field_name, tag: Tag.identifier };
-        return;
-      }
-
-      case "binOpIndex": {
-        const binOp = zigAnalysis.exprs[expr.binOpIndex];
-        yield* ex(binOp, opts);
-        return;
-      }
-
-      case "binOp": {
-        const lhsOp = zigAnalysis.exprs[expr.binOp.lhs];
-        const rhsOp = zigAnalysis.exprs[expr.binOp.rhs];
-
-        if (lhsOp["binOpIndex"] !== undefined) {
-          yield Tok.l_paren;
-          yield* ex(lhsOp, opts);
-          yield Tok.r_paren;
-        } else {
-          yield* ex(lhsOp, opts);
-        }
-
-        yield Tok.space;
-
-        switch (expr.binOp.name) {
-          case "add": {
-            yield { src: "+", tag: Tag.plus };
-            break;
-          }
-          case "addwrap": {
-            yield { src: "+%", tag: Tag.plus_percent };
-            break;
-          }
-          case "add_sat": {
-            yield { src: "+|", tag: Tag.plus_pipe };
-            break;
-          }
-          case "sub": {
-            yield { src: "-", tag: Tag.minus };
-            break;
-          }
-          case "subwrap": {
-            yield { src: "-%", tag: Tag.minus_percent };
-            break;
-          }
-          case "sub_sat": {
-            yield { src: "-|", tag: Tag.minus_pipe };
-            break;
-          }
-          case "mul": {
-            yield { src: "*", tag: Tag.asterisk };
-            break;
-          }
-          case "mulwrap": {
-            yield { src: "*%", tag: Tag.asterisk_percent };
-            break;
-          }
-          case "mul_sat": {
-            yield { src: "*|", tag: Tag.asterisk_pipe };
-            break;
-          }
-          case "div": {
-            yield { src: "/", tag: Tag.slash };
-            break;
-          }
-          case "xor": {
-            yield { src: "^", tag: Tag.caret };
-            break;
-          }
-          case "shl": {
-            yield { src: "<<", tag: Tag.angle_bracket_angle_bracket_left };
-            break;
-          }
-          case "shl_sat": {
-            yield { src: "<<|", tag: Tag.angle_bracket_angle_bracket_left_pipe };
-            break;
-          }
-          case "shr": {
-            yield { src: ">>", tag: Tag.angle_bracket_angle_bracket_right };
-            break;
-          }
-          case "bit_or": {
-            yield { src: "|", tag: Tag.pipe };
-            break;
-          }
-          case "bit_and": {
-            yield { src: "&", tag: Tag.ampersand };
-            break;
-          }
-          case "array_cat": {
-            yield { src: "++", tag: Tag.plus_plus };
-            break;
-          }
-          case "array_mul": {
-            yield { src: "**", tag: Tag.asterisk_asterisk };
-            break;
-          }
-          case "cmp_eq": {
-            yield { src: "==", tag: Tag.equal_equal };
-            break;
-          }
-          case "cmp_neq": {
-            yield { src: "!=", tag: Tag.bang_equal };
-            break;
-          }
-          case "cmp_gt": {
-            yield { src: ">", tag: Tag.angle_bracket_right };
-            break;
-          }
-          case "cmp_gte": {
-            yield { src: ">=", tag: Tag.angle_bracket_right_equal };
-            break;
-          }
-          case "cmp_lt": {
-            yield { src: "<", tag: Tag.angle_bracket_left };
-            break;
-          }
-          case "cmp_lte": {
-            yield { src: "<=", tag: Tag.angle_bracket_left_equal };
-            break;
-          }
-          case "bool_br_and": {
-            yield { src: "and", tag: Tag.keyword_and };
-            break;
-          }
-          case "bool_br_or": {
-            yield { src: "or", tag: Tag.keyword_or };
-            break;
-          }
-          default:
-            console.log("operator not handled yet or doesn't exist!");
-        }
-
-        yield Tok.space;
-
-        if (rhsOp["binOpIndex"] !== undefined) {
-          yield Tok.l_paren;
-          yield* ex(rhsOp, opts);
-          yield Tok.r_paren;
-        } else {
-          yield* ex(rhsOp, opts);
-        }
-        return;
-      }
-
-      case "builtinIndex": {
-        const builtin = zigAnalysis.exprs[expr.builtinIndex];
-        yield* ex(builtin, opts);
-        return;
-      }
-
-      case "builtin": {
-        const builtin = expr.builtin;
-        let name = "@";
-        const param = zigAnalysis.exprs[builtin.param];
-        switch (builtin.name) {
-          case "align_of": { name += "alignOf"; break; }
-          case "int_from_bool": { name += "intFromBool"; break; }
-          case "embed_file": { name += "embedFile"; break; }
-          case "error_name": { name += "errorName"; break; }
-          case "panic": { name += "panic"; break; }
-          case "set_runtime_safety": { name += "setRuntimeSafety"; break; }
-          case "sqrt": { name += "sqrt"; break; }
-          case "sin": { name += "sin"; break; }
-          case "cos": { name += "cos"; break; }
-          case "tan": { name += "tan"; break; }
-          case "exp": { name += "exp"; break; }
-          case "exp2": { name += "exp2"; break; }
-          case "log": { name += "log"; break; }
-          case "log2": { name += "log2"; break; }
-          case "log10": { name += "log10"; break; }
-          case "fabs": { name += "fabs"; break; }
-          case "floor": { name += "floor"; break; }
-          case "ceil": { name += "ceil"; break; }
-          case "trunc": { name += "trunc"; break; }
-          case "round": { name += "round"; break; }
-          case "tag_name": { name += "tagName"; break; }
-          case "type_name": { name += "typeName"; break; }
-          case "type_info": { name += "typeInfo"; break; }
-          case "frame_type": { name += "Frame"; break; }
-          case "frame_size": { name += "frameSize"; break; }
-          case "int_from_ptr": { name += "intFromPtr"; break; }
-          case "int_from_enum": { name += "intFromEnum"; break; }
-          case "clz": { name += "clz"; break; }
-          case "ctz": { name += "ctz"; break; }
-          case "pop_count": { name += "popCount"; break; }
-          case "byte_swap": { name += "byteSwap"; break; }
-          case "bit_reverse": { name += "bitReverse"; break; }
-          default: throw "builtin: `" + builtin.name + "` not implemented yet!";
-        }
-        yield { src: name, tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(param, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "builtinBinIndex": {
-        const builtinBinIndex = zigAnalysis.exprs[expr.builtinBinIndex];
-        yield* ex(builtinBinIndex, opts);
-        return;
-      }
-
-      case "builtinBin": {
-        const lhsOp = zigAnalysis.exprs[expr.builtinBin.lhs];
-        const rhsOp = zigAnalysis.exprs[expr.builtinBin.rhs];
-
-        let builtinName = "@";
-        switch (expr.builtinBin.name) {
-          case "int_from_float": {
-            builtinName += "intFromFloat";
-            break;
-          }
-          case "float_from_int": {
-            builtinName += "floatFromInt";
-            break;
-          }
-          case "ptr_from_int": {
-            builtinName += "ptrFromInt";
-            break;
-          }
-          case "enum_from_int": {
-            builtinName += "enumFromInt";
-            break;
-          }
-          case "float_cast": {
-            builtinName += "floatCast";
-            break;
-          }
-          case "int_cast": {
-            builtinName += "intCast";
-            break;
-          }
-          case "ptr_cast": {
-            builtinName += "ptrCast";
-            break;
-          }
-          case "const_cast": {
-            builtinName += "constCast";
-            break;
-          }
-          case "volatile_cast": {
-            builtinName += "volatileCast";
-            break;
-          }
-          case "truncate": {
-            builtinName += "truncate";
-            break;
-          }
-          case "has_decl": {
-            builtinName += "hasDecl";
-            break;
-          }
-          case "has_field": {
-            builtinName += "hasField";
-            break;
-          }
-          case "bit_reverse": {
-            builtinName += "bitReverse";
-            break;
-          }
-          case "div_exact": {
-            builtinName += "divExact";
-            break;
-          }
-          case "div_floor": {
-            builtinName += "divFloor";
-            break;
-          }
-          case "div_trunc": {
-            builtinName += "divTrunc";
-            break;
-          }
-          case "mod": {
-            builtinName += "mod";
-            break;
-          }
-          case "rem": {
-            builtinName += "rem";
-            break;
-          }
-          case "mod_rem": {
-            builtinName += "rem";
-            break;
-          }
-          case "shl_exact": {
-            builtinName += "shlExact";
-            break;
-          }
-          case "shr_exact": {
-            builtinName += "shrExact";
-            break;
-          }
-          case "bitcast": {
-            builtinName += "bitCast";
-            break;
-          }
-          case "align_cast": {
-            builtinName += "alignCast";
-            break;
-          }
-          case "vector_type": {
-            builtinName += "Vector";
-            break;
-          }
-          case "reduce": {
-            builtinName += "reduce";
-            break;
-          }
-          case "splat": {
-            builtinName += "splat";
-            break;
-          }
-          case "offset_of": {
-            builtinName += "offsetOf";
-            break;
-          }
-          case "bit_offset_of": {
-            builtinName += "bitOffsetOf";
-            break;
-          }
-          default:
-            console.log("builtin function not handled yet or doesn't exist!");
-        }
-
-        yield { src: builtinName, tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(lhsOp, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(rhsOp, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "unionInit": {
-        let ui = expr.unionInit;
-        let type = zigAnalysis.exprs[ui.type];
-        let field = zigAnalysis.exprs[ui.field];
-        let init = zigAnalysis.exprs[ui.init];
-        yield { src: "@unionInit", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(type, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(field, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(init, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "builtinCall": {
-        let bcall = expr.builtinCall;
-        let mods = zigAnalysis.exprs[bcall.modifier];
-        let calee = zigAnalysis.exprs[bcall.function];
-        let args = zigAnalysis.exprs[bcall.args];
-        yield { src: "@call", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(mods, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(calee, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(args, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "mulAdd": {
-        let muladd = expr.mulAdd;
-        let mul1 = zigAnalysis.exprs[muladd.mulend1];
-        let mul2 = zigAnalysis.exprs[muladd.mulend2];
-        let add = zigAnalysis.exprs[muladd.addend];
-        let type = zigAnalysis.exprs[muladd.type];
-        yield { src: "@mulAdd", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(type, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(mul1, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(mul2, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(add, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "cmpxchgIndex": {
-        const cmpxchg = zigAnalysis.exprs[expr.cmpxchgIndex];
-        yield* ex(cmpxchg, opts);
-        return;
-      }
-
-      case "cmpxchg": {
-        const type = zigAnalysis.exprs[expr.cmpxchg.type];
-        const ptr = zigAnalysis.exprs[expr.cmpxchg.ptr];
-        const expectedValue = zigAnalysis.exprs[expr.cmpxchg.expected_value];
-        const newValue = zigAnalysis.exprs[expr.cmpxchg.new_value];
-        const successOrder = zigAnalysis.exprs[expr.cmpxchg.success_order];
-        const failureOrder = zigAnalysis.exprs[expr.cmpxchg.failure_order];
-
-        let fnName = "@";
-        switch (expr.cmpxchg.name) {
-          case "cmpxchg_strong": {
-            fnName += "cmpxchgStrong";
-            break;
-          }
-          case "cmpxchg_weak": {
-            fnName += "cmpxchgWeak";
-            break;
-          }
-          default:
-            throw "Unexpected cmpxchg name: `" + expr.cmpxchg.name + "`!";
-        }
-        yield { src: fnName, tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(type, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(ptr, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(expectedValue, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(newValue, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(successOrder, opts);
-        yield Tok.comma;
-        yield Tok.space;
-        yield* ex(failureOrder, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "enumLiteral": {
-        let literal = expr.enumLiteral;
-        yield Tok.period;
-        yield { src: literal, tag: Tag.identifier };
-        return;
-      }
-
-      case "void": {
-        yield { src: "void", tag: Tag.identifier };
-        return;
-      }
-
-      case "null": {
-        yield { src: "null", tag: Tag.identifier };
-        return;
-      }
-
-      case "undefined": {
-        yield { src: "undefined", tag: Tag.identifier };
-        return;
-      }
-
-      case "anytype": {
-        yield { src: "anytype", tag: Tag.keyword_anytype };
-        return;
-      }
-
-      case "this": {
-        yield { src: "@This", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "switchIndex": {
-        const switchIndex = zigAnalysis.exprs[expr.switchIndex];
-        yield* ex(switchIndex, opts);
-        return;
-      }
-
-      case "errorSets": {
-        const errSetsObj = getType(expr.errorSets);
-        yield* ex(errSetsObj.lhs, opts);
-        yield Tok.space;
-        yield { src: "||", tag: Tag.pipe_pipe };
-        yield Tok.space;
-        yield* ex(errSetsObj.rhs, opts);
-        return;
-      }
-
-      case "errorUnion": {
-        const errUnionObj = getType(expr.errorUnion);
-        yield* ex(errUnionObj.lhs, opts);
-        yield { src: "!", tag: Tag.bang };
-        yield* ex(errUnionObj.rhs, opts);
-        return;
-      }
-
-      case "type": {
-        let name = "";
-
-        let typeObj = expr.type;
-        if (typeof typeObj === "number") typeObj = getType(typeObj);
-        switch (typeObj.kind) {
-          default:
-            throw "TODO: " + typeObj.kind;
-          case typeKinds.Type: {
-            yield { src: typeObj.name, tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.Void: {
-            yield { src: "void", tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.NoReturn: {
-            yield { src: "noreturn", tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.ComptimeExpr: {
-            yield { src: "anyopaque", tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.Bool: {
-            yield { src: "bool", tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.ComptimeInt: {
-            yield { src: "comptime_int", tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.ComptimeFloat: {
-            yield { src: "comptime_float", tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.Int: {
-            yield { src: typeObj.name, tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.Float: {
-            yield { src: typeObj.name, tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.Array: {
-            yield Tok.l_bracket;
-            yield* ex(typeObj.len, opts);
-            if (typeObj.sentinel) {
-              yield Tok.colon;
-              yield* ex(typeObj.sentinel, opts);
-            }
-            yield Tok.r_bracket;
-            yield* ex(typeObj.child, opts);
-            return;
-          }
-          case typeKinds.Optional: {
-            yield Tok.question_mark;
-            yield* ex(typeObj.child, opts);
-            return;
-          }
-          case typeKinds.Pointer: {
-            let ptrObj = typeObj;
-            switch (ptrObj.size) {
-              default:
-                console.log("TODO: implement unhandled pointer size case");
-              case pointerSizeEnum.One:
-                yield { src: "*", tag: Tag.asterisk };
-                break;
-              case pointerSizeEnum.Many:
-                yield Tok.l_bracket;
-                yield { src: "*", tag: Tag.asterisk };
-                if (ptrObj.sentinel !== null) {
-                  yield Tok.colon;
-                  yield* ex(ptrObj.sentinel, opts);
-                }
-                yield Tok.r_bracket;
-                break;
-              case pointerSizeEnum.Slice:
-                if (ptrObj.is_ref) {
-                  yield { src: "*", tag: Tag.asterisk };
-                }
-                yield Tok.l_bracket;
-                if (ptrObj.sentinel !== null) {
-                  yield Tok.colon;
-                  yield* ex(ptrObj.sentinel, opts);
-                }
-                yield Tok.r_bracket;
-                break;
-              case pointerSizeEnum.C:
-                yield Tok.l_bracket;
-                yield { src: "*", tag: Tag.asterisk };
-                yield { src: "c", tag: Tag.identifier };
-                if (typeObj.sentinel !== null) {
-                  yield Tok.colon;
-                  yield* ex(ptrObj.sentinel, opts);
-                }
-                yield Tok.r_bracket;
-                break;
-            }
-            if (!ptrObj.is_mutable) {
-              yield Tok.const;
-              yield Tok.space;
-            }
-            if (ptrObj.is_allowzero) {
-              yield { src: "allowzero", tag: Tag.keyword_allowzero };
-              yield Tok.space;
-            }
-            if (ptrObj.is_volatile) {
-              yield { src: "volatile", tag: Tag.keyword_volatile };
-            }
-            if (ptrObj.has_addrspace) {
-              yield { src: "addrspace", tag: Tag.keyword_addrspace };
-              yield Tok.l_paren;
-              yield Tok.period;
-              yield Tok.r_paren;
-            }
-            if (ptrObj.has_align) {
-              yield { src: "align", tag: Tag.keyword_align };
-              yield Tok.l_paren;
-              yield* ex(ptrObj.align, opts);
-              if (ptrObj.hostIntBytes !== undefined && ptrObj.hostIntBytes !== null) {
-                yield Tok.colon;
-                yield* ex(ptrObj.bitOffsetInHost, opts);
-                yield Tok.colon;
-                yield* ex(ptrObj.hostIntBytes, opts);
-              }
-              yield Tok.r_paren;
-              yield Tok.space;
-            }
-            yield* ex(ptrObj.child, opts);
-            return;
-          }
-          case typeKinds.Struct: {
-            let structObj = typeObj;
-            if (structObj.layout !== null) {
-              switch (structObj.layout.enumLiteral) {
-                case "Packed": {
-                  yield { src: "packed", tag: Tag.keyword_packed };
-                  break;
-                }
-                case "Extern": {
-                  yield { src: "extern", tag: Tag.keyword_extern };
-                  break;
-                }
-              }
-              yield Tok.space;
-            }
-            yield { src: "struct", tag: Tag.keyword_struct };
-            if (structObj.backing_int !== null) {
-              yield Tok.l_paren;
-              yield* ex(structObj.backing_int, opts);
-              yield Tok.r_paren;
-            }
-            yield Tok.space;
-            yield Tok.l_brace;
-
-            if (structObj.field_types.length > 1) {
-              yield Tok.enter;
-            } else {
-              yield Tok.space;
-            }
-
-            let indent = 0;
-            if (structObj.field_types.length > 1) {
-              indent = 1;
-            }
-            if (opts.indent && structObj.field_types.length > 1) {
-              indent += opts.ident;
-            }
-
-            let structNode = getAstNode(structObj.src);
-            for (let i = 0; i < structObj.field_types.length; i += 1) {
-              let fieldNode = getAstNode(structNode.fields[i]);
-              let fieldName = fieldNode.name;
-
-              for (let j = 0; j < indent; j += 1) {
-                yield Tok.tab;
-              }
-
-              if (!typeObj.is_tuple) {
-                yield { src: fieldName, tag: Tag.identifier };
-              }
-
-              let fieldTypeExpr = structObj.field_types[i];
-              if (!typeObj.is_tuple) {
-                yield Tok.colon;
-                yield Tok.space;
-              }
-              yield* ex(fieldTypeExpr, { ...opts, indent: indent });
-
-              if (structObj.field_defaults[i] !== null) {
-                yield Tok.space;
-                yield Tok.eql;
-                yield Tok.space;
-                yield* ex(structObj.field_defaults[i], opts);
-              }
-
-              if (structObj.field_types.length > 1) {
-                yield Tok.comma;
-                yield Tok.enter;
-              } else {
-                yield Tok.space;
-              }
-            }
-            yield Tok.r_brace;
-            return;
-          }
-          case typeKinds.Enum: {
-            let enumObj = typeObj;
-            yield { src: "enum", tag: Tag.keyword_enum };
-            if (enumObj.tag) {
-              yield Tok.l_paren;
-              yield* ex(enumObj.tag, opts);
-              yield Tok.r_paren;
-            }
-            yield Tok.space;
-            yield Tok.l_brace;
-
-            let enumNode = getAstNode(enumObj.src);
-            let fields_len = enumNode.fields.length;
-            if (enumObj.nonexhaustive) {
-              fields_len += 1;
-            }
-
-            if (fields_len > 1) {
-              yield Tok.enter;
-            } else {
-              yield Tok.space;
-            }
-
-            let indent = 0;
-            if (fields_len > 1) {
-              indent = 1;
-            }
-            if (opts.indent) {
-              indent += opts.indent;
-            }
-
-            for (let i = 0; i < enumNode.fields.length; i += 1) {
-              let fieldNode = getAstNode(enumNode.fields[i]);
-              let fieldName = fieldNode.name;
-
-              for (let j = 0; j < indent; j += 1) yield Tok.tab;
-              yield { src: fieldName, tag: Tag.identifier };
-
-              if (enumObj.values[i] !== null) {
-                yield Tok.space;
-                yield Tok.eql;
-                yield Tok.space;
-                yield* ex(enumObj.values[i], opts);
-              }
-
-              if (fields_len > 1) {
-                yield Tok.comma;
-                yield Tok.enter;
-              }
-            }
-            if (enumObj.nonexhaustive) {
-              for (let j = 0; j < indent; j += 1) yield Tok.tab;
-            
-              yield { src: "_", tag: Tag.identifier };
-            
-              if (fields_len > 1) {
-                yield Tok.comma;
-                yield Tok.enter;
-              }
-            }
-            if (opts.indent) {
-              for (let j = 0; j < opts.indent; j += 1) yield Tok.tab;
-            }
-            yield Tok.r_brace;
-            return;
-          }
-          case typeKinds.Union: {
-            let unionObj = typeObj;
-            if (unionObj.layout !== null) {
-              switch (unionObj.layout.enumLiteral) {
-                case "Packed": {
-                  yield { src: "packed", tag: Tag.keyword_packed };
-                  break;
-                }
-                case "Extern": {
-                  yield { src: "extern", tag: Tag.keyword_extern };
-                  break;
-                }
-              }
-              yield Tok.space;
-            }
-            yield { src: "union", tag: Tag.keyword_union };
-            if (unionObj.auto_tag) {
-              yield Tok.l_paren;
-              yield { src: "enum", tag: Tag.keyword_enum };
-              if (unionObj.tag) {
-                yield Tok.l_paren;
-                yield* ex(unionObj.tag, opts);
-                yield Tok.r_paren;
-                yield Tok.r_paren;
-              } else {
-                yield Tok.r_paren;
-              }
-            } else if (unionObj.tag) {
-              yield Tok.l_paren;
-              yield* ex(unionObj.tag, opts);
-              yield Tok.r_paren;
-            }
-            yield Tok.space;
-            yield Tok.l_brace;
-            if (unionObj.field_types.length > 1) {
-              yield Tok.enter;
-            } else {
-              yield Tok.space;
-            }
-            let indent = 0;
-            if (unionObj.field_types.length > 1) {
-              indent = 1;
-            }
-            if (opts.indent) {
-              indent += opts.indent;
-            }
-            let unionNode = getAstNode(unionObj.src);
-            for (let i = 0; i < unionObj.field_types.length; i += 1) {
-              let fieldNode = getAstNode(unionNode.fields[i]);
-              let fieldName = fieldNode.name;
-              for (let j = 0; j < indent; j += 1) yield Tok.tab;
-              yield { src: fieldName, tag: Tag.identifier };
-
-              let fieldTypeExpr = unionObj.field_types[i];
-              yield Tok.colon;
-              yield Tok.space;
-
-              yield* ex(fieldTypeExpr, { ...opts, indent: indent });
-
-              if (unionObj.field_types.length > 1) {
-                yield Tok.comma;
-                yield Tok.enter;
-              } else {
-                yield Tok.space;
-              }
-            }
-            if (opts.indent) {
-              for (let j = 0; j < opts.indent; j += 1) yield Tok.tab;
-            }
-            yield Tok.r_brace;
-            return;
-          }
-          case typeKinds.Opaque: {
-            yield { src: "opaque", tag: Tag.keyword_opaque };
-            yield Tok.space;
-            yield Tok.l_brace;
-            yield Tok.r_brace;
-            return;
-          }
-          case typeKinds.EnumLiteral: {
-            yield { src: "(enum literal)", tag: Tag.identifier };
-            return;
-          }
-          case typeKinds.ErrorSet: {
-            let errSetObj = typeObj;
-            if (errSetObj.fields === null) {
-              yield { src: "anyerror", tag: Tag.identifier };
-            } else if (errSetObj.fields.length == 0) {
-              yield { src: "error", tag: Tag.keyword_error };
-              yield Tok.l_brace;
-              yield Tok.r_brace;
-            } else if (errSetObj.fields.length == 1) {
-              yield { src: "error", tag: Tag.keyword_error };
-              yield Tok.l_brace;
-              yield { src: errSetObj.fields[0].name, tag: Tag.identifier };
-              yield Tok.r_brace;
-            } else {
-              yield { src: "error", tag: Tag.keyword_error };
-              yield Tok.l_brace;
-              yield { src: errSetObj.fields[0].name, tag: Tag.identifier };
-              for (let i = 1; i < errSetObj.fields.length; i++) {
-                yield Tok.comma;
-                yield Tok.space;
-                yield { src: errSetObj.fields[i].name, tag: Tag.identifier };
-              }
-              yield Tok.r_brace;
-            }
-            return;
-          }
-          case typeKinds.ErrorUnion: {
-            let errUnionObj = typeObj;
-            yield* ex(errUnionObj.lhs, opts);
-            yield { src: "!", tag: Tag.bang };
-            yield* ex(errUnionObj.rhs, opts);
-            return;
-          }
-          case typeKinds.InferredErrorUnion: {
-            let errUnionObj = typeObj;
-            yield { src: "!", tag: Tag.bang };
-            yield* ex(errUnionObj.payload, opts);
-            return;
-          }
-          case typeKinds.Fn: {
-            let fnObj = typeObj;
-            let fnDecl = opts.fnDecl;
-            let linkFnNameDecl = opts.linkFnNameDecl;
-            opts.fnDecl = null;
-            opts.linkFnNameDecl = null;
-            if (opts.addParensIfFnSignature && fnObj.src == 0) {
-              yield Tok.l_paren;
-            }
-            if (fnObj.is_extern) {
-              yield { src: "extern", tag: Tag.keyword_extern };
-              yield Tok.space;
-            } else if (fnObj.has_cc) {
-              let cc_expr = zigAnalysis.exprs[fnObj.cc];
-              if (cc_expr.enumLiteral === "Inline") {
-                yield { src: "inline", tag: Tag.keyword_inline };
-                yield Tok.space;
-              }
-            }
-            if (fnObj.has_lib_name) {
-              yield { src: '"' + fnObj.lib_name + '"', tag: Tag.string_literal };
-              yield Tok.space;
-            }
-            yield { src: "fn", tag: Tag.keyword_fn };
-            yield Tok.space;
-            if (fnDecl) {
-              if (linkFnNameDecl) {
-                yield { src: fnDecl.name, tag: Tag.identifier, link: linkFnNameDecl, fnDecl: false };
-              } else {
-                yield { src: fnDecl.name, tag: Tag.identifier, fnDecl: true };
-              }
-            }
-            yield Tok.l_paren;
-            if (fnObj.params) {
-              let fields = null;
-              let isVarArgs = false;
-              if (fnObj.src != 0) {
-                let fnNode = getAstNode(fnObj.src);
-                fields = fnNode.fields;
-                isVarArgs = fnNode.varArgs;
-              }
-
-              for (let i = 0; i < fnObj.params.length; i += 1) {
-                if (i != 0) {
-                  yield Tok.comma;
-                  yield Tok.space;
-                }
-
-                let value = fnObj.params[i];
-                let paramValue = resolveValue({ expr: value });
-
-                if (fields != null) {
-                  let paramNode = getAstNode(fields[i]);
-
-                  if (paramNode.varArgs) {
-                    yield Tok.period;
-                    yield Tok.period;
-                    yield Tok.period;
-                    continue;
-                  }
-
-                  if (paramNode.noalias) {
-                    yield { src: "noalias", tag: Tag.keyword_noalias };
-                    yield Tok.space;
-                  }
-
-                  if (paramNode.comptime) {
-                    yield { src: "comptime", tag: Tag.keyword_comptime };
-                    yield Tok.space;
-                  }
-
-                  let paramName = paramNode.name;
-                  if (paramName != null) {
-                    // skip if it matches the type name
-                    if (!shouldSkipParamName(paramValue, paramName)) {
-                      if (paramName === "") {
-                        paramName = "_";
-                      }
-                      yield { src: paramName, tag: Tag.identifier };
-                      yield Tok.colon;
-                      yield Tok.space;
-                    }
-                  }
-                }
-
-                // TODO: most of this seems redundant
-                if (isVarArgs && i === fnObj.params.length - 1) {
-                  yield Tok.period;
-                  yield Tok.period;
-                  yield Tok.period;
-                } else if ("alignOf" in value) {
-                  yield* ex(value, opts);
-                } else if ("typeOf" in value) {
-                  yield* ex(value, opts);
-                } else if ("typeOf_peer" in value) {
-                  yield* ex(value, opts);
-                } else if ("declRef" in value) {
-                  yield* ex(value, opts);
-                } else if ("call" in value) {
-                  yield* ex(value, opts);
-                } else if ("refPath" in value) {
-                  yield* ex(value, opts);
-                } else if ("type" in value) {
-                  yield* ex(value, opts);
-                  //payloadHtml += '<span class="tok-kw">' + name + "</span>";
-                } else if ("binOpIndex" in value) {
-                  yield* ex(value, opts);
-                } else if ("comptimeExpr" in value) {
-                  let comptimeExpr =
-                    zigAnalysis.comptimeExprs[value.comptimeExpr].code;
-                  yield* Tokenizer(comptimeExpr);
-                } else {
-                  yield { src: "anytype", tag: Tag.keyword_anytype };
-                }
-              }
-            }
-
-            yield Tok.r_paren;
-            yield Tok.space;
-
-            if (fnObj.has_align) {
-              let align = zigAnalysis.exprs[fnObj.align];
-              yield { src: "align", tag: Tag.keyword_align };
-              yield Tok.l_paren;
-              yield* ex(align, opts);
-              yield Tok.r_paren;
-              yield Tok.space;
-            }
-            if (fnObj.has_cc) {
-              let cc = zigAnalysis.exprs[fnObj.cc];
-              if (cc) {
-                if (cc.enumLiteral !== "Inline") {
-                  yield { src: "callconv", tag: Tag.keyword_callconv };
-                  yield Tok.l_paren;
-                  yield* ex(cc, opts);
-                  yield Tok.r_paren;
-                  yield Tok.space;
-                }
-              }
-            }
-
-            if (fnObj.is_inferred_error) {
-              yield { src: "!", tag: Tag.bang };
-            }
-            if (fnObj.ret != null) {
-              yield* ex(fnObj.ret, {
-                ...opts,
-                addParensIfFnSignature: true,
-              });
-            } else {
-              yield { src: "anytype", tag: Tag.keyword_anytype };
-            }
-
-            if (opts.addParensIfFnSignature && fnObj.src == 0) {
-              yield Tok.r_paren;
-            }
-            return;
-          }
-        }
-      }
-
-      case "typeOf": {
-        const typeRefArg = zigAnalysis.exprs[expr.typeOf];
-        yield { src: "@TypeOf", tag: Tag.builtin };
-        yield Tok.l_paren;
-        yield* ex(typeRefArg, opts);
-        yield Tok.r_paren;
-        return;
-      }
-
-      case "builtinField": {
-        yield { src: expr.builtinField, tag: Tag.identifier };
-        return;
-      }
-    }
-
-
-  }
-
-
-
-  function shouldSkipParamName(typeRef, paramName) {
-    let resolvedTypeRef = resolveValue({ expr: typeRef });
-    if ("type" in resolvedTypeRef) {
-      let typeObj = getType(resolvedTypeRef.type);
-      if (typeObj.kind === typeKinds.Pointer) {
-        let ptrObj = typeObj;
-        if (getPtrSize(ptrObj) === pointerSizeEnum.One) {
-          const value = resolveValue(ptrObj.child);
-          return typeValueName(value, false, true).toLowerCase() === paramName;
-        }
-      }
-    }
-    return false;
-  }
-
-  function getPtrSize(typeObj) {
-    return typeObj.size == null ? pointerSizeEnum.One : typeObj.size;
-  }
-
-  function renderType(typeObj) {
-    let name;
-    if (
-      rootIsStd &&
-      typeObj ===
-      getType(zigAnalysis.modules[zigAnalysis.rootMod].main)
-    ) {
-      name = renderSingleToken(Tok.identifier("std"));
-    } else {
-      name = renderTokens(ex({ type: typeObj }));
-    }
-    if (name != null && name != "") {
-      domHdrName.innerHTML = "<pre class='inline'>" + name + "</pre> ("
-        + zigAnalysis.typeKinds[typeObj.kind] + ")";
-      domHdrName.classList.remove("hidden");
-    }
-    if (typeObj.kind == typeKinds.ErrorSet) {
-      renderErrorSet(typeObj);
-    }
-  }
-
-  function renderErrorSet(errSetType) {
-    if (errSetType.fields == null) {
-      domFnErrorsAnyError.classList.remove("hidden");
-    } else {
-      let errorList = [];
-      for (let i = 0; i < errSetType.fields.length; i += 1) {
-        let errObj = errSetType.fields[i];
-        //let srcObj = zigAnalysis.astNodes[errObj.src];
-        errorList.push(errObj);
-      }
-      errorList.sort(function(a, b) {
-        return operatorCompare(a.name.toLowerCase(), b.name.toLowerCase());
-      });
-
-      resizeDomListDl(domListFnErrors, errorList.length);
-      for (let i = 0; i < errorList.length; i += 1) {
-        let nameTdDom = domListFnErrors.children[i * 2 + 0];
-        let descTdDom = domListFnErrors.children[i * 2 + 1];
-        nameTdDom.textContent = errorList[i].name;
-        let docs = errorList[i].docs;
-        if (docs != null) {
-          descTdDom.innerHTML = markdown(docs);
-        } else {
-          descTdDom.textContent = "";
-        }
-      }
-      domTableFnErrors.classList.remove("hidden");
-    }
-    domSectFnErrors.classList.remove("hidden");
-  }
-
-  //     function allCompTimeFnCallsHaveTypeResult(typeIndex, value) {
-  //         let srcIndex = zigAnalysis.fns[value].src;
-  //         let calls = nodesToCallsMap[srcIndex];
-  //         if (calls == null) return false;
-  //         for (let i = 0; i < calls.length; i += 1) {
-  //             let call = zigAnalysis.calls[calls[i]];
-  //             if (call.result.type !== typeTypeId) return false;
-  //         }
-  //         return true;
-  //     }
-  //
-  //     function allCompTimeFnCallsResult(calls) {
-  //         let firstTypeObj = null;
-  //         let containerObj = {
-  //             privDecls: [],
-  //         };
-  //         for (let callI = 0; callI < calls.length; callI += 1) {
-  //             let call = zigAnalysis.calls[calls[callI]];
-  //             if (call.result.type !== typeTypeId) return null;
-  //             let typeObj = zigAnalysis.types[call.result.value];
-  //             if (!typeKindIsContainer(typeObj.kind)) return null;
-  //             if (firstTypeObj == null) {
-  //                 firstTypeObj = typeObj;
-  //                 containerObj.src = typeObj.src;
-  //             } else if (firstTypeObj.src !== typeObj.src) {
-  //                 return null;
-  //             }
-  //
-  //             if (containerObj.fields == null) {
-  //                 containerObj.fields = (typeObj.fields || []).concat([]);
-  //             } else for (let fieldI = 0; fieldI < typeObj.fields.length; fieldI += 1) {
-  //                 let prev = containerObj.fields[fieldI];
-  //                 let next = typeObj.fields[fieldI];
-  //                 if (prev === next) continue;
-  //                 if (typeof(prev) === 'object') {
-  //                     if (prev[next] == null) prev[next] = typeObj;
-  //                 } else {
-  //                     containerObj.fields[fieldI] = {};
-  //                     containerObj.fields[fieldI][prev] = firstTypeObj;
-  //                     containerObj.fields[fieldI][next] = typeObj;
-  //                 }
-  //             }
-  //
-  //             if (containerObj.pubDecls == null) {
-  //                 containerObj.pubDecls = (typeObj.pubDecls || []).concat([]);
-  //             } else for (let declI = 0; declI < typeObj.pubDecls.length; declI += 1) {
-  //                 let prev = containerObj.pubDecls[declI];
-  //                 let next = typeObj.pubDecls[declI];
-  //                 if (prev === next) continue;
-  //                 // TODO instead of showing "examples" as the public declarations,
-  //                     // do logic like this:
-  //                 //if (typeof(prev) !== 'object') {
-  //                     //    let newDeclId = zigAnalysis.decls.length;
-  //                     //    prev = clone(zigAnalysis.decls[prev]);
-  //                     //    prev.id = newDeclId;
-  //                     //    zigAnalysis.decls.push(prev);
-  //                     //    containerObj.pubDecls[declI] = prev;
-  //                     //}
-  //                 //mergeDecls(prev, next, firstTypeObj, typeObj);
-  //             }
-  //         }
-  //         for (let declI = 0; declI < containerObj.pubDecls.length; declI += 1) {
-  //             let decl = containerObj.pubDecls[declI];
-  //             if (typeof(decl) === 'object') {
-  //                 containerObj.pubDecls[declI] = containerObj.pubDecls[declI].id;
-  //             }
-  //         }
-  //         return containerObj;
-  //     }
-
-  function renderValue(decl) {
-    let resolvedValue = resolveValue(decl.value);
-    if (resolvedValue.expr.fieldRef) {
-      const declRef = decl.value.expr.refPath[0].declRef;
-      const type = getDecl(declRef);
-
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.const;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          yield Tok.colon;
-          yield Tok.space;
-          yield Tok.identifier(type.name);
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(decl.value.expr, {});
-          yield Tok.semi;
-        })());
-    } else if (
-      resolvedValue.expr.string !== undefined ||
-      resolvedValue.expr.call !== undefined ||
-      resolvedValue.expr.comptimeExpr !== undefined
-    ) {
-      // TODO: we're using the resolved value but 
-      //       not keeping track of how we got there
-      //       that's important context that should
-      //       be shown to the user!
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.const;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          if (decl.value.typeRef) {
-            yield Tok.colon;
-            yield Tok.space;
-            yield* ex(decl.value.typeRef, {});
-          }
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(resolvedValue.expr, {});
-          yield Tok.semi;
-        })());
-    } else if (resolvedValue.expr.compileError) {
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.const;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(decl.value.expr, {});
-          yield Tok.semi;
-        })());
-    } else {
-      const parent = getType(decl.parent_container);
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.const;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          if (decl.value.typeRef !== null) {
-            yield Tok.colon;
-            yield Tok.space;
-            yield* ex(decl.value.typeRef, {});
-          }
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(decl.value.expr, {});
-          yield Tok.semi;
-        })());
-    }
-
-    let docs = getAstNode(decl.src).docs;
-    if (docs != null) {
-      // TODO: it shouldn't just be decl.parent_container, but rather 
-      //       the type that the decl holds (if the value is a type)
-      domTldDocs.innerHTML = markdown(docs, decl);
-
-      domTldDocs.classList.remove("hidden");
-    }
-
-    domFnProto.classList.remove("hidden");
-  }
-
-  function renderVar(decl) {
-    let resolvedVar = resolveValue(decl.value);
-
-    if (resolvedVar.expr.fieldRef) {
-      const declRef = decl.value.expr.refPath[0].declRef;
-      const type = getDecl(declRef);
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.var;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          yield Tok.colon;
-          yield Tok.space;
-          yield Tok.identifier(type.name);
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(decl.value.expr, {});
-          yield Tok.semi;
-        })());
-    } else if (
-      resolvedVar.expr.string !== undefined ||
-      resolvedVar.expr.call !== undefined ||
-      resolvedVar.expr.comptimeExpr !== undefined
-    ) {
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.var;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          if (decl.value.typeRef) {
-            yield Tok.colon;
-            yield Tok.space;
-            yield* ex(decl.value.typeRef, {});
-          }
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(decl.value.expr, {});
-          yield Tok.semi;
-        })());
-    } else if (resolvedVar.expr.compileError) {
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.var;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(decl.value.expr, {});
-          yield Tok.semi;
-        })());
-    } else {
-      domFnProtoCode.innerHTML = renderTokens(
-        (function*() {
-          yield Tok.var;
-          yield Tok.space;
-          yield Tok.identifier(decl.name);
-          yield Tok.colon;
-          yield Tok.space;
-          yield* ex(resolvedVar.typeRef, {});
-          yield Tok.space;
-          yield Tok.eql;
-          yield Tok.space;
-          yield* ex(decl.value.expr, {});
-          yield Tok.semi;
-        })());
-    }
-
-    let docs = getAstNode(decl.src).docs;
-    if (docs != null) {
-      domTldDocs.innerHTML = markdown(docs);
-      domTldDocs.classList.remove("hidden");
-    }
-
-    domFnProto.classList.remove("hidden");
-  }
-
-  function categorizeDecls(
-    decls,
-    typesList,
-    namespacesWithDocsList,
-    namespacesNoDocsList,
-    errSetsList,
-    fnsList,
-    varsList,
-    valsList,
-    testsList,
-    unsList
-  ) {
-    for (let i = 0; i < decls.length; i += 1) {
-      let decl = getDecl(decls[i]);
-      let declValue = resolveValue(decl.value);
-
-      // if (decl.isTest) {
-      //   testsList.push(decl);
-      //   continue;
-      // }
-
-      if (decl.kind === "var") {
-        varsList.push(decl);
-        continue;
-      }
-
-      if (decl.kind === "const") {
-        if ("type" in declValue.expr) {
-          // We have the actual type expression at hand.
-          const typeExpr = getType(declValue.expr.type);
-          if (typeExpr.kind == typeKinds.Fn) {
-            const funcRetExpr = resolveValue({
-              expr: typeExpr.ret,
-            });
-            if (
-              "type" in funcRetExpr.expr &&
-              funcRetExpr.expr.type == typeTypeId
-            ) {
-              if (typeIsErrSet(declValue.expr.type)) {
-                errSetsList.push(decl);
-              } else if (typeIsStructWithNoFields(declValue.expr.type)) {
-              
-                let docs = getAstNode(decl.src).docs;
-                if (!docs) {
-                  // If this is a re-export, try to fetch docs from the actual definition
-                  const { value, seenDecls } = resolveValue(decl.value, true);  
-                  if (seenDecls.length > 0) {
-                    const definitionDecl = getDecl(seenDecls[seenDecls.length - 1]);
-                    docs = getAstNode(definitionDecl.src).docs;
-                  } else {
-                    docs = getAstNode(getType(value.expr.type).src).docs;
-                  }
-                }
-                
-                if (docs) {
-                  namespacesWithDocsList.push({decl, docs});
-                } else {
-                  namespacesNoDocsList.push(decl);
-                }
-              } else {
-                typesList.push(decl);
-              }
-            } else {
-              fnsList.push(decl);
-            }
-          } else {
-            if (typeIsErrSet(declValue.expr.type)) {
-              errSetsList.push(decl);
-            } else if (typeIsStructWithNoFields(declValue.expr.type)) {
-              let docs = getAstNode(decl.src).docs;
-              if (!docs) {
-                // If this is a re-export, try to fetch docs from the actual definition
-                const { value, seenDecls } = resolveValue(decl.value, true);  
-                if (seenDecls.length > 0) {
-                  const definitionDecl = getDecl(seenDecls[seenDecls.length - 1]);
-                  docs = getAstNode(definitionDecl.src).docs;
-                } else {
-                  docs = getAstNode(getType(value.expr.type).src).docs;
-                }
-              }
-              if (docs) {
-                namespacesWithDocsList.push({decl, docs});
-              } else {
-                namespacesNoDocsList.push(decl);
-              }
-            } else {
-              typesList.push(decl);
-            }
-          }
-        } else if (declValue.typeRef) {
-          if ("type" in declValue.typeRef && declValue.typeRef == typeTypeId) {
-            // We don't know what the type expression is, but we know it's a type.
-            typesList.push(decl);
-          } else {
-            valsList.push(decl);
-          }
-        } else {
-          valsList.push(decl);
-        }
-      }
-
-      if (decl.is_uns) {
-        unsList.push(decl);
-      }
-    }
-  }
-
-  function sourceFileLink(decl) {
-    const srcNode = getAstNode(decl.src);
-    const srcFile = getFile(srcNode.file);
-    return sourceFileUrlTemplate.
-      replace("{{mod}}", zigAnalysis.modules[srcFile.modIndex].name).
-      replace("{{file}}", srcFile.name).
-      replace("{{line}}", srcNode.line + 1);
-  }
-
-  function renderContainer(container) {
-    let typesList = [];
-
-    let namespacesWithDocsList = [];
-    let namespacesNoDocsList = [];
-
-    let errSetsList = [];
-
-    let fnsList = [];
-
-    let varsList = [];
-
-    let valsList = [];
-
-    let testsList = [];
-
-    let unsList = [];
-
-    categorizeDecls(
-      container.pubDecls,
-      typesList,
-      namespacesWithDocsList,
-      namespacesNoDocsList,
-      errSetsList,
-      fnsList,
-      varsList,
-      valsList,
-      testsList,
-      unsList
-    );
-    if (curNav.showPrivDecls)
-      categorizeDecls(
-        container.privDecls,
-        typesList,
-        namespacesWithDocsList,
-        namespacesNoDocsList,
-        errSetsList,
-        fnsList,
-        varsList,
-        valsList,
-        testsList,
-        unsList
-      );
-
-    while (unsList.length > 0) {
-      let uns = unsList.shift();
-      let declValue = resolveValue(uns.value);
-      if (!("type" in declValue.expr)) continue;
-      let uns_container = getType(declValue.expr.type);
-      if (!isContainerType(uns_container)) continue;
-      categorizeDecls(
-        uns_container.pubDecls,
-        typesList,
-        namespacesWithDocsList,
-        namespacesNoDocsList,
-        errSetsList,
-        fnsList,
-        varsList,
-        valsList,
-        testsList,
-        unsList
-      );
-      if (curNav.showPrivDecls)
-        categorizeDecls(
-          uns_container.privDecls,
-          typesList,
-          namespacesWithDocsList,
-          namespacesNoDocsList,
-          errSetsList,
-          fnsList,
-          varsList,
-          valsList,
-          testsList,
-          unsList
-        );
-    }
-
-    typesList.sort(byNameProperty);
-    namespacesWithDocsList.sort(byNameProperty);
-    namespacesNoDocsList.sort(byNameProperty);
-    errSetsList.sort(byNameProperty);
-    fnsList.sort(byNameProperty);
-    varsList.sort(byNameProperty);
-    valsList.sort(byNameProperty);
-    testsList.sort(byNameProperty);
-
-    if (container.src != null) {
-      let docs = getAstNode(container.src).docs;
-      if (docs != null) {
-        domTldDocs.innerHTML = markdown(docs, container);
-        domTldDocs.classList.remove("hidden");
-      }
-    }
-
-    if (typesList.length !== 0) {
-      const splitPoint = Math.ceil(typesList.length / 2);
-      const template = '<li><a href="#"></a><div></div></li>';
-      resizeDomList(domListTypesLeft, splitPoint, template);
-      resizeDomList(domListTypesRight, typesList.length - splitPoint, template);
-
-      let activeList = domListTypesLeft;
-      let offset = 0;
-      for (let i = 0; i < typesList.length; i += 1) {
-        let liDom = activeList.children[i - offset];
-        let aDom = liDom.children[0];
-        let decl = typesList[i];
-        aDom.textContent = decl.name;
-        aDom.setAttribute("href", navLinkDecl(decl.name));
-        
-        let descDom = liDom.children[1];
-        let docs = getAstNode(decl.src).docs;
-        if (!docs) {
-          // If this is a re-export, try to fetch docs from the actual definition
-            const { value, seenDecls } = resolveValue(decl.value, true);  
-            if (seenDecls.length > 0) {
-              const definitionDecl = getDecl(seenDecls[seenDecls.length - 1]);
-              docs = getAstNode(definitionDecl.src).docs;
-            } else {
-              const type = getType(value.expr.type);
-              if ("src" in type) {
-                docs = getAstNode(type.src).docs;
-              }
-            }
-        }
-        
-        if (docs) {
-          descDom.innerHTML = markdown(shortDesc(docs));
-        } else {
-          descDom.innerHTML = "<p class='understated'><i>No documentation provided.</i></p>";
-        }
-        if (i == splitPoint - 1) {
-          activeList = domListTypesRight;
-          offset = splitPoint;
-        }
-      }
-      domSectTypes.classList.remove("hidden");
-    }
-    
-    if (namespacesWithDocsList.length !== 0) {
-      const splitPoint = Math.ceil(namespacesWithDocsList.length / 2);
-      const template = '<li><a href="#"></a><div></div></li>';
-      resizeDomList(domListNamespacesLeft, splitPoint, template);
-      resizeDomList(domListNamespacesRight, 
-        namespacesWithDocsList.length - splitPoint, 
-        template);
-
-      let activeList = domListNamespacesLeft;
-      let offset = 0;
-      for (let i = 0; i < namespacesWithDocsList.length; i += 1) {
-        let liDom = activeList.children[i - offset];
-        let aDom = liDom.children[0];
-        let { decl, docs } = namespacesWithDocsList[i];
-        aDom.textContent = decl.name;
-        aDom.setAttribute("href", navLinkDecl(decl.name));
-
-        
-        let descDom = liDom.children[1];
-        descDom.innerHTML = markdown(shortDesc(docs));
-        if (i == splitPoint - 1) {
-          activeList = domListNamespacesRight;
-          offset = splitPoint;
-        }
-      }
-
-      domListNamespacesLeft.classList.remove("hidden");
-      domListNamespacesRight.classList.remove("hidden");
-      domSectNamespaces.classList.remove("hidden");
-    }
-
-    if (namespacesNoDocsList.length !== 0) {
-      resizeDomList(
-        domNoDocsNamespaces,
-        namespacesNoDocsList.length,
-        '<span><a href="#"></a><span></span></span>'
-      );
-      for (let i = 0; i < namespacesNoDocsList.length; i += 1) {
-        let aDom = domNoDocsNamespaces.children[i].children[0];
-        let decl = namespacesNoDocsList[i];
-        aDom.textContent = decl.name;
-        aDom.setAttribute("href", navLinkDecl(decl.name));
-        let comma = domNoDocsNamespaces.children[i].children[1];
-        if (i == namespacesNoDocsList.length - 1) {
-          comma.textContent = "";
-        } else {
-          comma.textContent = ", ";
-        }
-      }
-
-      domNoDocsNamespaces.classList.remove("hidden");
-      domSectNamespaces.classList.remove("hidden");
-    }
-
-
-    
-
-    if (errSetsList.length !== 0) {
-      resizeDomList(
-        domListErrSets,
-        errSetsList.length,
-        '<li><a href="#"></a></li>'
-      );
-      for (let i = 0; i < errSetsList.length; i += 1) {
-        let liDom = domListErrSets.children[i];
-        let aDom = liDom.children[0];
-        let decl = errSetsList[i];
-        aDom.textContent = decl.name;
-        aDom.setAttribute("href", navLinkDecl(decl.name));
-      }
-      domSectErrSets.classList.remove("hidden");
-    }
-
-    if (fnsList.length !== 0) {
-      resizeDomList(
-        domListFns,
-        fnsList.length,
-        '<div><dt><pre class="inline fnSignature"></pre><div></div></dt><dd></dd></div>'
-      );
-
-      for (let i = 0; i < fnsList.length; i += 1) {
-        let decl = fnsList[i];
-        let trDom = domListFns.children[i];
-
-        let tdFnSignature = trDom.children[0].children[0];
-        let tdFnSrc = trDom.children[0].children[1];
-        let tdDesc = trDom.children[1];
-
-        let declType = resolveValue(decl.value);
-        console.assert("type" in declType.expr);
-        tdFnSignature.innerHTML = renderTokens(ex(declType.expr, {
-          fnDecl: decl,
-          linkFnNameDecl: navLinkDecl(decl.name),
-        }));
-        tdFnSrc.innerHTML = "<a style=\"float: right;\" target=\"_blank\" href=\"" +
-          sourceFileLink(decl) + "\">[src]</a>";
-
-        let docs = getAstNode(decl.src).docs;
-        if (docs != null) {
-          docs = docs.trim();
-          var short = shortDesc(docs);
-          if (short != docs) {
-            short = markdown(short, container);
-            var long = markdown(docs, container); // TODO: this needs to be the file top lvl struct
-            tdDesc.innerHTML =
-              "<div class=\"expand\" ><span class=\"button\" onclick=\"toggleExpand(event)\"></span><div class=\"sum-less\">" + short + "</div>" + "<div class=\"sum-more\">" + long + "</div></details>";
-          }
-          else {
-            tdDesc.innerHTML = markdown(short, container);
-          }
-        } else {
-          tdDesc.innerHTML = "<p class='understated'><i>No documentation provided.</i><p>";
-        }
-      }
-      domSectFns.classList.remove("hidden");
-    }
-
-    let containerNode = getAstNode(container.src);
-    if (containerNode.fields && containerNode.fields.length > 0) {
-      resizeDomList(domListFields, containerNode.fields.length, "<div></div>");
-
-      for (let i = 0; i < containerNode.fields.length; i += 1) {
-        let fieldNode = getAstNode(containerNode.fields[i]);
-        let divDom = domListFields.children[i];
-        let fieldName = fieldNode.name;
-        let docs = fieldNode.docs;
-        let docsNonEmpty = docs != null && docs !== "";
-        let extraPreClass = docsNonEmpty ? " fieldHasDocs" : "";
-
-        let html =
-          '<div class="mobile-scroll-container"><pre class="scroll-item' +
-          extraPreClass +
-          '">' +
-          escapeHtml(fieldName);
-
-        if (container.kind === typeKinds.Enum) {
-          let value = container.values[i];
-          if (value !== null) {
-            html += renderTokens((function*() {
-              yield Tok.space;
-              yield Tok.eql;
-              yield Tok.space;
-              yield* ex(value, {});
-            })());
-          }
-        } else {
-          let fieldTypeExpr = container.field_types[i];
-          if (container.kind !== typeKinds.Struct || !container.is_tuple) {
-            html += renderTokens((function*() {
-              yield Tok.colon;
-              yield Tok.space;
-            })());
-          }
-          html += renderTokens(ex(fieldTypeExpr, {}));
-          let tsn = typeShorthandName(fieldTypeExpr);
-          if (tsn) {
-            html += "<span> (" + tsn + ")</span>";
-          }
-          if (container.kind === typeKinds.Struct && !container.is_tuple) {
-            let defaultInitExpr = container.field_defaults[i];
-            if (defaultInitExpr !== null) {
-              html += renderTokens((function*() {
-                yield Tok.space;
-                yield Tok.eql;
-                yield Tok.space;
-                yield* ex(defaultInitExpr, {});
-              })());
-            }
-          }
-        }
-
-        html += ",</pre></div>";
-
-        if (docsNonEmpty) {
-          html += '<div class="fieldDocs">' + markdown(docs) + "</div>";
-        }
-        divDom.innerHTML = html;
-      }
-      domSectFields.classList.remove("hidden");
-    }
-
-    if (varsList.length !== 0) {
-      resizeDomList(
-        domListGlobalVars,
-        varsList.length,
-        '<tr><td><a href="#"></a></td><td><pre class="inline"></pre></td><td></td></tr>'
-      );
-      for (let i = 0; i < varsList.length; i += 1) {
-        let decl = varsList[i];
-        let trDom = domListGlobalVars.children[i];
-
-        let tdName = trDom.children[0];
-        let tdNameA = tdName.children[0];
-        let tdType = trDom.children[1];
-        let preType = tdType.children[0];
-        let tdDesc = trDom.children[2];
-
-        tdNameA.setAttribute("href", navLinkDecl(decl.name));
-        tdNameA.textContent = decl.name;
-
-        preType.innerHTML = renderTokens(ex(walkResultTypeRef(decl.value), {}));
-
-        let docs = getAstNode(decl.src).docs;
-        if (docs != null) {
-          tdDesc.innerHTML = shortDescMarkdown(docs);
-        } else {
-          tdDesc.textContent = "";
-        }
-      }
-      domSectGlobalVars.classList.remove("hidden");
-    }
-
-    if (valsList.length !== 0) {
-      resizeDomList(
-        domListValues,
-        valsList.length,
-        '<tr><td><a href="#"></a></td><td><pre class="inline"></pre></td><td></td></tr>'
-      );
-      for (let i = 0; i < valsList.length; i += 1) {
-        let decl = valsList[i];
-        let trDom = domListValues.children[i];
-
-        let tdName = trDom.children[0];
-        let tdNameA = tdName.children[0];
-        let tdType = trDom.children[1];
-        let preType = tdType.children[0];
-        let tdDesc = trDom.children[2];
-
-        tdNameA.setAttribute("href", navLinkDecl(decl.name));
-        tdNameA.textContent = decl.name;
-
-        preType.innerHTML = renderTokens(ex(walkResultTypeRef(decl.value), {}));
-
-        let docs = getAstNode(decl.src).docs;
-        if (docs != null) {
-          tdDesc.innerHTML = shortDescMarkdown(docs);
-        } else {
-          tdDesc.textContent = "";
-        }
-      }
-      domSectValues.classList.remove("hidden");
-    }
-
-    if (testsList.length !== 0) {
-      resizeDomList(
-        domListTests,
-        testsList.length,
-        '<tr><td><pre class="inline"></pre></td><td><pre class="inline"></pre></td><td></td></tr>'
-      );
-      for (let i = 0; i < testsList.length; i += 1) {
-        let decl = testsList[i];
-        let trDom = domListTests.children[i];
-
-        let tdName = trDom.children[0];
-        let tdNamePre = tdName.children[0];
-        let tdType = trDom.children[1];
-        let tdTypePre = tdType.children[0];
-        let tdDesc = trDom.children[2];
-
-        tdNamePre.innerHTML = renderSingleToken(Tok.identifier(decl.name));
-
-        tdTypePre.innerHTML = ex(walkResultTypeRef(decl.value), {});
-
-        let docs = getAstNode(decl.src).docs;
-        if (docs != null) {
-          tdDesc.innerHTML = shortDescMarkdown(docs);
-        } else {
-          tdDesc.textContent = "";
-        }
-      }
-      domSectTests.classList.remove("hidden");
-    }
-
-    if (container.kind !== typeKinds.Struct || containerNode.fields.length > 0) {
-      domHdrName.innerHTML = "<pre class='inline'>" +
-        zigAnalysis.typeKinds[container.kind] +
-        "</pre>";
-      domHdrName.classList.remove("hidden");
-    }
-  }
-
-  function operatorCompare(a, b) {
-    if (a === b) {
-      return 0;
-    } else if (a < b) {
-      return -1;
-    } else {
-      return 1;
-    }
-  }
-
-  function detectRootIsStd() {
-    let rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
-    if (rootMod.table["std"] == null) {
-      // no std mapped into the root module
-      return false;
-    }
-    let stdMod = zigAnalysis.modules[rootMod.table["std"]];
-    if (stdMod == null) return false;
-    return rootMod.file === stdMod.file;
-  }
-
-  function indexTypeKinds() {
-    let map = {};
-    for (let i = 0; i < zigAnalysis.typeKinds.length; i += 1) {
-      map[zigAnalysis.typeKinds[i]] = i;
-    }
-    // This is just for debugging purposes, not needed to function
-    let assertList = [
-      "Type",
-      "Void",
-      "Bool",
-      "NoReturn",
-      "Int",
-      "Float",
-      "Pointer",
-      "Array",
-      "Struct",
-      "ComptimeFloat",
-      "ComptimeInt",
-      "Undefined",
-      "Null",
-      "Optional",
-      "ErrorUnion",
-      "ErrorSet",
-      "Enum",
-      "Union",
-      "Fn",
-      "Opaque",
-      "Frame",
-      "AnyFrame",
-      "Vector",
-      "EnumLiteral",
-    ];
-    for (let i = 0; i < assertList.length; i += 1) {
-      if (map[assertList[i]] == null)
-        throw new Error("No type kind '" + assertList[i] + "' found");
-    }
-    return map;
-  }
-
-  function findTypeTypeId() {
-    for (let i = 0; i < zigAnalysis.types.length; i += 1) {
-      if (getType(i).kind == typeKinds.Type) {
-        return i;
-      }
-    }
-    throw new Error("No type 'type' found");
-  }
-
-
-  function updateCurNav() {
-    curNav = {
-      hash: location.hash,
-      mode: NAV_MODES.API,
-      modNames: [],
-      modObjs: [],
-      declNames: [],
-      declObjs: [],
-      callName: null,
-      activeGuide: null,
-      activeGuideScrollTo: null,
-    };
-    curNavSearch = "";
-
-    const mode = location.hash.substring(0, 3);
-    let query = location.hash.substring(3);
-
-    let qpos = query.indexOf("?");
-    let nonSearchPart;
-    if (qpos === -1) {
-      nonSearchPart = query;
-    } else {
-      nonSearchPart = query.substring(0, qpos);
-      curNavSearch = decodeURIComponent(query.substring(qpos + 1));
-    }
-
-    const DEFAULT_HASH = NAV_MODES.API + zigAnalysis.modules[zigAnalysis.rootMod].name;
-    switch (mode) {
-      case NAV_MODES.API:
-        // #A;MODULE:decl.decl.decl?search-term
-        curNav.mode = mode;
-        {
-        let parts = nonSearchPart.split(":");
-        if (parts[0] == "") {
-          location.hash = DEFAULT_HASH;
-        } else {
-          curNav.modNames = decodeURIComponent(parts[0]).split(".");
-        }
-
-        if (parts[1] != null) {
-          curNav.declNames = decodeURIComponent(parts[1]).split(".");
-        }
-        }
-        return;
-      case NAV_MODES.GUIDES:
-        curNav.mode = mode;
-
-        {
-          let parts = nonSearchPart.split(":");
-          curNav.activeGuide = parts[0];
-          if (parts[1] != null) {
-            curNav.activeGuideScrollTo = decodeURIComponent(":" + parts[1]);
-          }
-        }
-        return;
-      default:
-        location.hash = DEFAULT_HASH;
-        return;
-    }
-  }
-
-  function onHashChange(ev) {
-    scrollHistory[curNav.hash] = scrollMonitor.map(function (x) {
-      return [x, x.scrollTop]
-    });
-    
-    if (skipNextHashChange == decodeURIComponent(location.hash)) {
-      skipNextHashChange = null;
-      return;
-    }
-    skipNextHashChange = null;
-    updateCurNav();
-
-    if (domSearch.value !== curNavSearch) {
-      domSearch.value = curNavSearch;
-      if (domSearch.value.length == 0)
-        domSearchPlaceholder.classList.remove("hidden");
-      else
-        domSearchPlaceholder.classList.add("hidden");
-    }
-    render();
-    if (imFeelingLucky) {
-      imFeelingLucky = false;
-      activateSelectedResult();
-    }
-
-    scroll();
-  }
-
-  function scroll() {
-    const cur = scrollHistory[location.hash];
-    if (cur) {
-      for (let [elem, offset] of cur) {
-        elem.scrollTo(0, offset);
-      }
-    } else {
-      if (curNav.activeGuideScrollTo) return;
-      for (let elem of scrollMonitor) {
-        elem.scrollTo(0, 0);
-      }
-    }
-  }
-
-  function findSubDecl(parentTypeOrDecl, childName) {
-    let parentType = parentTypeOrDecl;
-    {
-      // Generic functions / resolving decls
-      if ("value" in parentType) {
-        const rv = resolveValue(parentType.value);
-        if ("type" in rv.expr) {
-          const t = getType(rv.expr.type);
-          parentType = t;
-          if (t.kind == typeKinds.Fn && t.generic_ret != null) {
-            let resolvedGenericRet = resolveValue({ expr: t.generic_ret });
-
-            if ("call" in resolvedGenericRet.expr) {
-              let call = zigAnalysis.calls[resolvedGenericRet.expr.call];
-              let resolvedFunc = resolveValue({ expr: call.func });
-              if (!("type" in resolvedFunc.expr)) return null;
-              let callee = getType(resolvedFunc.expr.type);
-              if (!callee.generic_ret) return null;
-              resolvedGenericRet = resolveValue({ expr: callee.generic_ret });
-            }
-
-            if ("type" in resolvedGenericRet.expr) {
-              parentType = getType(resolvedGenericRet.expr.type);
-            }
-          }
-        }
-      }
-    }
-
-    if (parentType.pubDecls) {
-      for (let i = 0; i < parentType.pubDecls.length; i += 1) {
-        let declIndex = parentType.pubDecls[i];
-        let childDecl = getDecl(declIndex);
-        if (childDecl.name === childName) {
-          childDecl.find_subdecl_idx = declIndex;
-          return childDecl;
-        } else if (childDecl.is_uns) {
-          let declValue = resolveValue(childDecl.value);
-          if (!("type" in declValue.expr)) continue;
-          let uns_container = getType(declValue.expr.type);
-          let uns_res = findSubDecl(uns_container, childName);
-          if (uns_res !== null) return uns_res;
-        }
-      }
-    }
-
-    if (parentType.privDecls) {
-      for (let i = 0; i < parentType.privDecls.length; i += 1) {
-        let declIndex = parentType.privDecls[i];
-        let childDecl = getDecl(declIndex);
-        if (childDecl.name === childName) {
-          childDecl.find_subdecl_idx = declIndex;
-          childDecl.is_private = true;
-          return childDecl;
-        } else if (childDecl.is_uns) {
-          let declValue = resolveValue(childDecl.value);
-          if (!("type" in declValue.expr)) continue;
-          let uns_container = getType(declValue.expr.type);
-          let uns_res = findSubDecl(uns_container, childName);
-          uns_res.is_private = true;
-          if (uns_res !== null) return uns_res;
-        }
-      }
-    }
-
-    return null;
-  }
-
-  function computeCanonicalModulePaths() {
-    let list = new Array(zigAnalysis.modules.length);
-    // Now we try to find all the modules from root.
-    let rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
-    // Breadth-first to keep the path shortest possible.
-    let stack = [
-      {
-        path: [],
-        mod: rootMod,
-      },
-    ];
-    while (stack.length !== 0) {
-      let item = stack.shift();
-      for (let key in item.mod.table) {
-        let childModIndex = item.mod.table[key];
-        if (list[childModIndex] != null) continue;
-        let childMod = zigAnalysis.modules[childModIndex];
-        if (childMod == null) continue;
-
-        let newPath = item.path.concat([key]);
-        list[childModIndex] = newPath;
-        stack.push({
-          path: newPath,
-          mod: childMod,
-        });
-      }
-    }
-
-    for (let i = 0; i < zigAnalysis.modules.length; i += 1) {
-      const p = zigAnalysis.modules[i];
-      // TODO
-      // declSearchIndex.add(p.name, {moduleId: i});
-    }
-    return list;
-  }
-
-  function computeCanonDeclPaths() {
-    let list = new Array(zigAnalysis.decls.length);
-    canonTypeDecls = new Array(zigAnalysis.types.length);
-
-    for (let modI = 0; modI < zigAnalysis.modules.length; modI += 1) {
-      let mod = zigAnalysis.modules[modI];
-      let modNames = canonModPaths[modI];
-      if (modNames === undefined) continue;
-
-      let stack = [
-        {
-          declNames: [],
-          declIndexes: [],
-          type: getType(mod.main),
-        },
-      ];
-      while (stack.length !== 0) {
-        let item = stack.shift();
-
-        if (isContainerType(item.type)) {
-          let t = item.type;
-
-          let len = t.pubDecls ? t.pubDecls.length : 0;
-          for (let declI = 0; declI < len; declI += 1) {
-            let declIndex = t.pubDecls[declI];
-            if (list[declIndex] != null) continue;
-
-            let decl = getDecl(declIndex);
-
-            if (decl.is_uns) {
-              let unsDeclList = [decl];
-              while (unsDeclList.length != 0) {
-                let unsDecl = unsDeclList.pop();
-                let unsDeclVal = resolveValue(unsDecl.value);
-                if (!("type" in unsDeclVal.expr)) continue;
-                let unsType = getType(unsDeclVal.expr.type);
-                if (!isContainerType(unsType)) continue;
-                let unsPubDeclLen = unsType.pubDecls ? unsType.pubDecls.length : 0;
-                for (let unsDeclI = 0; unsDeclI < unsPubDeclLen; unsDeclI += 1) {
-                  let childDeclIndex = unsType.pubDecls[unsDeclI];
-                  let childDecl = getDecl(childDeclIndex);
-
-                  if (childDecl.is_uns) {
-                    unsDeclList.push(childDecl);
-                  } else {
-                    addDeclToSearchResults(childDecl, childDeclIndex, modNames, item, list, stack);
-                  }
-                }
-              }
-            } else {
-              addDeclToSearchResults(decl, declIndex, modNames, item, list, stack);
-            }
-          }
-        }
-      }
-    }
-    window.cdp = list;
-    return list;
-  }
-
-  function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
-    let {value: declVal, seenDecls} = resolveValue(decl.value, true);
-    let declNames = item.declNames.concat([decl.name]);
-    let declIndexes = item.declIndexes.concat([declIndex]);
-
-    if (list[declIndex] != null) return;
-    list[declIndex] = {
-      modNames: modNames,
-      declNames: declNames,
-      declIndexes: declIndexes,
-    };
-
-    for (let sd of seenDecls) {
-      if (list[sd] != null) continue;
-      list[sd] = {
-        modNames: modNames,
-        declNames: declNames,
-        declIndexes: declIndexes,
-      };
-    }
-
-    // add to search index
-    {
-      declSearchIndex.add(decl.name, { declIndex });
-    }
-
-
-    if ("type" in declVal.expr) {
-      let value = getType(declVal.expr.type);
-      if (declCanRepresentTypeKind(value.kind)) {
-        canonTypeDecls[declVal.type] = declIndex;
-      }
-
-      if (isContainerType(value)) {
-        stack.push({
-          declNames: declNames,
-          declIndexes: declIndexes,
-          type: value,
-        });
-      }
-
-      // Generic function
-      if (typeIsGenericFn(declVal.expr.type)) {
-        let ret = resolveGenericRet(value);
-        if (ret != null && "type" in ret.expr) {
-          let generic_type = getType(ret.expr.type);
-          if (isContainerType(generic_type)) {
-            stack.push({
-              declNames: declNames,
-              declIndexes: declIndexes,
-              type: generic_type,
-            });
-          }
-        }
-      }
-    }
-  }
-
-  function declLinkOrSrcLink(index) {
-    
-    let match = getCanonDeclPath(index);
-    if (match) return navLink(match.modNames, match.declNames);
-
-    // could not find a precomputed decl path
-    const decl = getDecl(index);
-    
-    // try to find a public decl by scanning declRefs and declPaths
-    let value = decl.value;    
-    let i = 0;
-    while (true) {
-      i += 1;
-      if (i >= 10000) {
-        throw "getCanonDeclPath quota exceeded"
-      }
-
-      if ("refPath" in value.expr) {
-        value = { expr: value.expr.refPath[value.expr.refPath.length - 1] };
-        continue;
-      }
-
-      if ("declRef" in value.expr) {
-        let cp = canonDeclPaths[value.expr.declRef];
-        if (cp) return navLink(cp.modNames, cp.declNames);
-        
-        value = getDecl(value.expr.declRef).value;
-        continue;
-      }
-
-      if ("as" in value.expr) {
-        value = {
-          typeRef: zigAnalysis.exprs[value.expr.as.typeRefArg],
-          expr: zigAnalysis.exprs[value.expr.as.exprArg],
-        };
-        continue;
-      }
-
-      // if we got here it means that we failed 
-      // produce a link to source code instead
-      return sourceFileLink(decl);
-
-    }
-    
-  }
-
-  function getCanonDeclPath(index) {
-    if (canonDeclPaths == null) {
-      canonDeclPaths = computeCanonDeclPaths();
-    }
-    
-    return canonDeclPaths[index];
-
-      
-  }
-
-  function getCanonTypeDecl(index) {
-    getCanonDeclPath(0);
-    //let ct = (canonTypeDecls);
-    return canonTypeDecls[index];
-  }
-
-  function escapeHtml(text) {
-    return text.replace(/[&"<>]/g, function(m) {
-      return escapeHtmlReplacements[m];
-    });
-  }
-
-  function shortDesc(docs) {
-    const trimmed_docs = docs.trim();
-    let index = trimmed_docs.indexOf("\n\n");
-    let cut = false;
-
-    if (index < 0 || index > 130) {
-      if (trimmed_docs.length > 130) {
-        index = 130;
-        cut = true;
-      } else {
-        index = trimmed_docs.length;
-      }
-    }
-
-    let slice = trimmed_docs.slice(0, index);
-    if (cut) slice += "...";
-    return slice;
-  }
-
-  function shortDescMarkdown(docs) {
-    return markdown(shortDesc(docs));
-  }
-
-  function parseGuides() {
-    for (let j = 0; j < zigAnalysis.guideSections.length; j += 1) {
-      const section = zigAnalysis.guideSections[j];
-      for (let i = 0; i < section.guides.length; i += 1) {
-        let reader = new commonmark.Parser({ smart: true });
-        const guide = section.guides[i];
-
-        // Find the first text thing to use as a sidebar title
-        guide.title = null;
-        guide.toc = "";
-
-        // Discover Title & TOC for this guide
-        {
-          let reader = new commonmark.Parser({smart: true});
-          let ast = reader.parse(guide.body);        
-          let walker = ast.walker();
-          let heading_idx = 0;
-          let event, node, doc, last, last_ul;
-          while ((event = walker.next())) {
-            node = event.node;
-            if (event.entering) {
-              if (node.type === 'document')  {
-                doc = node;
-                continue;
-              }
-
-              
-              if (node.next) {
-                walker.resumeAt(node.next, true);
-              } else {
-                walker.resumeAt(node, false);
-              }
-              node.unlink();
-              
-              if (node.type === 'heading') {
-                if (node.level == 1) {
-                  if (guide.title == null) {
-                    let doc_node = new commonmark.Node("document", node.sourcepos);
-                    while (node.firstChild) {
-                      doc_node.appendChild(node.firstChild);
-                    }                    
-                    let writer = new commonmark.HtmlRenderer();              
-                    let result = writer.render(doc_node);      
-                    guide.title = result;
-                  }
-                  
-                  continue; // don't index H1
-                }
-
-                // turn heading node into list item & add link node to it
-                {
-                  node._type = "link";
-                  node.destination = NAV_MODES.GUIDES + guide.name + ":" + heading_idx;
-                  heading_idx += 1;
-                  let listItem = new commonmark.Node("item", node.sourcepos);
-                  // TODO: strip links from inside node
-                  listItem.appendChild(node);
-                  listItem.level = node.level;
-                  node = listItem;
-                }
-                
-                if (last_ul) {
-                  // are we inside or outside of it?
-
-                  let target_ul = last_ul;
-                  while(target_ul.level > node.level) {
-                    target_ul = target_ul.parent;
-                  } 
-                  while(target_ul.level < node.level) {
-                    let ul_node = new commonmark.Node("list", node.sourcepos);
-                    ul_node.level = target_ul.level + 1;
-                    ul_node.listType = "bullet";
-                    ul_node.listStart = null;
-                    target_ul.appendChild(ul_node);
-                    target_ul = ul_node;
-                  }
-
-                  target_ul.appendChild(node);
-                  last_ul = target_ul;
-                } else {
-                  let ul_node = new commonmark.Node("list", node.sourcepos);
-                  ul_node.level = 2;
-                  ul_node.listType = "bullet";
-                  ul_node.listStart = null;
-                  doc.prependChild(ul_node);
-            
-                  while (ul_node.level < node.level) {
-                    let current_ul_node = new commonmark.Node("list", node.sourcepos);
-                    current_ul_node.level = ul_node.level + 1;
-                    current_ul_node.listType = "bullet";
-                    current_ul_node.listStart = null;
-                    ul_node.appendChild(current_ul_node);
-                    ul_node = current_ul_node;
-                  }
-
-                  last_ul = ul_node;
-
-                  ul_node.appendChild(node);
-            }
-          }
-        }
-          }        
-          
-          let writer = new commonmark.HtmlRenderer();              
-          let result = writer.render(ast);      
-          guide.toc = result;
-        }
-        
-        // Index this guide
-        {
-          // let walker = guide.ast.walker();
-          // let event, node;
-          // while ((event = walker.next())) {
-          //   node = event.node;
-          //   if (event.entering == true && node.type === 'text') {
-          //       indexTextForGuide(j, i, node);          
-          //   }
-          // }        
-        }
-      }
-    }
-  }
-
-  function indexTextForGuide(section_idx, guide_idx, node) {
-    const terms = node.literal.split(" ");
-    for (let i = 0; i < terms.length; i += 1) {
-      const t = terms[i];
-      if (!guidesSearchIndex[t]) guidesSearchIndex[t] = new Set();
-      node.guide = { section_idx, guide_idx };
-      guidesSearchIndex[t].add(node);
-    }
-  }
-
-
-  function markdown(input, contextType) {
-    const parsed = new commonmark.Parser({ smart: true }).parse(input);
-
-    // Look for decl references in inline code (`ref`)
-    const walker = parsed.walker();
-    let event;
-    while ((event = walker.next())) {
-      const node = event.node;
-      if (node.type === "code") {
-        const declHash = detectDeclPath(node.literal, contextType);
-        if (declHash) {
-          const link = new commonmark.Node("link");
-          link.destination = declHash;
-          node.insertBefore(link);
-          link.appendChild(node);
-        }
-      }
-    }
-
-    return new commonmark.HtmlRenderer({ safe: true }).render(parsed);
-
-  }
-
-
-
-  // function detectDeclPath(text, context) {
-  //   let result = "";
-  //   let separator = ":";
-  //   const components = text.split(".");
-  //   let curDeclOrType = undefined;
-
-  //   let curContext = context;
-  //   let limit = 10000;
-  //   while (curContext) {
-  //     limit -= 1;
-
-  //     if (limit == 0) {
-  //       throw "too many iterations";
-  //     }
-
-  //     curDeclOrType = findSubDecl(curContext, components[0]);
-
-  //     if (!curDeclOrType) {
-  //       if (curContext.parent_container == null) break;
-  //       curContext = getType(curContext.parent_container);
-  //       continue;
-  //     }
-
-  //     if (curContext == context) {
-  //       separator = '.';
-  //       result = location.hash + separator + components[0];
-  //     } else {
-  //       // We had to go up, which means we need a new path!
-  //       const canonPath = getCanonDeclPath(curDeclOrType.find_subdecl_idx);
-  //       if (!canonPath) return;
-
-  //       let lastModName = canonPath.modNames[canonPath.modNames.length - 1];
-  //       let fullPath = lastModName + ":" + canonPath.declNames.join(".");
-
-  //       separator = '.';
-  //       result = "#A;" + fullPath;
-  //     }
-
-  //     break;
-  //   }
-
-  //   if (!curDeclOrType) {
-  //     for (let i = 0; i < zigAnalysis.modules.length; i += 1) {
-  //       const p = zigAnalysis.modules[i];
-  //       if (p.name == components[0]) {
-  //         curDeclOrType = getType(p.main);
-  //         result += "#A;" + components[0];
-  //         break;
-  //       }
-  //     }
-  //   }
-
-  //   if (!curDeclOrType) return null;
-
-  //   for (let i = 1; i < components.length; i += 1) {
-  //     curDeclOrType = findSubDecl(curDeclOrType, components[i]);
-  //     if (!curDeclOrType) return null;
-  //     result += separator + components[i];
-  //     separator = '.';
-  //   }
-
-  //   return result;
-
-  // }
-
-  function activateSelectedResult() {
-    if (domSectSearchResults.classList.contains("hidden")) {
-      return;
-    }
-
-    const searchResults = domListSearchResults.getElementsByTagName("li");
-    let liDom = searchResults[curSearchIndex];
-    if (liDom == null && searchResults.length !== 0) {
-      liDom = searchResults[0];
-    }
-    if (liDom != null) {
-      let aDom = liDom.children[0];
-      location.href = aDom.getAttribute("href");
-      curSearchIndex = -1;
-    }
-    domSearch.blur();
-  }
-
-  // hide the modal if it's visible or return to the previous result page and unfocus the search
-  function onEscape(ev) {
-    if (isModalVisible(domHelpModal)) {
-      hideModal(domHelpModal);
-      ev.preventDefault();
-      ev.stopPropagation();
-    } else if (isModalVisible(domPrefsModal)) {
-      hideModal(domPrefsModal);
-      ev.preventDefault();
-      ev.stopPropagation();
-    } else {
-      domSearch.value = "";
-      domSearch.blur();
-      domSearchPlaceholder.classList.remove("hidden");
-      curSearchIndex = -1;
-      ev.preventDefault();
-      ev.stopPropagation();
-      startSearch();
-    }
-  }
-
-
-  function onSearchKeyDown(ev) {
-    switch (getKeyString(ev)) {
-      case "Enter":
-        // detect if this search changes anything
-        let terms1 = getSearchTerms();
-        startSearch();
-        updateCurNav();
-        let terms2 = getSearchTerms();
-        // we might have to wait for onHashChange to trigger
-        imFeelingLucky = terms1.join(" ") !== terms2.join(" ");
-        if (!imFeelingLucky) activateSelectedResult();
-
-        ev.preventDefault();
-        ev.stopPropagation();
-        return;
-      case "Esc":
-        onEscape(ev);
-        return
-      case "Up":
-        moveSearchCursor(-1);
-        ev.preventDefault();
-        ev.stopPropagation();
-        return;
-      case "Down":
-        // TODO: make the page scroll down if the search cursor is out of the screen
-        moveSearchCursor(1);
-        ev.preventDefault();
-        ev.stopPropagation();
-        return;
-      default:
-        // Search is triggered via an `input` event handler, not on arbitrary `keydown` events.
-        ev.stopPropagation();
-        return;
-    }
-  }
-
-  let domDotsToggleTimeout = null;
-  function onSearchInput(ev) {
-    curSearchIndex = -1;
-  
-    let replaced = domSearch.value.replaceAll(".", " ")
-
-    // Ping red the help text if the user typed a dot.
-    if (replaced != domSearch.value) {
-      domSearchHelpSummary.classList.remove("normal");
-      if (domDotsToggleTimeout != null) {
-        clearTimeout(domDotsToggleTimeout);
-        domDotsToggleTimeout = null;
-      } 
-      domDotsToggleTimeout = setTimeout(function () { 
-        domSearchHelpSummary.classList.add("normal"); 
-      }, 1000);
-    }
-    
-    replaced = replaced.replace(/  +/g, ' ');
-    if (replaced != domSearch.value) {
-      domSearch.value = replaced;
-    } 
-    
-    startAsyncSearch();
-  }
-
-  function moveSearchCursor(dir) {
-    const searchResults = domListSearchResults.getElementsByTagName("li");
-    if (
-      curSearchIndex < 0 ||
-      curSearchIndex >= searchResults.length
-    ) {
-      if (dir > 0) {
-        curSearchIndex = -1 + dir;
-      } else if (dir < 0) {
-        curSearchIndex = searchResults.length + dir;
-      }
-    } else {
-      curSearchIndex += dir;
-    }
-    if (curSearchIndex < 0) {
-      curSearchIndex = 0;
-    }
-    if (curSearchIndex >= searchResults.length) {
-      curSearchIndex = searchResults.length - 1;
-    }
-    renderSearchCursor();
-  }
-
-  function getKeyString(ev) {
-    let name;
-    let ignoreShift = false;
-    switch (ev.which) {
-      case 13:
-        name = "Enter";
-        break;
-      case 27:
-        name = "Esc";
-        break;
-      case 38:
-        name = "Up";
-        break;
-      case 40:
-        name = "Down";
-        break;
-      default:
-        ignoreShift = true;
-        name =
-          ev.key != null
-            ? ev.key
-            : String.fromCharCode(ev.charCode || ev.keyCode);
-    }
-    if (!ignoreShift && ev.shiftKey) name = "Shift+" + name;
-    if (ev.altKey) name = "Alt+" + name;
-    if (ev.ctrlKey) name = "Ctrl+" + name;
-    return name;
-  }
-
-  function onWindowKeyDown(ev) {
-    switch (getKeyString(ev)) {
-      case "Esc":
-        onEscape(ev);
-        break;
-      case "/":
-        if (!getPrefSlashSearch()) break;
-      // fallthrough
-      case "s":
-        if (!isModalVisible(domHelpModal) && !isModalVisible(domPrefsModal)) {
-          if (ev.target == domSearch) break;
-
-          domSearch.focus();
-          domSearch.select();
-          domDocs.scrollTo(0, 0);
-          ev.preventDefault();
-          ev.stopPropagation();
-          startAsyncSearch();
-        }
-        break;
-      case "?":
-        if (!canToggleModal) break;
-
-        if (isModalVisible(domPrefsModal)) {
-          hideModal(domPrefsModal);
-        }
-
-        // toggle the help modal
-        if (isModalVisible(domHelpModal)) {
-          hideModal(domHelpModal);
-        } else {
-          showModal(domHelpModal);
-        }
-        ev.preventDefault();
-        ev.stopPropagation();
-        break;
-      case "p":
-        if (!canToggleModal) break;
-
-        if (isModalVisible(domHelpModal)) {
-          hideModal(domHelpModal);
-        }
-
-        // toggle the preferences modal
-        if (isModalVisible(domPrefsModal)) {
-          hideModal(domPrefsModal);
-        } else {
-          showModal(domPrefsModal);
-        }
-        ev.preventDefault();
-        ev.stopPropagation();
-    }
-  }
-
-  function isModalVisible(modal) {
-    return !modal.classList.contains("hidden");
-  }
-
-  function showModal(modal) {
-    modal.classList.remove("hidden");
-    modal.style.left =
-      window.innerWidth / 2 - modal.clientWidth / 2 + "px";
-    modal.style.top =
-      window.innerHeight / 2 - modal.clientHeight / 2 + "px";
-    const firstInput = modal.querySelector("input");
-    if (firstInput) {
-      firstInput.focus();
-    } else {
-      modal.focus();
-    }
-    domSearch.blur();
-    domBanner.inert = true;
-    domMain.inert = true;
-  }
-
-  function hideModal(modal) {
-    modal.classList.add("hidden");
-    domBanner.inert = false;
-    domMain.inert = false;
-    modal.blur();
-  }
-
-  function clearAsyncSearch() {
-    if (searchTimer != null) {
-      clearTimeout(searchTimer);
-      searchTimer = null;
-    }
-  }
-
-  function startAsyncSearch() {
-    clearAsyncSearch();
-    searchTimer = setTimeout(startSearch, 100);
-  }
-  function startSearch() {
-    clearAsyncSearch();
-    let oldHash = location.hash;
-    let parts = oldHash.split("?");
-    let newPart2 = domSearch.value === "" ? "" : "?" + domSearch.value;
-    location.replace(parts.length === 1 ? oldHash + newPart2 : parts[0] + newPart2);
-  }
-  function getSearchTerms() {
-    let list = curNavSearch.trim().split(/[ \r\n\t]+/);
-    return list;
-  }
-
-  function renderSearchGuides() {
-    const searchTrimmed = false;
-    let ignoreCase = curNavSearch.toLowerCase() === curNavSearch;
-
-    let terms = getSearchTerms();
-    let matchedItems = new Set();
-
-    for (let i = 0; i < terms.length; i += 1) {
-      const nodes = guidesSearchIndex[terms[i]];
-      if (nodes) {
-        for (const n of nodes) {
-          matchedItems.add(n);
-        }
-      }
-    }
-
-
-
-    if (matchedItems.size !== 0) {
-      // Build up the list of search results
-      let matchedItemsHTML = "";
-
-      for (const node of matchedItems) {
-        const text = node.literal;
-        const href = "";
-
-        matchedItemsHTML += "<li><a href=\"" + href + "\">" + text + "</a></li>";
-      }
-
-      // Replace the search results using our newly constructed HTML string
-      domListSearchResults.innerHTML = matchedItemsHTML;
-      if (searchTrimmed) {
-        domSectSearchAllResultsLink.classList.remove("hidden");
-      }
-      renderSearchCursor();
-
-      domSectSearchResults.classList.remove("hidden");
-    } else {
-      domSectSearchNoResults.classList.remove("hidden");
-    }
-  }
-
-  function renderSearchAPI() {
-    domSectSearchResults.prepend(
-      domSearchHelp.parentElement.removeChild(domSearchHelp)
-    );
-    if (canonDeclPaths == null) {
-      canonDeclPaths = computeCanonDeclPaths();
-    }
-    let declSet = new Set();
-    let otherDeclSet = new Set(); // for low quality results
-    let declScores = {};
-
-    let ignoreCase = curNavSearch.toLowerCase() === curNavSearch;
-    let term_list = getSearchTerms();
-    for (let i = 0; i < term_list.length; i += 1) {
-      let term = term_list[i];
-      let result = declSearchIndex.search(term.toLowerCase());
-      if (result == null) {
-        domSectSearchNoResults.prepend(
-          domSearchHelp.parentElement.removeChild(domSearchHelp)
-        );
-        domSectSearchNoResults.classList.remove("hidden");
-        
-        domSectSearchResults.classList.add("hidden");
-        return;
-      }
-
-      let termSet = new Set();
-      let termOtherSet = new Set();
-
-      for (let list of [result.full, result.partial]) {
-        for (let r of list) {
-          const d = r.declIndex;
-          const decl = getDecl(d);
-          const canonPath = getCanonDeclPath(d);
-
-          // collect unconditionally for the first term
-          if (i == 0) {
-            declSet.add(d);
-          } else {
-            // path intersection for subsequent terms
-            let found = false;
-            for (let p of canonPath.declIndexes) {
-              if (declSet.has(p)) {
-                found = true;
-                break;
-              }
-            }
-            if (!found) {
-              otherDeclSet.add(d);
-            } else {
-              termSet.add(d);
-            }
-          }
-
-          if (declScores[d] == undefined) declScores[d] = 0;
-
-          // scores (lower is better)
-          let decl_name = decl.name;
-          if (ignoreCase) decl_name = decl_name.toLowerCase();
-
-          // shallow path are preferable
-          const path_depth = canonPath.declNames.length * 50;
-          // matching the start of a decl name is good
-          const match_from_start = decl_name.startsWith(term) ? -term.length * (2 - ignoreCase) : (decl_name.length - term.length) + 1;
-          // being a perfect match is good
-          const is_full_match = (decl_name === term) ? -decl_name.length * (1 - ignoreCase) : Math.abs(decl_name.length - term.length);
-          // matching the end of a decl name is good
-          const matches_the_end = decl_name.endsWith(term) ? -term.length * (1 - ignoreCase) : (decl_name.length - term.length) + 1;
-          // explicitly penalizing scream case decls
-          const decl_is_scream_case = decl.name.toUpperCase() != decl.name ? 0 : decl.name.length;
-
-          const score = path_depth
-            + match_from_start
-            + is_full_match
-            + matches_the_end
-            + decl_is_scream_case;
-
-          declScores[d] += score;
-        }
-      }
-      if (i != 0) {
-        for (let d of declSet) {
-          if (termSet.has(d)) continue;
-          let found = false;
-          for (let p of getCanonDeclPath(d).declIndexes) {
-            if (termSet.has(p) || otherDeclSet.has(p)) {
-              found = true;
-              break;
-            }
-          }
-          if (found) {
-            declScores[d] = declScores[d] / term_list.length;
-          }
-
-          termOtherSet.add(d);
-        }
-        declSet = termSet;
-        for (let d of termOtherSet) {
-          otherDeclSet.add(d);
-        }
-
-      }
+    function setInputString(s) {
+      const jsArray = text_encoder.encode(s);
+      const len = jsArray.length;
+      const ptr = wasm_exports.set_input_string(len);
+      const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
+      wasmArray.set(jsArray);
     }
-
-    let matchedItems = {
-      high_quality: [],
-      low_quality: [],
-    };
-    for (let idx of declSet) {
-      matchedItems.high_quality.push({ points: declScores[idx], declIndex: idx })
-    }
-    for (let idx of otherDeclSet) {
-      matchedItems.low_quality.push({ points: declScores[idx], declIndex: idx })
-    }
-
-    matchedItems.high_quality.sort(function(a, b) {
-      let cmp = operatorCompare(a.points, b.points);
-      return cmp;
-    });
-    matchedItems.low_quality.sort(function(a, b) {
-      let cmp = operatorCompare(a.points, b.points);
-      return cmp;
-    });
-
-    // Build up the list of search results
-    let matchedItemsHTML = "";
-
-    for (let list of [matchedItems.high_quality, matchedItems.low_quality]) {
-      if (list == matchedItems.low_quality && list.length > 0) {
-        matchedItemsHTML += "<hr class='other-results'>"
-      }
-      for (let result of list) {
-        const points = result.points;
-        const match = result.declIndex;
-
-        let canonPath = getCanonDeclPath(match);
-        if (canonPath == null) continue;
-
-        let lastModName = canonPath.modNames[canonPath.modNames.length - 1];
-        let text = lastModName + "." + canonPath.declNames.join(".");
-
-
-        const href = navLink(canonPath.modNames, canonPath.declNames);
-
-        matchedItemsHTML += "<li><a href=\"" + href + "\">" + text + "</a></li>";
-      }
-    }
-
-    // Replace the search results using our newly constructed HTML string
-    domListSearchResults.innerHTML = matchedItemsHTML;
-    renderSearchCursor();
-
-    domSectSearchResults.classList.remove("hidden");
-  }
-
-  
-
-  function renderSearchCursor() {
-    const searchResults = domListSearchResults.getElementsByTagName("li");
-    for (let i = 0; i < searchResults.length; i += 1) {
-      let liDom = searchResults[i];
-      if (curSearchIndex === i) {
-        liDom.classList.add("selected");
-      } else {
-        liDom.classList.remove("selected");
-      }
-    }
-  }
-
-  function scrollGuidesTop(ev) {
-      document.getElementById("activeGuide").children[0].scrollIntoView({
-        behavior: "smooth",
-      }); 
-      ev.preventDefault();
-      ev.stopPropagation();
-  }
-  document.scrollGuidesTop = scrollGuidesTop;
-
-  function scrollToHeading(id, alreadyThere) {  
-    // Don't scroll if the current location has a scrolling history.
-    if (scrollHistory[location.hash]) return;
-
-    const c = document.getElementById(id);
-    if (c && alreadyThere) {
-      requestAnimationFrame(() => c.scrollIntoView({behavior: "smooth"}));    
-          } else {
-            requestAnimationFrame(() => c.scrollIntoView());    
-          }
-          return;
-        }
-  // function indexNodesToCalls() {
-  //     let map = {};
-  //     for (let i = 0; i < zigAnalysis.calls.length; i += 1) {
-  //         let call = zigAnalysis.calls[i];
-  //         let fn = zigAnalysis.fns[call.fn];
-  //         if (map[fn.src] == null) {
-  //             map[fn.src] = [i];
-  //         } else {
-  //             map[fn.src].push(i);
-  //         }
-  //     }
-  //     return map;
-  // }
-
-  function byNameProperty(a, b) {
-    return operatorCompare(a.name, b.name);
-  }
-
-
-  function getDecl(idx) {
-    const decl = zigAnalysis.decls[idx];
-    return {
-      name: decl[0],
-      kind: decl[1],
-      src: decl[2],
-      value: decl[3],
-      decltest: decl[4],
-      is_uns: decl[5],
-      parent_container: decl[6],
-    };
-  }
-
-  function getAstNode(idx) {
-    const ast = zigAnalysis.astNodes[idx];
-    return {
-      file: ast[0],
-      line: ast[1],
-      col: ast[2],
-      name: ast[3],
-      code: ast[4],
-      docs: ast[5],
-      fields: ast[6],
-      comptime: ast[7],
-    };
-  }
-
-  function getFile(idx) {
-    const file = zigAnalysis.files[idx];
-    return {
-      name: file[0],
-      modIndex: file[1],
-    };
-  }
-
-  function getType(idx) {
-    const ty = zigAnalysis.types[idx];
-    switch (ty[0]) {
-      default:
-        throw "unhandled type kind!";
-      case typeKinds.Unanalyzed:
-        throw "unanalyzed type!";
-      case typeKinds.Type:
-      case typeKinds.Void:
-      case typeKinds.Bool:
-      case typeKinds.NoReturn:
-      case typeKinds.Int:
-      case typeKinds.Float:
-        return { kind: ty[0], name: ty[1] };
-      case typeKinds.Pointer:
-        return {
-          kind: ty[0],
-          size: ty[1],
-          child: ty[2],
-          sentinel: ty[3],
-          align: ty[4],
-          address_space: ty[5],
-          bit_start: ty[6],
-          host_size: ty[7],
-          is_ref: ty[8],
-          is_allowzero: ty[9],
-          is_mutable: ty[10],
-          is_volatile: ty[11],
-          has_sentinel: ty[12],
-          has_align: ty[13],
-          has_addrspace: ty[14],
-          has_bit_range: ty[15],
-        };
-      case typeKinds.Array:
-        return {
-          kind: ty[0],
-          len: ty[1],
-          child: ty[2],
-          sentinel: ty[3],
-        };
-      case typeKinds.Struct:
-        return {
-          kind: ty[0],
-          name: ty[1],
-          src: ty[2],
-          privDecls: ty[3],
-          pubDecls: ty[4],
-          field_types: ty[5],
-          field_defaults: ty[6],
-          backing_int: ty[7],
-          is_tuple: ty[8],
-          line_number: ty[9],
-          parent_container: ty[10],
-          layout: ty[11],
-        };
-      case typeKinds.ComptimeExpr:
-      case typeKinds.ComptimeFloat:
-      case typeKinds.ComptimeInt:
-      case typeKinds.Undefined:
-      case typeKinds.Null:
-        return { kind: ty[0], name: ty[1] };
-      case typeKinds.Optional:
-        return {
-          kind: ty[0],
-          name: ty[1],
-          child: ty[2],
-        };
-      case typeKinds.ErrorUnion:
-        return {
-          kind: ty[0],
-          lhs: ty[1],
-          rhs: ty[2],
-        };
-      case typeKinds.InferredErrorUnion:
-        return {
-          kind: ty[0],
-          payload: ty[1],
-        };
-      case typeKinds.ErrorSet:
-        return {
-          kind: ty[0],
-          name: ty[1],
-          fields: ty[2],
-        };
-      case typeKinds.Enum:
-        return {
-          kind: ty[0],
-          name: ty[1],
-          src: ty[2],
-          privDecls: ty[3],
-          pubDecls: ty[4],
-          tag: ty[5],
-          values: ty[6],
-          nonexhaustive: ty[7],
-          parent_container: ty[8],
-        };
-      case typeKinds.Union:
-        return {
-          kind: ty[0],
-          name: ty[1],
-          src: ty[2],
-          privDecls: ty[3],
-          pubDecls: ty[4],
-          field_types: ty[5],
-          tag: ty[6],
-          auto_tag: ty[7],
-          parent_container: ty[8],
-          layout: ty[9],
-        };
-      case typeKinds.Fn:
-        return {
-          kind: ty[0],
-          name: ty[1],
-          src: ty[2],
-          ret: ty[3],
-          generic_ret: ty[4],
-          params: ty[5],
-          lib_name: ty[6],
-          is_var_args: ty[7],
-          is_inferred_error: ty[8],
-          has_lib_name: ty[9],
-          has_cc: ty[10],
-          cc: ty[11],
-          align: ty[12],
-          has_align: ty[13],
-          is_test: ty[14],
-          is_extern: ty[15],
-        };
-      case typeKinds.Opaque:
-        return {
-          kind: ty[0],
-          name: ty[1],
-          src: ty[2],
-          privDecls: ty[3],
-          pubDecls: ty[4],
-          parent_container: ty[5],
-        };
-      case typeKinds.Frame:
-      case typeKinds.AnyFrame:
-      case typeKinds.Vector:
-      case typeKinds.EnumLiteral:
-        return { kind: ty[0], name: ty[1] };
-    }
-  }
-
-  function getLocalStorage() {
-    if ("localStorage" in window) {
-      try {
-        return window.localStorage;
-      } catch (ignored) {
-        // localStorage may be disabled (SecurityError)
-      }
-    }
-    // If localStorage isn't available, persist preferences only for the current session
-    const sessionPrefs = {};
-    return {
-      getItem(key) {
-        return key in sessionPrefs ? sessionPrefs[key] : null;
-      },
-      setItem(key, value) {
-        sessionPrefs[key] = String(value);
-      },
-    };
-  }
-
-  function loadPrefs() {
-    const storedPrefSlashSearch = prefs.getItem("slashSearch");
-    if (storedPrefSlashSearch === null) {
-      // Slash search defaults to enabled for all browsers except Firefox
-      setPrefSlashSearch(navigator.userAgent.indexOf("Firefox") === -1);
-    } else {
-      setPrefSlashSearch(storedPrefSlashSearch === "true");
-    }
-  }
-
-  function getPrefSlashSearch() {
-    return prefs.getItem("slashSearch") === "true";
-  }
-
-  function setPrefSlashSearch(enabled) {
-    prefs.setItem("slashSearch", String(enabled));
-    domPrefSlashSearch.checked = enabled;
-    const searchKeys = enabled ? "<kbd>/</kbd> or <kbd>s</kbd>" : "<kbd>s</kbd>";
-    domSearchKeys.innerHTML = searchKeys;
-    domSearchPlaceholderText.innerHTML = searchKeys + " to search, <kbd>?</kbd> for more options";
-  }
 })();
 
-function toggleExpand(event) {
-  const parent = event.target.parentElement;
-  parent.toggleAttribute("open");
-
-  if (!parent.open && parent.getBoundingClientRect().top < 0) {
-    parent.parentElement.parentElement.scrollIntoView(true);
-  }
-}
-
-function RadixTree() {
-  this.root = null;
-
-  RadixTree.prototype.search = function(query) {
-    return this.root.search(query);
-
-  }
-
-  RadixTree.prototype.add = function(declName, value) {
-    if (this.root == null) {
-      this.root = new Node(declName.toLowerCase(), null, [value]);
-    } else {
-      this.root.add(declName.toLowerCase(), value);
-    }
-
-    const not_scream_case = declName.toUpperCase() != declName;
-    let found_separator = false;
-    for (let i = 1; i < declName.length; i += 1) {
-      if (declName[i] == '_' || declName[i] == '.') {
-        found_separator = true;
-        continue;
-      }
-
-
-      if (found_separator || (declName[i].toLowerCase() !== declName[i])) {
-        if (declName.length > i + 1
-          && declName[i + 1].toLowerCase() != declName[i + 1]) continue;
-        let suffix = declName.slice(i);
-        this.root.add(suffix.toLowerCase(), value);
-        found_separator = false;
-      }
-    }
-  }
-
-  function Node(labels, next, values) {
-    this.labels = labels;
-    this.next = next;
-    this.values = values;
-  }
-
-  Node.prototype.isCompressed = function() {
-    return !Array.isArray(this.next);
-  }
-
-  Node.prototype.search = function(word) {
-    let full_matches = [];
-    let partial_matches = [];
-    let subtree_root = null;
-
-    let cn = this;
-    char_loop: for (let i = 0; i < word.length;) {
-      if (cn.isCompressed()) {
-        for (let j = 0; j < cn.labels.length; j += 1) {
-          let current_idx = i + j;
-
-          if (current_idx == word.length) {
-            partial_matches = cn.values;
-            subtree_root = cn.next;
-            break char_loop;
-          }
-
-          if (word[current_idx] != cn.labels[j]) return null;
-        }
-
-        // the full label matched
-        let new_idx = i + cn.labels.length;
-        if (new_idx == word.length) {
-          full_matches = cn.values;
-          subtree_root = cn.next;
-          break char_loop;
-        }
-
-
-        i = new_idx;
-        cn = cn.next;
-        continue;
-      } else {
-        for (let j = 0; j < cn.labels.length; j += 1) {
-          if (word[i] == cn.labels[j]) {
-            if (i == word.length - 1) {
-              full_matches = cn.values[j];
-              subtree_root = cn.next[j];
-              break char_loop;
-            }
-
-            let next = cn.next[j];
-            if (next == null) return null;
-            cn = next;
-            i += 1;
-            continue char_loop;
-          }
-        }
-
-        // didn't find a match
-        return null;
-      }
-    }
-
-    // Match was found, let's collect all other 
-    // partial matches from the subtree
-    let stack = [subtree_root];
-    let node;
-    while (node = stack.pop()) {
-      if (node.isCompressed()) {
-        partial_matches = partial_matches.concat(node.values);
-        if (node.next != null) {
-          stack.push(node.next);
-        }
-      } else {
-        for (let v of node.values) {
-          partial_matches = partial_matches.concat(v);
-        }
-
-        for (let n of node.next) {
-          if (n != null) stack.push(n);
-        }
-      }
-    }
-
-    return { full: full_matches, partial: partial_matches };
-  }
-
-  Node.prototype.add = function(word, value) {
-    let cn = this;
-    char_loop: for (let i = 0; i < word.length;) {
-      if (cn.isCompressed()) {
-        for (let j = 0; j < cn.labels.length; j += 1) {
-          let current_idx = i + j;
-
-          if (current_idx == word.length) {
-            if (j < cn.labels.length - 1) {
-              let node = new Node(cn.labels.slice(j), cn.next, cn.values);
-              cn.labels = cn.labels.slice(0, j);
-              cn.next = node;
-              cn.values = [];
-            }
-            cn.values.push(value);
-            return;
-          }
-
-          if (word[current_idx] == cn.labels[j]) continue;
-
-          // if we're here, a mismatch was found
-          if (j != cn.labels.length - 1) {
-            // create a suffix node
-            const label_suffix = cn.labels.slice(j + 1);
-            let node = new Node(label_suffix, cn.next, [...cn.values]);
-            cn.next = node;
-            cn.values = [];
-          }
-
-          // turn current node into a split node
-          let node = null;
-          let word_values = [];
-          if (current_idx == word.length - 1) {
-            // mismatch happened in the last character of word
-            // meaning that the current node should hold its value
-            word_values.push(value);
-          } else {
-            node = new Node(word.slice(current_idx + 1), null, [value]);
-          }
-
-          cn.labels = cn.labels[j] + word[current_idx];
-          cn.next = [cn.next, node];
-          cn.values = [cn.values, word_values];
-
-          if (j != 0) {
-            // current node must be turned into a prefix node
-            let splitNode = new Node(cn.labels, cn.next, cn.values);
-            cn.labels = word.slice(i, current_idx);
-            cn.next = splitNode;
-            cn.values = [];
-          }
-
-          return;
-        }
-        // label matched fully with word, are there any more chars?
-        const new_idx = i + cn.labels.length;
-        if (new_idx == word.length) {
-          cn.values.push(value);
-          return;
-        } else {
-          if (cn.next == null) {
-            let node = new Node(word.slice(new_idx), null, [value]);
-            cn.next = node;
-            return;
-          } else {
-            cn = cn.next;
-            i = new_idx;
-            continue;
-          }
-        }
-      } else { // node is not compressed
-        let letter = word[i];
-        for (let j = 0; j < cn.labels.length; j += 1) {
-          if (letter == cn.labels[j]) {
-            if (i == word.length - 1) {
-              cn.values[j].push(value);
-              return;
-            }
-            if (cn.next[j] == null) {
-              let node = new Node(word.slice(i + 1), null, [value]);
-              cn.next[j] = node;
-              return;
-            } else {
-              cn = cn.next[j];
-              i += 1;
-              continue char_loop;
-            }
-          }
-        }
-
-        // if we're here we didn't find a match
-        cn.labels += letter;
-        if (i == word.length - 1) {
-          cn.next.push(null);
-          cn.values.push([value]);
-        } else {
-          let node = new Node(word.slice(i + 1), null, [value]);
-          cn.next.push(node);
-          cn.values.push([]);
-        }
-        return;
-      }
-    }
-  }
-}
-
-
-function slugify(str) {
-  return str.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
-}
-