lib/zen/src/markdown.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const code = @import("code.zig");
   3 const diagnostic = @import("diagnostic.zig");
   4 const diagram = @import("diagram/root.zig");
   5 const document = @import("document/root.zig");
   6 const footnote = @import("footnote/root.zig");
   7 const html = @import("html.zig");
   8 const markdown_model = @import("markdown");
   9 const math = @import("math/root.zig");
  10 const quiz = @import("quiz/root.zig");
  11 const slides = @import("slides/root.zig");
  12 
  13 const Allocator = std.mem.Allocator;
  14 
  15 pub const Diagnostic = diagnostic.Diagnostic;
  16 
  17 const RenderError = error{
  18     InvalidColor,
  19     InvalidContentsDirective,
  20     InvalidCharacter,
  21     InvalidData,
  22     InvalidField,
  23     InvalidFrame,
  24     InlineDelimiterCapacityExceeded,
  25     InvalidInlineLink,
  26     InvalidJsonl,
  27     InvalidRecord,
  28     InvalidScale,
  29     InvalidSemanticList,
  30     InvalidTransform,
  31     InlineNestingCapacityExceeded,
  32     MissingField,
  33     MissingKind,
  34     UnknownData,
  35     UnknownMark,
  36     UnknownRecordKind,
  37     UnknownTransform,
  38 };
  39 
  40 pub const Error = Allocator.Error || diagram.AsciiRenderError ||
  41     diagram.DocumentError || diagram.ego.svg.FragmentError || document.Error ||
  42     footnote.Error || math.Error || quiz.Error || RenderError;
  43 
  44 const InlineError = math.Error || error{
  45     InlineDelimiterCapacityExceeded,
  46     InvalidInlineLink,
  47     InlineNestingCapacityExceeded,
  48 };
  49 
  50 const CodeDelimiter = struct {
  51     start: usize,
  52     end: usize,
  53     length: usize,
  54     closer_end: ?usize = null,
  55 };
  56 
  57 /// Maximum backtick runs in one rendered inline fragment.
  58 ///
  59 /// Exceeding it returns `InlineDelimiterCapacityExceeded`; split the paragraph
  60 /// or code-bearing label into smaller block-level elements. The delimiter index
  61 /// occupies at most 2.5 MiB on the supported 64-bit target and is reused by
  62 /// nested inline markup.
  63 pub const max_inline_delimiters: usize = 65_536;
  64 /// Maximum body rows in one semantic pipe table admitted by the block parser.
  65 pub const max_table_body_rows: usize = markdown_model.max_table_rows - 1;
  66 const code_delimiter_bytes: usize = 40;
  67 const max_inline_delimiter_bytes: usize = max_inline_delimiters * code_delimiter_bytes;
  68 const delimiter_capacity_reason = std.fmt.comptimePrint(
  69     "inline source exceeds {d} backtick runs",
  70     .{max_inline_delimiters},
  71 );
  72 
  73 const CodeDelimiters = struct {
  74     items: []CodeDelimiter,
  75 
  76     fn deinit(self: *CodeDelimiters, allocator: Allocator) void {
  77         allocator.free(self.items);
  78         self.* = undefined;
  79     }
  80 
  81     fn closingEnd(self: CodeDelimiters, start: usize, range_end: usize) ?usize {
  82         var low: usize = 0;
  83         var high = self.items.len;
  84         while (low < high) {
  85             const middle = low + (high - low) / 2;
  86             if (self.items[middle].start < start) {
  87                 low = middle + 1;
  88             } else {
  89                 high = middle;
  90             }
  91         }
  92         if (low == self.items.len or self.items[low].start != start) return null;
  93         const closer_end = self.items[low].closer_end orelse return null;
  94         return if (closer_end <= range_end) closer_end else null;
  95     }
  96 
  97     fn span(
  98         self: CodeDelimiters,
  99         source: []const u8,
 100         start: usize,
 101         range_end: usize,
 102     ) ?CodeSpan {
 103         const closing_end = self.closingEnd(start, range_end) orelse return null;
 104         const opener_end = backtickRunEnd(source, start);
 105         const delimiter_length = opener_end - start;
 106         return .{
 107             .content = source[opener_end .. closing_end - delimiter_length],
 108             .end = closing_end,
 109         };
 110     }
 111 };
 112 
 113 /// Maximum recursive link/emphasis levels, label brackets, or target parentheses.
 114 ///
 115 /// Exceeding it returns `InlineNestingCapacityExceeded`; split or simplify the
 116 /// nested inline markup.
 117 pub const max_inline_nesting: usize = 32;
 118 const inline_nesting_reason = std.fmt.comptimePrint(
 119     "inline markup nesting exceeds {d} levels",
 120     .{max_inline_nesting},
 121 );
 122 const label_nesting_reason = std.fmt.comptimePrint(
 123     "link label nesting exceeds {d} brackets",
 124     .{max_inline_nesting},
 125 );
 126 const target_nesting_reason = std.fmt.comptimePrint(
 127     "link target nesting exceeds {d} parentheses",
 128     .{max_inline_nesting},
 129 );
 130 
 131 /// Selects ordinary document HTML or slide-deck HTML.
 132 pub const Mode = enum {
 133     /// Renders a continuous Markdown document.
 134     document,
 135     /// Renders explicit slides and optional speaker notes.
 136     slides,
 137 };
 138 
 139 /// Bounds Markdown features and optionally captures a source-local diagnostic.
 140 pub const Options = struct {
 141     /// Chooses document or slide rendering.
 142     mode: Mode = .document,
 143     /// Emits heading IDs when true; IDs are still inspected when false.
 144     heading_anchors: bool = true,
 145     /// Renders `zen-diagram` fences when true and leaves them as code when false.
 146     ascii_diagrams: bool = true,
 147     /// Keeps one exact call-map stylesheet per rendered page when true.
 148     one_call_map_style_per_page: bool = false,
 149     /// Requested ASCII width, clamped to 24...200 columns.
 150     diagram_width: usize = 72,
 151     /// Requested ASCII height, clamped to 8...80 rows.
 152     diagram_height: usize = 22,
 153     /// Controls syntax-highlighted code blocks.
 154     code_blocks: code.Options = .{},
 155     /// Rejects Markdown whose measured heading demand exceeds these limits.
 156     heading_limits: document.Limits = document.default_limits,
 157     /// Rejects Markdown whose measured footnote demand exceeds these limits.
 158     footnote_limits: footnote.Limits = footnote.default_limits,
 159     /// Rejects one quiz fence whose measured demand exceeds these limits.
 160     quiz_limits: quiz.Limits = quiz.default_limits,
 161     /// Controls slide layout when `mode` is `slides`.
 162     slides: slides.Options = .{},
 163     /// Receives source-local repair detail after a supported parse failure.
 164     diagnostic: ?*diagnostic.Diagnostic = null,
 165     /// Receives one record per rendered link and image when set.
 166     link_report: ?*LinkReport = null,
 167 };
 168 
 169 /// Owns the rendered HTML and the storage borrowed by its document view.
 170 /// Release it with `deinit` and the allocator passed to `render`.
 171 pub const Rendered = struct {
 172     html: []u8,
 173     document: document.Document,
 174     document_storage: document.Storage,
 175 
 176     pub fn deinit(self: *Rendered, allocator: Allocator) void {
 177         allocator.free(self.html);
 178         self.document_storage.reset();
 179         self.document_storage.deinit(allocator);
 180         self.* = undefined;
 181     }
 182 };
 183 
 184 /// One link or image the renderer emitted, located in its rendered source.
 185 ///
 186 /// Offsets are valid only under the flag that admits them: `start` and `end`
 187 /// under `located`, the target pair under `located` and a locatable target.
 188 pub const RenderedLink = struct {
 189     start: u32 = 0,
 190     end: u32 = 0,
 191     target_start: u32 = 0,
 192     target_end: u32 = 0,
 193     located: bool = false,
 194     contiguous: bool = false,
 195     image: bool = false,
 196 };
 197 
 198 /// Caller-owned, fixed-capacity sink for rendered link records.
 199 ///
 200 /// Records arrive in emission order, which is not document order: an
 201 /// enclosing link is recorded before the label it contains.
 202 pub const LinkReport = struct {
 203     storage: []RenderedLink,
 204     count: usize = 0,
 205     overflowed: bool = false,
 206 
 207     /// Drops every record and clears the overflow flag.
 208     pub fn reset(self: *LinkReport) void {
 209         self.count = 0;
 210         self.overflowed = false;
 211     }
 212 
 213     /// Returns the records, borrowing `storage`.
 214     pub fn links(self: *const LinkReport) []const RenderedLink {
 215         return self.storage[0..self.count];
 216     }
 217 
 218     fn record(self: *LinkReport, link: RenderedLink) void {
 219         if (self.count == self.storage.len) {
 220             self.overflowed = true;
 221             return;
 222         }
 223         self.storage[self.count] = link;
 224         self.count += 1;
 225     }
 226 };
 227 
 228 const Fence = struct {
 229     marker: u8,
 230     count: usize,
 231     info: []const u8,
 232     source_offset: usize = 0,
 233 };
 234 
 235 const EgoStylesheet = struct {
 236     output_start: usize,
 237     length: usize,
 238 };
 239 
 240 const QuizOwner = struct {
 241     storage: ?quiz.Storage = null,
 242 
 243     fn deinit(self: *QuizOwner, allocator: Allocator) void {
 244         if (self.storage) |*storage| storage.deinit(allocator);
 245         self.* = .{};
 246     }
 247 
 248     fn parse(
 249         self: *QuizOwner,
 250         allocator: Allocator,
 251         source: []const u8,
 252         limits: quiz.Limits,
 253     ) Error!quiz.Quiz {
 254         if (self.storage == null) {
 255             var storage = try quiz.Storage.init(allocator, limits);
 256             storage.activate();
 257             self.storage = storage;
 258         }
 259         const storage = if (self.storage) |*active| active else unreachable;
 260         std.debug.assert(std.meta.eql(storage.capacity.limits, limits));
 261         return quiz.parse(storage, source);
 262     }
 263 
 264     fn reset(self: *QuizOwner) void {
 265         const storage = if (self.storage) |*active| active else unreachable;
 266         storage.reset();
 267     }
 268 };
 269 
 270 const ListKind = enum {
 271     unordered,
 272     ordered,
 273 };
 274 
 275 const SemanticList = enum {
 276     family_index,
 277     module_index,
 278 };
 279 
 280 /// Computes exact heading, text, and ID demand without allocating.
 281 pub fn plan(source: []const u8) document.Error!document.Plan {
 282     return planDiagnostic(source, null);
 283 }
 284 
 285 fn planDiagnostic(
 286     source: []const u8,
 287     diag: ?*diagnostic.Diagnostic,
 288 ) document.Error!document.Plan {
 289     try validateHeadingAnchors(source, diag);
 290     var result = document.Plan.init(source.len);
 291     var iterator = HeadingIterator.init(source, diag);
 292     while (try iterator.next()) |heading| {
 293         const id = try resolveHeadingId(source, heading);
 294         try result.includeHeading(heading.text, try id.length());
 295     }
 296     return result;
 297 }
 298 
 299 /// Fills active caller-owned storage; the returned document borrows it until reset.
 300 pub fn inspect(storage: *document.Storage, source: []const u8) document.Error!document.Document {
 301     return inspectPlanned(storage, source, try plan(source));
 302 }
 303 
 304 fn inspectPlanned(
 305     storage: *document.Storage,
 306     source: []const u8,
 307     heading_plan: document.Plan,
 308 ) document.Error!document.Document {
 309     const regions = try storage.acquire(heading_plan);
 310     var heading_index: usize = 0;
 311     var text_index: usize = 0;
 312     var id_index: usize = 0;
 313     var iterator = HeadingIterator.init(source, null);
 314     while (try iterator.next()) |heading| {
 315         const text = regions.text[text_index..][0..heading.text.len];
 316         @memcpy(text, heading.text);
 317         text_index += text.len;
 318         const resolved = try resolveHeadingId(source, heading);
 319         const id_length = try resolved.length();
 320         const id = resolved.write(regions.ids[id_index..][0..id_length]);
 321         id_index += id.len;
 322         regions.headings[heading_index] = .{
 323             .level = heading.level,
 324             .text = text,
 325             .id = id,
 326             .source_offset = heading.source_offset,
 327         };
 328         heading_index += 1;
 329     }
 330     std.debug.assert(heading_index == regions.plan.headings);
 331     std.debug.assert(text_index == regions.plan.text_bytes);
 332     std.debug.assert(id_index == regions.plan.id_bytes);
 333     return .{ .headings = regions.headings };
 334 }
 335 
 336 /// Renders Markdown and returns an owner that must be deinitialized with the same allocator.
 337 ///
 338 /// A heading may end in `{#exact.id}`. Explicit IDs are exact, case-sensitive,
 339 /// page-wide unique, and omitted from the visible heading and contents label.
 340 pub fn render(allocator: Allocator, source: []const u8, options: Options) Error!Rendered {
 341     const heading_plan = try planDiagnostic(source, options.diagnostic);
 342     try heading_plan.require(options.heading_limits);
 343     var document_storage = try document.Storage.init(allocator, heading_plan.exactLimits());
 344     errdefer document_storage.deinit(allocator);
 345     document_storage.activate();
 346     const doc = try inspectPlanned(&document_storage, source, heading_plan);
 347     errdefer document_storage.reset();
 348     const footnote_plan = try footnote.Plan.inspect(source, options.footnote_limits);
 349     var footnote_storage = try footnote.Storage.init(allocator, footnote_plan.exactLimits());
 350     defer footnote_storage.deinit(allocator);
 351     footnote_storage.activate();
 352     var footnotes = try footnote.parse(&footnote_storage, source);
 353     defer footnote_storage.reset();
 354     var out: std.ArrayList(u8) = .empty;
 355     errdefer out.deinit(allocator);
 356     const track_origins = options.link_report != null;
 357     var paragraph: InlineBuffer = .{ .track = track_origins };
 358     defer paragraph.deinit(allocator);
 359     var list_item: InlineBuffer = .{ .track = track_origins };
 360     defer list_item.deinit(allocator);
 361     var blockquote: InlineBuffer = .{ .track = track_origins };
 362     defer blockquote.deinit(allocator);
 363     var code_block: std.ArrayList(u8) = .empty;
 364     defer code_block.deinit(allocator);
 365     var math_block: std.ArrayList(u8) = .empty;
 366     defer math_block.deinit(allocator);
 367     var quiz_owner: QuizOwner = .{};
 368     defer quiz_owner.deinit(allocator);
 369     var table_storage: markdown_model.TableStorage = .{};
 370     var math_open = false;
 371     var cursor: usize = 0;
 372     var code_fence: ?Fence = null;
 373     var list_kind: ?ListKind = null;
 374     var semantic_list: ?SemanticList = null;
 375     var blockquote_open = false;
 376     var footnote_definition = false;
 377     var rendered_heading_index: usize = 0;
 378     var ego_stylesheet: ?EgoStylesheet = null;
 379     var deck = slides.Deck{ .options = options.slides };
 380     if (options.mode == .slides) try deck.begin(&out, allocator);
 381 
 382     while (true) {
 383         const line_start = cursor;
 384         const raw_line = nextLine(source, &cursor) orelse break;
 385         const line = std.mem.trim(u8, raw_line, "\r");
 386 
 387         if (code_fence) |fence| {
 388             if (closingFence(line, fence)) {
 389                 try flushFence(
 390                     allocator,
 391                     &out,
 392                     fence,
 393                     code_block.items,
 394                     options,
 395                     doc,
 396                     &quiz_owner,
 397                     &ego_stylesheet,
 398                 );
 399                 code_block.clearRetainingCapacity();
 400                 code_fence = null;
 401             } else {
 402                 try code_block.appendSlice(allocator, line);
 403                 try code_block.append(allocator, '\n');
 404             }
 405             continue;
 406         }
 407 
 408         if (math_open) {
 409             const inner = std.mem.trim(u8, line, " \t");
 410             if (std.mem.eql(u8, inner, "$$")) {
 411                 try flushDisplayMath(allocator, &out, &math_block, options.diagnostic);
 412                 math_open = false;
 413             } else if (std.mem.endsWith(u8, inner, "$$")) {
 414                 if (math_block.items.len != 0) try math_block.append(allocator, ' ');
 415                 try math_block.appendSlice(allocator, std.mem.trimEnd(u8, inner[0 .. inner.len - 2], " \t"));
 416                 try flushDisplayMath(allocator, &out, &math_block, options.diagnostic);
 417                 math_open = false;
 418             } else if (inner.len != 0) {
 419                 if (math_block.items.len != 0) try math_block.append(allocator, ' ');
 420                 try math_block.appendSlice(allocator, inner);
 421             }
 422             continue;
 423         }
 424 
 425         if (parseFence(line)) |fence| {
 426             if (semantic_list != null) return error.InvalidSemanticList;
 427             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 428             try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 429             try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options);
 430             code_block.clearRetainingCapacity();
 431             var opened = fence;
 432             opened.source_offset = line_start;
 433             code_fence = opened;
 434             footnote_definition = false;
 435             continue;
 436         }
 437 
 438         const trimmed = std.mem.trim(u8, line, " \t");
 439         if (parseSemanticList(trimmed)) |role| {
 440             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 441             try closeList(
 442                 allocator,
 443                 &out,
 444                 &list_kind,
 445                 &list_item,
 446                 &footnotes,
 447                 options,
 448             );
 449             try closeBlockquote(
 450                 allocator,
 451                 &out,
 452                 &blockquote_open,
 453                 &blockquote,
 454                 &footnotes,
 455                 options,
 456             );
 457             if (semantic_list != null) return error.InvalidSemanticList;
 458             semantic_list = role;
 459             continue;
 460         }
 461         if (std.mem.startsWith(u8, trimmed, "$$")) {
 462             if (semantic_list != null) return error.InvalidSemanticList;
 463             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 464             try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 465             try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options);
 466             footnote_definition = false;
 467             const rest = std.mem.trim(u8, trimmed[2..], " \t");
 468             if (rest.len >= 2 and std.mem.endsWith(u8, rest, "$$")) {
 469                 try math_block.appendSlice(allocator, std.mem.trimEnd(u8, rest[0 .. rest.len - 2], " \t"));
 470                 try flushDisplayMath(allocator, &out, &math_block, options.diagnostic);
 471             } else {
 472                 math_open = true;
 473                 try math_block.appendSlice(allocator, rest);
 474             }
 475             continue;
 476         }
 477         if (trimmed.len == 0) {
 478             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 479             try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 480             try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options);
 481             footnote_definition = false;
 482             continue;
 483         }
 484 
 485         const parsed_list_item = parseListItem(line);
 486         if (semantic_list != null and parsed_list_item == null) {
 487             return error.InvalidSemanticList;
 488         }
 489 
 490         if (footnote.definition(line) != null) {
 491             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 492             try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 493             try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options);
 494             footnote_definition = true;
 495             continue;
 496         }
 497         if (footnote_definition and footnote.continuation(line) != null) continue;
 498         footnote_definition = false;
 499 
 500         const table_source = source[line_start..];
 501         switch (markdown_model.parseTable(&table_storage, table_source)) {
 502             .table => |table| {
 503                 try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 504                 try closeList(
 505                     allocator,
 506                     &out,
 507                     &list_kind,
 508                     &list_item,
 509                     &footnotes,
 510                     options,
 511                 );
 512                 try closeBlockquote(
 513                     allocator,
 514                     &out,
 515                     &blockquote_open,
 516                     &blockquote,
 517                     &footnotes,
 518                     options,
 519                 );
 520                 try appendTable(
 521                     allocator,
 522                     &out,
 523                     source,
 524                     table_source,
 525                     table,
 526                     &footnotes,
 527                     options,
 528                 );
 529                 cursor = line_start + table.source.end;
 530                 continue;
 531             },
 532             .paragraph, .rejected => {},
 533         }
 534 
 535         if (documentationArtifactBlock(trimmed)) {
 536             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 537             try closeList(
 538                 allocator,
 539                 &out,
 540                 &list_kind,
 541                 &list_item,
 542                 &footnotes,
 543                 options,
 544             );
 545             try closeBlockquote(
 546                 allocator,
 547                 &out,
 548                 &blockquote_open,
 549                 &blockquote,
 550                 &footnotes,
 551                 options,
 552             );
 553             try appendInline(
 554                 &out,
 555                 allocator,
 556                 trimmed,
 557                 &footnotes,
 558                 options.diagnostic,
 559                 originWithin(source, trimmed),
 560                 options.link_report,
 561             );
 562             try out.append(allocator, '\n');
 563             continue;
 564         }
 565 
 566         if (parseBlockquote(line)) |quote| {
 567             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 568             try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 569             if (!blockquote_open) blockquote_open = true;
 570             try blockquote.appendLine(allocator, source, quote);
 571             continue;
 572         }
 573         try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options);
 574 
 575         if (parsed_list_item) |item| {
 576             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 577             if (list_kind == null or list_kind.? != item.kind) {
 578                 try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 579                 try openList(allocator, &out, item.kind, semantic_list);
 580                 semantic_list = null;
 581                 list_kind = item.kind;
 582             }
 583             try flushListItem(allocator, &out, &list_item, &footnotes, options);
 584             try list_item.appendLine(allocator, source, item.text);
 585             continue;
 586         }
 587         if (list_kind != null) {
 588             if (listContinuation(line)) |continuation| {
 589                 try list_item.appendLine(allocator, source, continuation);
 590                 continue;
 591             }
 592         }
 593         try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 594 
 595         if (try parseHeading(line, line_start, options.diagnostic)) |heading| {
 596             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 597             std.debug.assert(rendered_heading_index < doc.headings.len);
 598             const inspected = doc.headings[rendered_heading_index];
 599             std.debug.assert(inspected.source_offset == line_start);
 600             std.debug.assert(std.mem.eql(u8, inspected.text, heading.text));
 601             rendered_heading_index += 1;
 602             try out.appendSlice(allocator, "<h");
 603             try appendDecimal(&out, allocator, heading.level);
 604             if (options.heading_anchors) {
 605                 try out.appendSlice(allocator, " id=\"");
 606                 try html.appendAttributeEscaped(&out, allocator, inspected.id);
 607                 try out.append(allocator, '"');
 608             }
 609             try out.append(allocator, '>');
 610             try appendInline(
 611                 &out,
 612                 allocator,
 613                 heading.text,
 614                 &footnotes,
 615                 options.diagnostic,
 616                 originWithin(source, heading.text),
 617                 options.link_report,
 618             );
 619             try out.appendSlice(allocator, "</h");
 620             try appendDecimal(&out, allocator, heading.level);
 621             try out.appendSlice(allocator, ">\n");
 622             continue;
 623         }
 624 
 625         if (isThematicBreak(trimmed)) {
 626             try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 627             try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 628             try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options);
 629             if (options.mode == .slides) {
 630                 try deck.split(&out, allocator);
 631                 continue;
 632             }
 633             try out.appendSlice(allocator, "<hr>\n");
 634             continue;
 635         }
 636 
 637         try paragraph.appendLine(allocator, source, trimmed);
 638     }
 639 
 640     if (math_open) {
 641         if (options.diagnostic) |d| d.setEquation(math_block.items, .{
 642             .reason = "missing closing '$$'",
 643             .offset = math_block.items.len,
 644         });
 645         return error.InvalidEquation;
 646     }
 647     if (code_fence) |fence| try flushFence(
 648         allocator,
 649         &out,
 650         fence,
 651         code_block.items,
 652         options,
 653         doc,
 654         &quiz_owner,
 655         &ego_stylesheet,
 656     );
 657     if (semantic_list != null) return error.InvalidSemanticList;
 658     try flushParagraph(allocator, &out, &paragraph, &footnotes, options);
 659     try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options);
 660     try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options);
 661     try appendFootnotes(allocator, &out, source, &footnotes, options);
 662     std.debug.assert(rendered_heading_index == doc.headings.len);
 663     if (options.mode == .slides) try deck.finish(&out, allocator);
 664 
 665     return .{
 666         .html = try out.toOwnedSlice(allocator),
 667         .document = doc,
 668         .document_storage = document_storage,
 669     };
 670 }
 671 
 672 fn flushFence(
 673     allocator: Allocator,
 674     out: *std.ArrayList(u8),
 675     fence: Fence,
 676     source: []const u8,
 677     options: Options,
 678     doc: document.Document,
 679     quiz_owner: *QuizOwner,
 680     ego_stylesheet: *?EgoStylesheet,
 681 ) Error!void {
 682     if (options.mode == .slides and notesFence(fence.info)) {
 683         try appendSpeakerNotes(allocator, out, source, options);
 684         return;
 685     }
 686 
 687     if (contentsFence(fence.info)) {
 688         try appendContents(allocator, out, doc, fence, source, options.diagnostic);
 689         return;
 690     }
 691 
 692     if (quizFence(fence.info)) {
 693         try appendQuiz(
 694             allocator,
 695             out,
 696             source,
 697             options.diagnostic,
 698             quiz_owner,
 699             options.quiz_limits,
 700         );
 701         return;
 702     }
 703 
 704     if (egoFence(fence.info)) {
 705         try diagram.ego.svg.validateDocumentFragment(source);
 706         try appendEgoFragment(
 707             allocator,
 708             out,
 709             source,
 710             options.one_call_map_style_per_page,
 711             ego_stylesheet,
 712         );
 713         return;
 714     }
 715 
 716     if (options.ascii_diagrams and diagramFence(fence.info)) {
 717         var rendered = try diagram.renderAscii(allocator, source, .{
 718             .width = options.diagram_width,
 719             .height = options.diagram_height,
 720         });
 721         defer rendered.deinit(allocator);
 722         try out.appendSlice(
 723             allocator,
 724             "<pre class=\"zen-diagram zen-role-wide\"><code>",
 725         );
 726         try html.appendEscaped(out, allocator, rendered.output);
 727         try out.appendSlice(allocator, "</code></pre>\n");
 728         return;
 729     }
 730 
 731     try code.render(allocator, out, fence.info, source, options.code_blocks);
 732 }
 733 
 734 fn appendEgoFragment(
 735     allocator: Allocator,
 736     out: *std.ArrayList(u8),
 737     source: []const u8,
 738     one_stylesheet_per_page: bool,
 739     stylesheet: *?EgoStylesheet,
 740 ) Allocator.Error!void {
 741     const fragment = std.mem.trim(u8, source, " \t\r\n");
 742     if (!one_stylesheet_per_page) {
 743         try out.appendSlice(allocator, fragment);
 744         try out.append(allocator, '\n');
 745         return;
 746     }
 747     const range = egoStylesheetRange(fragment) orelse {
 748         try out.appendSlice(allocator, fragment);
 749         try out.append(allocator, '\n');
 750         return;
 751     };
 752     const candidate = fragment[range.start..range.end];
 753     if (stylesheet.*) |first| {
 754         std.debug.assert(first.output_start <= out.items.len);
 755         std.debug.assert(first.length <= out.items.len - first.output_start);
 756         const retained = out.items[first.output_start..][0..first.length];
 757         if (std.mem.eql(u8, retained, candidate)) {
 758             try out.appendSlice(allocator, fragment[0..range.start]);
 759             try out.appendSlice(allocator, fragment[range.end..]);
 760             try out.append(allocator, '\n');
 761             return;
 762         }
 763     } else {
 764         stylesheet.* = .{
 765             .output_start = out.items.len + range.start,
 766             .length = candidate.len,
 767         };
 768     }
 769     try out.appendSlice(allocator, fragment);
 770     try out.append(allocator, '\n');
 771 }
 772 
 773 const EgoStylesheetRange = struct {
 774     start: usize,
 775     end: usize,
 776 };
 777 
 778 fn egoStylesheetRange(fragment: []const u8) ?EgoStylesheetRange {
 779     const open = "<style>";
 780     const close = "</style>";
 781     const start = std.mem.indexOf(u8, fragment, open) orelse return null;
 782     const close_start = std.mem.indexOfPos(u8, fragment, start + open.len, close) orelse
 783         return null;
 784     return .{ .start = start, .end = close_start + close.len };
 785 }
 786 
 787 fn appendSpeakerNotes(
 788     allocator: Allocator,
 789     out: *std.ArrayList(u8),
 790     source: []const u8,
 791     options: Options,
 792 ) Error!void {
 793     var note_options = options;
 794     note_options.mode = .document;
 795     note_options.slides.runtime = false;
 796     var rendered = try render(allocator, source, note_options);
 797     defer rendered.deinit(allocator);
 798     if (std.mem.trim(u8, rendered.html, " \t\r\n").len == 0) return;
 799     try out.appendSlice(allocator, "<aside class=\"zen-speaker-notes\" data-zen-speaker-notes hidden>\n");
 800     try out.appendSlice(allocator, rendered.html);
 801     try out.appendSlice(allocator, "</aside>\n");
 802 }
 803 
 804 const ContentsScope = enum {
 805     document,
 806     following,
 807 };
 808 
 809 const ContentsOptions = struct {
 810     scope: ContentsScope = .following,
 811     min_level: u8 = 1,
 812     max_level: u8 = 6,
 813     title: []const u8 = "Contents:",
 814 };
 815 
 816 fn contentsFence(info: []const u8) bool {
 817     const language = firstFenceWord(info);
 818     return std.mem.eql(u8, language, "zen-toc") or
 819         std.mem.eql(u8, language, "zen-contents") or
 820         std.mem.eql(u8, language, "toc");
 821 }
 822 
 823 fn egoFence(info: []const u8) bool {
 824     return std.mem.eql(u8, firstFenceWord(info), "zen-ego");
 825 }
 826 
 827 fn appendContents(
 828     allocator: Allocator,
 829     out: *std.ArrayList(u8),
 830     doc: document.Document,
 831     fence: Fence,
 832     source: []const u8,
 833     diag: ?*diagnostic.Diagnostic,
 834 ) !void {
 835     const options = try parseContentsOptions(source);
 836     var count: usize = 0;
 837     for (doc.headings) |heading| {
 838         if (!contentsIncludes(options, fence, heading)) continue;
 839         count += 1;
 840     }
 841     if (count == 0) return;
 842 
 843     try out.appendSlice(
 844         allocator,
 845         "<nav class=\"zen-contents zen-role-wide\" aria-label=\"Contents\">\n<p>",
 846     );
 847     try html.appendEscaped(out, allocator, options.title);
 848     try out.appendSlice(allocator, "</p>\n<ul>\n");
 849     for (doc.headings) |heading| {
 850         if (!contentsIncludes(options, fence, heading)) continue;
 851         try appendContentsItem(allocator, out, heading, options, diag);
 852         try out.appendSlice(allocator, "</li>\n");
 853     }
 854     try out.appendSlice(allocator, "</ul>\n</nav>\n");
 855 }
 856 
 857 fn appendContentsItem(
 858     allocator: Allocator,
 859     out: *std.ArrayList(u8),
 860     heading: document.Heading,
 861     options: ContentsOptions,
 862     diag: ?*diagnostic.Diagnostic,
 863 ) !void {
 864     try out.appendSlice(allocator, "<li class=\"zen-contents-level-");
 865     try appendDecimal(out, allocator, heading.level);
 866     if (heading.level > options.min_level) {
 867         try out.appendSlice(allocator, "\" style=\"margin-left:calc(");
 868         try appendDecimal(out, allocator, heading.level - options.min_level);
 869         try out.appendSlice(allocator, " * 1.5rem)\"><a href=\"#");
 870     } else {
 871         try out.appendSlice(allocator, "\"><a href=\"#");
 872     }
 873     try html.appendAttributeEscaped(out, allocator, heading.id);
 874     try out.appendSlice(allocator, "\">");
 875     try appendInline(out, allocator, heading.text, null, diag, .untracked, null);
 876     try out.appendSlice(allocator, "</a>");
 877 }
 878 
 879 fn contentsIncludes(options: ContentsOptions, fence: Fence, heading: document.Heading) bool {
 880     if (heading.level < options.min_level or heading.level > options.max_level) return false;
 881     return switch (options.scope) {
 882         .document => true,
 883         .following => heading.source_offset > fence.source_offset,
 884     };
 885 }
 886 
 887 fn parseContentsOptions(source: []const u8) !ContentsOptions {
 888     var options: ContentsOptions = .{};
 889     var cursor: usize = 0;
 890     while (nextLine(source, &cursor)) |raw_line| {
 891         const line = std.mem.trim(u8, std.mem.trim(u8, raw_line, "\r"), " \t");
 892         if (line.len == 0) continue;
 893         const split = std.mem.indexOfScalar(u8, line, ':') orelse return error.InvalidContentsDirective;
 894         const key = std.mem.trim(u8, line[0..split], " \t");
 895         const value = std.mem.trim(u8, line[split + 1 ..], " \t");
 896         if (std.mem.eql(u8, key, "scope")) {
 897             options.scope = if (std.mem.eql(u8, value, "document") or std.mem.eql(u8, value, "page"))
 898                 .document
 899             else if (std.mem.eql(u8, value, "following"))
 900                 .following
 901             else
 902                 return error.InvalidContentsDirective;
 903         } else if (std.mem.eql(u8, key, "min-level")) {
 904             options.min_level = try parseHeadingLevel(value);
 905         } else if (std.mem.eql(u8, key, "max-level")) {
 906             options.max_level = try parseHeadingLevel(value);
 907         } else if (std.mem.eql(u8, key, "title")) {
 908             options.title = value;
 909         } else {
 910             return error.InvalidContentsDirective;
 911         }
 912     }
 913     if (options.min_level > options.max_level) return error.InvalidContentsDirective;
 914     return options;
 915 }
 916 
 917 fn parseHeadingLevel(value: []const u8) !u8 {
 918     const level = std.fmt.parseUnsigned(u8, value, 10) catch return error.InvalidContentsDirective;
 919     if (level < 1 or level > 6) return error.InvalidContentsDirective;
 920     return level;
 921 }
 922 
 923 fn diagramFence(info: []const u8) bool {
 924     const language = firstFenceWord(info);
 925     return std.mem.eql(u8, language, "diagram") or
 926         std.mem.eql(u8, language, "zen-diagram") or
 927         std.mem.eql(u8, language, "zen-ascii");
 928 }
 929 
 930 fn quizFence(info: []const u8) bool {
 931     const language = firstFenceWord(info);
 932     return std.mem.eql(u8, language, "zen-quiz") or
 933         std.mem.eql(u8, language, "quiz");
 934 }
 935 
 936 fn appendQuiz(
 937     allocator: Allocator,
 938     out: *std.ArrayList(u8),
 939     source: []const u8,
 940     diag: ?*diagnostic.Diagnostic,
 941     owner: *QuizOwner,
 942     limits: quiz.Limits,
 943 ) Error!void {
 944     const parsed = try owner.parse(allocator, source, limits);
 945     defer owner.reset();
 946 
 947     try out.appendSlice(allocator, "<section class=\"zen-quiz\" aria-label=\"Quiz\">\n<ol class=\"zen-quiz-questions\">\n");
 948     for (parsed.questions) |question| {
 949         try out.appendSlice(allocator, "<li class=\"zen-quiz-question\">\n<p class=\"zen-quiz-prompt\">");
 950         try appendInline(out, allocator, question.prompt, null, diag, .untracked, null);
 951         try out.appendSlice(allocator, "</p>\n");
 952         if (question.options.len != 0) {
 953             try out.appendSlice(allocator, "<ul class=\"zen-quiz-options\">\n");
 954             for (question.options) |option| {
 955                 try out.appendSlice(allocator, "<li>");
 956                 try appendInline(out, allocator, option.text, null, diag, .untracked, null);
 957                 try out.appendSlice(allocator, "</li>\n");
 958             }
 959             try out.appendSlice(allocator, "</ul>\n");
 960         }
 961         try out.appendSlice(allocator, "<details class=\"zen-quiz-answer\">\n<summary>Reveal answer</summary>\n");
 962         for (question.options) |option| {
 963             if (!option.correct) continue;
 964             try out.appendSlice(allocator, "<p class=\"zen-quiz-correct\"><strong>");
 965             try appendInline(out, allocator, option.text, null, diag, .untracked, null);
 966             try out.appendSlice(allocator, "</strong></p>\n");
 967         }
 968         if (question.explanation.len != 0) {
 969             try out.appendSlice(allocator, "<p class=\"zen-quiz-explanation\">");
 970             try appendInline(out, allocator, question.explanation, null, diag, .untracked, null);
 971             try out.appendSlice(allocator, "</p>\n");
 972         }
 973         try out.appendSlice(allocator, "</details>\n</li>\n");
 974     }
 975     try out.appendSlice(allocator, "</ol>\n</section>\n");
 976 }
 977 
 978 fn notesFence(info: []const u8) bool {
 979     const language = firstFenceWord(info);
 980     return std.mem.eql(u8, language, "notes") or
 981         std.mem.eql(u8, language, "speaker-notes") or
 982         std.mem.eql(u8, language, "zen-notes");
 983 }
 984 
 985 fn firstFenceWord(info: []const u8) []const u8 {
 986     const trimmed = std.mem.trim(u8, info, " \t");
 987     var end: usize = 0;
 988     while (end < trimmed.len and trimmed[end] != ' ' and trimmed[end] != '\t') : (end += 1) {}
 989     return trimmed[0..end];
 990 }
 991 
 992 const MathMode = enum { inline_math, display };
 993 
 994 fn renderMath(
 995     out: *std.ArrayList(u8),
 996     allocator: Allocator,
 997     source: []const u8,
 998     mode: MathMode,
 999     diag: ?*diagnostic.Diagnostic,
1000 ) math.Error!void {
1001     var detail: math.Diagnostic = .{};
1002     (switch (mode) {
1003         .inline_math => math.renderInline(out, allocator, source, &detail),
1004         .display => math.renderDisplay(out, allocator, source, &detail),
1005     }) catch |err| {
1006         switch (err) {
1007             error.InvalidEquation => if (diag) |d| d.setEquation(source, detail),
1008             error.OutOfMemory => {},
1009         }
1010         return err;
1011     };
1012 }
1013 
1014 fn flushDisplayMath(
1015     allocator: Allocator,
1016     out: *std.ArrayList(u8),
1017     block: *std.ArrayList(u8),
1018     diag: ?*diagnostic.Diagnostic,
1019 ) math.Error!void {
1020     try renderMath(out, allocator, block.items, .display, diag);
1021     try out.append(allocator, '\n');
1022     block.clearRetainingCapacity();
1023 }
1024 
1025 fn flushParagraph(
1026     allocator: Allocator,
1027     out: *std.ArrayList(u8),
1028     paragraph: *InlineBuffer,
1029     footnotes: *footnote.Set,
1030     options: Options,
1031 ) InlineError!void {
1032     if (paragraph.items().len == 0) return;
1033     const container = try paragraphContainer(
1034         allocator,
1035         paragraph.items(),
1036         options.mode,
1037         options.diagnostic,
1038     );
1039     if (container) |value| {
1040         try out.appendSlice(allocator, "<p class=\"");
1041         try out.appendSlice(allocator, value.classes);
1042         try out.appendSlice(allocator, "\">");
1043     } else {
1044         try out.appendSlice(allocator, "<p>");
1045     }
1046     const full_size_image = if (container) |value| value.full_size_image else null;
1047     if (full_size_image) |image| {
1048         try out.appendSlice(allocator, "<a href=\"");
1049         try appendMarkdownEscaped(out, allocator, image.target);
1050         try out.appendSlice(
1051             allocator,
1052             "\" target=\"_blank\" rel=\"noopener\" aria-label=\"Open ",
1053         );
1054         try appendMarkdownEscaped(out, allocator, image.label);
1055         try out.appendSlice(allocator, " at full size\">");
1056     }
1057     try appendInline(
1058         out,
1059         allocator,
1060         paragraph.items(),
1061         footnotes,
1062         options.diagnostic,
1063         paragraph.origin(),
1064         options.link_report,
1065     );
1066     if (full_size_image != null) try out.appendSlice(allocator, "</a>");
1067     try out.appendSlice(allocator, "</p>\n");
1068     paragraph.clearRetainingCapacity();
1069 }
1070 
1071 fn appendTable(
1072     allocator: Allocator,
1073     out: *std.ArrayList(u8),
1074     source: []const u8,
1075     table_source: []const u8,
1076     table: markdown_model.Table,
1077     footnotes: *footnote.Set,
1078     options: Options,
1079 ) InlineError!void {
1080     const alignments = table.delimiter.alignments;
1081     std.debug.assert(table.header.cells.len == alignments.len);
1082     try out.appendSlice(
1083         allocator,
1084         "<div class=\"content-table zen-role-wide\" tabindex=\"0\">\n",
1085     );
1086     try out.appendSlice(allocator, "<table>\n<thead>\n");
1087     try appendTableRow(
1088         allocator,
1089         out,
1090         source,
1091         table_source,
1092         table.header,
1093         alignments,
1094         true,
1095         footnotes,
1096         options,
1097     );
1098     try out.appendSlice(allocator, "</thead>\n<tbody>\n");
1099     for (table.body) |row| {
1100         std.debug.assert(row.cells.len == alignments.len);
1101         try appendTableRow(
1102             allocator,
1103             out,
1104             source,
1105             table_source,
1106             row,
1107             alignments,
1108             false,
1109             footnotes,
1110             options,
1111         );
1112     }
1113     try out.appendSlice(allocator, "</tbody>\n</table>\n</div>\n");
1114 }
1115 
1116 fn appendTableRow(
1117     allocator: Allocator,
1118     out: *std.ArrayList(u8),
1119     source: []const u8,
1120     table_source: []const u8,
1121     row: markdown_model.TableRow,
1122     alignments: []const markdown_model.TableAlignment,
1123     header: bool,
1124     footnotes: *footnote.Set,
1125     options: Options,
1126 ) InlineError!void {
1127     std.debug.assert(row.cells.len == alignments.len);
1128     try out.appendSlice(allocator, "<tr>\n");
1129     for (row.cells, alignments) |cell, alignment| {
1130         if (header) {
1131             try out.appendSlice(allocator, "<th scope=\"col\"");
1132         } else {
1133             try out.appendSlice(allocator, "<td");
1134         }
1135         if (tableAlignmentClass(alignment)) |class| {
1136             try out.appendSlice(allocator, " class=\"");
1137             try out.appendSlice(allocator, class);
1138             try out.append(allocator, '"');
1139         }
1140         try out.append(allocator, '>');
1141         const cell_text = cell.source.bytes(table_source);
1142         try appendInline(
1143             out,
1144             allocator,
1145             cell_text,
1146             footnotes,
1147             options.diagnostic,
1148             originWithin(source, cell_text),
1149             options.link_report,
1150         );
1151         try out.appendSlice(allocator, if (header) "</th>\n" else "</td>\n");
1152     }
1153     try out.appendSlice(allocator, "</tr>\n");
1154 }
1155 
1156 fn tableAlignmentClass(alignment: markdown_model.TableAlignment) ?[]const u8 {
1157     return switch (alignment) {
1158         .none => null,
1159         .left => "zen-table-align-left",
1160         .center => "zen-table-align-center",
1161         .right => "zen-table-align-right",
1162     };
1163 }
1164 
1165 fn flushListItem(
1166     allocator: Allocator,
1167     out: *std.ArrayList(u8),
1168     list_item: *InlineBuffer,
1169     footnotes: *footnote.Set,
1170     options: Options,
1171 ) InlineError!void {
1172     if (list_item.items().len == 0) return;
1173     try out.appendSlice(allocator, "<li>");
1174     try appendInline(
1175         out,
1176         allocator,
1177         list_item.items(),
1178         footnotes,
1179         options.diagnostic,
1180         list_item.origin(),
1181         options.link_report,
1182     );
1183     try out.appendSlice(allocator, "</li>\n");
1184     list_item.clearRetainingCapacity();
1185 }
1186 
1187 fn openList(
1188     allocator: Allocator,
1189     out: *std.ArrayList(u8),
1190     kind: ListKind,
1191     semantic: ?SemanticList,
1192 ) Allocator.Error!void {
1193     try out.appendSlice(allocator, switch (kind) {
1194         .unordered => "<ul",
1195         .ordered => "<ol",
1196     });
1197     if (semantic) |role| {
1198         try out.appendSlice(allocator, " class=\"");
1199         try out.appendSlice(allocator, semanticListClasses(role));
1200         try out.append(allocator, '"');
1201     }
1202     try out.appendSlice(allocator, ">\n");
1203 }
1204 
1205 fn closeList(
1206     allocator: Allocator,
1207     out: *std.ArrayList(u8),
1208     list_kind: *?ListKind,
1209     list_item: *InlineBuffer,
1210     footnotes: *footnote.Set,
1211     options: Options,
1212 ) InlineError!void {
1213     if (list_kind.*) |kind| {
1214         try flushListItem(allocator, out, list_item, footnotes, options);
1215         switch (kind) {
1216             .unordered => try out.appendSlice(allocator, "</ul>\n"),
1217             .ordered => try out.appendSlice(allocator, "</ol>\n"),
1218         }
1219         list_kind.* = null;
1220     }
1221 }
1222 
1223 fn closeBlockquote(
1224     allocator: Allocator,
1225     out: *std.ArrayList(u8),
1226     open: *bool,
1227     blockquote: *InlineBuffer,
1228     footnotes: *footnote.Set,
1229     options: Options,
1230 ) InlineError!void {
1231     if (!open.*) return;
1232     try out.appendSlice(allocator, "<blockquote><p>");
1233     try appendInline(
1234         out,
1235         allocator,
1236         blockquote.items(),
1237         footnotes,
1238         options.diagnostic,
1239         blockquote.origin(),
1240         options.link_report,
1241     );
1242     try out.appendSlice(allocator, "</p></blockquote>\n");
1243     open.* = false;
1244     blockquote.clearRetainingCapacity();
1245 }
1246 
1247 fn appendDecimal(out: *std.ArrayList(u8), allocator: Allocator, value: u8) Allocator.Error!void {
1248     var buffer: [3]u8 = undefined;
1249     const text = std.fmt.bufPrint(&buffer, "{d}", .{value}) catch unreachable;
1250     try out.appendSlice(allocator, text);
1251 }
1252 
1253 fn appendUsize(out: *std.ArrayList(u8), allocator: Allocator, value: usize) Allocator.Error!void {
1254     var buffer: [20]u8 = undefined;
1255     const text = std.fmt.bufPrint(&buffer, "{d}", .{value}) catch unreachable;
1256     try out.appendSlice(allocator, text);
1257 }
1258 
1259 fn nextLine(source: []const u8, cursor: *usize) ?[]const u8 {
1260     if (cursor.* >= source.len) return null;
1261     const start = cursor.*;
1262     if (std.mem.indexOfScalarPos(u8, source, start, '\n')) |end| {
1263         cursor.* = end + 1;
1264         return source[start..end];
1265     }
1266     cursor.* = source.len;
1267     return source[start..];
1268 }
1269 
1270 fn parseFence(line: []const u8) ?Fence {
1271     const trimmed = trimLeft(line);
1272     if (trimmed.len < 3) return null;
1273     const marker = trimmed[0];
1274     if (marker != '`' and marker != '~') return null;
1275     var count: usize = 0;
1276     while (count < trimmed.len and trimmed[count] == marker) : (count += 1) {}
1277     if (count < 3) return null;
1278     return .{
1279         .marker = marker,
1280         .count = count,
1281         .info = std.mem.trim(u8, trimmed[count..], " \t"),
1282     };
1283 }
1284 
1285 fn closingFence(line: []const u8, fence: Fence) bool {
1286     const trimmed = std.mem.trim(u8, line, " \t");
1287     var count: usize = 0;
1288     while (count < trimmed.len and trimmed[count] == fence.marker) : (count += 1) {}
1289     return count >= fence.count and std.mem.trim(u8, trimmed[count..], " \t").len == 0;
1290 }
1291 
1292 const Heading = struct {
1293     level: u8,
1294     text: []const u8,
1295     explicit_id: ?[]const u8 = null,
1296     anchor_offset: ?usize = null,
1297 };
1298 
1299 fn parseHeading(
1300     line: []const u8,
1301     source_offset: usize,
1302     diag: ?*diagnostic.Diagnostic,
1303 ) document.Error!?Heading {
1304     const trimmed = trimLeft(line);
1305     var level: u8 = 0;
1306     while (level < trimmed.len and level < 6 and trimmed[level] == '#') : (level += 1) {}
1307     if (level == 0) return null;
1308     if (level < trimmed.len and trimmed[level] != ' ' and trimmed[level] != '\t') return null;
1309     const text = std.mem.trim(u8, trimmed[level..], " \t#");
1310     const anchor_start = headingAnchorStart(text) orelse return .{
1311         .level = level,
1312         .text = text,
1313     };
1314     const anchor_offset = source_offset +
1315         (@intFromPtr(text.ptr) - @intFromPtr(line.ptr)) + anchor_start;
1316     const anchor = text[anchor_start..];
1317     if (anchor.len < 4 or anchor[anchor.len - 1] != '}') {
1318         setHeadingDiagnostic(diag, "invalid heading anchor", anchor_offset);
1319         return error.InvalidHeadingAnchor;
1320     }
1321     const explicit_id = anchor[2 .. anchor.len - 1];
1322     if (!validHeadingId(explicit_id)) {
1323         setHeadingDiagnostic(diag, "invalid heading anchor", anchor_offset);
1324         return error.InvalidHeadingAnchor;
1325     }
1326     return .{
1327         .level = level,
1328         .text = std.mem.trimEnd(u8, text[0..anchor_start], " \t"),
1329         .explicit_id = explicit_id,
1330         .anchor_offset = anchor_offset,
1331     };
1332 }
1333 
1334 fn headingAnchorStart(text: []const u8) ?usize {
1335     var index = text.len;
1336     while (index > 1) : (index -= 1) {
1337         const start = index - 2;
1338         if (text[start] != '{' or text[start + 1] != '#') continue;
1339         if (start == 0 or text[start - 1] == ' ' or text[start - 1] == '\t') {
1340             return start;
1341         }
1342     }
1343     return null;
1344 }
1345 
1346 fn validHeadingId(id: []const u8) bool {
1347     if (id.len == 0) return false;
1348     for (id) |byte| {
1349         if (asciiAlphanumeric(byte)) continue;
1350         if (byte != '-' and byte != '_' and byte != '.' and byte != ':' and
1351             byte != '/' and byte != '~') return false;
1352     }
1353     return true;
1354 }
1355 
1356 fn setHeadingDiagnostic(
1357     diag: ?*diagnostic.Diagnostic,
1358     reason: []const u8,
1359     offset: usize,
1360 ) void {
1361     if (diag) |active| active.setHeading(reason, offset);
1362 }
1363 
1364 const LocatedHeading = struct {
1365     level: u8,
1366     text: []const u8,
1367     explicit_id: ?[]const u8,
1368     anchor_offset: ?usize,
1369     source_offset: usize,
1370 };
1371 
1372 const HeadingIterator = struct {
1373     source: []const u8,
1374     diagnostic: ?*diagnostic.Diagnostic,
1375     cursor: usize = 0,
1376     code_fence: ?Fence = null,
1377     math_open: bool = false,
1378     footnote_definition: bool = false,
1379     list_open: bool = false,
1380 
1381     fn init(source: []const u8, diag: ?*diagnostic.Diagnostic) HeadingIterator {
1382         return .{ .source = source, .diagnostic = diag };
1383     }
1384 
1385     fn next(self: *HeadingIterator) document.Error!?LocatedHeading {
1386         while (true) {
1387             const line_start = self.cursor;
1388             const raw_line = nextLine(self.source, &self.cursor) orelse return null;
1389             const line = std.mem.trim(u8, raw_line, "\r");
1390 
1391             if (self.code_fence) |fence| {
1392                 if (closingFence(line, fence)) self.code_fence = null;
1393                 continue;
1394             }
1395 
1396             if (self.math_open) {
1397                 const inner = std.mem.trim(u8, line, " \t");
1398                 if (std.mem.eql(u8, inner, "$$") or
1399                     std.mem.endsWith(u8, inner, "$$")) self.math_open = false;
1400                 continue;
1401             }
1402 
1403             if (parseFence(line)) |parsed| {
1404                 self.code_fence = parsed;
1405                 self.footnote_definition = false;
1406                 self.list_open = false;
1407                 continue;
1408             }
1409 
1410             const trimmed = std.mem.trim(u8, line, " \t");
1411             if (std.mem.startsWith(u8, trimmed, "$$")) {
1412                 const rest = std.mem.trim(u8, trimmed[2..], " \t");
1413                 self.math_open = !(rest.len >= 2 and std.mem.endsWith(u8, rest, "$$"));
1414                 self.footnote_definition = false;
1415                 self.list_open = false;
1416                 continue;
1417             }
1418             if (trimmed.len == 0) {
1419                 self.footnote_definition = false;
1420                 self.list_open = false;
1421                 continue;
1422             }
1423 
1424             if (footnote.definition(line) != null) {
1425                 self.footnote_definition = true;
1426                 self.list_open = false;
1427                 continue;
1428             }
1429             if (self.footnote_definition and footnote.continuation(line) != null) continue;
1430             self.footnote_definition = false;
1431 
1432             if (parseBlockquote(line) != null) {
1433                 self.list_open = false;
1434                 continue;
1435             }
1436 
1437             if (parseListItem(line) != null) {
1438                 self.list_open = true;
1439                 continue;
1440             }
1441             if (self.list_open and listContinuation(line) != null) continue;
1442             self.list_open = false;
1443 
1444             if (try parseHeading(line, line_start, self.diagnostic)) |heading| return .{
1445                 .level = heading.level,
1446                 .text = heading.text,
1447                 .explicit_id = heading.explicit_id,
1448                 .anchor_offset = heading.anchor_offset,
1449                 .source_offset = line_start,
1450             };
1451         }
1452     }
1453 };
1454 
1455 const ResolvedHeadingId = union(enum) {
1456     explicit: []const u8,
1457     generated: struct {
1458         text: []const u8,
1459         suffix: ?usize,
1460     },
1461 
1462     fn length(self: ResolvedHeadingId) error{CapacityOverflow}!usize {
1463         return switch (self) {
1464             .explicit => |id| id.len,
1465             .generated => |generated| if (generated.suffix) |suffix|
1466                 std.math.add(
1467                     usize,
1468                     html.slugLength(generated.text),
1469                     1 + decimalLength(suffix),
1470                 ) catch error.CapacityOverflow
1471             else
1472                 html.slugLength(generated.text),
1473         };
1474     }
1475 
1476     fn write(self: ResolvedHeadingId, out: []u8) []u8 {
1477         std.debug.assert(out.len == (self.length() catch unreachable));
1478         switch (self) {
1479             .explicit => |id| @memcpy(out, id),
1480             .generated => |generated| {
1481                 const base_length = html.slugLength(generated.text);
1482                 _ = html.writeSlug(out[0..base_length], generated.text);
1483                 if (generated.suffix) |suffix| {
1484                     out[base_length] = '~';
1485                     _ = std.fmt.bufPrint(out[base_length + 1 ..], "{d}", .{suffix}) catch
1486                         unreachable;
1487                 }
1488             },
1489         }
1490         return out;
1491     }
1492 };
1493 
1494 fn validateHeadingAnchors(
1495     source: []const u8,
1496     diag: ?*diagnostic.Diagnostic,
1497 ) document.Error!void {
1498     var iterator = HeadingIterator.init(source, diag);
1499     while (try iterator.next()) |heading| {
1500         const id = heading.explicit_id orelse continue;
1501         var prior = HeadingIterator.init(source, null);
1502         while (try prior.next()) |candidate| {
1503             if (candidate.source_offset >= heading.source_offset) break;
1504             const candidate_id = candidate.explicit_id orelse continue;
1505             if (!std.mem.eql(u8, candidate_id, id)) continue;
1506             setHeadingDiagnostic(diag, "duplicate heading anchor", heading.anchor_offset.?);
1507             return error.DuplicateHeadingAnchor;
1508         }
1509     }
1510 }
1511 
1512 fn resolveHeadingId(
1513     source: []const u8,
1514     target: LocatedHeading,
1515 ) document.Error!ResolvedHeadingId {
1516     if (target.explicit_id) |id| return .{ .explicit = id };
1517     var occurrence: usize = 0;
1518     var explicit_reserved = false;
1519     var iterator = HeadingIterator.init(source, null);
1520     while (try iterator.next()) |heading| {
1521         if (heading.explicit_id) |id| {
1522             explicit_reserved = explicit_reserved or slugMatches(target.text, id);
1523             continue;
1524         }
1525         if (heading.source_offset > target.source_offset) continue;
1526         if (!slugsEqual(target.text, heading.text)) continue;
1527         occurrence = std.math.add(usize, occurrence, 1) catch
1528             return error.CapacityOverflow;
1529     }
1530     std.debug.assert(occurrence > 0);
1531     const suffix = if (explicit_reserved or occurrence > 1) occurrence else null;
1532     return .{ .generated = .{ .text = target.text, .suffix = suffix } };
1533 }
1534 
1535 const SlugIterator = struct {
1536     source: []const u8,
1537     index: usize = 0,
1538     emitted: usize = 0,
1539     pending_separator: bool = false,
1540     delayed: ?u8 = null,
1541     fallback_index: usize = 0,
1542 
1543     fn next(self: *SlugIterator) ?u8 {
1544         if (self.delayed) |byte| {
1545             self.delayed = null;
1546             self.emitted += 1;
1547             return byte;
1548         }
1549         while (self.index < self.source.len) {
1550             const byte = self.source[self.index];
1551             self.index += 1;
1552             if (asciiAlphanumeric(byte)) {
1553                 const normalized = std.ascii.toLower(byte);
1554                 if (self.pending_separator and self.emitted != 0) {
1555                     self.pending_separator = false;
1556                     self.delayed = normalized;
1557                     self.emitted += 1;
1558                     return '-';
1559                 }
1560                 self.pending_separator = false;
1561                 self.emitted += 1;
1562                 return normalized;
1563             }
1564             if (byte == ' ' or byte == '\t' or byte == '-' or byte == '_') {
1565                 if (self.emitted != 0) self.pending_separator = true;
1566             }
1567         }
1568         if (self.emitted != 0 or self.fallback_index == "section".len) return null;
1569         const byte = "section"[self.fallback_index];
1570         self.fallback_index += 1;
1571         return byte;
1572     }
1573 };
1574 
1575 fn slugsEqual(left: []const u8, right: []const u8) bool {
1576     var left_iterator = SlugIterator{ .source = left };
1577     var right_iterator = SlugIterator{ .source = right };
1578     while (true) {
1579         const left_byte = left_iterator.next();
1580         const right_byte = right_iterator.next();
1581         if (left_byte == null or right_byte == null) return left_byte == right_byte;
1582         if (left_byte.? != right_byte.?) return false;
1583     }
1584 }
1585 
1586 fn slugMatches(text: []const u8, id: []const u8) bool {
1587     var iterator = SlugIterator{ .source = text };
1588     var index: usize = 0;
1589     while (iterator.next()) |byte| {
1590         if (index == id.len or id[index] != byte) return false;
1591         index += 1;
1592     }
1593     return index == id.len;
1594 }
1595 
1596 fn asciiAlphanumeric(byte: u8) bool {
1597     return (byte >= 'a' and byte <= 'z') or
1598         (byte >= 'A' and byte <= 'Z') or
1599         (byte >= '0' and byte <= '9');
1600 }
1601 
1602 fn decimalLength(value: usize) usize {
1603     var buffer: [20]u8 = undefined;
1604     return (std.fmt.bufPrint(&buffer, "{d}", .{value}) catch unreachable).len;
1605 }
1606 
1607 const ListItem = struct {
1608     kind: ListKind,
1609     text: []const u8,
1610 };
1611 
1612 fn parseListItem(line: []const u8) ?ListItem {
1613     const trimmed = trimLeft(line);
1614     if (trimmed.len >= 2 and (trimmed[0] == '-' or trimmed[0] == '*' or trimmed[0] == '+') and (trimmed[1] == ' ' or trimmed[1] == '\t')) {
1615         return .{ .kind = .unordered, .text = std.mem.trim(u8, trimmed[2..], " \t") };
1616     }
1617     var index: usize = 0;
1618     while (index < trimmed.len and trimmed[index] >= '0' and trimmed[index] <= '9') : (index += 1) {}
1619     if (index == 0 or index + 1 >= trimmed.len or trimmed[index] != '.') return null;
1620     if (trimmed[index + 1] != ' ' and trimmed[index + 1] != '\t') return null;
1621     return .{ .kind = .ordered, .text = std.mem.trim(u8, trimmed[index + 2 ..], " \t") };
1622 }
1623 
1624 fn parseSemanticList(line: []const u8) ?SemanticList {
1625     if (std.mem.eql(
1626         u8,
1627         line,
1628         "{.docs-index-family .zen-role-wide}",
1629     )) return .family_index;
1630     if (std.mem.eql(
1631         u8,
1632         line,
1633         "{.docs-index-module .zen-role-wide}",
1634     )) return .module_index;
1635     return null;
1636 }
1637 
1638 fn semanticListClasses(role: SemanticList) []const u8 {
1639     return switch (role) {
1640         .family_index => "docs-index-family zen-role-wide",
1641         .module_index => "docs-index-module zen-role-wide",
1642     };
1643 }
1644 
1645 fn listContinuation(line: []const u8) ?[]const u8 {
1646     var index: usize = 0;
1647     while (index < line.len and (line[index] == ' ' or line[index] == '\t')) : (index += 1) {}
1648     if (index == 0) return null;
1649     const continuation = std.mem.trim(u8, line[index..], " \t");
1650     if (continuation.len == 0) return null;
1651     return continuation;
1652 }
1653 
1654 fn parseBlockquote(line: []const u8) ?[]const u8 {
1655     const trimmed = trimLeft(line);
1656     if (trimmed.len == 0 or trimmed[0] != '>') return null;
1657     return std.mem.trim(u8, trimmed[1..], " \t");
1658 }
1659 
1660 fn isThematicBreak(line: []const u8) bool {
1661     if (line.len < 3) return false;
1662     const marker = line[0];
1663     if (marker != '-' and marker != '*' and marker != '_') return false;
1664     for (line) |byte| {
1665         if (byte != marker) return false;
1666     }
1667     return true;
1668 }
1669 
1670 fn trimLeft(line: []const u8) []const u8 {
1671     var index: usize = 0;
1672     while (index < line.len and (line[index] == ' ' or line[index] == '\t')) : (index += 1) {}
1673     return line[index..];
1674 }
1675 
1676 fn appendFootnotes(
1677     allocator: Allocator,
1678     out: *std.ArrayList(u8),
1679     source: []const u8,
1680     footnotes: *footnote.Set,
1681     options: Options,
1682 ) InlineError!void {
1683     if (!footnotes.hasReferences()) return;
1684     try out.appendSlice(allocator, "<section class=\"footnotes\" aria-label=\"References\">\n<ol>\n");
1685     var number: usize = 1;
1686     while (number < footnotes.next_number) : (number += 1) {
1687         for (footnotes.definitions) |definition| {
1688             if (definition.number != number) continue;
1689             try out.appendSlice(allocator, "<li id=\"fn-");
1690             try html.appendAttributeEscaped(out, allocator, definition.key);
1691             try out.appendSlice(allocator, "\">");
1692             try appendInline(
1693                 out,
1694                 allocator,
1695                 definition.text,
1696                 null,
1697                 options.diagnostic,
1698                 originWithin(source, definition.text),
1699                 options.link_report,
1700             );
1701             try out.appendSlice(allocator, " <a class=\"footnote-backref\" href=\"#fnref-");
1702             try html.appendAttributeEscaped(out, allocator, definition.key);
1703             try out.appendSlice(allocator, "\" aria-label=\"Back to reference\">&#8617;</a></li>\n");
1704         }
1705     }
1706     try out.appendSlice(allocator, "</ol>\n</section>\n");
1707 }
1708 
1709 fn appendFootnoteReference(
1710     out: *std.ArrayList(u8),
1711     allocator: Allocator,
1712     footnotes: ?*footnote.Set,
1713     key: []const u8,
1714 ) Allocator.Error!bool {
1715     const notes = footnotes orelse return false;
1716     const mark = notes.markFor(key) orelse return false;
1717     try out.appendSlice(allocator, "<sup id=\"fnref-");
1718     try html.appendAttributeEscaped(out, allocator, key);
1719     if (mark.reference > 1) {
1720         try out.append(allocator, '-');
1721         try appendUsize(out, allocator, mark.reference);
1722     }
1723     try out.appendSlice(allocator, "\"><a class=\"footnote-ref\" href=\"#fn-");
1724     try html.appendAttributeEscaped(out, allocator, key);
1725     try out.appendSlice(allocator, "\">[");
1726     try appendUsize(out, allocator, mark.number);
1727     try out.appendSlice(allocator, "]</a></sup>");
1728     return true;
1729 }
1730 
1731 fn appendInline(
1732     out: *std.ArrayList(u8),
1733     allocator: Allocator,
1734     value: []const u8,
1735     footnotes: ?*footnote.Set,
1736     diag: ?*diagnostic.Diagnostic,
1737     origin: InlineOrigin,
1738     report: ?*LinkReport,
1739 ) InlineError!void {
1740     var delimiters = indexCodeDelimiters(allocator, value, null) catch |err| {
1741         if (err == error.InlineDelimiterCapacityExceeded) {
1742             if (diag) |detail| detail.setInline(value, delimiter_capacity_reason, 0);
1743         }
1744         return err;
1745     };
1746     defer delimiters.deinit(allocator);
1747     try appendInlineIndexed(
1748         out,
1749         allocator,
1750         value,
1751         value,
1752         footnotes,
1753         diag,
1754         0,
1755         false,
1756         delimiters,
1757         origin,
1758         report,
1759     );
1760 }
1761 
1762 fn appendInlineIndexed(
1763     out: *std.ArrayList(u8),
1764     allocator: Allocator,
1765     source: []const u8,
1766     value: []const u8,
1767     footnotes: ?*footnote.Set,
1768     diag: ?*diagnostic.Diagnostic,
1769     depth: usize,
1770     inside_link: bool,
1771     delimiters: CodeDelimiters,
1772     origin: InlineOrigin,
1773     report: ?*LinkReport,
1774 ) InlineError!void {
1775     std.debug.assert(depth <= max_inline_nesting);
1776     std.debug.assert(@intFromPtr(value.ptr) >= @intFromPtr(source.ptr));
1777     const base = @intFromPtr(value.ptr) - @intFromPtr(source.ptr);
1778     std.debug.assert(base <= source.len);
1779     std.debug.assert(value.len <= source.len - base);
1780     var index: usize = 0;
1781     while (index < value.len) {
1782         if (documentationHtmlTagEnd(value, index)) |end| {
1783             try out.appendSlice(allocator, value[index..end]);
1784             index = end;
1785             continue;
1786         }
1787         if (value[index] == '`') {
1788             const opener_end = backtickRunEnd(value, index);
1789             if (delimiters.span(source, base + index, base + value.len)) |span| {
1790                 try out.appendSlice(allocator, "<code>");
1791                 try html.appendEscaped(out, allocator, span.content);
1792                 try out.appendSlice(allocator, "</code>");
1793                 index = span.end - base;
1794                 continue;
1795             }
1796             try html.appendEscaped(out, allocator, value[index..opener_end]);
1797             index = opener_end;
1798             continue;
1799         }
1800 
1801         if (value[index] == '\\' and index + 1 < value.len and
1802             isAsciiPunctuation(value[index + 1]))
1803         {
1804             try html.appendEscapedByte(out, allocator, value[index + 1]);
1805             index += 2;
1806             continue;
1807         }
1808 
1809         if (value[index] == '$') {
1810             if (inlineMathEnd(value, index)) |end| {
1811                 try renderMath(out, allocator, value[index + 1 .. end], .inline_math, diag);
1812                 index = end + 1;
1813                 continue;
1814             }
1815         }
1816 
1817         if (index + 1 < value.len and value[index] == '!' and value[index + 1] == '[') {
1818             if (try parseInlineLink(
1819                 source,
1820                 value,
1821                 base,
1822                 index,
1823                 true,
1824                 diag,
1825                 delimiters,
1826             )) |link| {
1827                 recordRenderedLink(report, origin, base, index, value, link, true);
1828                 const video = std.mem.endsWith(u8, link.target, ".webm");
1829                 const attributes = parseImageAttributes(value, link.end);
1830                 var motion_base: ?usize = null;
1831                 if (!video) {
1832                     if (attributes) |image_attributes| {
1833                         if (hasAttributeClass(
1834                             image_attributes.content,
1835                             "zen-slide-motion",
1836                         )) {
1837                             motion_base = motionPosterBase(link.target);
1838                         }
1839                     }
1840                 }
1841                 if (video) {
1842                     try out.appendSlice(allocator, "<video src=\"");
1843                     try appendMarkdownEscaped(out, allocator, link.target);
1844                     try out.appendSlice(allocator, "\" poster=\"");
1845                     try appendMarkdownEscaped(
1846                         out,
1847                         allocator,
1848                         link.target[0 .. link.target.len - ".webm".len],
1849                     );
1850                     try out.appendSlice(allocator, ".webp\" aria-label=\"");
1851                 } else {
1852                     try out.appendSlice(allocator, "<img src=\"");
1853                     if (motion_base) |motion_offset| {
1854                         try appendMarkdownEscaped(out, allocator, link.target[0..motion_offset]);
1855                         try out.appendSlice(allocator, ".poster.svg");
1856                         try appendMarkdownEscaped(
1857                             out,
1858                             allocator,
1859                             link.target[motion_offset + ".svg".len ..],
1860                         );
1861                         try out.appendSlice(allocator, "\" data-zen-motion-src=\"");
1862                         try appendMarkdownEscaped(out, allocator, link.target);
1863                         try out.appendSlice(allocator, "\" alt=\"");
1864                     } else {
1865                         try appendMarkdownEscaped(out, allocator, link.target);
1866                         try out.appendSlice(allocator, "\" alt=\"");
1867                     }
1868                 }
1869                 try appendMarkdownEscaped(out, allocator, link.label);
1870                 try out.append(allocator, '"');
1871                 if (video) try out.appendSlice(allocator, " autoplay loop muted playsinline");
1872                 if (motion_base != null) try out.appendSlice(allocator, " decoding=\"async\"");
1873                 var end = link.end;
1874                 if (attributes) |image_attributes| {
1875                     try appendImageAttributes(out, allocator, image_attributes.content);
1876                     end = image_attributes.end;
1877                 }
1878                 try out.append(allocator, '>');
1879                 if (video) try out.appendSlice(allocator, "</video>");
1880                 index = end;
1881                 continue;
1882             }
1883         }
1884 
1885         if (value[index] == '[') {
1886             if (!inside_link) {
1887                 if (parseFootnoteReference(value, index)) |reference| {
1888                     if (try appendFootnoteReference(out, allocator, footnotes, reference.key)) {
1889                         index = reference.end;
1890                         continue;
1891                     }
1892                 }
1893             }
1894             if (!inside_link) {
1895                 if (try parseInlineLink(
1896                     source,
1897                     value,
1898                     base,
1899                     index,
1900                     false,
1901                     diag,
1902                     delimiters,
1903                 )) |link| {
1904                     recordRenderedLink(report, origin, base, index, value, link, false);
1905                     try out.appendSlice(allocator, "<a href=\"");
1906                     try appendMarkdownEscaped(out, allocator, link.target);
1907                     try out.appendSlice(allocator, "\">");
1908                     try appendNestedInline(
1909                         out,
1910                         allocator,
1911                         source,
1912                         link.label,
1913                         footnotes,
1914                         diag,
1915                         depth,
1916                         true,
1917                         delimiters,
1918                         origin,
1919                         report,
1920                     );
1921                     try out.appendSlice(allocator, "</a>");
1922                     index = link.end;
1923                     continue;
1924                 }
1925             }
1926         }
1927 
1928         if (index + 1 < value.len and ((value[index] == '*' and value[index + 1] == '*') or (value[index] == '_' and value[index + 1] == '_'))) {
1929             const token = value[index .. index + 2];
1930             if (std.mem.indexOfPos(u8, value, index + 2, token)) |end| {
1931                 try out.appendSlice(allocator, "<strong>");
1932                 try appendNestedInline(
1933                     out,
1934                     allocator,
1935                     source,
1936                     value[index + 2 .. end],
1937                     footnotes,
1938                     diag,
1939                     depth,
1940                     inside_link,
1941                     delimiters,
1942                     origin,
1943                     report,
1944                 );
1945                 try out.appendSlice(allocator, "</strong>");
1946                 index = end + 2;
1947                 continue;
1948             }
1949         }
1950 
1951         if (value[index] == '*' or value[index] == '_') {
1952             if (std.mem.indexOfScalarPos(u8, value, index + 1, value[index])) |end| {
1953                 try out.appendSlice(allocator, "<em>");
1954                 try appendNestedInline(
1955                     out,
1956                     allocator,
1957                     source,
1958                     value[index + 1 .. end],
1959                     footnotes,
1960                     diag,
1961                     depth,
1962                     inside_link,
1963                     delimiters,
1964                     origin,
1965                     report,
1966                 );
1967                 try out.appendSlice(allocator, "</em>");
1968                 index = end + 1;
1969                 continue;
1970             }
1971         }
1972 
1973         try html.appendEscapedByte(out, allocator, value[index]);
1974         index += 1;
1975     }
1976 }
1977 
1978 fn documentationHtmlTagEnd(value: []const u8, start: usize) ?usize {
1979     const tags = [_][]const u8{
1980         "<span class=\"docs-breadcrumb\" role=\"navigation\" aria-label=\"Declaration hierarchy\">",
1981         "<span class=\"docs-breadcrumb\" role=\"navigation\" aria-label=\"Verification hierarchy\">",
1982         "<span aria-hidden=\"true\">",
1983         "<span aria-current=\"page\">",
1984         "<span class=\"docs-sidenote zen-role-margin\">",
1985         "<span class=\"docs-verification-source\">",
1986         "<span class=\"docs-verification-freshness-content\">",
1987         "<span class=\"docs-verification-freshness-content docs-verification-freshness-content-local\">",
1988         "<span class=\"docs-margin-figure zen-role-margin\">",
1989         "<span class=\"zen-role-reading\">",
1990         "<span class=\"zen-role-wide\">",
1991         "<span class=\"zen-role-margin\">",
1992         "<span class=\"zen-role-full\">",
1993         "<table class=\"docs-audit-table zen-role-wide\">",
1994         "</span>",
1995         "</table>",
1996         "<thead>",
1997         "</thead>",
1998         "<tbody>",
1999         "</tbody>",
2000         "<tr>",
2001         "</tr>",
2002         "<th>",
2003         "</th>",
2004         "<td>",
2005         "</td>",
2006         "<strong>",
2007         "</strong>",
2008         "<code>",
2009         "</code>",
2010         "<br>",
2011     };
2012     if (start >= value.len or value[start] != '<') return null;
2013     for (tags) |tag| {
2014         if (std.mem.startsWith(u8, value[start..], tag)) return start + tag.len;
2015     }
2016     return null;
2017 }
2018 
2019 fn documentationArtifactBlock(value: []const u8) bool {
2020     return std.mem.startsWith(
2021         u8,
2022         value,
2023         "<table class=\"docs-audit-table zen-role-wide\">",
2024     ) and std.mem.endsWith(u8, value, "</table>");
2025 }
2026 
2027 const ParagraphContainer = struct {
2028     classes: []const u8,
2029     full_size_image: ?InlineLink = null,
2030 };
2031 
2032 const ParagraphRole = struct {
2033     open: []const u8,
2034     classes: []const u8,
2035 };
2036 
2037 const paragraph_roles = [_]ParagraphRole{
2038     .{
2039         .open = "<span class=\"docs-sidenote zen-role-margin\">",
2040         .classes = "docs-sidenote-container zen-role-margin",
2041     },
2042     .{
2043         .open = "<span class=\"docs-verification-freshness-content\">",
2044         .classes = "docs-verification-freshness",
2045     },
2046     .{
2047         .open = "<span class=\"docs-verification-freshness-content " ++
2048             "docs-verification-freshness-content-local\">",
2049         .classes = "docs-verification-freshness docs-verification-freshness-local",
2050     },
2051     .{
2052         .open = "<span class=\"docs-margin-figure zen-role-margin\">",
2053         .classes = "docs-margin-figure-container zen-role-margin",
2054     },
2055     .{
2056         .open = "<span class=\"zen-role-reading\">",
2057         .classes = "zen-role-container zen-role-reading",
2058     },
2059     .{
2060         .open = "<span class=\"zen-role-wide\">",
2061         .classes = "zen-role-container zen-role-wide",
2062     },
2063     .{
2064         .open = "<span class=\"zen-role-margin\">",
2065         .classes = "zen-role-container zen-role-margin",
2066     },
2067     .{
2068         .open = "<span class=\"zen-role-full\">",
2069         .classes = "zen-role-container zen-role-full",
2070     },
2071 };
2072 
2073 fn paragraphContainer(
2074     allocator: Allocator,
2075     value: []const u8,
2076     mode: Mode,
2077     diag: ?*diagnostic.Diagnostic,
2078 ) InlineError!?ParagraphContainer {
2079     if (std.mem.endsWith(u8, value, "</span>")) {
2080         for (paragraph_roles) |role| {
2081             if (std.mem.startsWith(u8, value, role.open)) {
2082                 return .{ .classes = role.classes };
2083             }
2084         }
2085     }
2086     if (!std.mem.startsWith(u8, value, "![")) return null;
2087     var delimiters = try indexCodeDelimiters(allocator, value, null);
2088     defer delimiters.deinit(allocator);
2089     const link = try parseInlineLink(
2090         value,
2091         value,
2092         0,
2093         0,
2094         true,
2095         diag,
2096         delimiters,
2097     ) orelse return null;
2098     var end = link.end;
2099     const attributes = parseImageAttributes(value, end);
2100     var wide_figure = false;
2101     const role = if (attributes) |image_attributes| role: {
2102         end = image_attributes.end;
2103         wide_figure = hasAttributeClass(
2104             image_attributes.content,
2105             "zen-slide-figure-wide",
2106         );
2107         if (mode == .document and wide_figure) {
2108             break :role "zen-media-block zen-role-wide";
2109         }
2110         if (hasAttributeClass(image_attributes.content, "zen-role-wide")) {
2111             break :role "zen-media-block zen-role-wide";
2112         }
2113         if (hasAttributeClass(image_attributes.content, "zen-role-margin")) {
2114             break :role "zen-media-block zen-role-margin";
2115         }
2116         if (hasAttributeClass(image_attributes.content, "zen-role-full")) {
2117             break :role "zen-media-block zen-role-full";
2118         }
2119         break :role "zen-media-block zen-role-reading";
2120     } else "zen-media-block zen-role-reading";
2121     if (end != value.len) return null;
2122     return .{
2123         .classes = role,
2124         .full_size_image = if (mode == .document and wide_figure and
2125             !std.mem.endsWith(u8, link.target, ".webm")) link else null,
2126     };
2127 }
2128 
2129 fn recordRenderedLink(
2130     report: ?*LinkReport,
2131     origin: InlineOrigin,
2132     base: usize,
2133     start: usize,
2134     value: []const u8,
2135     link: InlineLink,
2136     image: bool,
2137 ) void {
2138     const sink = report orelse return;
2139     var entry: RenderedLink = .{ .image = image };
2140     if (origin.locate(base + start, base + link.end)) |span| {
2141         entry.start = span.start;
2142         entry.end = span.end;
2143         entry.located = true;
2144     }
2145     if (entry.located) {
2146         const offset = @intFromPtr(link.target.ptr) - @intFromPtr(value.ptr);
2147         if (origin.locate(base + offset, base + offset + link.target.len)) |span| {
2148             entry.target_start = span.start;
2149             entry.target_end = span.end;
2150             entry.contiguous = span.contiguous;
2151         }
2152     }
2153     sink.record(entry);
2154 }
2155 
2156 fn appendNestedInline(
2157     out: *std.ArrayList(u8),
2158     allocator: Allocator,
2159     source: []const u8,
2160     value: []const u8,
2161     footnotes: ?*footnote.Set,
2162     diag: ?*diagnostic.Diagnostic,
2163     depth: usize,
2164     inside_link: bool,
2165     delimiters: CodeDelimiters,
2166     origin: InlineOrigin,
2167     report: ?*LinkReport,
2168 ) InlineError!void {
2169     if (depth == max_inline_nesting) {
2170         if (diag) |detail| detail.setInline(value, inline_nesting_reason, 0);
2171         return error.InlineNestingCapacityExceeded;
2172     }
2173     try appendInlineIndexed(
2174         out,
2175         allocator,
2176         source,
2177         value,
2178         footnotes,
2179         diag,
2180         depth + 1,
2181         inside_link,
2182         delimiters,
2183         origin,
2184         report,
2185     );
2186 }
2187 
2188 /// One run of an inline value, and where that run started in the source.
2189 const SourceSegment = struct {
2190     joined_start: u32,
2191     source_start: u32,
2192     len: u32,
2193 };
2194 
2195 /// A located span of the rendered source, and whether it is one run.
2196 const LocatedSpan = struct {
2197     start: u32,
2198     end: u32,
2199     contiguous: bool,
2200 };
2201 
2202 /// How an inline value's own offsets map back onto the rendered source.
2203 const InlineOrigin = union(enum) {
2204     direct: u32,
2205     joined: []const SourceSegment,
2206     untracked,
2207 
2208     fn locate(self: InlineOrigin, start: usize, end: usize) ?LocatedSpan {
2209         std.debug.assert(start <= end);
2210         switch (self) {
2211             .untracked => return null,
2212             .direct => |base| {
2213                 const located_start = std.math.cast(u32, base + start) orelse return null;
2214                 const located_end = std.math.cast(u32, base + end) orelse return null;
2215                 return .{ .start = located_start, .end = located_end, .contiguous = true };
2216             },
2217             .joined => |segments| {
2218                 const first = segmentContaining(segments, start) orelse return null;
2219                 const located_start = std.math.cast(
2220                     u32,
2221                     first.source_start + (start - first.joined_start),
2222                 ) orelse return null;
2223                 const last_index = if (end > start) end - 1 else start;
2224                 const last = segmentContaining(segments, last_index) orelse return null;
2225                 const same = last.joined_start == first.joined_start;
2226                 const located_end = if (same)
2227                     std.math.cast(u32, first.source_start + (end - first.joined_start)) orelse
2228                         return null
2229                 else
2230                     std.math.cast(u32, first.source_start + first.len) orelse return null;
2231                 if (located_end < located_start) return null;
2232                 return .{ .start = located_start, .end = located_end, .contiguous = same };
2233             },
2234         }
2235     }
2236 };
2237 
2238 fn segmentContaining(segments: []const SourceSegment, offset: usize) ?SourceSegment {
2239     for (segments) |segment| {
2240         if (offset < segment.joined_start) continue;
2241         if (offset - segment.joined_start < segment.len) return segment;
2242     }
2243     return null;
2244 }
2245 
2246 /// Reports where `value` starts inside `source`, or null when it is a copy.
2247 fn sourceOffset(source: []const u8, value: []const u8) ?u32 {
2248     const base = @intFromPtr(source.ptr);
2249     const start = @intFromPtr(value.ptr);
2250     if (start < base) return null;
2251     const offset = start - base;
2252     if (offset > source.len or source.len - offset < value.len) return null;
2253     return std.math.cast(u32, offset);
2254 }
2255 
2256 fn originWithin(source: []const u8, value: []const u8) InlineOrigin {
2257     return if (sourceOffset(source, value)) |offset| .{ .direct = offset } else .untracked;
2258 }
2259 
2260 /// Accumulates one inline value from trimmed source lines, recording the
2261 /// origin of each line only while a link report is attached.
2262 const InlineBuffer = struct {
2263     bytes: std.ArrayList(u8) = .empty,
2264     segments: std.ArrayList(SourceSegment) = .empty,
2265     track: bool = false,
2266 
2267     fn deinit(self: *InlineBuffer, allocator: Allocator) void {
2268         self.bytes.deinit(allocator);
2269         self.segments.deinit(allocator);
2270     }
2271 
2272     fn items(self: *const InlineBuffer) []const u8 {
2273         return self.bytes.items;
2274     }
2275 
2276     fn appendLine(
2277         self: *InlineBuffer,
2278         allocator: Allocator,
2279         source: []const u8,
2280         line: []const u8,
2281     ) Allocator.Error!void {
2282         if (self.bytes.items.len != 0) try self.bytes.append(allocator, ' ');
2283         if (self.track) try self.appendSegment(allocator, source, line);
2284         try self.bytes.appendSlice(allocator, line);
2285     }
2286 
2287     fn appendSegment(
2288         self: *InlineBuffer,
2289         allocator: Allocator,
2290         source: []const u8,
2291         line: []const u8,
2292     ) Allocator.Error!void {
2293         const source_start = sourceOffset(source, line) orelse return;
2294         const joined_start = std.math.cast(u32, self.bytes.items.len) orelse return;
2295         const len = std.math.cast(u32, line.len) orelse return;
2296         try self.segments.append(allocator, .{
2297             .joined_start = joined_start,
2298             .source_start = source_start,
2299             .len = len,
2300         });
2301     }
2302 
2303     fn origin(self: *const InlineBuffer) InlineOrigin {
2304         if (!self.track) return .untracked;
2305         return .{ .joined = self.segments.items };
2306     }
2307 
2308     fn clearRetainingCapacity(self: *InlineBuffer) void {
2309         self.bytes.clearRetainingCapacity();
2310         self.segments.clearRetainingCapacity();
2311     }
2312 };
2313 
2314 const CodeSpan = struct {
2315     content: []const u8,
2316     end: usize,
2317 };
2318 
2319 fn indexCodeDelimiters(
2320     allocator: Allocator,
2321     value: []const u8,
2322     visits: ?*usize,
2323 ) (Allocator.Error || error{InlineDelimiterCapacityExceeded})!CodeDelimiters {
2324     var count: usize = 0;
2325     var index: usize = 0;
2326     while (index < value.len) {
2327         if (visits) |measured| measured.* += 1;
2328         if (value[index] != '`') {
2329             index += 1;
2330             continue;
2331         }
2332         if (count == max_inline_delimiters) return error.InlineDelimiterCapacityExceeded;
2333         count += 1;
2334         index = measuredBacktickRunEnd(value, index, visits);
2335     }
2336 
2337     const items = try allocator.alloc(CodeDelimiter, count);
2338     errdefer allocator.free(items);
2339     index = 0;
2340     var cursor: usize = 0;
2341     while (index < value.len) {
2342         if (visits) |measured| measured.* += 1;
2343         if (value[index] != '`') {
2344             index += 1;
2345             continue;
2346         }
2347         const end = measuredBacktickRunEnd(value, index, visits);
2348         items[cursor] = .{ .start = index, .end = end, .length = end - index };
2349         cursor += 1;
2350         index = end;
2351     }
2352     std.debug.assert(cursor == items.len);
2353     pairCodeDelimiters(items);
2354     return .{ .items = items };
2355 }
2356 
2357 fn measuredBacktickRunEnd(
2358     value: []const u8,
2359     start: usize,
2360     visits: ?*usize,
2361 ) usize {
2362     std.debug.assert(start < value.len);
2363     std.debug.assert(value[start] == '`');
2364     var end = start + 1;
2365     while (end < value.len and value[end] == '`') : (end += 1) {
2366         if (visits) |measured| measured.* += 1;
2367     }
2368     return end;
2369 }
2370 
2371 fn pairCodeDelimiters(items: []CodeDelimiter) void {
2372     if (items.len < 2) return;
2373     std.mem.sort(CodeDelimiter, items, {}, codeDelimiterLessThan);
2374     var run_start: usize = 0;
2375     while (run_start < items.len) {
2376         var run_end = run_start + 1;
2377         while (run_end < items.len and items[run_end].length == items[run_start].length) {
2378             run_end += 1;
2379         }
2380         var index = run_start;
2381         while (index + 1 < run_end) : (index += 1) {
2382             items[index].closer_end = items[index + 1].end;
2383         }
2384         run_start = run_end;
2385     }
2386     std.mem.sort(CodeDelimiter, items, {}, codeDelimiterByStart);
2387 }
2388 
2389 fn codeDelimiterLessThan(_: void, left: CodeDelimiter, right: CodeDelimiter) bool {
2390     if (left.length != right.length) return left.length < right.length;
2391     return left.start < right.start;
2392 }
2393 
2394 fn codeDelimiterByStart(_: void, left: CodeDelimiter, right: CodeDelimiter) bool {
2395     return left.start < right.start;
2396 }
2397 
2398 fn backtickRunEnd(value: []const u8, start: usize) usize {
2399     std.debug.assert(start < value.len);
2400     std.debug.assert(value[start] == '`');
2401     var end = start + 1;
2402     while (end < value.len and value[end] == '`') : (end += 1) {}
2403     return end;
2404 }
2405 
2406 fn adversarialDelimiterSource(out: []u8) []u8 {
2407     var used: usize = 0;
2408     var length: usize = 1;
2409     while (used + length + 1 <= out.len and length <= max_inline_delimiters) : (length += 1) {
2410         @memset(out[used..][0..length], '`');
2411         used += length;
2412         out[used] = 'x';
2413         used += 1;
2414     }
2415     return out[0..used];
2416 }
2417 
2418 fn exerciseNestedIndex(
2419     source: []const u8,
2420     delimiters: CodeDelimiters,
2421     depth: usize,
2422     max_depth: *usize,
2423 ) void {
2424     std.debug.assert(depth <= max_inline_nesting);
2425     max_depth.* = @max(max_depth.*, depth);
2426     _ = delimiters.span(source, 0, source.len);
2427     if (depth == max_inline_nesting) return;
2428     exerciseNestedIndex(source, delimiters, depth + 1, max_depth);
2429 }
2430 
2431 fn appendMarkdownEscaped(
2432     out: *std.ArrayList(u8),
2433     allocator: Allocator,
2434     value: []const u8,
2435 ) Allocator.Error!void {
2436     var index: usize = 0;
2437     while (index < value.len) {
2438         if (value[index] == '\\' and index + 1 < value.len and
2439             isAsciiPunctuation(value[index + 1]))
2440         {
2441             index += 1;
2442         }
2443         try html.appendEscapedByte(out, allocator, value[index]);
2444         index += 1;
2445     }
2446 }
2447 
2448 fn isAsciiPunctuation(byte: u8) bool {
2449     return (byte >= '!' and byte <= '/') or
2450         (byte >= ':' and byte <= '@') or
2451         (byte >= '[' and byte <= '`') or
2452         (byte >= '{' and byte <= '~');
2453 }
2454 
2455 fn inlineMathEnd(value: []const u8, start: usize) ?usize {
2456     if (start + 1 >= value.len) return null;
2457     const first = value[start + 1];
2458     if (first == '$' or first == ' ' or first == '\t') return null;
2459     var index = start + 1;
2460     while (index < value.len) : (index += 1) {
2461         if (value[index] == '\\') {
2462             index += 1;
2463             continue;
2464         }
2465         if (value[index] != '$') continue;
2466         if (index == start + 1) return null;
2467         const previous = value[index - 1];
2468         if (previous == ' ' or previous == '\t') return null;
2469         if (index + 1 < value.len and std.ascii.isDigit(value[index + 1])) return null;
2470         return index;
2471     }
2472     return null;
2473 }
2474 
2475 const InlineLink = struct {
2476     label: []const u8,
2477     target: []const u8,
2478     end: usize,
2479 };
2480 
2481 const InlineParseError = error{
2482     InvalidInlineLink,
2483     InlineNestingCapacityExceeded,
2484 };
2485 
2486 const ImageAttributes = struct {
2487     content: []const u8,
2488     end: usize,
2489 };
2490 
2491 const FootnoteReference = struct {
2492     key: []const u8,
2493     end: usize,
2494 };
2495 
2496 fn parseFootnoteReference(value: []const u8, start: usize) ?FootnoteReference {
2497     if (start + 3 > value.len or value[start] != '[' or value[start + 1] != '^') return null;
2498     const key_end = std.mem.indexOfScalarPos(u8, value, start + 2, ']') orelse return null;
2499     const key = std.mem.trim(u8, value[start + 2 .. key_end], " \t");
2500     if (key.len == 0) return null;
2501     return .{ .key = key, .end = key_end + 1 };
2502 }
2503 
2504 fn parseInlineLink(
2505     source: []const u8,
2506     value: []const u8,
2507     base: usize,
2508     start: usize,
2509     image: bool,
2510     diag: ?*diagnostic.Diagnostic,
2511     delimiters: CodeDelimiters,
2512 ) InlineParseError!?InlineLink {
2513     const label_start = start + if (image) @as(usize, 2) else @as(usize, 1);
2514     const label_end = try scanLinkLabelEnd(
2515         value,
2516         label_start,
2517         diag,
2518         delimiters,
2519         source,
2520         base,
2521     ) orelse return null;
2522     if (label_end + 1 >= value.len or value[label_end + 1] != '(') return null;
2523     const target_start = label_end + 2;
2524     return try scanLinkTarget(value, label_start, label_end, target_start, diag);
2525 }
2526 
2527 fn scanLinkLabelEnd(
2528     value: []const u8,
2529     start: usize,
2530     diag: ?*diagnostic.Diagnostic,
2531     delimiters: CodeDelimiters,
2532     source: []const u8,
2533     base: usize,
2534 ) InlineParseError!?usize {
2535     var depth: usize = 0;
2536     var index = start;
2537     while (index < value.len) {
2538         if (value[index] == '\\' and index + 1 < value.len) {
2539             index += 2;
2540             continue;
2541         }
2542         if (value[index] == '`') {
2543             const run_end = backtickRunEnd(value, index);
2544             if (delimiters.span(source, base + index, base + value.len)) |span| {
2545                 index = span.end - base;
2546             } else {
2547                 index = run_end;
2548             }
2549             continue;
2550         }
2551         if (value[index] == '[') {
2552             if (depth == max_inline_nesting) {
2553                 if (diag) |detail| detail.setInline(value, label_nesting_reason, index);
2554                 return error.InlineNestingCapacityExceeded;
2555             }
2556             depth += 1;
2557         } else if (value[index] == ']') {
2558             if (depth == 0) return index;
2559             depth -= 1;
2560         }
2561         index += 1;
2562     }
2563     return null;
2564 }
2565 
2566 fn scanLinkTarget(
2567     value: []const u8,
2568     label_start: usize,
2569     label_end: usize,
2570     target_start: usize,
2571     diag: ?*diagnostic.Diagnostic,
2572 ) InlineParseError!InlineLink {
2573     var depth: usize = 1;
2574     var index = target_start;
2575     while (index < value.len) {
2576         if (value[index] == '\\' and index + 1 < value.len) {
2577             index += 2;
2578             continue;
2579         }
2580         if (value[index] == '(') {
2581             if (depth == max_inline_nesting) {
2582                 if (diag) |detail| detail.setInline(value, target_nesting_reason, index);
2583                 return error.InlineNestingCapacityExceeded;
2584             }
2585             depth += 1;
2586         } else if (value[index] == ')') {
2587             depth -= 1;
2588             if (depth == 0) return .{
2589                 .label = value[label_start..label_end],
2590                 .target = std.mem.trim(u8, value[target_start..index], " \t"),
2591                 .end = index + 1,
2592             };
2593         }
2594         index += 1;
2595     }
2596     if (diag) |detail| {
2597         detail.setInline(value, "link target is missing closing ')'", target_start - 1);
2598     }
2599     return error.InvalidInlineLink;
2600 }
2601 
2602 fn parseImageAttributes(value: []const u8, start: usize) ?ImageAttributes {
2603     if (start >= value.len or value[start] != '{') return null;
2604     const end = std.mem.indexOfScalarPos(u8, value, start + 1, '}') orelse return null;
2605     const content = std.mem.trim(u8, value[start + 1 .. end], " \t\r\n");
2606     if (content.len == 0) return null;
2607     var widget_count: u8 = 0;
2608     var tokens = std.mem.tokenizeAny(u8, content, " \t\r\n");
2609     while (tokens.next()) |token| {
2610         if (token.len >= 2 and token[0] == '.') {
2611             for (token[1..]) |byte| {
2612                 if (!isClassByte(byte)) return null;
2613             }
2614             continue;
2615         }
2616         if (widgetAttributeValue(token) == null or widget_count != 0) return null;
2617         widget_count += 1;
2618     }
2619     return .{ .content = content, .end = end + 1 };
2620 }
2621 
2622 fn appendImageAttributes(
2623     out: *std.ArrayList(u8),
2624     allocator: Allocator,
2625     content: []const u8,
2626 ) Allocator.Error!void {
2627     var class_count: u8 = 0;
2628     var tokens = std.mem.tokenizeAny(u8, content, " \t\r\n");
2629     while (tokens.next()) |token| {
2630         if (token[0] == '.') class_count += 1;
2631     }
2632     if (class_count != 0) try out.appendSlice(allocator, " class=\"");
2633     var first = true;
2634     tokens = std.mem.tokenizeAny(u8, content, " \t\r\n");
2635     while (tokens.next()) |token| {
2636         if (token[0] != '.') continue;
2637         if (!first) try out.append(allocator, ' ');
2638         try html.appendAttributeEscaped(out, allocator, token[1..]);
2639         first = false;
2640     }
2641     if (class_count != 0) try out.append(allocator, '"');
2642     tokens = std.mem.tokenizeAny(u8, content, " \t\r\n");
2643     while (tokens.next()) |token| {
2644         const widget = widgetAttributeValue(token) orelse continue;
2645         try out.appendSlice(allocator, " data-widget=\"");
2646         try html.appendAttributeEscaped(out, allocator, widget);
2647         try out.append(allocator, '"');
2648     }
2649 }
2650 
2651 fn hasAttributeClass(content: []const u8, expected: []const u8) bool {
2652     var tokens = std.mem.tokenizeAny(u8, content, " \t\r\n");
2653     while (tokens.next()) |token| {
2654         if (token[0] != '.') continue;
2655         if (std.mem.eql(u8, token[1..], expected)) return true;
2656     }
2657     return false;
2658 }
2659 
2660 fn motionPosterBase(target: []const u8) ?usize {
2661     const path_end = std.mem.indexOfAny(u8, target, "?#") orelse target.len;
2662     const path = target[0..path_end];
2663     if (!std.mem.endsWith(u8, path, ".svg")) return null;
2664     return path.len - ".svg".len;
2665 }
2666 
2667 fn isClassByte(byte: u8) bool {
2668     return std.ascii.isAlphanumeric(byte) or byte == '-' or byte == '_';
2669 }
2670 
2671 fn widgetAttributeValue(token: []const u8) ?[]const u8 {
2672     const prefix = "data-widget=\"";
2673     if (!std.mem.startsWith(u8, token, prefix) or
2674         token.len <= prefix.len or token[token.len - 1] != '"')
2675     {
2676         return null;
2677     }
2678     const name = token[prefix.len .. token.len - 1];
2679     if (!validWidgetName(name)) return null;
2680     return name;
2681 }
2682 
2683 fn validWidgetName(name: []const u8) bool {
2684     if (name.len == 0 or name[0] == '-' or name[name.len - 1] == '-') return false;
2685     var previous_hyphen = false;
2686     for (name) |byte| {
2687         const hyphen = byte == '-';
2688         if (!std.ascii.isLower(byte) and !std.ascii.isDigit(byte) and !hyphen) {
2689             return false;
2690         }
2691         if (hyphen and previous_hyphen) return false;
2692         previous_hyphen = hyphen;
2693     }
2694     return true;
2695 }
2696 
2697 const TestInspection = struct {
2698     storage: document.Storage,
2699     value: document.Document,
2700 
2701     fn init(allocator: Allocator, source: []const u8) !TestInspection {
2702         const heading_plan = try plan(source);
2703         var storage = try document.Storage.init(allocator, heading_plan.exactLimits());
2704         errdefer storage.deinit(allocator);
2705         storage.activate();
2706         const value = try inspect(&storage, source);
2707         return .{ .storage = storage, .value = value };
2708     }
2709 
2710     fn deinit(self: *TestInspection, allocator: Allocator) void {
2711         self.storage.reset();
2712         self.storage.deinit(allocator);
2713         self.* = undefined;
2714     }
2715 };
2716 
2717 const heading_witness =
2718     "# Page\n\n" ++
2719     "```zig\n" ++
2720     "# Not a heading\n" ++
2721     "```\n\n" ++
2722     "> # Quote\n\n" ++
2723     "- item\n" ++
2724     "  # Continuation\n\n" ++
2725     "## Section\n";
2726 
2727 test "markdown renders headings paragraphs emphasis links and escaping" {
2728     var rendered = try render(
2729         std.testing.allocator,
2730         "# Hello <Zen>\n\n" ++
2731             "A **fast** [site](/docs) with `code` & text.\n",
2732         .{},
2733     );
2734     defer rendered.deinit(std.testing.allocator);
2735     try std.testing.expectEqualStrings(
2736         "Hello <Zen>",
2737         rendered.document.firstHeading().?.text,
2738     );
2739     try std.testing.expectEqual(@as(usize, 1), rendered.document.headings.len);
2740     try std.testing.expectEqual(@as(u8, 1), rendered.document.headings[0].level);
2741     try std.testing.expectEqualStrings("hello-zen", rendered.document.headings[0].id);
2742     try std.testing.expectEqualStrings(
2743         "<h1 id=\"hello-zen\">Hello &lt;Zen&gt;</h1>\n" ++
2744             "<p>A <strong>fast</strong> <a href=\"/docs\">site</a> with <code>code</code> &amp; text.</p>\n",
2745         rendered.html,
2746     );
2747 }
2748 
2749 test "markdown renders semantic pipe tables with inline cell spans" {
2750     var rendered = try render(
2751         std.testing.allocator,
2752         "| Name | Detail | Time |\n" ++
2753             "| :--- | :---: | ---: |\n" ++
2754             "| **fast** | [docs](/docs) and `code` | 12 ms |\n",
2755         .{},
2756     );
2757     defer rendered.deinit(std.testing.allocator);
2758     try std.testing.expectEqualStrings(
2759         "<div class=\"content-table zen-role-wide\" tabindex=\"0\">\n" ++
2760             "<table>\n<thead>\n<tr>\n" ++
2761             "<th scope=\"col\" class=\"zen-table-align-left\">Name</th>\n" ++
2762             "<th scope=\"col\" class=\"zen-table-align-center\">Detail</th>\n" ++
2763             "<th scope=\"col\" class=\"zen-table-align-right\">Time</th>\n" ++
2764             "</tr>\n</thead>\n<tbody>\n<tr>\n" ++
2765             "<td class=\"zen-table-align-left\"><strong>fast</strong></td>\n" ++
2766             "<td class=\"zen-table-align-center\">" ++
2767             "<a href=\"/docs\">docs</a> and <code>code</code></td>\n" ++
2768             "<td class=\"zen-table-align-right\">12 ms</td>\n" ++
2769             "</tr>\n</tbody>\n</table>\n</div>\n",
2770         rendered.html,
2771     );
2772 }
2773 
2774 test "markdown applies an explicit semantic role to a generated index list" {
2775     var rendered = try render(
2776         std.testing.allocator,
2777         "{.docs-index-family .zen-role-wide}\n" ++
2778             "- [one](/one)\n" ++
2779             "- [two](/two)\n",
2780         .{},
2781     );
2782     defer rendered.deinit(std.testing.allocator);
2783     try std.testing.expectEqualStrings(
2784         "<ul class=\"docs-index-family zen-role-wide\">\n" ++
2785             "<li><a href=\"/one\">one</a></li>\n" ++
2786             "<li><a href=\"/two\">two</a></li>\n" ++
2787             "</ul>\n",
2788         rendered.html,
2789     );
2790 }
2791 
2792 test "markdown rejects a semantic list role without its artifact" {
2793     try std.testing.expectError(
2794         error.InvalidSemanticList,
2795         render(
2796             std.testing.allocator,
2797             "{.docs-index-module .zen-role-wide}\nNot a list.\n",
2798             .{},
2799         ),
2800     );
2801 }
2802 
2803 test "markdown promotes explicit roles to artifact paragraph containers" {
2804     var rendered = try render(
2805         std.testing.allocator,
2806         "<span class=\"zen-role-wide\">Dense comparison.</span>\n\n" ++
2807             "![Plot](/plot.svg){.zen-role-margin}\n",
2808         .{},
2809     );
2810     defer rendered.deinit(std.testing.allocator);
2811     try std.testing.expectEqualStrings(
2812         "<p class=\"zen-role-container zen-role-wide\">" ++
2813             "<span class=\"zen-role-wide\">Dense comparison.</span></p>\n" ++
2814             "<p class=\"zen-media-block zen-role-margin\">" ++
2815             "<img src=\"/plot.svg\" alt=\"Plot\" " ++
2816             "class=\"zen-role-margin\"></p>\n",
2817         rendered.html,
2818     );
2819 }
2820 
2821 test "markdown admits documentation presentation tags only" {
2822     var rendered = try render(
2823         std.testing.allocator,
2824         "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++
2825             "aria-label=\"Declaration hierarchy\">[Reference](/reference/) " ++
2826             "<span aria-hidden=\"true\">›</span> " ++
2827             "<span aria-current=\"page\">`tiny.pretty`</span></span>\n\n" ++
2828             "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++
2829             "aria-label=\"Verification hierarchy\">[Reference](/reference/) " ++
2830             "<span aria-hidden=\"true\">›</span> " ++
2831             "<span aria-current=\"page\">Verification</span></span>\n\n" ++
2832             "<span class=\"docs-sidenote zen-role-margin\">" ++
2833             "Evidence **matters**.</span>\n\n" ++
2834             "<span class=\"docs-verification-freshness-content\">" ++
2835             "**Publication-qualified.** " ++
2836             "<span class=\"docs-verification-source\">" ++
2837             "[`0123`](/source)</span>.</span>\n\n" ++
2838             "<table class=\"docs-audit-table zen-role-wide\">" ++
2839             "<tbody><tr><th>Source</th>" ++
2840             "<td>`root.zig`</td></tr></tbody></table>\n\n" ++
2841             "<span class=\"unsafe\">No.</span><script>No.</script>\n",
2842         .{},
2843     );
2844     defer rendered.deinit(std.testing.allocator);
2845     try std.testing.expect(std.mem.indexOf(
2846         u8,
2847         rendered.html,
2848         "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++
2849             "aria-label=\"Declaration hierarchy\"><a href=\"/reference/\">Reference</a> " ++
2850             "<span aria-hidden=\"true\">›</span> " ++
2851             "<span aria-current=\"page\"><code>tiny.pretty</code></span></span>",
2852     ) != null);
2853     try std.testing.expect(std.mem.indexOf(
2854         u8,
2855         rendered.html,
2856         "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++
2857             "aria-label=\"Verification hierarchy\"><a href=\"/reference/\">Reference</a> " ++
2858             "<span aria-hidden=\"true\">›</span> " ++
2859             "<span aria-current=\"page\">Verification</span></span>",
2860     ) != null);
2861     try std.testing.expect(std.mem.indexOf(
2862         u8,
2863         rendered.html,
2864         "<p class=\"docs-verification-freshness\">" ++
2865             "<span class=\"docs-verification-freshness-content\">" ++
2866             "<strong>Publication-qualified.</strong> " ++
2867             "<span class=\"docs-verification-source\">" ++
2868             "<a href=\"/source\"><code>0123</code></a></span>.</span></p>",
2869     ) != null);
2870     try std.testing.expect(std.mem.indexOf(
2871         u8,
2872         rendered.html,
2873         "<p class=\"docs-sidenote-container zen-role-margin\">" ++
2874             "<span class=\"docs-sidenote zen-role-margin\">" ++
2875             "Evidence <strong>matters</strong>.</span></p>",
2876     ) != null);
2877     try std.testing.expect(std.mem.indexOf(
2878         u8,
2879         rendered.html,
2880         "<table class=\"docs-audit-table zen-role-wide\">" ++
2881             "<tbody><tr><th>Source</th><td><code>root.zig</code>",
2882     ) != null);
2883     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "class=\"unsafe\"") == null);
2884     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<script>") == null);
2885 }
2886 
2887 test "markdown admits validated static call map fences" {
2888     const fragment =
2889         "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">" ++
2890         "<svg role=\"group\"><a class=\"zen-ego-node\" href=\"./\">" ++
2891         "<rect x=\"0\"/><text class=\"zen-ego-label\">run</text></a></svg>" ++
2892         "<figcaption>Static calls.</figcaption></figure>";
2893     var rendered = try render(
2894         std.testing.allocator,
2895         "```zen-ego\n" ++ fragment ++ "\n```\n",
2896         .{},
2897     );
2898     defer rendered.deinit(std.testing.allocator);
2899     try std.testing.expectEqualStrings(fragment ++ "\n", rendered.html);
2900     try std.testing.expectError(
2901         error.UnsafeDocumentFragment,
2902         render(
2903             std.testing.allocator,
2904             "```zen-ego\n" ++
2905                 "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">" ++
2906                 "<svg onload=\"x\"></svg>" ++
2907                 "<figcaption>Static calls.</figcaption></figure>\n```\n",
2908             .{},
2909         ),
2910     );
2911 }
2912 
2913 test "markdown emits one stylesheet for repeated static call maps" {
2914     var svg_output: [diagram.ego.svg.output_bytes_max]u8 = undefined;
2915     const call_map = try diagram.ego.render(&svg_output, .{
2916         .subject = .{
2917             .label = "run",
2918             .context = "tiny.chic",
2919             .href = "#run",
2920         },
2921     }, .wide);
2922     const style_start = std.mem.indexOf(u8, call_map, "<style>") orelse
2923         return error.TestUnexpectedResult;
2924     const style_close = std.mem.indexOfPos(u8, call_map, style_start, "</style>") orelse
2925         return error.TestUnexpectedResult;
2926     const style_end = style_close + "</style>".len;
2927     const figure_open =
2928         "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">";
2929     const figure_close = "<figcaption>Static calls.</figcaption></figure>";
2930     var source: [diagram.ego.svg.document_fragment_bytes_max * 2]u8 = undefined;
2931     var writer = std.Io.Writer.fixed(&source);
2932     for (0..2) |_| {
2933         try writer.writeAll("```zen-ego\n");
2934         try writer.writeAll(figure_open);
2935         try writer.writeAll(call_map);
2936         try writer.writeAll(figure_close);
2937         try writer.writeAll("\n```\n");
2938     }
2939     var preserved = try render(std.testing.allocator, writer.buffered(), .{});
2940     defer preserved.deinit(std.testing.allocator);
2941     var rendered = try render(std.testing.allocator, writer.buffered(), .{
2942         .one_call_map_style_per_page = true,
2943     });
2944     defer rendered.deinit(std.testing.allocator);
2945     var preserved_expected_storage: [diagram.ego.svg.document_fragment_bytes_max * 2]u8 = undefined;
2946     var preserved_expected = std.Io.Writer.fixed(&preserved_expected_storage);
2947     for (0..2) |_| {
2948         try preserved_expected.writeAll(figure_open);
2949         try preserved_expected.writeAll(call_map);
2950         try preserved_expected.writeAll(figure_close);
2951         try preserved_expected.writeByte('\n');
2952     }
2953     try std.testing.expectEqualStrings(preserved_expected.buffered(), preserved.html);
2954     var expected_storage: [diagram.ego.svg.document_fragment_bytes_max * 2]u8 = undefined;
2955     var expected = std.Io.Writer.fixed(&expected_storage);
2956     try expected.writeAll(figure_open);
2957     try expected.writeAll(call_map);
2958     try expected.writeAll(figure_close);
2959     try expected.writeByte('\n');
2960     try expected.writeAll(figure_open);
2961     try expected.writeAll(call_map[0..style_start]);
2962     try expected.writeAll(call_map[style_end..]);
2963     try expected.writeAll(figure_close);
2964     try expected.writeByte('\n');
2965     try std.testing.expectEqualStrings(expected.buffered(), rendered.html);
2966     try std.testing.expectEqual(@as(usize, 1), std.mem.count(
2967         u8,
2968         rendered.html,
2969         "<style>",
2970     ));
2971     for ([_][]const u8{ "<svg ", "class=\"zen-ego-node", " href=\"" }) |marker| {
2972         try std.testing.expectEqual(
2973             std.mem.count(u8, call_map, marker) * 2,
2974             std.mem.count(u8, rendered.html, marker),
2975         );
2976     }
2977     try std.testing.expectEqual(
2978         @as(usize, 2),
2979         std.mem.count(u8, rendered.html, figure_close),
2980     );
2981 }
2982 
2983 test "markdown leaves a page without call maps style-free" {
2984     var rendered = try render(std.testing.allocator, "# Page\n\nNo call maps.\n", .{
2985         .one_call_map_style_per_page = true,
2986     });
2987     defer rendered.deinit(std.testing.allocator);
2988     try std.testing.expectEqual(@as(usize, 0), std.mem.count(
2989         u8,
2990         rendered.html,
2991         "<style>",
2992     ));
2993 }
2994 
2995 test "markdown preserves distinct static call-map styles" {
2996     const opening =
2997         "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">" ++
2998         "<svg role=\"group\"><style>";
2999     const closing =
3000         "</style><text class=\"node\">run</text></svg>" ++
3001         "<figcaption>Static calls.</figcaption></figure>";
3002     var source: [2048]u8 = undefined;
3003     var writer = std.Io.Writer.fixed(&source);
3004     for ([_][]const u8{ ".node{fill:red}", ".node{fill:blue}" }) |style| {
3005         try writer.writeAll("```zen-ego\n");
3006         try writer.writeAll(opening);
3007         try writer.writeAll(style);
3008         try writer.writeAll(closing);
3009         try writer.writeAll("\n```\n");
3010     }
3011     var rendered = try render(std.testing.allocator, writer.buffered(), .{
3012         .one_call_map_style_per_page = true,
3013     });
3014     defer rendered.deinit(std.testing.allocator);
3015     try std.testing.expectEqual(@as(usize, 2), std.mem.count(
3016         u8,
3017         rendered.html,
3018         "<style>",
3019     ));
3020     try std.testing.expect(
3021         std.mem.indexOf(u8, rendered.html, ".node{fill:red}") != null,
3022     );
3023     try std.testing.expect(
3024         std.mem.indexOf(u8, rendered.html, ".node{fill:blue}") != null,
3025     );
3026 }
3027 
3028 test "markdown preserves variable backtick spans inside link labels" {
3029     var rendered = try render(
3030         std.testing.allocator,
3031         "[``@\"tick`](/wrong)\"``](safe/#anchor)\n",
3032         .{},
3033     );
3034     defer rendered.deinit(std.testing.allocator);
3035     try std.testing.expectEqualStrings(
3036         "<p><a href=\"safe/#anchor\"><code>@&quot;tick`](/wrong)&quot;</code></a></p>\n",
3037         rendered.html,
3038     );
3039 }
3040 
3041 test "markdown pairs each code opener after spans consumed by other lengths" {
3042     var rendered = try render(
3043         std.testing.allocator,
3044         "``a ` b`` c `d`\n",
3045         .{},
3046     );
3047     defer rendered.deinit(std.testing.allocator);
3048     try std.testing.expectEqualStrings(
3049         "<p><code>a ` b</code> c <code>d</code></p>\n",
3050         rendered.html,
3051     );
3052 
3053     var linked = try render(
3054         std.testing.allocator,
3055         "[``a ` ](/wrong) b`` c `d](/still-wrong)`](safe)\n",
3056         .{},
3057     );
3058     defer linked.deinit(std.testing.allocator);
3059     try std.testing.expectEqualStrings(
3060         "<p><a href=\"safe\"><code>a ` ](/wrong) b</code> c " ++
3061             "<code>d](/still-wrong)</code></a></p>\n",
3062         linked.html,
3063     );
3064 }
3065 
3066 test "markdown scans escaped labels and balanced link targets" {
3067     var rendered = try render(
3068         std.testing.allocator,
3069         "[Zig \\[](/types/@\"name\"(u8\\))) and `plain ] )` plus $x$.\n",
3070         .{},
3071     );
3072     defer rendered.deinit(std.testing.allocator);
3073     try std.testing.expectEqualStrings(
3074         "<p><a href=\"/types/@&quot;name&quot;(u8))\">Zig [</a> and " ++
3075             "<code>plain ] )</code> plus <math><mi>x</mi></math>.</p>\n",
3076         rendered.html,
3077     );
3078 }
3079 
3080 test "markdown never emits anchors inside link labels" {
3081     var rendered = try render(
3082         std.testing.allocator,
3083         "[outer [inner](x) and [^note]](y)\n\n[^note]: Note.\n",
3084         .{},
3085     );
3086     defer rendered.deinit(std.testing.allocator);
3087     try std.testing.expectEqualStrings(
3088         "<p><a href=\"y\">outer [inner](x) and [^note]</a></p>\n",
3089         rendered.html,
3090     );
3091     const first_anchor = std.mem.indexOf(u8, rendered.html, "<a href=").?;
3092     try std.testing.expect(std.mem.indexOfPos(
3093         u8,
3094         rendered.html,
3095         first_anchor + "<a href=".len,
3096         "<a href=",
3097     ) == null);
3098 }
3099 
3100 test "markdown names malformed inline links and fixed nesting excess" {
3101     var diag: Diagnostic = .{};
3102     const malformed = "Read [the source](/missing.\n";
3103     try std.testing.expectError(error.InvalidInlineLink, render(
3104         std.testing.allocator,
3105         malformed,
3106         .{ .diagnostic = &diag },
3107     ));
3108     try std.testing.expectEqualStrings(malformed[0 .. malformed.len - 1], diag.inlineSource());
3109     try std.testing.expectEqualStrings("link target is missing closing ')'", diag.reason);
3110     try std.testing.expectEqual(@as(usize, 17), diag.offset);
3111 
3112     diag.reset();
3113     const nested: [max_inline_nesting + 1]u8 = @splat('[');
3114     const excessive = "[" ++ nested ++ "x](safe)\n";
3115     try std.testing.expectError(error.InlineNestingCapacityExceeded, render(
3116         std.testing.allocator,
3117         excessive,
3118         .{ .diagnostic = &diag },
3119     ));
3120     try std.testing.expectEqualStrings(label_nesting_reason, diag.reason);
3121     try std.testing.expectEqual(@as(usize, max_inline_nesting + 1), diag.offset);
3122 }
3123 
3124 test "markdown accepts exact inline scanner boundaries" {
3125     const labels: [max_inline_nesting]u8 = @splat('[');
3126     const label_closes: [max_inline_nesting]u8 = @splat(']');
3127     const target_opens: [max_inline_nesting - 1]u8 = @splat('(');
3128     const target_closes: [max_inline_nesting - 1]u8 = @splat(')');
3129     const source = "[" ++ labels ++ "x" ++ label_closes ++ "](safe) [x](" ++
3130         target_opens ++ "safe" ++ target_closes ++ ")\n";
3131     var rendered = try render(std.testing.allocator, source, .{});
3132     defer rendered.deinit(std.testing.allocator);
3133     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "href=\"safe\"") != null);
3134 
3135     var diag: Diagnostic = .{};
3136     const excessive: [max_inline_nesting]u8 = @splat('(');
3137     try std.testing.expectError(error.InlineNestingCapacityExceeded, render(
3138         std.testing.allocator,
3139         "[x](" ++ excessive ++ "safe\n",
3140         .{ .diagnostic = &diag },
3141     ));
3142     try std.testing.expectEqualStrings(target_nesting_reason, diag.reason);
3143     try std.testing.expectEqual(@as(usize, max_inline_nesting + 3), diag.offset);
3144 }
3145 
3146 test "markdown keeps incomplete ordinary inline delimiters literal" {
3147     var rendered = try render(
3148         std.testing.allocator,
3149         "Use ``unfinished `code and [plain label without a target.\n",
3150         .{},
3151     );
3152     defer rendered.deinit(std.testing.allocator);
3153     try std.testing.expectEqualStrings(
3154         "<p>Use ``unfinished `code and [plain label without a target.</p>\n",
3155         rendered.html,
3156     );
3157 }
3158 
3159 test "markdown delimiter indexing has a linear byte-visit bound" {
3160     const source_capacity = 8 * 1024 * 1024;
3161     const source = try std.testing.allocator.alloc(u8, source_capacity);
3162     defer std.testing.allocator.free(source);
3163     const adversarial = adversarialDelimiterSource(source);
3164     try std.testing.expect(adversarial.len > 8 * 1024 * 1024 - 8 * 1024);
3165     var visits: usize = 0;
3166     var delimiters = try indexCodeDelimiters(std.testing.allocator, adversarial, &visits);
3167     defer delimiters.deinit(std.testing.allocator);
3168     try std.testing.expect(visits <= 4 * adversarial.len);
3169 }
3170 
3171 test "markdown rejects delimiter run capacity before allocation" {
3172     try std.testing.expectEqual(code_delimiter_bytes, @sizeOf(CodeDelimiter));
3173     try std.testing.expectEqual(@as(usize, 2_621_440), max_inline_delimiter_bytes);
3174     const exact_bytes = max_inline_delimiters * 2;
3175     const source = try std.testing.allocator.alloc(u8, exact_bytes + 2);
3176     defer std.testing.allocator.free(source);
3177     var index: usize = 0;
3178     while (index < source.len) : (index += 2) {
3179         source[index] = '`';
3180         source[index + 1] = 'x';
3181     }
3182     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
3183     var exact = try indexCodeDelimiters(counting.allocator(), source[0..exact_bytes], null);
3184     defer exact.deinit(counting.allocator());
3185     const allocations = counting.alloc_index;
3186     try std.testing.expectEqual(@as(usize, 1), allocations);
3187     try std.testing.expectEqual(
3188         max_inline_delimiter_bytes,
3189         counting.allocated_bytes,
3190     );
3191     try std.testing.expectError(
3192         error.InlineDelimiterCapacityExceeded,
3193         indexCodeDelimiters(counting.allocator(), source, null),
3194     );
3195     try std.testing.expectEqual(allocations, counting.alloc_index);
3196 }
3197 
3198 test "markdown code spans never close outside their recursive subrange" {
3199     const source = "_`inside_ outside`";
3200     var delimiters = try indexCodeDelimiters(std.testing.allocator, source, null);
3201     defer delimiters.deinit(std.testing.allocator);
3202     var out: std.ArrayList(u8) = .empty;
3203     defer out.deinit(std.testing.allocator);
3204     try appendInlineIndexed(
3205         &out,
3206         std.testing.allocator,
3207         source,
3208         source[1..8],
3209         null,
3210         null,
3211         1,
3212         false,
3213         delimiters,
3214         .untracked,
3215         null,
3216     );
3217     try std.testing.expectEqualStrings("`inside", out.items);
3218 }
3219 
3220 test "markdown reuses one delimiter index through maximum recursion" {
3221     const source = "`code`";
3222     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
3223     var delimiters = try indexCodeDelimiters(counting.allocator(), source, null);
3224     defer delimiters.deinit(counting.allocator());
3225     const index_allocations = counting.alloc_index;
3226     var max_depth: usize = 0;
3227     exerciseNestedIndex(source, delimiters, 0, &max_depth);
3228     try std.testing.expectEqual(max_inline_nesting, max_depth);
3229     try std.testing.expectEqual(index_allocations, counting.alloc_index);
3230 
3231     var out: std.ArrayList(u8) = .empty;
3232     defer out.deinit(std.testing.allocator);
3233     try out.ensureTotalCapacity(std.testing.allocator, 64);
3234     try appendInlineIndexed(
3235         &out,
3236         counting.allocator(),
3237         source,
3238         source,
3239         null,
3240         null,
3241         0,
3242         false,
3243         delimiters,
3244         .untracked,
3245         null,
3246     );
3247     try std.testing.expectEqualStrings("<code>code</code>", out.items);
3248     try std.testing.expectEqual(index_allocations, counting.alloc_index);
3249 }
3250 
3251 test "markdown variable fences preserve embedded structural spellings" {
3252     const source =
3253         "````````text\n" ++
3254         "```\n" ++
3255         "```````\n" ++
3256         "</code><a href=\"/wrong\">injection</a>\n" ++
3257         "````````\n";
3258     var rendered = try render(
3259         std.testing.allocator,
3260         source,
3261         .{ .code_blocks = .{ .highlighting = false, .line_numbers = false } },
3262     );
3263     defer rendered.deinit(std.testing.allocator);
3264     try std.testing.expect(std.mem.indexOf(
3265         u8,
3266         rendered.html,
3267         "```</span></span><span class=\"zen-code-line\">",
3268     ) != null);
3269     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "```````") != null);
3270     try std.testing.expect(std.mem.indexOf(
3271         u8,
3272         rendered.html,
3273         "&lt;/code&gt;&lt;a href=&quot;/wrong&quot;&gt;injection&lt;/a&gt;",
3274     ) != null);
3275     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<a href=\"/wrong\">") == null);
3276 }
3277 
3278 test "markdown reserves explicit anchors and resolves generated collisions" {
3279     const source =
3280         "```zen-toc\n" ++
3281         "scope: document\n" ++
3282         "```\n\n" ++
3283         "# Generated\n\n" ++
3284         "## Generated\n\n" ++
3285         "## Stable API {#generated}\n\n" ++
3286         "### Render {#tiny.zen.render}\n";
3287     var rendered = try render(std.testing.allocator, source, .{});
3288     defer rendered.deinit(std.testing.allocator);
3289     try std.testing.expectEqualStrings("generated~1", rendered.document.headings[0].id);
3290     try std.testing.expectEqualStrings("generated~2", rendered.document.headings[1].id);
3291     try std.testing.expectEqualStrings("generated", rendered.document.headings[2].id);
3292     try std.testing.expectEqualStrings("tiny.zen.render", rendered.document.headings[3].id);
3293     try std.testing.expect(std.mem.indexOf(
3294         u8,
3295         rendered.html,
3296         "<a href=\"#generated\">Stable API</a>",
3297     ) != null);
3298     try std.testing.expect(std.mem.indexOf(
3299         u8,
3300         rendered.html,
3301         "<h3 id=\"tiny.zen.render\">Render</h3>",
3302     ) != null);
3303     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "{#") == null);
3304 }
3305 
3306 test "markdown preserves typed path anchors" {
3307     var rendered = try render(
3308         std.testing.allocator,
3309         "## Run {#method/Options/run}\n",
3310         .{},
3311     );
3312     defer rendered.deinit(std.testing.allocator);
3313     try std.testing.expectEqualStrings(
3314         "method/Options/run",
3315         rendered.document.headings[0].id,
3316     );
3317     try std.testing.expectEqualStrings(
3318         "<h2 id=\"method/Options/run\">Run</h2>\n",
3319         rendered.html,
3320     );
3321 }
3322 
3323 test "markdown suffixes generated heading collisions in source order" {
3324     var rendered = try render(
3325         std.testing.allocator,
3326         "# Repeat\n\n# Repeat\n\n# Repeat\n",
3327         .{},
3328     );
3329     defer rendered.deinit(std.testing.allocator);
3330     try std.testing.expectEqualStrings("repeat", rendered.document.headings[0].id);
3331     try std.testing.expectEqualStrings("repeat~2", rendered.document.headings[1].id);
3332     try std.testing.expectEqualStrings("repeat~3", rendered.document.headings[2].id);
3333 }
3334 
3335 test "explicit anchor syntax preserves unanchored heading output" {
3336     var rendered = try render(
3337         std.testing.allocator,
3338         "# Render {#tiny.zen.render}\n",
3339         .{ .heading_anchors = false },
3340     );
3341     defer rendered.deinit(std.testing.allocator);
3342     try std.testing.expectEqualStrings("tiny.zen.render", rendered.document.headings[0].id);
3343     try std.testing.expectEqualStrings("<h1>Render</h1>\n", rendered.html);
3344 }
3345 
3346 test "explicit anchor syntax accepts case markers" {
3347     var rendered = try render(
3348         std.testing.allocator,
3349         "# Diagnostic {#decl/~c0~Diagnostic}\n",
3350         .{},
3351     );
3352     defer rendered.deinit(std.testing.allocator);
3353     try std.testing.expectEqualStrings(
3354         "decl/~c0~Diagnostic",
3355         rendered.document.headings[0].id,
3356     );
3357 }
3358 
3359 test "markdown reports invalid and duplicate explicit heading anchors" {
3360     var diag: diagnostic.Diagnostic = .{};
3361     const duplicate = "# First {#same}\n\n## Second {#same}\n";
3362     try std.testing.expectError(error.DuplicateHeadingAnchor, render(
3363         std.testing.allocator,
3364         duplicate,
3365         .{ .diagnostic = &diag },
3366     ));
3367     try std.testing.expectEqualStrings("duplicate heading anchor", diag.reason);
3368     try std.testing.expectEqual(
3369         std.mem.lastIndexOf(u8, duplicate, "{#same}").?,
3370         diag.offset,
3371     );
3372     const invalid = "# Symbol {#not safe}\n";
3373     try std.testing.expectError(error.InvalidHeadingAnchor, render(
3374         std.testing.allocator,
3375         invalid,
3376         .{ .diagnostic = &diag },
3377     ));
3378     try std.testing.expectEqualStrings("invalid heading anchor", diag.reason);
3379     try std.testing.expectEqual(std.mem.indexOf(u8, invalid, "{#").?, diag.offset);
3380 }
3381 
3382 test "markdown inspects headings outside inert blocks" {
3383     var inspected = try TestInspection.init(
3384         std.testing.allocator,
3385         heading_witness,
3386     );
3387     defer inspected.deinit(std.testing.allocator);
3388     const doc = inspected.value;
3389     try std.testing.expectEqual(@as(usize, 2), doc.headings.len);
3390     try std.testing.expectEqualStrings("Page", doc.headings[0].text);
3391     try std.testing.expectEqualStrings("page", doc.headings[0].id);
3392     try std.testing.expectEqualStrings("Section", doc.headings[1].text);
3393     try std.testing.expectEqualStrings("section", doc.headings[1].id);
3394 }
3395 
3396 test "heading storage rejects every limit before backing mutation" {
3397     comptime {
3398         @stardustClaim(
3399             @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_boundaries"),
3400             null,
3401             null,
3402             null,
3403             null,
3404             null,
3405             null,
3406         );
3407     }
3408 
3409     var storage = try document.Storage.init(std.testing.allocator, .{
3410         .max_headings = 2,
3411         .max_text_bytes = 10,
3412         .max_id_bytes = 7,
3413     });
3414     defer storage.deinit(std.testing.allocator);
3415     storage.activate();
3416     @memset(storage.bytes, 42);
3417 
3418     try std.testing.expectError(
3419         error.HeadingCapacityExceeded,
3420         inspect(&storage, "# A\n## B\n### C\n"),
3421     );
3422     for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte);
3423     try std.testing.expectError(
3424         error.HeadingTextByteCapacityExceeded,
3425         inspect(&storage, "# abcdef\n## ghijkl\n"),
3426     );
3427     for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte);
3428     try std.testing.expectError(
3429         error.HeadingIdByteCapacityExceeded,
3430         inspect(&storage, "# A\n## ?\n"),
3431     );
3432     for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte);
3433     try std.testing.expectEqual(@as(u64, 3), storage.status().rejected_source_count);
3434     try std.testing.expect(!storage.status().in_use);
3435 }
3436 
3437 test "heading storage reuses one region after success and rejection" {
3438     comptime {
3439         @stardustClaim(
3440             @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_reuse"),
3441             null,
3442             null,
3443             null,
3444             null,
3445             null,
3446             null,
3447         );
3448     }
3449 
3450     var storage = try document.Storage.init(std.testing.allocator, .{
3451         .max_headings = 2,
3452         .max_text_bytes = 16,
3453         .max_id_bytes = 16,
3454     });
3455     defer storage.deinit(std.testing.allocator);
3456     storage.activate();
3457     const base = @intFromPtr(storage.bytes.ptr);
3458 
3459     const first = try inspect(&storage, "# First\n## Second\n");
3460     try std.testing.expectEqualStrings("First", first.headings[0].text);
3461     try std.testing.expectError(
3462         error.DocumentStorageInUse,
3463         inspect(&storage, "# Other\n"),
3464     );
3465     try std.testing.expectEqualStrings("First", first.headings[0].text);
3466     storage.reset();
3467 
3468     try std.testing.expectError(
3469         error.HeadingCapacityExceeded,
3470         inspect(&storage, "# A\n## B\n### C\n"),
3471     );
3472     const second = try inspect(&storage, "# Other\n");
3473     try std.testing.expectEqualStrings("Other", second.headings[0].text);
3474     storage.reset();
3475     try std.testing.expectEqual(base, @intFromPtr(storage.bytes.ptr));
3476     try std.testing.expectEqual(@as(usize, 2), storage.status().high_water_headings);
3477     try std.testing.expectEqual(@as(u64, 2), storage.status().rejected_source_count);
3478 }
3479 
3480 test "activated heading inspection performs no backing allocation" {
3481     comptime {
3482         @stardustClaim(
3483             @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_sealed"),
3484             null,
3485             null,
3486             null,
3487             null,
3488             null,
3489             null,
3490         );
3491     }
3492 
3493     const heading_plan = try plan(heading_witness);
3494     const capacity = try document.Capacity.derive(heading_plan.exactLimits());
3495     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
3496     var storage = try document.Storage.init(counting.allocator(), heading_plan.exactLimits());
3497     defer storage.deinit(counting.allocator());
3498     storage.activate();
3499     const allocations = counting.alloc_index;
3500     const resizes = counting.resize_index;
3501     const bytes = counting.allocated_bytes;
3502     counting.fail_index = allocations;
3503     counting.resize_fail_index = resizes;
3504 
3505     const doc = try inspect(&storage, heading_witness);
3506     defer storage.reset();
3507     try std.testing.expectEqual(@as(usize, 2), doc.headings.len);
3508     try std.testing.expectEqual(@as(usize, 1), allocations);
3509     try std.testing.expectEqual(@as(usize, 0), resizes);
3510     try std.testing.expectEqual(capacity.storage_bytes, bytes);
3511     try std.testing.expectEqual(allocations, counting.alloc_index);
3512     try std.testing.expectEqual(resizes, counting.resize_index);
3513     try std.testing.expectEqual(bytes, counting.allocated_bytes);
3514 }
3515 
3516 test "heading storage owns text independently from the Markdown source" {
3517     var source = [_]u8{ '#', ' ', 'P', 'a', 'g', 'e', '\n' };
3518     const heading_plan = try plan(&source);
3519     var storage = try document.Storage.init(std.testing.allocator, heading_plan.exactLimits());
3520     defer storage.deinit(std.testing.allocator);
3521     storage.activate();
3522     const doc = try inspect(&storage, &source);
3523     defer storage.reset();
3524     @memset(&source, 'x');
3525     try std.testing.expectEqualStrings("Page", doc.headings[0].text);
3526     try std.testing.expectEqualStrings("page", doc.headings[0].id);
3527 }
3528 
3529 test "Markdown rendering owns exact heading storage" {
3530     comptime {
3531         @stardustClaim(
3532             @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_consumer"),
3533             null,
3534             null,
3535             null,
3536             null,
3537             null,
3538             null,
3539         );
3540     }
3541 
3542     const source = "# Page\n\n## Section\n";
3543     const heading_plan = try plan(source);
3544     const capacity = try document.Capacity.derive(heading_plan.exactLimits());
3545     var rendered = try render(std.testing.allocator, source, .{});
3546     defer rendered.deinit(std.testing.allocator);
3547     try std.testing.expectEqual(capacity.storage_bytes, rendered.document_storage.bytes.len);
3548     try std.testing.expectEqualStrings("Page", rendered.document.firstHeading().?.text);
3549 }
3550 
3551 test "markdown renders generated contents from following headings" {
3552     var rendered = try render(
3553         std.testing.allocator,
3554         "# Page\n\n" ++
3555             "```zen-toc\n" ++
3556             "scope: following\n" ++
3557             "min-level: 2\n" ++
3558             "max-level: 3\n" ++
3559             "```\n\n" ++
3560             "## First\n\n" ++
3561             "### Detail\n\n" ++
3562             "# Next page root\n",
3563         .{},
3564     );
3565     defer rendered.deinit(std.testing.allocator);
3566     try std.testing.expectEqualStrings(
3567         "<h1 id=\"page\">Page</h1>\n" ++
3568             "<nav class=\"zen-contents zen-role-wide\" aria-label=\"Contents\">\n" ++
3569             "<p>Contents:</p>\n" ++
3570             "<ul>\n" ++
3571             "<li class=\"zen-contents-level-2\"><a href=\"#first\">First</a></li>\n" ++
3572             "<li class=\"zen-contents-level-3\" style=\"margin-left:calc(1 * 1.5rem)\"><a href=\"#detail\">Detail</a></li>\n" ++
3573             "</ul>\n" ++
3574             "</nav>\n" ++
3575             "<h2 id=\"first\">First</h2>\n" ++
3576             "<h3 id=\"detail\">Detail</h3>\n" ++
3577             "<h1 id=\"next-page-root\">Next page root</h1>\n",
3578         rendered.html,
3579     );
3580 }
3581 
3582 test "markdown rejects invalid contents directives" {
3583     try std.testing.expectError(error.InvalidContentsDirective, render(
3584         std.testing.allocator,
3585         "```zen-toc\n" ++
3586             "scope: nowhere\n" ++
3587             "```\n",
3588         .{},
3589     ));
3590 }
3591 
3592 test "markdown renders quiz fences with hidden answers" {
3593     var rendered = try render(
3594         std.testing.allocator,
3595         "```zen-quiz\n" ++
3596             "? Which module owns the grammar?\n" ++
3597             "- `markdown.zig`\n" ++
3598             "* `quiz.zig`\n" ++
3599             "! The typed data lives beside the renderer.\n" ++
3600             "```\n",
3601         .{},
3602     );
3603     defer rendered.deinit(std.testing.allocator);
3604     try std.testing.expectEqualStrings(
3605         "<section class=\"zen-quiz\" aria-label=\"Quiz\">\n" ++
3606             "<ol class=\"zen-quiz-questions\">\n" ++
3607             "<li class=\"zen-quiz-question\">\n" ++
3608             "<p class=\"zen-quiz-prompt\">Which module owns the grammar?</p>\n" ++
3609             "<ul class=\"zen-quiz-options\">\n" ++
3610             "<li><code>markdown.zig</code></li>\n" ++
3611             "<li><code>quiz.zig</code></li>\n" ++
3612             "</ul>\n" ++
3613             "<details class=\"zen-quiz-answer\">\n" ++
3614             "<summary>Reveal answer</summary>\n" ++
3615             "<p class=\"zen-quiz-correct\"><strong><code>quiz.zig</code></strong></p>\n" ++
3616             "<p class=\"zen-quiz-explanation\">The typed data lives beside the renderer.</p>\n" ++
3617             "</details>\n" ++
3618             "</li>\n" ++
3619             "</ol>\n" ++
3620             "</section>\n",
3621         rendered.html,
3622     );
3623 }
3624 
3625 test "markdown renders recall quiz questions without options" {
3626     var rendered = try render(
3627         std.testing.allocator,
3628         "```quiz\n" ++
3629             "? What does the reveal block hold?\n" ++
3630             "! The explanation.\n" ++
3631             "```\n",
3632         .{},
3633     );
3634     defer rendered.deinit(std.testing.allocator);
3635     try std.testing.expectEqualStrings(
3636         "<section class=\"zen-quiz\" aria-label=\"Quiz\">\n" ++
3637             "<ol class=\"zen-quiz-questions\">\n" ++
3638             "<li class=\"zen-quiz-question\">\n" ++
3639             "<p class=\"zen-quiz-prompt\">What does the reveal block hold?</p>\n" ++
3640             "<details class=\"zen-quiz-answer\">\n" ++
3641             "<summary>Reveal answer</summary>\n" ++
3642             "<p class=\"zen-quiz-explanation\">The explanation.</p>\n" ++
3643             "</details>\n" ++
3644             "</li>\n" ++
3645             "</ol>\n" ++
3646             "</section>\n",
3647         rendered.html,
3648     );
3649 }
3650 
3651 test "markdown rejects invalid quiz fences" {
3652     try std.testing.expectError(error.InvalidQuizDirective, render(
3653         std.testing.allocator,
3654         "```zen-quiz\n" ++
3655             "? A prompt with one lonely option\n" ++
3656             "* only\n" ++
3657             "```\n",
3658         .{},
3659     ));
3660 }
3661 
3662 test "Markdown reuses one quiz region across fences" {
3663     comptime {
3664         @stardustClaim(
3665             @import("alloc_phase").capacity.witness(@import("./quiz/root.zig").Storage, "zen_quiz_consumer"),
3666             null,
3667             null,
3668             null,
3669             null,
3670             null,
3671             null,
3672         );
3673     }
3674 
3675     const limits = quiz.Limits{
3676         .max_questions = 1,
3677         .max_options = 2,
3678         .max_joined_text_bytes = "First answer continued".len,
3679     };
3680     const capacity = try quiz.Capacity.derive(limits);
3681     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
3682     var owner: QuizOwner = .{};
3683     defer owner.deinit(counting.allocator());
3684 
3685     const first = try owner.parse(
3686         counting.allocator(),
3687         "? First\n! First answer\ncontinued\n",
3688         limits,
3689     );
3690     try std.testing.expectEqualStrings("First answer continued", first.questions[0].explanation);
3691     const base = @intFromPtr(owner.storage.?.bytes.ptr);
3692     owner.reset();
3693     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
3694     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
3695     counting.fail_index = counting.alloc_index;
3696     counting.resize_fail_index = counting.resize_index;
3697 
3698     const second = try owner.parse(
3699         counting.allocator(),
3700         "? Second\n* Correct\n- Other\n",
3701         limits,
3702     );
3703     try std.testing.expectEqualStrings("Second", second.questions[0].prompt);
3704     try std.testing.expectEqual(@as(usize, 2), second.questions[0].options.len);
3705     owner.reset();
3706     try std.testing.expectEqual(base, @intFromPtr(owner.storage.?.bytes.ptr));
3707     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
3708     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
3709 }
3710 
3711 test "Markdown exposes quiz limits at the render boundary" {
3712     try std.testing.expectError(error.QuestionCapacityExceeded, render(
3713         std.testing.allocator,
3714         "```quiz\n? Question\n! Answer\n```\n",
3715         .{ .quiz_limits = .{
3716             .max_questions = 0,
3717             .max_options = 0,
3718             .max_joined_text_bytes = 0,
3719         } },
3720     ));
3721 }
3722 
3723 test "Markdown exposes heading limits at the render boundary" {
3724     try std.testing.expectError(error.HeadingCapacityExceeded, render(
3725         std.testing.allocator,
3726         "# Heading\n",
3727         .{ .heading_limits = .{
3728             .max_headings = 0,
3729             .max_text_bytes = 0,
3730             .max_id_bytes = 0,
3731         } },
3732     ));
3733 }
3734 
3735 test "Markdown exposes footnote limits at the render boundary" {
3736     const source = "[^one]: First\n  continued\n";
3737     try std.testing.expectError(error.FootnoteDefinitionCapacityExceeded, render(
3738         std.testing.allocator,
3739         source,
3740         .{ .footnote_limits = .{
3741             .max_definitions = 0,
3742             .max_joined_text_bytes = 0,
3743         } },
3744     ));
3745     try std.testing.expectError(error.FootnoteJoinedTextByteCapacityExceeded, render(
3746         std.testing.allocator,
3747         source,
3748         .{ .footnote_limits = .{
3749             .max_definitions = 1,
3750             .max_joined_text_bytes = "First continued".len - 1,
3751         } },
3752     ));
3753 }
3754 
3755 test "markdown renders explicit slide decks" {
3756     var rendered = try render(
3757         std.testing.allocator,
3758         "# Title\n\n" ++
3759             "---\n\n" ++
3760             "## Second\n\n" ++
3761             "---\n",
3762         .{ .mode = .slides, .slides = .{ .runtime = false } },
3763     );
3764     defer rendered.deinit(std.testing.allocator);
3765     try std.testing.expectEqualStrings(
3766         "<div class=\"zen-slides\" data-zen-slides>\n" ++
3767             "<section class=\"zen-slide\" id=\"slide-1\" aria-label=\"Slide 1\">\n" ++
3768             "<div class=\"zen-slide-frame\">\n" ++
3769             "<h1 id=\"title\">Title</h1>\n" ++
3770             "</div>\n" ++
3771             "</section>\n" ++
3772             "<section class=\"zen-slide\" id=\"slide-2\" aria-label=\"Slide 2\">\n" ++
3773             "<div class=\"zen-slide-frame\">\n" ++
3774             "<h2 id=\"second\">Second</h2>\n" ++
3775             "</div>\n" ++
3776             "</section>\n" ++
3777             "</div>\n" ++
3778             slides.print.stylesheet,
3779         rendered.html,
3780     );
3781 }
3782 
3783 test "markdown renders speaker notes in slide decks" {
3784     var rendered = try render(
3785         std.testing.allocator,
3786         "# Title\n\n" ++
3787             "```notes\n" ++
3788             "Remember the concrete example.\n" ++
3789             "```\n",
3790         .{ .mode = .slides, .slides = .{ .runtime = false } },
3791     );
3792     defer rendered.deinit(std.testing.allocator);
3793     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<aside class=\"zen-speaker-notes\" data-zen-speaker-notes hidden>") != null);
3794     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<p>Remember the concrete example.</p>") != null);
3795     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "zen-code-block") == null);
3796 }
3797 
3798 test "markdown drops empty speaker notes in slide decks" {
3799     var rendered = try render(
3800         std.testing.allocator,
3801         "# Title\n\n" ++
3802             "```notes\n" ++
3803             "\n" ++
3804             "```\n",
3805         .{ .mode = .slides, .slides = .{ .runtime = false } },
3806     );
3807     defer rendered.deinit(std.testing.allocator);
3808     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<aside class=\"zen-speaker-notes\"") == null);
3809 }
3810 
3811 test "markdown leaves notes fences as code outside slide decks" {
3812     var rendered = try render(
3813         std.testing.allocator,
3814         "```notes\n" ++
3815             "ordinary note code\n" ++
3816             "```\n",
3817         .{},
3818     );
3819     defer rendered.deinit(std.testing.allocator);
3820     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "zen-speaker-notes") == null);
3821     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "ordinary note code") != null);
3822 }
3823 
3824 test "markdown opens wide document figures at full size" {
3825     var rendered = try render(
3826         std.testing.allocator,
3827         "![Plot](/plot.svg){.zen-slide-figure-wide}\n",
3828         .{},
3829     );
3830     defer rendered.deinit(std.testing.allocator);
3831     try std.testing.expectEqualStrings(
3832         "<p class=\"zen-media-block zen-role-wide\">" ++
3833             "<a href=\"/plot.svg\" target=\"_blank\" rel=\"noopener\" " ++
3834             "aria-label=\"Open Plot at full size\">" ++
3835             "<img src=\"/plot.svg\" alt=\"Plot\" " ++
3836             "class=\"zen-slide-figure-wide\"></a></p>\n",
3837         rendered.html,
3838     );
3839 }
3840 
3841 test "markdown slide decks preserve media math code and diagrams" {
3842     var rendered = try render(
3843         std.testing.allocator,
3844         "# Media\n\n" ++
3845             "![Plot](/plot.svg){.zen-slide-figure-wide}\n\n" ++
3846             "---\n\n" ++
3847             "## Math and Code\n\n" ++
3848             "$$ E = mc^2 $$\n\n" ++
3849             "```zig\n" ++
3850             "const x = 1 < 2;\n" ++
3851             "```\n\n" ++
3852             "---\n\n" ++
3853             "```zen-diagram\n" ++
3854             "{\"kind\":\"frame\",\"title\":\"Coin\",\"y_min\":0,\"y_max\":1}\n" ++
3855             "{\"kind\":\"bar\",\"x\":\"heads\",\"y\":0.62,\"label\":\"heads\"}\n" ++
3856             "```\n",
3857         .{ .mode = .slides, .diagram_width = 36, .diagram_height = 10 },
3858     );
3859     defer rendered.deinit(std.testing.allocator);
3860     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<section class=\"zen-slide\" id=\"slide-3\"") != null);
3861     try std.testing.expect(std.mem.indexOf(
3862         u8,
3863         rendered.html,
3864         "<p class=\"zen-media-block zen-role-reading\">" ++
3865             "<img src=\"/plot.svg\" alt=\"Plot\" " ++
3866             "class=\"zen-slide-figure-wide\"></p>",
3867     ) != null);
3868     try std.testing.expect(std.mem.indexOf(
3869         u8,
3870         rendered.html,
3871         "<a href=\"/plot.svg\"",
3872     ) == null);
3873     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<math display=\"block\">") != null);
3874     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "zen-code-block") != null);
3875     try std.testing.expect(std.mem.indexOf(
3876         u8,
3877         rendered.html,
3878         "<pre class=\"zen-diagram zen-role-wide\"><code>",
3879     ) != null);
3880     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<hr>") == null);
3881 }
3882 
3883 test "markdown renders webm figures as looping videos with poster fallback" {
3884     var rendered = try render(
3885         std.testing.allocator,
3886         "![Dam break](/assets/sph.webm){.zen-slide-figure-wide}\n",
3887         .{},
3888     );
3889     defer rendered.deinit(std.testing.allocator);
3890     try std.testing.expectEqualStrings(
3891         "<p class=\"zen-media-block zen-role-wide\">" ++
3892             "<video src=\"/assets/sph.webm\" poster=\"/assets/sph.webp\" " ++
3893             "aria-label=\"Dam break\" autoplay loop muted playsinline " ++
3894             "class=\"zen-slide-figure-wide\"></video></p>\n",
3895         rendered.html,
3896     );
3897 }
3898 
3899 test "markdown renders motion figures as static posters" {
3900     var rendered = try render(
3901         std.testing.allocator,
3902         "![Agent loop](/assets/loop.svg?revision=1){.zen-slide-motion\n.zen-slide-figure-wide}\n",
3903         .{ .mode = .slides, .slides = .{ .runtime = false } },
3904     );
3905     defer rendered.deinit(std.testing.allocator);
3906     try std.testing.expect(std.mem.indexOf(
3907         u8,
3908         rendered.html,
3909         "<img src=\"/assets/loop.poster.svg?revision=1\" data-zen-motion-src=\"/assets/loop.svg?revision=1\" alt=\"Agent loop\" decoding=\"async\" class=\"zen-slide-motion zen-slide-figure-wide\">",
3910     ) != null);
3911 }
3912 
3913 test "markdown renders a bounded widget selector on an image" {
3914     var rendered = try render(
3915         std.testing.allocator,
3916         "![Accy demo](/assets/poster.svg){.demo data-widget=\"accy-wos\"}\n",
3917         .{},
3918     );
3919     defer rendered.deinit(std.testing.allocator);
3920     try std.testing.expectEqualStrings(
3921         "<p class=\"zen-media-block zen-role-reading\">" ++
3922             "<img src=\"/assets/poster.svg\" alt=\"Accy demo\" " ++
3923             "class=\"demo\" data-widget=\"accy-wos\"></p>\n",
3924         rendered.html,
3925     );
3926 }
3927 
3928 test "markdown renders lists blockquotes code fences and images" {
3929     var rendered = try render(
3930         std.testing.allocator,
3931         "- one\n" ++
3932             "- two\n\n" ++
3933             "> quoted\n" ++
3934             "> text\n\n" ++
3935             "```zig\n" ++
3936             "const x = 1 < 2;\n" ++
3937             "```\n\n" ++
3938             "![Logo](/logo.svg)\n",
3939         .{},
3940     );
3941     defer rendered.deinit(std.testing.allocator);
3942     try std.testing.expectEqualStrings(
3943         "<ul>\n" ++
3944             "<li>one</li>\n" ++
3945             "<li>two</li>\n" ++
3946             "</ul>\n" ++
3947             "<blockquote><p>quoted text</p></blockquote>\n" ++
3948             "<div class=\"zen-code-block zen-code-wrap zen-code-example " ++
3949             "zen-role-reading\" data-language=\"zig\"><div class=\"zen-code-header\">" ++
3950             "<span class=\"zen-code-language\">zig</span></div>" ++
3951             "<pre class=\"zen-code\"><code class=\"language-zig\">" ++
3952             "<span class=\"zen-code-line\"><span class=\"zen-code-line-number\" " ++
3953             "aria-hidden=\"true\">1</span><span class=\"zen-code-line-source\">" ++
3954             "<span class=\"zen-code-keyword\">const</span> x = " ++
3955             "<span class=\"zen-code-number\">1</span> &lt; " ++
3956             "<span class=\"zen-code-number\">2</span>;</span></span>" ++
3957             "</code></pre></div>\n" ++
3958             "<p class=\"zen-media-block zen-role-reading\">" ++
3959             "<img src=\"/logo.svg\" alt=\"Logo\"></p>\n",
3960         rendered.html,
3961     );
3962 }
3963 
3964 test "markdown renders continued list items and blockquote inline markup" {
3965     var rendered = try render(
3966         std.testing.allocator,
3967         "- first item wraps\n" ++
3968             "  onto the next line\n" ++
3969             "- second _Item_\n\n" ++
3970             "> **John R. Levine, _Linkers &\n" ++
3971             "> Loaders_**\n",
3972         .{},
3973     );
3974     defer rendered.deinit(std.testing.allocator);
3975     try std.testing.expectEqualStrings(
3976         "<ul>\n" ++
3977             "<li>first item wraps onto the next line</li>\n" ++
3978             "<li>second <em>Item</em></li>\n" ++
3979             "</ul>\n" ++
3980             "<blockquote><p><strong>John R. Levine, <em>Linkers &amp; Loaders</em></strong></p></blockquote>\n",
3981         rendered.html,
3982     );
3983 }
3984 
3985 test "markdown renders footnote references and definitions" {
3986     comptime {
3987         @stardustClaim(
3988             @import("alloc_phase").capacity.witness(@import("./footnote/root.zig").Storage, "zen_footnote_consumer"),
3989             null,
3990             null,
3991             null,
3992             null,
3993             null,
3994             null,
3995         );
3996     }
3997 
3998     var rendered = try render(
3999         std.testing.allocator,
4000         "Alpha[^paper] beta[^paper] gamma[^block] missing[^none].\n\n" ++
4001             "[^paper]: A [paper](/paper)\n" ++
4002             "  with `code`.\n" ++
4003             "[^block]:\n" ++
4004             "    Block style.\n" ++
4005             "[^unused]: Hidden.\n",
4006         .{},
4007     );
4008     defer rendered.deinit(std.testing.allocator);
4009     try std.testing.expectEqualStrings(
4010         "<p>Alpha<sup id=\"fnref-paper\"><a class=\"footnote-ref\" href=\"#fn-paper\">[1]</a></sup> beta<sup id=\"fnref-paper-2\"><a class=\"footnote-ref\" href=\"#fn-paper\">[1]</a></sup> gamma<sup id=\"fnref-block\"><a class=\"footnote-ref\" href=\"#fn-block\">[2]</a></sup> missing[^none].</p>\n" ++
4011             "<section class=\"footnotes\" aria-label=\"References\">\n" ++
4012             "<ol>\n" ++
4013             "<li id=\"fn-paper\">A <a href=\"/paper\">paper</a> with <code>code</code>. <a class=\"footnote-backref\" href=\"#fnref-paper\" aria-label=\"Back to reference\">&#8617;</a></li>\n" ++
4014             "<li id=\"fn-block\">Block style. <a class=\"footnote-backref\" href=\"#fnref-block\" aria-label=\"Back to reference\">&#8617;</a></li>\n" ++
4015             "</ol>\n" ++
4016             "</section>\n",
4017         rendered.html,
4018     );
4019 }
4020 
4021 test "markdown keeps the first duplicate footnote definition" {
4022     var rendered = try render(
4023         std.testing.allocator,
4024         "Use[^same].\n\n" ++
4025             "[^same]: First.\n" ++
4026             "[^same]: Second\n" ++
4027             "  ignored continuation.\n",
4028         .{},
4029     );
4030     defer rendered.deinit(std.testing.allocator);
4031     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "First.") != null);
4032     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "Second") == null);
4033 }
4034 
4035 test "markdown renders inline and display math" {
4036     var rendered = try render(
4037         std.testing.allocator,
4038         "Euler saw $e^{i\\pi} + 1 = 0$ here.\n\n" ++
4039             "$$\n" ++
4040             "u(x) = \\mathbb{E}[u(x + R\\omega)]\n" ++
4041             "$$\n",
4042         .{},
4043     );
4044     defer rendered.deinit(std.testing.allocator);
4045     try std.testing.expectEqualStrings(
4046         "<p>Euler saw <math><mrow><msup><mi>e</mi><mrow><mi>i</mi><mi>π</mi></mrow></msup>" ++
4047             "<mo>+</mo><mn>1</mn><mo>=</mo><mn>0</mn></mrow></math> here.</p>\n" ++
4048             "<math display=\"block\"><mrow><mi>u</mi><mo stretchy=\"false\">(</mo><mi>x</mi><mo stretchy=\"false\">)</mo><mo>=</mo>" ++
4049             "<mi>𝔼</mi><mo stretchy=\"false\">[</mo><mi>u</mi><mo stretchy=\"false\">(</mo><mi>x</mi><mo>+</mo><mi>R</mi><mi>ω</mi>" ++
4050             "<mo stretchy=\"false\">)</mo><mo stretchy=\"false\">]</mo></mrow></math>\n",
4051         rendered.html,
4052     );
4053 }
4054 
4055 test "markdown renders single line display math" {
4056     var rendered = try render(std.testing.allocator, "$$ E = mc^2 $$\n", .{});
4057     defer rendered.deinit(std.testing.allocator);
4058     try std.testing.expectEqualStrings(
4059         "<math display=\"block\"><mrow><mi>E</mi><mo>=</mo><mi>m</mi>" ++
4060             "<msup><mi>c</mi><mn>2</mn></msup></mrow></math>\n",
4061         rendered.html,
4062     );
4063 }
4064 
4065 test "markdown renders display environments across lines" {
4066     var rendered = try render(
4067         std.testing.allocator,
4068         "$$\n" ++
4069             "\\begin{aligned}\n" ++
4070             "a &= b \\\\\n" ++
4071             "&= c\n" ++
4072             "\\end{aligned}\n" ++
4073             "$$\n",
4074         .{},
4075     );
4076     defer rendered.deinit(std.testing.allocator);
4077     try std.testing.expectEqualStrings(
4078         "<math display=\"block\"><mtable displaystyle=\"true\" class=\"zen-aligned\">" ++
4079             "<mtr><mtd><mi>a</mi></mtd><mtd><mo>=</mo><mi>b</mi></mtd></mtr>" ++
4080             "<mtr><mtd></mtd><mtd><mo>=</mo><mi>c</mi></mtd></mtr>" ++
4081             "</mtable></math>\n",
4082         rendered.html,
4083     );
4084 }
4085 
4086 test "markdown guards prose dollars and honors escapes" {
4087     var rendered = try render(
4088         std.testing.allocator,
4089         "It costs $5 and $10 to run, but \\$x\\$ stays literal.\n",
4090         .{},
4091     );
4092     defer rendered.deinit(std.testing.allocator);
4093     try std.testing.expectEqualStrings(
4094         "<p>It costs $5 and $10 to run, but $x$ stays literal.</p>\n",
4095         rendered.html,
4096     );
4097 }
4098 
4099 test "markdown fails loudly on invalid math" {
4100     try std.testing.expectError(error.InvalidEquation, render(
4101         std.testing.allocator,
4102         "Broken $\\nonesuch$ command.\n",
4103         .{},
4104     ));
4105     try std.testing.expectError(error.InvalidEquation, render(
4106         std.testing.allocator,
4107         "$$\nx = 1\n",
4108         .{},
4109     ));
4110 }
4111 
4112 test "markdown names the offending equation" {
4113     var diag: Diagnostic = .{};
4114     try std.testing.expectError(error.InvalidEquation, render(
4115         std.testing.allocator,
4116         "Broken $\\nonesuch$ command.\n",
4117         .{ .diagnostic = &diag },
4118     ));
4119     try std.testing.expectEqualStrings("\\nonesuch", diag.equation());
4120     try std.testing.expectEqualStrings("unknown command", diag.reason);
4121     try std.testing.expectEqual(@as(usize, 0), diag.offset);
4122 
4123     diag.reset();
4124     try std.testing.expectError(error.InvalidEquation, render(
4125         std.testing.allocator,
4126         "$$\nu(x) = \\frac{1}\n$$\n",
4127         .{ .diagnostic = &diag },
4128     ));
4129     try std.testing.expectEqualStrings("u(x) = \\frac{1}", diag.equation());
4130     try std.testing.expectEqualStrings("missing argument", diag.reason);
4131     try std.testing.expectEqual(@as(usize, 15), diag.offset);
4132 
4133     diag.reset();
4134     try std.testing.expectError(error.InvalidEquation, render(
4135         std.testing.allocator,
4136         "$$\nx = 1\n",
4137         .{ .diagnostic = &diag },
4138     ));
4139     try std.testing.expectEqualStrings("x = 1", diag.equation());
4140     try std.testing.expectEqualStrings("missing closing '$$'", diag.reason);
4141 }
4142 
4143 test "markdown leaves math inside code spans alone" {
4144     var rendered = try render(std.testing.allocator, "Use `$x$` verbatim.\n", .{});
4145     defer rendered.deinit(std.testing.allocator);
4146     try std.testing.expectEqualStrings(
4147         "<p>Use <code>$x$</code> verbatim.</p>\n",
4148         rendered.html,
4149     );
4150 }
4151 
4152 test "markdown renders diagram fences as ASCII" {
4153     comptime {
4154         @stardustClaim(
4155             @import("alloc_phase").capacity.witness(@import("./diagram/ascii/root.zig").RenderStorage, "zen_ascii_render_consumer"),
4156             null,
4157             null,
4158             null,
4159             null,
4160             null,
4161             null,
4162         );
4163     }
4164 
4165     var rendered = try render(
4166         std.testing.allocator,
4167         "```zen-diagram\n" ++
4168             "{\"kind\":\"frame\",\"title\":\"Coin\",\"y_min\":0,\"y_max\":1}\n" ++
4169             "{\"kind\":\"bar\",\"x\":\"heads\",\"y\":0.62,\"label\":\"heads\"}\n" ++
4170             "```\n",
4171         .{ .diagram_width = 36, .diagram_height = 10 },
4172     );
4173     defer rendered.deinit(std.testing.allocator);
4174     try std.testing.expect(std.mem.indexOf(
4175         u8,
4176         rendered.html,
4177         "<pre class=\"zen-diagram zen-role-wide\"><code>",
4178     ) != null);
4179     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "Coin") != null);
4180     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "#") != null);
4181     try std.testing.expect(std.mem.indexOf(u8, rendered.html, "{\"kind\"") == null);
4182 }
4183 
4184 test "link reports locate a direct paragraph target" {
4185     const source = "See [the allocator](allocator.zig) for detail.\n";
4186     var storage: [4]RenderedLink = undefined;
4187     var report: LinkReport = .{ .storage = &storage };
4188     var rendered = try render(
4189         std.testing.allocator,
4190         source,
4191         .{ .link_report = &report },
4192     );
4193     defer rendered.deinit(std.testing.allocator);
4194     const links = report.links();
4195     try std.testing.expectEqual(@as(usize, 1), links.len);
4196     try std.testing.expect(links[0].located);
4197     try std.testing.expect(links[0].contiguous);
4198     try std.testing.expect(!links[0].image);
4199     try std.testing.expectEqualStrings(
4200         "allocator.zig",
4201         source[links[0].target_start..links[0].target_end],
4202     );
4203     try std.testing.expectEqualStrings(
4204         "[the allocator](allocator.zig)",
4205         source[links[0].start..links[0].end],
4206     );
4207 }
4208 
4209 test "link reports locate a target carried across joined source lines" {
4210     const source = "See [the\nallocator](allocator.zig) for detail.\n";
4211     var storage: [4]RenderedLink = undefined;
4212     var report: LinkReport = .{ .storage = &storage };
4213     var rendered = try render(
4214         std.testing.allocator,
4215         source,
4216         .{ .link_report = &report },
4217     );
4218     defer rendered.deinit(std.testing.allocator);
4219     const links = report.links();
4220     try std.testing.expectEqual(@as(usize, 1), links.len);
4221     try std.testing.expect(links[0].located);
4222     try std.testing.expect(links[0].contiguous);
4223     try std.testing.expectEqualStrings(
4224         "allocator.zig",
4225         source[links[0].target_start..links[0].target_end],
4226     );
4227 }
4228 
4229 test "link reports emit a nested image after its enclosing link" {
4230     const source = "[![badge](badge.svg)](capacity/root.zig)\n";
4231     var storage: [4]RenderedLink = undefined;
4232     var report: LinkReport = .{ .storage = &storage };
4233     var rendered = try render(
4234         std.testing.allocator,
4235         source,
4236         .{ .link_report = &report },
4237     );
4238     defer rendered.deinit(std.testing.allocator);
4239     const links = report.links();
4240     try std.testing.expectEqual(@as(usize, 2), links.len);
4241     try std.testing.expect(!links[0].image);
4242     try std.testing.expect(links[1].image);
4243     try std.testing.expectEqualStrings(
4244         "capacity/root.zig",
4245         source[links[0].target_start..links[0].target_end],
4246     );
4247     try std.testing.expectEqualStrings(
4248         "badge.svg",
4249         source[links[1].target_start..links[1].target_end],
4250     );
4251     try std.testing.expect(links[1].target_start < links[0].target_start);
4252 }
4253 
4254 test "link reports name the exhausted report capacity" {
4255     var storage: [1]RenderedLink = undefined;
4256     var report: LinkReport = .{ .storage = &storage };
4257     var rendered = try render(
4258         std.testing.allocator,
4259         "[a](a.zig) and [b](b.zig)\n",
4260         .{ .link_report = &report },
4261     );
4262     defer rendered.deinit(std.testing.allocator);
4263     try std.testing.expect(report.overflowed);
4264     try std.testing.expectEqual(@as(usize, 1), report.count);
4265 }
4266 
4267 test "link reports ignore targets inside fenced and inline code" {
4268     const source = "```\n[a](a.zig)\n```\n\nText `[b](b.zig)` text.\n";
4269     var storage: [4]RenderedLink = undefined;
4270     var report: LinkReport = .{ .storage = &storage };
4271     var rendered = try render(
4272         std.testing.allocator,
4273         source,
4274         .{ .link_report = &report },
4275     );
4276     defer rendered.deinit(std.testing.allocator);
4277     try std.testing.expectEqual(@as(usize, 0), report.links().len);
4278 }
4279 
4280 test "link reporting leaves ordinary rendered output unchanged" {
4281     const source =
4282         "# Title\n\nSee [one](one.zig) and ![two](two.svg).\n\n" ++
4283         "- item [three](three.zig)\n- item two\n\n" ++
4284         "> quoted [four](four.zig)\n\n" ++
4285         "| head | head |\n| :--- | :--- |\n| [five](five.zig) | cell |\n";
4286     var plain = try render(std.testing.allocator, source, .{});
4287     defer plain.deinit(std.testing.allocator);
4288     var storage: [16]RenderedLink = undefined;
4289     var report: LinkReport = .{ .storage = &storage };
4290     var reported = try render(
4291         std.testing.allocator,
4292         source,
4293         .{ .link_report = &report },
4294     );
4295     defer reported.deinit(std.testing.allocator);
4296     try std.testing.expectEqualStrings(plain.html, reported.html);
4297     try std.testing.expectEqual(@as(usize, 5), report.links().len);
4298 }
4299 
4300 test "link reports cover list, blockquote and table blocks" {
4301     const source =
4302         "- item [one](one.zig)\n\n" ++
4303         "> quoted [two](two.zig)\n\n" ++
4304         "| head |\n| :--- |\n| [three](three.zig) |\n";
4305     var storage: [8]RenderedLink = undefined;
4306     var report: LinkReport = .{ .storage = &storage };
4307     var rendered = try render(
4308         std.testing.allocator,
4309         source,
4310         .{ .link_report = &report },
4311     );
4312     defer rendered.deinit(std.testing.allocator);
4313     const links = report.links();
4314     try std.testing.expectEqual(@as(usize, 3), links.len);
4315     for (links, [_][]const u8{ "one.zig", "two.zig", "three.zig" }) |link, expected| {
4316         try std.testing.expect(link.located);
4317         try std.testing.expect(link.contiguous);
4318         try std.testing.expectEqualStrings(
4319             expected,
4320             source[link.target_start..link.target_end],
4321         );
4322     }
4323 }