tiny.zen.markdown
Defined in tiny.zen.
API (13)
Actions
Public operations.
inspect: Fills active caller-owned storage; the returned document borrows it until reset.plan: Computes exact heading, text, and ID demand without allocating.render: Renders Markdown and returns an owner that must be deinitialized with the same allocator.
Types and contracts
Public types and contracts.
Diagnostic: Fixed-capacity detail for a failed page, equation, heading, or inline feature.ErrorLinkReport: Caller-owned, fixed-capacity sink for rendered link records.Mode: Selects ordinary document HTML or slide-deck HTML.Options: Bounds Markdown features and optionally captures a source-local diagnostic.Rendered: Owns the rendered HTML and the storage borrowed by its document view.RenderedLink: One link or image the renderer emitted, located in its rendered source.
Values and defaults
Public values and defaults.
max_inline_delimiters: Maximum backtick runs in one rendered inline fragment.max_inline_nesting: Maximum recursive link/emphasis levels, label brackets, or target parentheses.max_table_body_rows: Maximum body rows in one semantic pipe table admitted by the block parser.
Source
Source: lib/zen/src/markdown.zig
zig
const std = @import("std");const code = @import("code.zig");const diagnostic = @import("diagnostic.zig");const diagram = @import("diagram/root.zig");const document = @import("document/root.zig");const footnote = @import("footnote/root.zig");const html = @import("html.zig");const markdown_model = @import("markdown");const math = @import("math/root.zig");const quiz = @import("quiz/root.zig");const slides = @import("slides/root.zig");const Allocator = std.mem.Allocator;pub const Diagnostic = diagnostic.Diagnostic;const RenderError = error{ InvalidColor, InvalidContentsDirective, InvalidCharacter, InvalidData, InvalidField, InvalidFrame, InlineDelimiterCapacityExceeded, InvalidInlineLink, InvalidJsonl, InvalidRecord, InvalidScale, InvalidSemanticList, InvalidTransform, InlineNestingCapacityExceeded, MissingField, MissingKind, UnknownData, UnknownMark, UnknownRecordKind, UnknownTransform,};pub const Error = Allocator.Error || diagram.AsciiRenderError || diagram.DocumentError || diagram.ego.svg.FragmentError || document.Error || footnote.Error || math.Error || quiz.Error || RenderError;const InlineError = math.Error || error{ InlineDelimiterCapacityExceeded, InvalidInlineLink, InlineNestingCapacityExceeded,};const CodeDelimiter = struct { start: usize, end: usize, length: usize, closer_end: ?usize = null,};/// Maximum backtick runs in one rendered inline fragment.////// Exceeding it returns `InlineDelimiterCapacityExceeded`; split the paragraph/// or code-bearing label into smaller block-level elements. The delimiter index/// occupies at most 2.5 MiB on the supported 64-bit target and is reused by/// nested inline markup.pub const max_inline_delimiters: usize = 65_536;/// Maximum body rows in one semantic pipe table admitted by the block parser.pub const max_table_body_rows: usize = markdown_model.max_table_rows - 1;const code_delimiter_bytes: usize = 40;const max_inline_delimiter_bytes: usize = max_inline_delimiters * code_delimiter_bytes;const delimiter_capacity_reason = std.fmt.comptimePrint( "inline source exceeds {d} backtick runs", .{max_inline_delimiters},);const CodeDelimiters = struct { items: []CodeDelimiter, fn deinit(self: *CodeDelimiters, allocator: Allocator) void { allocator.free(self.items); self.* = undefined; } fn closingEnd(self: CodeDelimiters, start: usize, range_end: usize) ?usize { var low: usize = 0; var high = self.items.len; while (low < high) { const middle = low + (high - low) / 2; if (self.items[middle].start < start) { low = middle + 1; } else { high = middle; } } if (low == self.items.len or self.items[low].start != start) return null; const closer_end = self.items[low].closer_end orelse return null; return if (closer_end <= range_end) closer_end else null; } fn span( self: CodeDelimiters, source: []const u8, start: usize, range_end: usize, ) ?CodeSpan { const closing_end = self.closingEnd(start, range_end) orelse return null; const opener_end = backtickRunEnd(source, start); const delimiter_length = opener_end - start; return .{ .content = source[opener_end .. closing_end - delimiter_length], .end = closing_end, }; }};/// Maximum recursive link/emphasis levels, label brackets, or target parentheses.////// Exceeding it returns `InlineNestingCapacityExceeded`; split or simplify the/// nested inline markup.pub const max_inline_nesting: usize = 32;const inline_nesting_reason = std.fmt.comptimePrint( "inline markup nesting exceeds {d} levels", .{max_inline_nesting},);const label_nesting_reason = std.fmt.comptimePrint( "link label nesting exceeds {d} brackets", .{max_inline_nesting},);const target_nesting_reason = std.fmt.comptimePrint( "link target nesting exceeds {d} parentheses", .{max_inline_nesting},);/// Selects ordinary document HTML or slide-deck HTML.pub const Mode = enum { /// Renders a continuous Markdown document. document, /// Renders explicit slides and optional speaker notes. slides,};/// Bounds Markdown features and optionally captures a source-local diagnostic.pub const Options = struct { /// Chooses document or slide rendering. mode: Mode = .document, /// Emits heading IDs when true; IDs are still inspected when false. heading_anchors: bool = true, /// Renders `zen-diagram` fences when true and leaves them as code when false. ascii_diagrams: bool = true, /// Keeps one exact call-map stylesheet per rendered page when true. one_call_map_style_per_page: bool = false, /// Requested ASCII width, clamped to 24...200 columns. diagram_width: usize = 72, /// Requested ASCII height, clamped to 8...80 rows. diagram_height: usize = 22, /// Controls syntax-highlighted code blocks. code_blocks: code.Options = .{}, /// Rejects Markdown whose measured heading demand exceeds these limits. heading_limits: document.Limits = document.default_limits, /// Rejects Markdown whose measured footnote demand exceeds these limits. footnote_limits: footnote.Limits = footnote.default_limits, /// Rejects one quiz fence whose measured demand exceeds these limits. quiz_limits: quiz.Limits = quiz.default_limits, /// Controls slide layout when `mode` is `slides`. slides: slides.Options = .{}, /// Receives source-local repair detail after a supported parse failure. diagnostic: ?*diagnostic.Diagnostic = null, /// Receives one record per rendered link and image when set. link_report: ?*LinkReport = null,};/// Owns the rendered HTML and the storage borrowed by its document view./// Release it with `deinit` and the allocator passed to `render`.pub const Rendered = struct { html: []u8, document: document.Document, document_storage: document.Storage, pub fn deinit(self: *Rendered, allocator: Allocator) void { allocator.free(self.html); self.document_storage.reset(); self.document_storage.deinit(allocator); self.* = undefined; }};/// One link or image the renderer emitted, located in its rendered source.////// Offsets are valid only under the flag that admits them: `start` and `end`/// under `located`, the target pair under `located` and a locatable target.pub const RenderedLink = struct { start: u32 = 0, end: u32 = 0, target_start: u32 = 0, target_end: u32 = 0, located: bool = false, contiguous: bool = false, image: bool = false,};/// Caller-owned, fixed-capacity sink for rendered link records.////// Records arrive in emission order, which is not document order: an/// enclosing link is recorded before the label it contains.pub const LinkReport = struct { storage: []RenderedLink, count: usize = 0, overflowed: bool = false, /// Drops every record and clears the overflow flag. pub fn reset(self: *LinkReport) void { self.count = 0; self.overflowed = false; } /// Returns the records, borrowing `storage`. pub fn links(self: *const LinkReport) []const RenderedLink { return self.storage[0..self.count]; } fn record(self: *LinkReport, link: RenderedLink) void { if (self.count == self.storage.len) { self.overflowed = true; return; } self.storage[self.count] = link; self.count += 1; }};const Fence = struct { marker: u8, count: usize, info: []const u8, source_offset: usize = 0,};const EgoStylesheet = struct { output_start: usize, length: usize,};const QuizOwner = struct { storage: ?quiz.Storage = null, fn deinit(self: *QuizOwner, allocator: Allocator) void { if (self.storage) |*storage| storage.deinit(allocator); self.* = .{}; } fn parse( self: *QuizOwner, allocator: Allocator, source: []const u8, limits: quiz.Limits, ) Error!quiz.Quiz { if (self.storage == null) { var storage = try quiz.Storage.init(allocator, limits); storage.activate(); self.storage = storage; } const storage = if (self.storage) |*active| active else unreachable; std.debug.assert(std.meta.eql(storage.capacity.limits, limits)); return quiz.parse(storage, source); } fn reset(self: *QuizOwner) void { const storage = if (self.storage) |*active| active else unreachable; storage.reset(); }};const ListKind = enum { unordered, ordered,};const SemanticList = enum { family_index, module_index,};/// Computes exact heading, text, and ID demand without allocating.pub fn plan(source: []const u8) document.Error!document.Plan { return planDiagnostic(source, null);}fn planDiagnostic( source: []const u8, diag: ?*diagnostic.Diagnostic,) document.Error!document.Plan { try validateHeadingAnchors(source, diag); var result = document.Plan.init(source.len); var iterator = HeadingIterator.init(source, diag); while (try iterator.next()) |heading| { const id = try resolveHeadingId(source, heading); try result.includeHeading(heading.text, try id.length()); } return result;}/// Fills active caller-owned storage; the returned document borrows it until reset.pub fn inspect(storage: *document.Storage, source: []const u8) document.Error!document.Document { return inspectPlanned(storage, source, try plan(source));}fn inspectPlanned( storage: *document.Storage, source: []const u8, heading_plan: document.Plan,) document.Error!document.Document { const regions = try storage.acquire(heading_plan); var heading_index: usize = 0; var text_index: usize = 0; var id_index: usize = 0; var iterator = HeadingIterator.init(source, null); while (try iterator.next()) |heading| { const text = regions.text[text_index..][0..heading.text.len]; @memcpy(text, heading.text); text_index += text.len; const resolved = try resolveHeadingId(source, heading); const id_length = try resolved.length(); const id = resolved.write(regions.ids[id_index..][0..id_length]); id_index += id.len; regions.headings[heading_index] = .{ .level = heading.level, .text = text, .id = id, .source_offset = heading.source_offset, }; heading_index += 1; } std.debug.assert(heading_index == regions.plan.headings); std.debug.assert(text_index == regions.plan.text_bytes); std.debug.assert(id_index == regions.plan.id_bytes); return .{ .headings = regions.headings };}/// Renders Markdown and returns an owner that must be deinitialized with the same allocator.////// A heading may end in `{#exact.id}`. Explicit IDs are exact, case-sensitive,/// page-wide unique, and omitted from the visible heading and contents label.pub fn render(allocator: Allocator, source: []const u8, options: Options) Error!Rendered { const heading_plan = try planDiagnostic(source, options.diagnostic); try heading_plan.require(options.heading_limits); var document_storage = try document.Storage.init(allocator, heading_plan.exactLimits()); errdefer document_storage.deinit(allocator); document_storage.activate(); const doc = try inspectPlanned(&document_storage, source, heading_plan); errdefer document_storage.reset(); const footnote_plan = try footnote.Plan.inspect(source, options.footnote_limits); var footnote_storage = try footnote.Storage.init(allocator, footnote_plan.exactLimits()); defer footnote_storage.deinit(allocator); footnote_storage.activate(); var footnotes = try footnote.parse(&footnote_storage, source); defer footnote_storage.reset(); var out: std.ArrayList(u8) = .empty; errdefer out.deinit(allocator); const track_origins = options.link_report != null; var paragraph: InlineBuffer = .{ .track = track_origins }; defer paragraph.deinit(allocator); var list_item: InlineBuffer = .{ .track = track_origins }; defer list_item.deinit(allocator); var blockquote: InlineBuffer = .{ .track = track_origins }; defer blockquote.deinit(allocator); var code_block: std.ArrayList(u8) = .empty; defer code_block.deinit(allocator); var math_block: std.ArrayList(u8) = .empty; defer math_block.deinit(allocator); var quiz_owner: QuizOwner = .{}; defer quiz_owner.deinit(allocator); var table_storage: markdown_model.TableStorage = .{}; var math_open = false; var cursor: usize = 0; var code_fence: ?Fence = null; var list_kind: ?ListKind = null; var semantic_list: ?SemanticList = null; var blockquote_open = false; var footnote_definition = false; var rendered_heading_index: usize = 0; var ego_stylesheet: ?EgoStylesheet = null; var deck = slides.Deck{ .options = options.slides }; if (options.mode == .slides) try deck.begin(&out, allocator); while (true) { const line_start = cursor; const raw_line = nextLine(source, &cursor) orelse break; const line = std.mem.trim(u8, raw_line, "\r"); if (code_fence) |fence| { if (closingFence(line, fence)) { try flushFence( allocator, &out, fence, code_block.items, options, doc, &quiz_owner, &ego_stylesheet, ); code_block.clearRetainingCapacity(); code_fence = null; } else { try code_block.appendSlice(allocator, line); try code_block.append(allocator, '\n'); } continue; } if (math_open) { const inner = std.mem.trim(u8, line, " \t"); if (std.mem.eql(u8, inner, "$$")) { try flushDisplayMath(allocator, &out, &math_block, options.diagnostic); math_open = false; } else if (std.mem.endsWith(u8, inner, "$$")) { if (math_block.items.len != 0) try math_block.append(allocator, ' '); try math_block.appendSlice(allocator, std.mem.trimEnd(u8, inner[0 .. inner.len - 2], " \t")); try flushDisplayMath(allocator, &out, &math_block, options.diagnostic); math_open = false; } else if (inner.len != 0) { if (math_block.items.len != 0) try math_block.append(allocator, ' '); try math_block.appendSlice(allocator, inner); } continue; } if (parseFence(line)) |fence| { if (semantic_list != null) return error.InvalidSemanticList; try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options); code_block.clearRetainingCapacity(); var opened = fence; opened.source_offset = line_start; code_fence = opened; footnote_definition = false; continue; } const trimmed = std.mem.trim(u8, line, " \t"); if (parseSemanticList(trimmed)) |role| { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList( allocator, &out, &list_kind, &list_item, &footnotes, options, ); try closeBlockquote( allocator, &out, &blockquote_open, &blockquote, &footnotes, options, ); if (semantic_list != null) return error.InvalidSemanticList; semantic_list = role; continue; } if (std.mem.startsWith(u8, trimmed, "$$")) { if (semantic_list != null) return error.InvalidSemanticList; try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options); footnote_definition = false; const rest = std.mem.trim(u8, trimmed[2..], " \t"); if (rest.len >= 2 and std.mem.endsWith(u8, rest, "$$")) { try math_block.appendSlice(allocator, std.mem.trimEnd(u8, rest[0 .. rest.len - 2], " \t")); try flushDisplayMath(allocator, &out, &math_block, options.diagnostic); } else { math_open = true; try math_block.appendSlice(allocator, rest); } continue; } if (trimmed.len == 0) { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options); footnote_definition = false; continue; } const parsed_list_item = parseListItem(line); if (semantic_list != null and parsed_list_item == null) { return error.InvalidSemanticList; } if (footnote.definition(line) != null) { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options); footnote_definition = true; continue; } if (footnote_definition and footnote.continuation(line) != null) continue; footnote_definition = false; const table_source = source[line_start..]; switch (markdown_model.parseTable(&table_storage, table_source)) { .table => |table| { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList( allocator, &out, &list_kind, &list_item, &footnotes, options, ); try closeBlockquote( allocator, &out, &blockquote_open, &blockquote, &footnotes, options, ); try appendTable( allocator, &out, source, table_source, table, &footnotes, options, ); cursor = line_start + table.source.end; continue; }, .paragraph, .rejected => {}, } if (documentationArtifactBlock(trimmed)) { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList( allocator, &out, &list_kind, &list_item, &footnotes, options, ); try closeBlockquote( allocator, &out, &blockquote_open, &blockquote, &footnotes, options, ); try appendInline( &out, allocator, trimmed, &footnotes, options.diagnostic, originWithin(source, trimmed), options.link_report, ); try out.append(allocator, '\n'); continue; } if (parseBlockquote(line)) |quote| { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); if (!blockquote_open) blockquote_open = true; try blockquote.appendLine(allocator, source, quote); continue; } try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options); if (parsed_list_item) |item| { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); if (list_kind == null or list_kind.? != item.kind) { try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); try openList(allocator, &out, item.kind, semantic_list); semantic_list = null; list_kind = item.kind; } try flushListItem(allocator, &out, &list_item, &footnotes, options); try list_item.appendLine(allocator, source, item.text); continue; } if (list_kind != null) { if (listContinuation(line)) |continuation| { try list_item.appendLine(allocator, source, continuation); continue; } } try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); if (try parseHeading(line, line_start, options.diagnostic)) |heading| { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); std.debug.assert(rendered_heading_index < doc.headings.len); const inspected = doc.headings[rendered_heading_index]; std.debug.assert(inspected.source_offset == line_start); std.debug.assert(std.mem.eql(u8, inspected.text, heading.text)); rendered_heading_index += 1; try out.appendSlice(allocator, "<h"); try appendDecimal(&out, allocator, heading.level); if (options.heading_anchors) { try out.appendSlice(allocator, " id=\""); try html.appendAttributeEscaped(&out, allocator, inspected.id); try out.append(allocator, '"'); } try out.append(allocator, '>'); try appendInline( &out, allocator, heading.text, &footnotes, options.diagnostic, originWithin(source, heading.text), options.link_report, ); try out.appendSlice(allocator, "</h"); try appendDecimal(&out, allocator, heading.level); try out.appendSlice(allocator, ">\n"); continue; } if (isThematicBreak(trimmed)) { try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options); if (options.mode == .slides) { try deck.split(&out, allocator); continue; } try out.appendSlice(allocator, "<hr>\n"); continue; } try paragraph.appendLine(allocator, source, trimmed); } if (math_open) { if (options.diagnostic) |d| d.setEquation(math_block.items, .{ .reason = "missing closing '$$'", .offset = math_block.items.len, }); return error.InvalidEquation; } if (code_fence) |fence| try flushFence( allocator, &out, fence, code_block.items, options, doc, &quiz_owner, &ego_stylesheet, ); if (semantic_list != null) return error.InvalidSemanticList; try flushParagraph(allocator, &out, ¶graph, &footnotes, options); try closeList(allocator, &out, &list_kind, &list_item, &footnotes, options); try closeBlockquote(allocator, &out, &blockquote_open, &blockquote, &footnotes, options); try appendFootnotes(allocator, &out, source, &footnotes, options); std.debug.assert(rendered_heading_index == doc.headings.len); if (options.mode == .slides) try deck.finish(&out, allocator); return .{ .html = try out.toOwnedSlice(allocator), .document = doc, .document_storage = document_storage, };}fn flushFence( allocator: Allocator, out: *std.ArrayList(u8), fence: Fence, source: []const u8, options: Options, doc: document.Document, quiz_owner: *QuizOwner, ego_stylesheet: *?EgoStylesheet,) Error!void { if (options.mode == .slides and notesFence(fence.info)) { try appendSpeakerNotes(allocator, out, source, options); return; } if (contentsFence(fence.info)) { try appendContents(allocator, out, doc, fence, source, options.diagnostic); return; } if (quizFence(fence.info)) { try appendQuiz( allocator, out, source, options.diagnostic, quiz_owner, options.quiz_limits, ); return; } if (egoFence(fence.info)) { try diagram.ego.svg.validateDocumentFragment(source); try appendEgoFragment( allocator, out, source, options.one_call_map_style_per_page, ego_stylesheet, ); return; } if (options.ascii_diagrams and diagramFence(fence.info)) { var rendered = try diagram.renderAscii(allocator, source, .{ .width = options.diagram_width, .height = options.diagram_height, }); defer rendered.deinit(allocator); try out.appendSlice( allocator, "<pre class=\"zen-diagram zen-role-wide\"><code>", ); try html.appendEscaped(out, allocator, rendered.output); try out.appendSlice(allocator, "</code></pre>\n"); return; } try code.render(allocator, out, fence.info, source, options.code_blocks);}fn appendEgoFragment( allocator: Allocator, out: *std.ArrayList(u8), source: []const u8, one_stylesheet_per_page: bool, stylesheet: *?EgoStylesheet,) Allocator.Error!void { const fragment = std.mem.trim(u8, source, " \t\r\n"); if (!one_stylesheet_per_page) { try out.appendSlice(allocator, fragment); try out.append(allocator, '\n'); return; } const range = egoStylesheetRange(fragment) orelse { try out.appendSlice(allocator, fragment); try out.append(allocator, '\n'); return; }; const candidate = fragment[range.start..range.end]; if (stylesheet.*) |first| { std.debug.assert(first.output_start <= out.items.len); std.debug.assert(first.length <= out.items.len - first.output_start); const retained = out.items[first.output_start..][0..first.length]; if (std.mem.eql(u8, retained, candidate)) { try out.appendSlice(allocator, fragment[0..range.start]); try out.appendSlice(allocator, fragment[range.end..]); try out.append(allocator, '\n'); return; } } else { stylesheet.* = .{ .output_start = out.items.len + range.start, .length = candidate.len, }; } try out.appendSlice(allocator, fragment); try out.append(allocator, '\n');}const EgoStylesheetRange = struct { start: usize, end: usize,};fn egoStylesheetRange(fragment: []const u8) ?EgoStylesheetRange { const open = "<style>"; const close = "</style>"; const start = std.mem.indexOf(u8, fragment, open) orelse return null; const close_start = std.mem.indexOfPos(u8, fragment, start + open.len, close) orelse return null; return .{ .start = start, .end = close_start + close.len };}fn appendSpeakerNotes( allocator: Allocator, out: *std.ArrayList(u8), source: []const u8, options: Options,) Error!void { var note_options = options; note_options.mode = .document; note_options.slides.runtime = false; var rendered = try render(allocator, source, note_options); defer rendered.deinit(allocator); if (std.mem.trim(u8, rendered.html, " \t\r\n").len == 0) return; try out.appendSlice(allocator, "<aside class=\"zen-speaker-notes\" data-zen-speaker-notes hidden>\n"); try out.appendSlice(allocator, rendered.html); try out.appendSlice(allocator, "</aside>\n");}const ContentsScope = enum { document, following,};const ContentsOptions = struct { scope: ContentsScope = .following, min_level: u8 = 1, max_level: u8 = 6, title: []const u8 = "Contents:",};fn contentsFence(info: []const u8) bool { const language = firstFenceWord(info); return std.mem.eql(u8, language, "zen-toc") or std.mem.eql(u8, language, "zen-contents") or std.mem.eql(u8, language, "toc");}fn egoFence(info: []const u8) bool { return std.mem.eql(u8, firstFenceWord(info), "zen-ego");}fn appendContents( allocator: Allocator, out: *std.ArrayList(u8), doc: document.Document, fence: Fence, source: []const u8, diag: ?*diagnostic.Diagnostic,) !void { const options = try parseContentsOptions(source); var count: usize = 0; for (doc.headings) |heading| { if (!contentsIncludes(options, fence, heading)) continue; count += 1; } if (count == 0) return; try out.appendSlice( allocator, "<nav class=\"zen-contents zen-role-wide\" aria-label=\"Contents\">\n<p>", ); try html.appendEscaped(out, allocator, options.title); try out.appendSlice(allocator, "</p>\n<ul>\n"); for (doc.headings) |heading| { if (!contentsIncludes(options, fence, heading)) continue; try appendContentsItem(allocator, out, heading, options, diag); try out.appendSlice(allocator, "</li>\n"); } try out.appendSlice(allocator, "</ul>\n</nav>\n");}fn appendContentsItem( allocator: Allocator, out: *std.ArrayList(u8), heading: document.Heading, options: ContentsOptions, diag: ?*diagnostic.Diagnostic,) !void { try out.appendSlice(allocator, "<li class=\"zen-contents-level-"); try appendDecimal(out, allocator, heading.level); if (heading.level > options.min_level) { try out.appendSlice(allocator, "\" style=\"margin-left:calc("); try appendDecimal(out, allocator, heading.level - options.min_level); try out.appendSlice(allocator, " * 1.5rem)\"><a href=\"#"); } else { try out.appendSlice(allocator, "\"><a href=\"#"); } try html.appendAttributeEscaped(out, allocator, heading.id); try out.appendSlice(allocator, "\">"); try appendInline(out, allocator, heading.text, null, diag, .untracked, null); try out.appendSlice(allocator, "</a>");}fn contentsIncludes(options: ContentsOptions, fence: Fence, heading: document.Heading) bool { if (heading.level < options.min_level or heading.level > options.max_level) return false; return switch (options.scope) { .document => true, .following => heading.source_offset > fence.source_offset, };}fn parseContentsOptions(source: []const u8) !ContentsOptions { var options: ContentsOptions = .{}; var cursor: usize = 0; while (nextLine(source, &cursor)) |raw_line| { const line = std.mem.trim(u8, std.mem.trim(u8, raw_line, "\r"), " \t"); if (line.len == 0) continue; const split = std.mem.indexOfScalar(u8, line, ':') orelse return error.InvalidContentsDirective; const key = std.mem.trim(u8, line[0..split], " \t"); const value = std.mem.trim(u8, line[split + 1 ..], " \t"); if (std.mem.eql(u8, key, "scope")) { options.scope = if (std.mem.eql(u8, value, "document") or std.mem.eql(u8, value, "page")) .document else if (std.mem.eql(u8, value, "following")) .following else return error.InvalidContentsDirective; } else if (std.mem.eql(u8, key, "min-level")) { options.min_level = try parseHeadingLevel(value); } else if (std.mem.eql(u8, key, "max-level")) { options.max_level = try parseHeadingLevel(value); } else if (std.mem.eql(u8, key, "title")) { options.title = value; } else { return error.InvalidContentsDirective; } } if (options.min_level > options.max_level) return error.InvalidContentsDirective; return options;}fn parseHeadingLevel(value: []const u8) !u8 { const level = std.fmt.parseUnsigned(u8, value, 10) catch return error.InvalidContentsDirective; if (level < 1 or level > 6) return error.InvalidContentsDirective; return level;}fn diagramFence(info: []const u8) bool { const language = firstFenceWord(info); return std.mem.eql(u8, language, "diagram") or std.mem.eql(u8, language, "zen-diagram") or std.mem.eql(u8, language, "zen-ascii");}fn quizFence(info: []const u8) bool { const language = firstFenceWord(info); return std.mem.eql(u8, language, "zen-quiz") or std.mem.eql(u8, language, "quiz");}fn appendQuiz( allocator: Allocator, out: *std.ArrayList(u8), source: []const u8, diag: ?*diagnostic.Diagnostic, owner: *QuizOwner, limits: quiz.Limits,) Error!void { const parsed = try owner.parse(allocator, source, limits); defer owner.reset(); try out.appendSlice(allocator, "<section class=\"zen-quiz\" aria-label=\"Quiz\">\n<ol class=\"zen-quiz-questions\">\n"); for (parsed.questions) |question| { try out.appendSlice(allocator, "<li class=\"zen-quiz-question\">\n<p class=\"zen-quiz-prompt\">"); try appendInline(out, allocator, question.prompt, null, diag, .untracked, null); try out.appendSlice(allocator, "</p>\n"); if (question.options.len != 0) { try out.appendSlice(allocator, "<ul class=\"zen-quiz-options\">\n"); for (question.options) |option| { try out.appendSlice(allocator, "<li>"); try appendInline(out, allocator, option.text, null, diag, .untracked, null); try out.appendSlice(allocator, "</li>\n"); } try out.appendSlice(allocator, "</ul>\n"); } try out.appendSlice(allocator, "<details class=\"zen-quiz-answer\">\n<summary>Reveal answer</summary>\n"); for (question.options) |option| { if (!option.correct) continue; try out.appendSlice(allocator, "<p class=\"zen-quiz-correct\"><strong>"); try appendInline(out, allocator, option.text, null, diag, .untracked, null); try out.appendSlice(allocator, "</strong></p>\n"); } if (question.explanation.len != 0) { try out.appendSlice(allocator, "<p class=\"zen-quiz-explanation\">"); try appendInline(out, allocator, question.explanation, null, diag, .untracked, null); try out.appendSlice(allocator, "</p>\n"); } try out.appendSlice(allocator, "</details>\n</li>\n"); } try out.appendSlice(allocator, "</ol>\n</section>\n");}fn notesFence(info: []const u8) bool { const language = firstFenceWord(info); return std.mem.eql(u8, language, "notes") or std.mem.eql(u8, language, "speaker-notes") or std.mem.eql(u8, language, "zen-notes");}fn firstFenceWord(info: []const u8) []const u8 { const trimmed = std.mem.trim(u8, info, " \t"); var end: usize = 0; while (end < trimmed.len and trimmed[end] != ' ' and trimmed[end] != '\t') : (end += 1) {} return trimmed[0..end];}const MathMode = enum { inline_math, display };fn renderMath( out: *std.ArrayList(u8), allocator: Allocator, source: []const u8, mode: MathMode, diag: ?*diagnostic.Diagnostic,) math.Error!void { var detail: math.Diagnostic = .{}; (switch (mode) { .inline_math => math.renderInline(out, allocator, source, &detail), .display => math.renderDisplay(out, allocator, source, &detail), }) catch |err| { switch (err) { error.InvalidEquation => if (diag) |d| d.setEquation(source, detail), error.OutOfMemory => {}, } return err; };}fn flushDisplayMath( allocator: Allocator, out: *std.ArrayList(u8), block: *std.ArrayList(u8), diag: ?*diagnostic.Diagnostic,) math.Error!void { try renderMath(out, allocator, block.items, .display, diag); try out.append(allocator, '\n'); block.clearRetainingCapacity();}fn flushParagraph( allocator: Allocator, out: *std.ArrayList(u8), paragraph: *InlineBuffer, footnotes: *footnote.Set, options: Options,) InlineError!void { if (paragraph.items().len == 0) return; const container = try paragraphContainer( allocator, paragraph.items(), options.mode, options.diagnostic, ); if (container) |value| { try out.appendSlice(allocator, "<p class=\""); try out.appendSlice(allocator, value.classes); try out.appendSlice(allocator, "\">"); } else { try out.appendSlice(allocator, "<p>"); } const full_size_image = if (container) |value| value.full_size_image else null; if (full_size_image) |image| { try out.appendSlice(allocator, "<a href=\""); try appendMarkdownEscaped(out, allocator, image.target); try out.appendSlice( allocator, "\" target=\"_blank\" rel=\"noopener\" aria-label=\"Open ", ); try appendMarkdownEscaped(out, allocator, image.label); try out.appendSlice(allocator, " at full size\">"); } try appendInline( out, allocator, paragraph.items(), footnotes, options.diagnostic, paragraph.origin(), options.link_report, ); if (full_size_image != null) try out.appendSlice(allocator, "</a>"); try out.appendSlice(allocator, "</p>\n"); paragraph.clearRetainingCapacity();}fn appendTable( allocator: Allocator, out: *std.ArrayList(u8), source: []const u8, table_source: []const u8, table: markdown_model.Table, footnotes: *footnote.Set, options: Options,) InlineError!void { const alignments = table.delimiter.alignments; std.debug.assert(table.header.cells.len == alignments.len); try out.appendSlice( allocator, "<div class=\"content-table zen-role-wide\" tabindex=\"0\">\n", ); try out.appendSlice(allocator, "<table>\n<thead>\n"); try appendTableRow( allocator, out, source, table_source, table.header, alignments, true, footnotes, options, ); try out.appendSlice(allocator, "</thead>\n<tbody>\n"); for (table.body) |row| { std.debug.assert(row.cells.len == alignments.len); try appendTableRow( allocator, out, source, table_source, row, alignments, false, footnotes, options, ); } try out.appendSlice(allocator, "</tbody>\n</table>\n</div>\n");}fn appendTableRow( allocator: Allocator, out: *std.ArrayList(u8), source: []const u8, table_source: []const u8, row: markdown_model.TableRow, alignments: []const markdown_model.TableAlignment, header: bool, footnotes: *footnote.Set, options: Options,) InlineError!void { std.debug.assert(row.cells.len == alignments.len); try out.appendSlice(allocator, "<tr>\n"); for (row.cells, alignments) |cell, alignment| { if (header) { try out.appendSlice(allocator, "<th scope=\"col\""); } else { try out.appendSlice(allocator, "<td"); } if (tableAlignmentClass(alignment)) |class| { try out.appendSlice(allocator, " class=\""); try out.appendSlice(allocator, class); try out.append(allocator, '"'); } try out.append(allocator, '>'); const cell_text = cell.source.bytes(table_source); try appendInline( out, allocator, cell_text, footnotes, options.diagnostic, originWithin(source, cell_text), options.link_report, ); try out.appendSlice(allocator, if (header) "</th>\n" else "</td>\n"); } try out.appendSlice(allocator, "</tr>\n");}fn tableAlignmentClass(alignment: markdown_model.TableAlignment) ?[]const u8 { return switch (alignment) { .none => null, .left => "zen-table-align-left", .center => "zen-table-align-center", .right => "zen-table-align-right", };}fn flushListItem( allocator: Allocator, out: *std.ArrayList(u8), list_item: *InlineBuffer, footnotes: *footnote.Set, options: Options,) InlineError!void { if (list_item.items().len == 0) return; try out.appendSlice(allocator, "<li>"); try appendInline( out, allocator, list_item.items(), footnotes, options.diagnostic, list_item.origin(), options.link_report, ); try out.appendSlice(allocator, "</li>\n"); list_item.clearRetainingCapacity();}fn openList( allocator: Allocator, out: *std.ArrayList(u8), kind: ListKind, semantic: ?SemanticList,) Allocator.Error!void { try out.appendSlice(allocator, switch (kind) { .unordered => "<ul", .ordered => "<ol", }); if (semantic) |role| { try out.appendSlice(allocator, " class=\""); try out.appendSlice(allocator, semanticListClasses(role)); try out.append(allocator, '"'); } try out.appendSlice(allocator, ">\n");}fn closeList( allocator: Allocator, out: *std.ArrayList(u8), list_kind: *?ListKind, list_item: *InlineBuffer, footnotes: *footnote.Set, options: Options,) InlineError!void { if (list_kind.*) |kind| { try flushListItem(allocator, out, list_item, footnotes, options); switch (kind) { .unordered => try out.appendSlice(allocator, "</ul>\n"), .ordered => try out.appendSlice(allocator, "</ol>\n"), } list_kind.* = null; }}fn closeBlockquote( allocator: Allocator, out: *std.ArrayList(u8), open: *bool, blockquote: *InlineBuffer, footnotes: *footnote.Set, options: Options,) InlineError!void { if (!open.*) return; try out.appendSlice(allocator, "<blockquote><p>"); try appendInline( out, allocator, blockquote.items(), footnotes, options.diagnostic, blockquote.origin(), options.link_report, ); try out.appendSlice(allocator, "</p></blockquote>\n"); open.* = false; blockquote.clearRetainingCapacity();}fn appendDecimal(out: *std.ArrayList(u8), allocator: Allocator, value: u8) Allocator.Error!void { var buffer: [3]u8 = undefined; const text = std.fmt.bufPrint(&buffer, "{d}", .{value}) catch unreachable; try out.appendSlice(allocator, text);}fn appendUsize(out: *std.ArrayList(u8), allocator: Allocator, value: usize) Allocator.Error!void { var buffer: [20]u8 = undefined; const text = std.fmt.bufPrint(&buffer, "{d}", .{value}) catch unreachable; try out.appendSlice(allocator, text);}fn nextLine(source: []const u8, cursor: *usize) ?[]const u8 { if (cursor.* >= source.len) return null; const start = cursor.*; if (std.mem.indexOfScalarPos(u8, source, start, '\n')) |end| { cursor.* = end + 1; return source[start..end]; } cursor.* = source.len; return source[start..];}fn parseFence(line: []const u8) ?Fence { const trimmed = trimLeft(line); if (trimmed.len < 3) return null; const marker = trimmed[0]; if (marker != '`' and marker != '~') return null; var count: usize = 0; while (count < trimmed.len and trimmed[count] == marker) : (count += 1) {} if (count < 3) return null; return .{ .marker = marker, .count = count, .info = std.mem.trim(u8, trimmed[count..], " \t"), };}fn closingFence(line: []const u8, fence: Fence) bool { const trimmed = std.mem.trim(u8, line, " \t"); var count: usize = 0; while (count < trimmed.len and trimmed[count] == fence.marker) : (count += 1) {} return count >= fence.count and std.mem.trim(u8, trimmed[count..], " \t").len == 0;}const Heading = struct { level: u8, text: []const u8, explicit_id: ?[]const u8 = null, anchor_offset: ?usize = null,};fn parseHeading( line: []const u8, source_offset: usize, diag: ?*diagnostic.Diagnostic,) document.Error!?Heading { const trimmed = trimLeft(line); var level: u8 = 0; while (level < trimmed.len and level < 6 and trimmed[level] == '#') : (level += 1) {} if (level == 0) return null; if (level < trimmed.len and trimmed[level] != ' ' and trimmed[level] != '\t') return null; const text = std.mem.trim(u8, trimmed[level..], " \t#"); const anchor_start = headingAnchorStart(text) orelse return .{ .level = level, .text = text, }; const anchor_offset = source_offset + (@intFromPtr(text.ptr) - @intFromPtr(line.ptr)) + anchor_start; const anchor = text[anchor_start..]; if (anchor.len < 4 or anchor[anchor.len - 1] != '}') { setHeadingDiagnostic(diag, "invalid heading anchor", anchor_offset); return error.InvalidHeadingAnchor; } const explicit_id = anchor[2 .. anchor.len - 1]; if (!validHeadingId(explicit_id)) { setHeadingDiagnostic(diag, "invalid heading anchor", anchor_offset); return error.InvalidHeadingAnchor; } return .{ .level = level, .text = std.mem.trimEnd(u8, text[0..anchor_start], " \t"), .explicit_id = explicit_id, .anchor_offset = anchor_offset, };}fn headingAnchorStart(text: []const u8) ?usize { var index = text.len; while (index > 1) : (index -= 1) { const start = index - 2; if (text[start] != '{' or text[start + 1] != '#') continue; if (start == 0 or text[start - 1] == ' ' or text[start - 1] == '\t') { return start; } } return null;}fn validHeadingId(id: []const u8) bool { if (id.len == 0) return false; for (id) |byte| { if (asciiAlphanumeric(byte)) continue; if (byte != '-' and byte != '_' and byte != '.' and byte != ':' and byte != '/' and byte != '~') return false; } return true;}fn setHeadingDiagnostic( diag: ?*diagnostic.Diagnostic, reason: []const u8, offset: usize,) void { if (diag) |active| active.setHeading(reason, offset);}const LocatedHeading = struct { level: u8, text: []const u8, explicit_id: ?[]const u8, anchor_offset: ?usize, source_offset: usize,};const HeadingIterator = struct { source: []const u8, diagnostic: ?*diagnostic.Diagnostic, cursor: usize = 0, code_fence: ?Fence = null, math_open: bool = false, footnote_definition: bool = false, list_open: bool = false, fn init(source: []const u8, diag: ?*diagnostic.Diagnostic) HeadingIterator { return .{ .source = source, .diagnostic = diag }; } fn next(self: *HeadingIterator) document.Error!?LocatedHeading { while (true) { const line_start = self.cursor; const raw_line = nextLine(self.source, &self.cursor) orelse return null; const line = std.mem.trim(u8, raw_line, "\r"); if (self.code_fence) |fence| { if (closingFence(line, fence)) self.code_fence = null; continue; } if (self.math_open) { const inner = std.mem.trim(u8, line, " \t"); if (std.mem.eql(u8, inner, "$$") or std.mem.endsWith(u8, inner, "$$")) self.math_open = false; continue; } if (parseFence(line)) |parsed| { self.code_fence = parsed; self.footnote_definition = false; self.list_open = false; continue; } const trimmed = std.mem.trim(u8, line, " \t"); if (std.mem.startsWith(u8, trimmed, "$$")) { const rest = std.mem.trim(u8, trimmed[2..], " \t"); self.math_open = !(rest.len >= 2 and std.mem.endsWith(u8, rest, "$$")); self.footnote_definition = false; self.list_open = false; continue; } if (trimmed.len == 0) { self.footnote_definition = false; self.list_open = false; continue; } if (footnote.definition(line) != null) { self.footnote_definition = true; self.list_open = false; continue; } if (self.footnote_definition and footnote.continuation(line) != null) continue; self.footnote_definition = false; if (parseBlockquote(line) != null) { self.list_open = false; continue; } if (parseListItem(line) != null) { self.list_open = true; continue; } if (self.list_open and listContinuation(line) != null) continue; self.list_open = false; if (try parseHeading(line, line_start, self.diagnostic)) |heading| return .{ .level = heading.level, .text = heading.text, .explicit_id = heading.explicit_id, .anchor_offset = heading.anchor_offset, .source_offset = line_start, }; } }};const ResolvedHeadingId = union(enum) { explicit: []const u8, generated: struct { text: []const u8, suffix: ?usize, }, fn length(self: ResolvedHeadingId) error{CapacityOverflow}!usize { return switch (self) { .explicit => |id| id.len, .generated => |generated| if (generated.suffix) |suffix| std.math.add( usize, html.slugLength(generated.text), 1 + decimalLength(suffix), ) catch error.CapacityOverflow else html.slugLength(generated.text), }; } fn write(self: ResolvedHeadingId, out: []u8) []u8 { std.debug.assert(out.len == (self.length() catch unreachable)); switch (self) { .explicit => |id| @memcpy(out, id), .generated => |generated| { const base_length = html.slugLength(generated.text); _ = html.writeSlug(out[0..base_length], generated.text); if (generated.suffix) |suffix| { out[base_length] = '~'; _ = std.fmt.bufPrint(out[base_length + 1 ..], "{d}", .{suffix}) catch unreachable; } }, } return out; }};fn validateHeadingAnchors( source: []const u8, diag: ?*diagnostic.Diagnostic,) document.Error!void { var iterator = HeadingIterator.init(source, diag); while (try iterator.next()) |heading| { const id = heading.explicit_id orelse continue; var prior = HeadingIterator.init(source, null); while (try prior.next()) |candidate| { if (candidate.source_offset >= heading.source_offset) break; const candidate_id = candidate.explicit_id orelse continue; if (!std.mem.eql(u8, candidate_id, id)) continue; setHeadingDiagnostic(diag, "duplicate heading anchor", heading.anchor_offset.?); return error.DuplicateHeadingAnchor; } }}fn resolveHeadingId( source: []const u8, target: LocatedHeading,) document.Error!ResolvedHeadingId { if (target.explicit_id) |id| return .{ .explicit = id }; var occurrence: usize = 0; var explicit_reserved = false; var iterator = HeadingIterator.init(source, null); while (try iterator.next()) |heading| { if (heading.explicit_id) |id| { explicit_reserved = explicit_reserved or slugMatches(target.text, id); continue; } if (heading.source_offset > target.source_offset) continue; if (!slugsEqual(target.text, heading.text)) continue; occurrence = std.math.add(usize, occurrence, 1) catch return error.CapacityOverflow; } std.debug.assert(occurrence > 0); const suffix = if (explicit_reserved or occurrence > 1) occurrence else null; return .{ .generated = .{ .text = target.text, .suffix = suffix } };}const SlugIterator = struct { source: []const u8, index: usize = 0, emitted: usize = 0, pending_separator: bool = false, delayed: ?u8 = null, fallback_index: usize = 0, fn next(self: *SlugIterator) ?u8 { if (self.delayed) |byte| { self.delayed = null; self.emitted += 1; return byte; } while (self.index < self.source.len) { const byte = self.source[self.index]; self.index += 1; if (asciiAlphanumeric(byte)) { const normalized = std.ascii.toLower(byte); if (self.pending_separator and self.emitted != 0) { self.pending_separator = false; self.delayed = normalized; self.emitted += 1; return '-'; } self.pending_separator = false; self.emitted += 1; return normalized; } if (byte == ' ' or byte == '\t' or byte == '-' or byte == '_') { if (self.emitted != 0) self.pending_separator = true; } } if (self.emitted != 0 or self.fallback_index == "section".len) return null; const byte = "section"[self.fallback_index]; self.fallback_index += 1; return byte; }};fn slugsEqual(left: []const u8, right: []const u8) bool { var left_iterator = SlugIterator{ .source = left }; var right_iterator = SlugIterator{ .source = right }; while (true) { const left_byte = left_iterator.next(); const right_byte = right_iterator.next(); if (left_byte == null or right_byte == null) return left_byte == right_byte; if (left_byte.? != right_byte.?) return false; }}fn slugMatches(text: []const u8, id: []const u8) bool { var iterator = SlugIterator{ .source = text }; var index: usize = 0; while (iterator.next()) |byte| { if (index == id.len or id[index] != byte) return false; index += 1; } return index == id.len;}fn asciiAlphanumeric(byte: u8) bool { return (byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') or (byte >= '0' and byte <= '9');}fn decimalLength(value: usize) usize { var buffer: [20]u8 = undefined; return (std.fmt.bufPrint(&buffer, "{d}", .{value}) catch unreachable).len;}const ListItem = struct { kind: ListKind, text: []const u8,};fn parseListItem(line: []const u8) ?ListItem { const trimmed = trimLeft(line); if (trimmed.len >= 2 and (trimmed[0] == '-' or trimmed[0] == '*' or trimmed[0] == '+') and (trimmed[1] == ' ' or trimmed[1] == '\t')) { return .{ .kind = .unordered, .text = std.mem.trim(u8, trimmed[2..], " \t") }; } var index: usize = 0; while (index < trimmed.len and trimmed[index] >= '0' and trimmed[index] <= '9') : (index += 1) {} if (index == 0 or index + 1 >= trimmed.len or trimmed[index] != '.') return null; if (trimmed[index + 1] != ' ' and trimmed[index + 1] != '\t') return null; return .{ .kind = .ordered, .text = std.mem.trim(u8, trimmed[index + 2 ..], " \t") };}fn parseSemanticList(line: []const u8) ?SemanticList { if (std.mem.eql( u8, line, "{.docs-index-family .zen-role-wide}", )) return .family_index; if (std.mem.eql( u8, line, "{.docs-index-module .zen-role-wide}", )) return .module_index; return null;}fn semanticListClasses(role: SemanticList) []const u8 { return switch (role) { .family_index => "docs-index-family zen-role-wide", .module_index => "docs-index-module zen-role-wide", };}fn listContinuation(line: []const u8) ?[]const u8 { var index: usize = 0; while (index < line.len and (line[index] == ' ' or line[index] == '\t')) : (index += 1) {} if (index == 0) return null; const continuation = std.mem.trim(u8, line[index..], " \t"); if (continuation.len == 0) return null; return continuation;}fn parseBlockquote(line: []const u8) ?[]const u8 { const trimmed = trimLeft(line); if (trimmed.len == 0 or trimmed[0] != '>') return null; return std.mem.trim(u8, trimmed[1..], " \t");}fn isThematicBreak(line: []const u8) bool { if (line.len < 3) return false; const marker = line[0]; if (marker != '-' and marker != '*' and marker != '_') return false; for (line) |byte| { if (byte != marker) return false; } return true;}fn trimLeft(line: []const u8) []const u8 { var index: usize = 0; while (index < line.len and (line[index] == ' ' or line[index] == '\t')) : (index += 1) {} return line[index..];}fn appendFootnotes( allocator: Allocator, out: *std.ArrayList(u8), source: []const u8, footnotes: *footnote.Set, options: Options,) InlineError!void { if (!footnotes.hasReferences()) return; try out.appendSlice(allocator, "<section class=\"footnotes\" aria-label=\"References\">\n<ol>\n"); var number: usize = 1; while (number < footnotes.next_number) : (number += 1) { for (footnotes.definitions) |definition| { if (definition.number != number) continue; try out.appendSlice(allocator, "<li id=\"fn-"); try html.appendAttributeEscaped(out, allocator, definition.key); try out.appendSlice(allocator, "\">"); try appendInline( out, allocator, definition.text, null, options.diagnostic, originWithin(source, definition.text), options.link_report, ); try out.appendSlice(allocator, " <a class=\"footnote-backref\" href=\"#fnref-"); try html.appendAttributeEscaped(out, allocator, definition.key); try out.appendSlice(allocator, "\" aria-label=\"Back to reference\">↩</a></li>\n"); } } try out.appendSlice(allocator, "</ol>\n</section>\n");}fn appendFootnoteReference( out: *std.ArrayList(u8), allocator: Allocator, footnotes: ?*footnote.Set, key: []const u8,) Allocator.Error!bool { const notes = footnotes orelse return false; const mark = notes.markFor(key) orelse return false; try out.appendSlice(allocator, "<sup id=\"fnref-"); try html.appendAttributeEscaped(out, allocator, key); if (mark.reference > 1) { try out.append(allocator, '-'); try appendUsize(out, allocator, mark.reference); } try out.appendSlice(allocator, "\"><a class=\"footnote-ref\" href=\"#fn-"); try html.appendAttributeEscaped(out, allocator, key); try out.appendSlice(allocator, "\">["); try appendUsize(out, allocator, mark.number); try out.appendSlice(allocator, "]</a></sup>"); return true;}fn appendInline( out: *std.ArrayList(u8), allocator: Allocator, value: []const u8, footnotes: ?*footnote.Set, diag: ?*diagnostic.Diagnostic, origin: InlineOrigin, report: ?*LinkReport,) InlineError!void { var delimiters = indexCodeDelimiters(allocator, value, null) catch |err| { if (err == error.InlineDelimiterCapacityExceeded) { if (diag) |detail| detail.setInline(value, delimiter_capacity_reason, 0); } return err; }; defer delimiters.deinit(allocator); try appendInlineIndexed( out, allocator, value, value, footnotes, diag, 0, false, delimiters, origin, report, );}fn appendInlineIndexed( out: *std.ArrayList(u8), allocator: Allocator, source: []const u8, value: []const u8, footnotes: ?*footnote.Set, diag: ?*diagnostic.Diagnostic, depth: usize, inside_link: bool, delimiters: CodeDelimiters, origin: InlineOrigin, report: ?*LinkReport,) InlineError!void { std.debug.assert(depth <= max_inline_nesting); std.debug.assert(@intFromPtr(value.ptr) >= @intFromPtr(source.ptr)); const base = @intFromPtr(value.ptr) - @intFromPtr(source.ptr); std.debug.assert(base <= source.len); std.debug.assert(value.len <= source.len - base); var index: usize = 0; while (index < value.len) { if (documentationHtmlTagEnd(value, index)) |end| { try out.appendSlice(allocator, value[index..end]); index = end; continue; } if (value[index] == '`') { const opener_end = backtickRunEnd(value, index); if (delimiters.span(source, base + index, base + value.len)) |span| { try out.appendSlice(allocator, "<code>"); try html.appendEscaped(out, allocator, span.content); try out.appendSlice(allocator, "</code>"); index = span.end - base; continue; } try html.appendEscaped(out, allocator, value[index..opener_end]); index = opener_end; continue; } if (value[index] == '\\' and index + 1 < value.len and isAsciiPunctuation(value[index + 1])) { try html.appendEscapedByte(out, allocator, value[index + 1]); index += 2; continue; } if (value[index] == '$') { if (inlineMathEnd(value, index)) |end| { try renderMath(out, allocator, value[index + 1 .. end], .inline_math, diag); index = end + 1; continue; } } if (index + 1 < value.len and value[index] == '!' and value[index + 1] == '[') { if (try parseInlineLink( source, value, base, index, true, diag, delimiters, )) |link| { recordRenderedLink(report, origin, base, index, value, link, true); const video = std.mem.endsWith(u8, link.target, ".webm"); const attributes = parseImageAttributes(value, link.end); var motion_base: ?usize = null; if (!video) { if (attributes) |image_attributes| { if (hasAttributeClass( image_attributes.content, "zen-slide-motion", )) { motion_base = motionPosterBase(link.target); } } } if (video) { try out.appendSlice(allocator, "<video src=\""); try appendMarkdownEscaped(out, allocator, link.target); try out.appendSlice(allocator, "\" poster=\""); try appendMarkdownEscaped( out, allocator, link.target[0 .. link.target.len - ".webm".len], ); try out.appendSlice(allocator, ".webp\" aria-label=\""); } else { try out.appendSlice(allocator, "<img src=\""); if (motion_base) |motion_offset| { try appendMarkdownEscaped(out, allocator, link.target[0..motion_offset]); try out.appendSlice(allocator, ".poster.svg"); try appendMarkdownEscaped( out, allocator, link.target[motion_offset + ".svg".len ..], ); try out.appendSlice(allocator, "\" data-zen-motion-src=\""); try appendMarkdownEscaped(out, allocator, link.target); try out.appendSlice(allocator, "\" alt=\""); } else { try appendMarkdownEscaped(out, allocator, link.target); try out.appendSlice(allocator, "\" alt=\""); } } try appendMarkdownEscaped(out, allocator, link.label); try out.append(allocator, '"'); if (video) try out.appendSlice(allocator, " autoplay loop muted playsinline"); if (motion_base != null) try out.appendSlice(allocator, " decoding=\"async\""); var end = link.end; if (attributes) |image_attributes| { try appendImageAttributes(out, allocator, image_attributes.content); end = image_attributes.end; } try out.append(allocator, '>'); if (video) try out.appendSlice(allocator, "</video>"); index = end; continue; } } if (value[index] == '[') { if (!inside_link) { if (parseFootnoteReference(value, index)) |reference| { if (try appendFootnoteReference(out, allocator, footnotes, reference.key)) { index = reference.end; continue; } } } if (!inside_link) { if (try parseInlineLink( source, value, base, index, false, diag, delimiters, )) |link| { recordRenderedLink(report, origin, base, index, value, link, false); try out.appendSlice(allocator, "<a href=\""); try appendMarkdownEscaped(out, allocator, link.target); try out.appendSlice(allocator, "\">"); try appendNestedInline( out, allocator, source, link.label, footnotes, diag, depth, true, delimiters, origin, report, ); try out.appendSlice(allocator, "</a>"); index = link.end; continue; } } } if (index + 1 < value.len and ((value[index] == '*' and value[index + 1] == '*') or (value[index] == '_' and value[index + 1] == '_'))) { const token = value[index .. index + 2]; if (std.mem.indexOfPos(u8, value, index + 2, token)) |end| { try out.appendSlice(allocator, "<strong>"); try appendNestedInline( out, allocator, source, value[index + 2 .. end], footnotes, diag, depth, inside_link, delimiters, origin, report, ); try out.appendSlice(allocator, "</strong>"); index = end + 2; continue; } } if (value[index] == '*' or value[index] == '_') { if (std.mem.indexOfScalarPos(u8, value, index + 1, value[index])) |end| { try out.appendSlice(allocator, "<em>"); try appendNestedInline( out, allocator, source, value[index + 1 .. end], footnotes, diag, depth, inside_link, delimiters, origin, report, ); try out.appendSlice(allocator, "</em>"); index = end + 1; continue; } } try html.appendEscapedByte(out, allocator, value[index]); index += 1; }}fn documentationHtmlTagEnd(value: []const u8, start: usize) ?usize { const tags = [_][]const u8{ "<span class=\"docs-breadcrumb\" role=\"navigation\" aria-label=\"Declaration hierarchy\">", "<span class=\"docs-breadcrumb\" role=\"navigation\" aria-label=\"Verification hierarchy\">", "<span aria-hidden=\"true\">", "<span aria-current=\"page\">", "<span class=\"docs-sidenote zen-role-margin\">", "<span class=\"docs-verification-source\">", "<span class=\"docs-verification-freshness-content\">", "<span class=\"docs-verification-freshness-content docs-verification-freshness-content-local\">", "<span class=\"docs-margin-figure zen-role-margin\">", "<span class=\"zen-role-reading\">", "<span class=\"zen-role-wide\">", "<span class=\"zen-role-margin\">", "<span class=\"zen-role-full\">", "<table class=\"docs-audit-table zen-role-wide\">", "</span>", "</table>", "<thead>", "</thead>", "<tbody>", "</tbody>", "<tr>", "</tr>", "<th>", "</th>", "<td>", "</td>", "<strong>", "</strong>", "<code>", "</code>", "<br>", }; if (start >= value.len or value[start] != '<') return null; for (tags) |tag| { if (std.mem.startsWith(u8, value[start..], tag)) return start + tag.len; } return null;}fn documentationArtifactBlock(value: []const u8) bool { return std.mem.startsWith( u8, value, "<table class=\"docs-audit-table zen-role-wide\">", ) and std.mem.endsWith(u8, value, "</table>");}const ParagraphContainer = struct { classes: []const u8, full_size_image: ?InlineLink = null,};const ParagraphRole = struct { open: []const u8, classes: []const u8,};const paragraph_roles = [_]ParagraphRole{ .{ .open = "<span class=\"docs-sidenote zen-role-margin\">", .classes = "docs-sidenote-container zen-role-margin", }, .{ .open = "<span class=\"docs-verification-freshness-content\">", .classes = "docs-verification-freshness", }, .{ .open = "<span class=\"docs-verification-freshness-content " ++ "docs-verification-freshness-content-local\">", .classes = "docs-verification-freshness docs-verification-freshness-local", }, .{ .open = "<span class=\"docs-margin-figure zen-role-margin\">", .classes = "docs-margin-figure-container zen-role-margin", }, .{ .open = "<span class=\"zen-role-reading\">", .classes = "zen-role-container zen-role-reading", }, .{ .open = "<span class=\"zen-role-wide\">", .classes = "zen-role-container zen-role-wide", }, .{ .open = "<span class=\"zen-role-margin\">", .classes = "zen-role-container zen-role-margin", }, .{ .open = "<span class=\"zen-role-full\">", .classes = "zen-role-container zen-role-full", },};fn paragraphContainer( allocator: Allocator, value: []const u8, mode: Mode, diag: ?*diagnostic.Diagnostic,) InlineError!?ParagraphContainer { if (std.mem.endsWith(u8, value, "</span>")) { for (paragraph_roles) |role| { if (std.mem.startsWith(u8, value, role.open)) { return .{ .classes = role.classes }; } } } if (!std.mem.startsWith(u8, value, "![")) return null; var delimiters = try indexCodeDelimiters(allocator, value, null); defer delimiters.deinit(allocator); const link = try parseInlineLink( value, value, 0, 0, true, diag, delimiters, ) orelse return null; var end = link.end; const attributes = parseImageAttributes(value, end); var wide_figure = false; const role = if (attributes) |image_attributes| role: { end = image_attributes.end; wide_figure = hasAttributeClass( image_attributes.content, "zen-slide-figure-wide", ); if (mode == .document and wide_figure) { break :role "zen-media-block zen-role-wide"; } if (hasAttributeClass(image_attributes.content, "zen-role-wide")) { break :role "zen-media-block zen-role-wide"; } if (hasAttributeClass(image_attributes.content, "zen-role-margin")) { break :role "zen-media-block zen-role-margin"; } if (hasAttributeClass(image_attributes.content, "zen-role-full")) { break :role "zen-media-block zen-role-full"; } break :role "zen-media-block zen-role-reading"; } else "zen-media-block zen-role-reading"; if (end != value.len) return null; return .{ .classes = role, .full_size_image = if (mode == .document and wide_figure and !std.mem.endsWith(u8, link.target, ".webm")) link else null, };}fn recordRenderedLink( report: ?*LinkReport, origin: InlineOrigin, base: usize, start: usize, value: []const u8, link: InlineLink, image: bool,) void { const sink = report orelse return; var entry: RenderedLink = .{ .image = image }; if (origin.locate(base + start, base + link.end)) |span| { entry.start = span.start; entry.end = span.end; entry.located = true; } if (entry.located) { const offset = @intFromPtr(link.target.ptr) - @intFromPtr(value.ptr); if (origin.locate(base + offset, base + offset + link.target.len)) |span| { entry.target_start = span.start; entry.target_end = span.end; entry.contiguous = span.contiguous; } } sink.record(entry);}fn appendNestedInline( out: *std.ArrayList(u8), allocator: Allocator, source: []const u8, value: []const u8, footnotes: ?*footnote.Set, diag: ?*diagnostic.Diagnostic, depth: usize, inside_link: bool, delimiters: CodeDelimiters, origin: InlineOrigin, report: ?*LinkReport,) InlineError!void { if (depth == max_inline_nesting) { if (diag) |detail| detail.setInline(value, inline_nesting_reason, 0); return error.InlineNestingCapacityExceeded; } try appendInlineIndexed( out, allocator, source, value, footnotes, diag, depth + 1, inside_link, delimiters, origin, report, );}/// One run of an inline value, and where that run started in the source.const SourceSegment = struct { joined_start: u32, source_start: u32, len: u32,};/// A located span of the rendered source, and whether it is one run.const LocatedSpan = struct { start: u32, end: u32, contiguous: bool,};/// How an inline value's own offsets map back onto the rendered source.const InlineOrigin = union(enum) { direct: u32, joined: []const SourceSegment, untracked, fn locate(self: InlineOrigin, start: usize, end: usize) ?LocatedSpan { std.debug.assert(start <= end); switch (self) { .untracked => return null, .direct => |base| { const located_start = std.math.cast(u32, base + start) orelse return null; const located_end = std.math.cast(u32, base + end) orelse return null; return .{ .start = located_start, .end = located_end, .contiguous = true }; }, .joined => |segments| { const first = segmentContaining(segments, start) orelse return null; const located_start = std.math.cast( u32, first.source_start + (start - first.joined_start), ) orelse return null; const last_index = if (end > start) end - 1 else start; const last = segmentContaining(segments, last_index) orelse return null; const same = last.joined_start == first.joined_start; const located_end = if (same) std.math.cast(u32, first.source_start + (end - first.joined_start)) orelse return null else std.math.cast(u32, first.source_start + first.len) orelse return null; if (located_end < located_start) return null; return .{ .start = located_start, .end = located_end, .contiguous = same }; }, } }};fn segmentContaining(segments: []const SourceSegment, offset: usize) ?SourceSegment { for (segments) |segment| { if (offset < segment.joined_start) continue; if (offset - segment.joined_start < segment.len) return segment; } return null;}/// Reports where `value` starts inside `source`, or null when it is a copy.fn sourceOffset(source: []const u8, value: []const u8) ?u32 { const base = @intFromPtr(source.ptr); const start = @intFromPtr(value.ptr); if (start < base) return null; const offset = start - base; if (offset > source.len or source.len - offset < value.len) return null; return std.math.cast(u32, offset);}fn originWithin(source: []const u8, value: []const u8) InlineOrigin { return if (sourceOffset(source, value)) |offset| .{ .direct = offset } else .untracked;}/// Accumulates one inline value from trimmed source lines, recording the/// origin of each line only while a link report is attached.const InlineBuffer = struct { bytes: std.ArrayList(u8) = .empty, segments: std.ArrayList(SourceSegment) = .empty, track: bool = false, fn deinit(self: *InlineBuffer, allocator: Allocator) void { self.bytes.deinit(allocator); self.segments.deinit(allocator); } fn items(self: *const InlineBuffer) []const u8 { return self.bytes.items; } fn appendLine( self: *InlineBuffer, allocator: Allocator, source: []const u8, line: []const u8, ) Allocator.Error!void { if (self.bytes.items.len != 0) try self.bytes.append(allocator, ' '); if (self.track) try self.appendSegment(allocator, source, line); try self.bytes.appendSlice(allocator, line); } fn appendSegment( self: *InlineBuffer, allocator: Allocator, source: []const u8, line: []const u8, ) Allocator.Error!void { const source_start = sourceOffset(source, line) orelse return; const joined_start = std.math.cast(u32, self.bytes.items.len) orelse return; const len = std.math.cast(u32, line.len) orelse return; try self.segments.append(allocator, .{ .joined_start = joined_start, .source_start = source_start, .len = len, }); } fn origin(self: *const InlineBuffer) InlineOrigin { if (!self.track) return .untracked; return .{ .joined = self.segments.items }; } fn clearRetainingCapacity(self: *InlineBuffer) void { self.bytes.clearRetainingCapacity(); self.segments.clearRetainingCapacity(); }};const CodeSpan = struct { content: []const u8, end: usize,};fn indexCodeDelimiters( allocator: Allocator, value: []const u8, visits: ?*usize,) (Allocator.Error || error{InlineDelimiterCapacityExceeded})!CodeDelimiters { var count: usize = 0; var index: usize = 0; while (index < value.len) { if (visits) |measured| measured.* += 1; if (value[index] != '`') { index += 1; continue; } if (count == max_inline_delimiters) return error.InlineDelimiterCapacityExceeded; count += 1; index = measuredBacktickRunEnd(value, index, visits); } const items = try allocator.alloc(CodeDelimiter, count); errdefer allocator.free(items); index = 0; var cursor: usize = 0; while (index < value.len) { if (visits) |measured| measured.* += 1; if (value[index] != '`') { index += 1; continue; } const end = measuredBacktickRunEnd(value, index, visits); items[cursor] = .{ .start = index, .end = end, .length = end - index }; cursor += 1; index = end; } std.debug.assert(cursor == items.len); pairCodeDelimiters(items); return .{ .items = items };}fn measuredBacktickRunEnd( value: []const u8, start: usize, visits: ?*usize,) usize { std.debug.assert(start < value.len); std.debug.assert(value[start] == '`'); var end = start + 1; while (end < value.len and value[end] == '`') : (end += 1) { if (visits) |measured| measured.* += 1; } return end;}fn pairCodeDelimiters(items: []CodeDelimiter) void { if (items.len < 2) return; std.mem.sort(CodeDelimiter, items, {}, codeDelimiterLessThan); var run_start: usize = 0; while (run_start < items.len) { var run_end = run_start + 1; while (run_end < items.len and items[run_end].length == items[run_start].length) { run_end += 1; } var index = run_start; while (index + 1 < run_end) : (index += 1) { items[index].closer_end = items[index + 1].end; } run_start = run_end; } std.mem.sort(CodeDelimiter, items, {}, codeDelimiterByStart);}fn codeDelimiterLessThan(_: void, left: CodeDelimiter, right: CodeDelimiter) bool { if (left.length != right.length) return left.length < right.length; return left.start < right.start;}fn codeDelimiterByStart(_: void, left: CodeDelimiter, right: CodeDelimiter) bool { return left.start < right.start;}fn backtickRunEnd(value: []const u8, start: usize) usize { std.debug.assert(start < value.len); std.debug.assert(value[start] == '`'); var end = start + 1; while (end < value.len and value[end] == '`') : (end += 1) {} return end;}fn adversarialDelimiterSource(out: []u8) []u8 { var used: usize = 0; var length: usize = 1; while (used + length + 1 <= out.len and length <= max_inline_delimiters) : (length += 1) { @memset(out[used..][0..length], '`'); used += length; out[used] = 'x'; used += 1; } return out[0..used];}fn exerciseNestedIndex( source: []const u8, delimiters: CodeDelimiters, depth: usize, max_depth: *usize,) void { std.debug.assert(depth <= max_inline_nesting); max_depth.* = @max(max_depth.*, depth); _ = delimiters.span(source, 0, source.len); if (depth == max_inline_nesting) return; exerciseNestedIndex(source, delimiters, depth + 1, max_depth);}fn appendMarkdownEscaped( out: *std.ArrayList(u8), allocator: Allocator, value: []const u8,) Allocator.Error!void { var index: usize = 0; while (index < value.len) { if (value[index] == '\\' and index + 1 < value.len and isAsciiPunctuation(value[index + 1])) { index += 1; } try html.appendEscapedByte(out, allocator, value[index]); index += 1; }}fn isAsciiPunctuation(byte: u8) bool { return (byte >= '!' and byte <= '/') or (byte >= ':' and byte <= '@') or (byte >= '[' and byte <= '`') or (byte >= '{' and byte <= '~');}fn inlineMathEnd(value: []const u8, start: usize) ?usize { if (start + 1 >= value.len) return null; const first = value[start + 1]; if (first == '$' or first == ' ' or first == '\t') return null; var index = start + 1; while (index < value.len) : (index += 1) { if (value[index] == '\\') { index += 1; continue; } if (value[index] != '$') continue; if (index == start + 1) return null; const previous = value[index - 1]; if (previous == ' ' or previous == '\t') return null; if (index + 1 < value.len and std.ascii.isDigit(value[index + 1])) return null; return index; } return null;}const InlineLink = struct { label: []const u8, target: []const u8, end: usize,};const InlineParseError = error{ InvalidInlineLink, InlineNestingCapacityExceeded,};const ImageAttributes = struct { content: []const u8, end: usize,};const FootnoteReference = struct { key: []const u8, end: usize,};fn parseFootnoteReference(value: []const u8, start: usize) ?FootnoteReference { if (start + 3 > value.len or value[start] != '[' or value[start + 1] != '^') return null; const key_end = std.mem.indexOfScalarPos(u8, value, start + 2, ']') orelse return null; const key = std.mem.trim(u8, value[start + 2 .. key_end], " \t"); if (key.len == 0) return null; return .{ .key = key, .end = key_end + 1 };}fn parseInlineLink( source: []const u8, value: []const u8, base: usize, start: usize, image: bool, diag: ?*diagnostic.Diagnostic, delimiters: CodeDelimiters,) InlineParseError!?InlineLink { const label_start = start + if (image) @as(usize, 2) else @as(usize, 1); const label_end = try scanLinkLabelEnd( value, label_start, diag, delimiters, source, base, ) orelse return null; if (label_end + 1 >= value.len or value[label_end + 1] != '(') return null; const target_start = label_end + 2; return try scanLinkTarget(value, label_start, label_end, target_start, diag);}fn scanLinkLabelEnd( value: []const u8, start: usize, diag: ?*diagnostic.Diagnostic, delimiters: CodeDelimiters, source: []const u8, base: usize,) InlineParseError!?usize { var depth: usize = 0; var index = start; while (index < value.len) { if (value[index] == '\\' and index + 1 < value.len) { index += 2; continue; } if (value[index] == '`') { const run_end = backtickRunEnd(value, index); if (delimiters.span(source, base + index, base + value.len)) |span| { index = span.end - base; } else { index = run_end; } continue; } if (value[index] == '[') { if (depth == max_inline_nesting) { if (diag) |detail| detail.setInline(value, label_nesting_reason, index); return error.InlineNestingCapacityExceeded; } depth += 1; } else if (value[index] == ']') { if (depth == 0) return index; depth -= 1; } index += 1; } return null;}fn scanLinkTarget( value: []const u8, label_start: usize, label_end: usize, target_start: usize, diag: ?*diagnostic.Diagnostic,) InlineParseError!InlineLink { var depth: usize = 1; var index = target_start; while (index < value.len) { if (value[index] == '\\' and index + 1 < value.len) { index += 2; continue; } if (value[index] == '(') { if (depth == max_inline_nesting) { if (diag) |detail| detail.setInline(value, target_nesting_reason, index); return error.InlineNestingCapacityExceeded; } depth += 1; } else if (value[index] == ')') { depth -= 1; if (depth == 0) return .{ .label = value[label_start..label_end], .target = std.mem.trim(u8, value[target_start..index], " \t"), .end = index + 1, }; } index += 1; } if (diag) |detail| { detail.setInline(value, "link target is missing closing ')'", target_start - 1); } return error.InvalidInlineLink;}fn parseImageAttributes(value: []const u8, start: usize) ?ImageAttributes { if (start >= value.len or value[start] != '{') return null; const end = std.mem.indexOfScalarPos(u8, value, start + 1, '}') orelse return null; const content = std.mem.trim(u8, value[start + 1 .. end], " \t\r\n"); if (content.len == 0) return null; var widget_count: u8 = 0; var tokens = std.mem.tokenizeAny(u8, content, " \t\r\n"); while (tokens.next()) |token| { if (token.len >= 2 and token[0] == '.') { for (token[1..]) |byte| { if (!isClassByte(byte)) return null; } continue; } if (widgetAttributeValue(token) == null or widget_count != 0) return null; widget_count += 1; } return .{ .content = content, .end = end + 1 };}fn appendImageAttributes( out: *std.ArrayList(u8), allocator: Allocator, content: []const u8,) Allocator.Error!void { var class_count: u8 = 0; var tokens = std.mem.tokenizeAny(u8, content, " \t\r\n"); while (tokens.next()) |token| { if (token[0] == '.') class_count += 1; } if (class_count != 0) try out.appendSlice(allocator, " class=\""); var first = true; tokens = std.mem.tokenizeAny(u8, content, " \t\r\n"); while (tokens.next()) |token| { if (token[0] != '.') continue; if (!first) try out.append(allocator, ' '); try html.appendAttributeEscaped(out, allocator, token[1..]); first = false; } if (class_count != 0) try out.append(allocator, '"'); tokens = std.mem.tokenizeAny(u8, content, " \t\r\n"); while (tokens.next()) |token| { const widget = widgetAttributeValue(token) orelse continue; try out.appendSlice(allocator, " data-widget=\""); try html.appendAttributeEscaped(out, allocator, widget); try out.append(allocator, '"'); }}fn hasAttributeClass(content: []const u8, expected: []const u8) bool { var tokens = std.mem.tokenizeAny(u8, content, " \t\r\n"); while (tokens.next()) |token| { if (token[0] != '.') continue; if (std.mem.eql(u8, token[1..], expected)) return true; } return false;}fn motionPosterBase(target: []const u8) ?usize { const path_end = std.mem.indexOfAny(u8, target, "?#") orelse target.len; const path = target[0..path_end]; if (!std.mem.endsWith(u8, path, ".svg")) return null; return path.len - ".svg".len;}fn isClassByte(byte: u8) bool { return std.ascii.isAlphanumeric(byte) or byte == '-' or byte == '_';}fn widgetAttributeValue(token: []const u8) ?[]const u8 { const prefix = "data-widget=\""; if (!std.mem.startsWith(u8, token, prefix) or token.len <= prefix.len or token[token.len - 1] != '"') { return null; } const name = token[prefix.len .. token.len - 1]; if (!validWidgetName(name)) return null; return name;}fn validWidgetName(name: []const u8) bool { if (name.len == 0 or name[0] == '-' or name[name.len - 1] == '-') return false; var previous_hyphen = false; for (name) |byte| { const hyphen = byte == '-'; if (!std.ascii.isLower(byte) and !std.ascii.isDigit(byte) and !hyphen) { return false; } if (hyphen and previous_hyphen) return false; previous_hyphen = hyphen; } return true;}const TestInspection = struct { storage: document.Storage, value: document.Document, fn init(allocator: Allocator, source: []const u8) !TestInspection { const heading_plan = try plan(source); var storage = try document.Storage.init(allocator, heading_plan.exactLimits()); errdefer storage.deinit(allocator); storage.activate(); const value = try inspect(&storage, source); return .{ .storage = storage, .value = value }; } fn deinit(self: *TestInspection, allocator: Allocator) void { self.storage.reset(); self.storage.deinit(allocator); self.* = undefined; }};const heading_witness = "# Page\n\n" ++ "```zig\n" ++ "# Not a heading\n" ++ "```\n\n" ++ "> # Quote\n\n" ++ "- item\n" ++ " # Continuation\n\n" ++ "## Section\n";test "markdown renders headings paragraphs emphasis links and escaping" { var rendered = try render( std.testing.allocator, "# Hello <Zen>\n\n" ++ "A **fast** [site](/docs) with `code` & text.\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "Hello <Zen>", rendered.document.firstHeading().?.text, ); try std.testing.expectEqual(@as(usize, 1), rendered.document.headings.len); try std.testing.expectEqual(@as(u8, 1), rendered.document.headings[0].level); try std.testing.expectEqualStrings("hello-zen", rendered.document.headings[0].id); try std.testing.expectEqualStrings( "<h1 id=\"hello-zen\">Hello <Zen></h1>\n" ++ "<p>A <strong>fast</strong> <a href=\"/docs\">site</a> with <code>code</code> & text.</p>\n", rendered.html, );}test "markdown renders semantic pipe tables with inline cell spans" { var rendered = try render( std.testing.allocator, "| Name | Detail | Time |\n" ++ "| :--- | :---: | ---: |\n" ++ "| **fast** | [docs](/docs) and `code` | 12 ms |\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<div class=\"content-table zen-role-wide\" tabindex=\"0\">\n" ++ "<table>\n<thead>\n<tr>\n" ++ "<th scope=\"col\" class=\"zen-table-align-left\">Name</th>\n" ++ "<th scope=\"col\" class=\"zen-table-align-center\">Detail</th>\n" ++ "<th scope=\"col\" class=\"zen-table-align-right\">Time</th>\n" ++ "</tr>\n</thead>\n<tbody>\n<tr>\n" ++ "<td class=\"zen-table-align-left\"><strong>fast</strong></td>\n" ++ "<td class=\"zen-table-align-center\">" ++ "<a href=\"/docs\">docs</a> and <code>code</code></td>\n" ++ "<td class=\"zen-table-align-right\">12 ms</td>\n" ++ "</tr>\n</tbody>\n</table>\n</div>\n", rendered.html, );}test "markdown applies an explicit semantic role to a generated index list" { var rendered = try render( std.testing.allocator, "{.docs-index-family .zen-role-wide}\n" ++ "- [one](/one)\n" ++ "- [two](/two)\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<ul class=\"docs-index-family zen-role-wide\">\n" ++ "<li><a href=\"/one\">one</a></li>\n" ++ "<li><a href=\"/two\">two</a></li>\n" ++ "</ul>\n", rendered.html, );}test "markdown rejects a semantic list role without its artifact" { try std.testing.expectError( error.InvalidSemanticList, render( std.testing.allocator, "{.docs-index-module .zen-role-wide}\nNot a list.\n", .{}, ), );}test "markdown promotes explicit roles to artifact paragraph containers" { var rendered = try render( std.testing.allocator, "<span class=\"zen-role-wide\">Dense comparison.</span>\n\n" ++ "{.zen-role-margin}\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p class=\"zen-role-container zen-role-wide\">" ++ "<span class=\"zen-role-wide\">Dense comparison.</span></p>\n" ++ "<p class=\"zen-media-block zen-role-margin\">" ++ "<img src=\"/plot.svg\" alt=\"Plot\" " ++ "class=\"zen-role-margin\"></p>\n", rendered.html, );}test "markdown admits documentation presentation tags only" { var rendered = try render( std.testing.allocator, "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++ "aria-label=\"Declaration hierarchy\">[Reference](/reference/) " ++ "<span aria-hidden=\"true\">›</span> " ++ "<span aria-current=\"page\">`tiny.pretty`</span></span>\n\n" ++ "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++ "aria-label=\"Verification hierarchy\">[Reference](/reference/) " ++ "<span aria-hidden=\"true\">›</span> " ++ "<span aria-current=\"page\">Verification</span></span>\n\n" ++ "<span class=\"docs-sidenote zen-role-margin\">" ++ "Evidence **matters**.</span>\n\n" ++ "<span class=\"docs-verification-freshness-content\">" ++ "**Publication-qualified.** " ++ "<span class=\"docs-verification-source\">" ++ "[`0123`](/source)</span>.</span>\n\n" ++ "<table class=\"docs-audit-table zen-role-wide\">" ++ "<tbody><tr><th>Source</th>" ++ "<td>`root.zig`</td></tr></tbody></table>\n\n" ++ "<span class=\"unsafe\">No.</span><script>No.</script>\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++ "aria-label=\"Declaration hierarchy\"><a href=\"/reference/\">Reference</a> " ++ "<span aria-hidden=\"true\">›</span> " ++ "<span aria-current=\"page\"><code>tiny.pretty</code></span></span>", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<span class=\"docs-breadcrumb\" role=\"navigation\" " ++ "aria-label=\"Verification hierarchy\"><a href=\"/reference/\">Reference</a> " ++ "<span aria-hidden=\"true\">›</span> " ++ "<span aria-current=\"page\">Verification</span></span>", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<p class=\"docs-verification-freshness\">" ++ "<span class=\"docs-verification-freshness-content\">" ++ "<strong>Publication-qualified.</strong> " ++ "<span class=\"docs-verification-source\">" ++ "<a href=\"/source\"><code>0123</code></a></span>.</span></p>", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<p class=\"docs-sidenote-container zen-role-margin\">" ++ "<span class=\"docs-sidenote zen-role-margin\">" ++ "Evidence <strong>matters</strong>.</span></p>", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<table class=\"docs-audit-table zen-role-wide\">" ++ "<tbody><tr><th>Source</th><td><code>root.zig</code>", ) != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "class=\"unsafe\"") == null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<script>") == null);}test "markdown admits validated static call map fences" { const fragment = "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">" ++ "<svg role=\"group\"><a class=\"zen-ego-node\" href=\"./\">" ++ "<rect x=\"0\"/><text class=\"zen-ego-label\">run</text></a></svg>" ++ "<figcaption>Static calls.</figcaption></figure>"; var rendered = try render( std.testing.allocator, "```zen-ego\n" ++ fragment ++ "\n```\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings(fragment ++ "\n", rendered.html); try std.testing.expectError( error.UnsafeDocumentFragment, render( std.testing.allocator, "```zen-ego\n" ++ "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">" ++ "<svg onload=\"x\"></svg>" ++ "<figcaption>Static calls.</figcaption></figure>\n```\n", .{}, ), );}test "markdown emits one stylesheet for repeated static call maps" { var svg_output: [diagram.ego.svg.output_bytes_max]u8 = undefined; const call_map = try diagram.ego.render(&svg_output, .{ .subject = .{ .label = "run", .context = "tiny.chic", .href = "#run", }, }, .wide); const style_start = std.mem.indexOf(u8, call_map, "<style>") orelse return error.TestUnexpectedResult; const style_close = std.mem.indexOfPos(u8, call_map, style_start, "</style>") orelse return error.TestUnexpectedResult; const style_end = style_close + "</style>".len; const figure_open = "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">"; const figure_close = "<figcaption>Static calls.</figcaption></figure>"; var source: [diagram.ego.svg.document_fragment_bytes_max * 2]u8 = undefined; var writer = std.Io.Writer.fixed(&source); for (0..2) |_| { try writer.writeAll("```zen-ego\n"); try writer.writeAll(figure_open); try writer.writeAll(call_map); try writer.writeAll(figure_close); try writer.writeAll("\n```\n"); } var preserved = try render(std.testing.allocator, writer.buffered(), .{}); defer preserved.deinit(std.testing.allocator); var rendered = try render(std.testing.allocator, writer.buffered(), .{ .one_call_map_style_per_page = true, }); defer rendered.deinit(std.testing.allocator); var preserved_expected_storage: [diagram.ego.svg.document_fragment_bytes_max * 2]u8 = undefined; var preserved_expected = std.Io.Writer.fixed(&preserved_expected_storage); for (0..2) |_| { try preserved_expected.writeAll(figure_open); try preserved_expected.writeAll(call_map); try preserved_expected.writeAll(figure_close); try preserved_expected.writeByte('\n'); } try std.testing.expectEqualStrings(preserved_expected.buffered(), preserved.html); var expected_storage: [diagram.ego.svg.document_fragment_bytes_max * 2]u8 = undefined; var expected = std.Io.Writer.fixed(&expected_storage); try expected.writeAll(figure_open); try expected.writeAll(call_map); try expected.writeAll(figure_close); try expected.writeByte('\n'); try expected.writeAll(figure_open); try expected.writeAll(call_map[0..style_start]); try expected.writeAll(call_map[style_end..]); try expected.writeAll(figure_close); try expected.writeByte('\n'); try std.testing.expectEqualStrings(expected.buffered(), rendered.html); try std.testing.expectEqual(@as(usize, 1), std.mem.count( u8, rendered.html, "<style>", )); for ([_][]const u8{ "<svg ", "class=\"zen-ego-node", " href=\"" }) |marker| { try std.testing.expectEqual( std.mem.count(u8, call_map, marker) * 2, std.mem.count(u8, rendered.html, marker), ); } try std.testing.expectEqual( @as(usize, 2), std.mem.count(u8, rendered.html, figure_close), );}test "markdown leaves a page without call maps style-free" { var rendered = try render(std.testing.allocator, "# Page\n\nNo call maps.\n", .{ .one_call_map_style_per_page = true, }); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), std.mem.count( u8, rendered.html, "<style>", ));}test "markdown preserves distinct static call-map styles" { const opening = "<figure class=\"docs-call-map docs-call-map-primary zen-role-wide\">" ++ "<svg role=\"group\"><style>"; const closing = "</style><text class=\"node\">run</text></svg>" ++ "<figcaption>Static calls.</figcaption></figure>"; var source: [2048]u8 = undefined; var writer = std.Io.Writer.fixed(&source); for ([_][]const u8{ ".node{fill:red}", ".node{fill:blue}" }) |style| { try writer.writeAll("```zen-ego\n"); try writer.writeAll(opening); try writer.writeAll(style); try writer.writeAll(closing); try writer.writeAll("\n```\n"); } var rendered = try render(std.testing.allocator, writer.buffered(), .{ .one_call_map_style_per_page = true, }); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 2), std.mem.count( u8, rendered.html, "<style>", )); try std.testing.expect( std.mem.indexOf(u8, rendered.html, ".node{fill:red}") != null, ); try std.testing.expect( std.mem.indexOf(u8, rendered.html, ".node{fill:blue}") != null, );}test "markdown preserves variable backtick spans inside link labels" { var rendered = try render( std.testing.allocator, "[``@\"tick`](/wrong)\"``](safe/#anchor)\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p><a href=\"safe/#anchor\"><code>@"tick`](/wrong)"</code></a></p>\n", rendered.html, );}test "markdown pairs each code opener after spans consumed by other lengths" { var rendered = try render( std.testing.allocator, "``a ` b`` c `d`\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p><code>a ` b</code> c <code>d</code></p>\n", rendered.html, ); var linked = try render( std.testing.allocator, "[``a ` ](/wrong) b`` c `d](/still-wrong)`](safe)\n", .{}, ); defer linked.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p><a href=\"safe\"><code>a ` ](/wrong) b</code> c " ++ "<code>d](/still-wrong)</code></a></p>\n", linked.html, );}test "markdown scans escaped labels and balanced link targets" { var rendered = try render( std.testing.allocator, "[Zig \\[](/types/@\"name\"(u8\\))) and `plain ] )` plus $x$.\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p><a href=\"/types/@"name"(u8))\">Zig [</a> and " ++ "<code>plain ] )</code> plus <math><mi>x</mi></math>.</p>\n", rendered.html, );}test "markdown never emits anchors inside link labels" { var rendered = try render( std.testing.allocator, "[outer [inner](x) and [^note]](y)\n\n[^note]: Note.\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p><a href=\"y\">outer [inner](x) and [^note]</a></p>\n", rendered.html, ); const first_anchor = std.mem.indexOf(u8, rendered.html, "<a href=").?; try std.testing.expect(std.mem.indexOfPos( u8, rendered.html, first_anchor + "<a href=".len, "<a href=", ) == null);}test "markdown names malformed inline links and fixed nesting excess" { var diag: Diagnostic = .{}; const malformed = "Read [the source](/missing.\n"; try std.testing.expectError(error.InvalidInlineLink, render( std.testing.allocator, malformed, .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings(malformed[0 .. malformed.len - 1], diag.inlineSource()); try std.testing.expectEqualStrings("link target is missing closing ')'", diag.reason); try std.testing.expectEqual(@as(usize, 17), diag.offset); diag.reset(); const nested: [max_inline_nesting + 1]u8 = @splat('['); const excessive = "[" ++ nested ++ "x](safe)\n"; try std.testing.expectError(error.InlineNestingCapacityExceeded, render( std.testing.allocator, excessive, .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings(label_nesting_reason, diag.reason); try std.testing.expectEqual(@as(usize, max_inline_nesting + 1), diag.offset);}test "markdown accepts exact inline scanner boundaries" { const labels: [max_inline_nesting]u8 = @splat('['); const label_closes: [max_inline_nesting]u8 = @splat(']'); const target_opens: [max_inline_nesting - 1]u8 = @splat('('); const target_closes: [max_inline_nesting - 1]u8 = @splat(')'); const source = "[" ++ labels ++ "x" ++ label_closes ++ "](safe) [x](" ++ target_opens ++ "safe" ++ target_closes ++ ")\n"; var rendered = try render(std.testing.allocator, source, .{}); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "href=\"safe\"") != null); var diag: Diagnostic = .{}; const excessive: [max_inline_nesting]u8 = @splat('('); try std.testing.expectError(error.InlineNestingCapacityExceeded, render( std.testing.allocator, "[x](" ++ excessive ++ "safe\n", .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings(target_nesting_reason, diag.reason); try std.testing.expectEqual(@as(usize, max_inline_nesting + 3), diag.offset);}test "markdown keeps incomplete ordinary inline delimiters literal" { var rendered = try render( std.testing.allocator, "Use ``unfinished `code and [plain label without a target.\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p>Use ``unfinished `code and [plain label without a target.</p>\n", rendered.html, );}test "markdown delimiter indexing has a linear byte-visit bound" { const source_capacity = 8 * 1024 * 1024; const source = try std.testing.allocator.alloc(u8, source_capacity); defer std.testing.allocator.free(source); const adversarial = adversarialDelimiterSource(source); try std.testing.expect(adversarial.len > 8 * 1024 * 1024 - 8 * 1024); var visits: usize = 0; var delimiters = try indexCodeDelimiters(std.testing.allocator, adversarial, &visits); defer delimiters.deinit(std.testing.allocator); try std.testing.expect(visits <= 4 * adversarial.len);}test "markdown rejects delimiter run capacity before allocation" { try std.testing.expectEqual(code_delimiter_bytes, @sizeOf(CodeDelimiter)); try std.testing.expectEqual(@as(usize, 2_621_440), max_inline_delimiter_bytes); const exact_bytes = max_inline_delimiters * 2; const source = try std.testing.allocator.alloc(u8, exact_bytes + 2); defer std.testing.allocator.free(source); var index: usize = 0; while (index < source.len) : (index += 2) { source[index] = '`'; source[index + 1] = 'x'; } var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var exact = try indexCodeDelimiters(counting.allocator(), source[0..exact_bytes], null); defer exact.deinit(counting.allocator()); const allocations = counting.alloc_index; try std.testing.expectEqual(@as(usize, 1), allocations); try std.testing.expectEqual( max_inline_delimiter_bytes, counting.allocated_bytes, ); try std.testing.expectError( error.InlineDelimiterCapacityExceeded, indexCodeDelimiters(counting.allocator(), source, null), ); try std.testing.expectEqual(allocations, counting.alloc_index);}test "markdown code spans never close outside their recursive subrange" { const source = "_`inside_ outside`"; var delimiters = try indexCodeDelimiters(std.testing.allocator, source, null); defer delimiters.deinit(std.testing.allocator); var out: std.ArrayList(u8) = .empty; defer out.deinit(std.testing.allocator); try appendInlineIndexed( &out, std.testing.allocator, source, source[1..8], null, null, 1, false, delimiters, .untracked, null, ); try std.testing.expectEqualStrings("`inside", out.items);}test "markdown reuses one delimiter index through maximum recursion" { const source = "`code`"; var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var delimiters = try indexCodeDelimiters(counting.allocator(), source, null); defer delimiters.deinit(counting.allocator()); const index_allocations = counting.alloc_index; var max_depth: usize = 0; exerciseNestedIndex(source, delimiters, 0, &max_depth); try std.testing.expectEqual(max_inline_nesting, max_depth); try std.testing.expectEqual(index_allocations, counting.alloc_index); var out: std.ArrayList(u8) = .empty; defer out.deinit(std.testing.allocator); try out.ensureTotalCapacity(std.testing.allocator, 64); try appendInlineIndexed( &out, counting.allocator(), source, source, null, null, 0, false, delimiters, .untracked, null, ); try std.testing.expectEqualStrings("<code>code</code>", out.items); try std.testing.expectEqual(index_allocations, counting.alloc_index);}test "markdown variable fences preserve embedded structural spellings" { const source = "````````text\n" ++ "```\n" ++ "```````\n" ++ "</code><a href=\"/wrong\">injection</a>\n" ++ "````````\n"; var rendered = try render( std.testing.allocator, source, .{ .code_blocks = .{ .highlighting = false, .line_numbers = false } }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "```</span></span><span class=\"zen-code-line\">", ) != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "```````") != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "</code><a href="/wrong">injection</a>", ) != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<a href=\"/wrong\">") == null);}test "markdown reserves explicit anchors and resolves generated collisions" { const source = "```zen-toc\n" ++ "scope: document\n" ++ "```\n\n" ++ "# Generated\n\n" ++ "## Generated\n\n" ++ "## Stable API {#generated}\n\n" ++ "### Render {#tiny.zen.render}\n"; var rendered = try render(std.testing.allocator, source, .{}); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings("generated~1", rendered.document.headings[0].id); try std.testing.expectEqualStrings("generated~2", rendered.document.headings[1].id); try std.testing.expectEqualStrings("generated", rendered.document.headings[2].id); try std.testing.expectEqualStrings("tiny.zen.render", rendered.document.headings[3].id); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<a href=\"#generated\">Stable API</a>", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<h3 id=\"tiny.zen.render\">Render</h3>", ) != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "{#") == null);}test "markdown preserves typed path anchors" { var rendered = try render( std.testing.allocator, "## Run {#method/Options/run}\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "method/Options/run", rendered.document.headings[0].id, ); try std.testing.expectEqualStrings( "<h2 id=\"method/Options/run\">Run</h2>\n", rendered.html, );}test "markdown suffixes generated heading collisions in source order" { var rendered = try render( std.testing.allocator, "# Repeat\n\n# Repeat\n\n# Repeat\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings("repeat", rendered.document.headings[0].id); try std.testing.expectEqualStrings("repeat~2", rendered.document.headings[1].id); try std.testing.expectEqualStrings("repeat~3", rendered.document.headings[2].id);}test "explicit anchor syntax preserves unanchored heading output" { var rendered = try render( std.testing.allocator, "# Render {#tiny.zen.render}\n", .{ .heading_anchors = false }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings("tiny.zen.render", rendered.document.headings[0].id); try std.testing.expectEqualStrings("<h1>Render</h1>\n", rendered.html);}test "explicit anchor syntax accepts case markers" { var rendered = try render( std.testing.allocator, "# Diagnostic {#decl/~c0~Diagnostic}\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "decl/~c0~Diagnostic", rendered.document.headings[0].id, );}test "markdown reports invalid and duplicate explicit heading anchors" { var diag: diagnostic.Diagnostic = .{}; const duplicate = "# First {#same}\n\n## Second {#same}\n"; try std.testing.expectError(error.DuplicateHeadingAnchor, render( std.testing.allocator, duplicate, .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings("duplicate heading anchor", diag.reason); try std.testing.expectEqual( std.mem.lastIndexOf(u8, duplicate, "{#same}").?, diag.offset, ); const invalid = "# Symbol {#not safe}\n"; try std.testing.expectError(error.InvalidHeadingAnchor, render( std.testing.allocator, invalid, .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings("invalid heading anchor", diag.reason); try std.testing.expectEqual(std.mem.indexOf(u8, invalid, "{#").?, diag.offset);}test "markdown inspects headings outside inert blocks" { var inspected = try TestInspection.init( std.testing.allocator, heading_witness, ); defer inspected.deinit(std.testing.allocator); const doc = inspected.value; try std.testing.expectEqual(@as(usize, 2), doc.headings.len); try std.testing.expectEqualStrings("Page", doc.headings[0].text); try std.testing.expectEqualStrings("page", doc.headings[0].id); try std.testing.expectEqualStrings("Section", doc.headings[1].text); try std.testing.expectEqualStrings("section", doc.headings[1].id);}test "heading storage rejects every limit before backing mutation" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_boundaries"), null, null, null, null, null, null, ); } var storage = try document.Storage.init(std.testing.allocator, .{ .max_headings = 2, .max_text_bytes = 10, .max_id_bytes = 7, }); defer storage.deinit(std.testing.allocator); storage.activate(); @memset(storage.bytes, 42); try std.testing.expectError( error.HeadingCapacityExceeded, inspect(&storage, "# A\n## B\n### C\n"), ); for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte); try std.testing.expectError( error.HeadingTextByteCapacityExceeded, inspect(&storage, "# abcdef\n## ghijkl\n"), ); for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte); try std.testing.expectError( error.HeadingIdByteCapacityExceeded, inspect(&storage, "# A\n## ?\n"), ); for (storage.bytes) |byte| try std.testing.expectEqual(@as(u8, 42), byte); try std.testing.expectEqual(@as(u64, 3), storage.status().rejected_source_count); try std.testing.expect(!storage.status().in_use);}test "heading storage reuses one region after success and rejection" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_reuse"), null, null, null, null, null, null, ); } var storage = try document.Storage.init(std.testing.allocator, .{ .max_headings = 2, .max_text_bytes = 16, .max_id_bytes = 16, }); defer storage.deinit(std.testing.allocator); storage.activate(); const base = @intFromPtr(storage.bytes.ptr); const first = try inspect(&storage, "# First\n## Second\n"); try std.testing.expectEqualStrings("First", first.headings[0].text); try std.testing.expectError( error.DocumentStorageInUse, inspect(&storage, "# Other\n"), ); try std.testing.expectEqualStrings("First", first.headings[0].text); storage.reset(); try std.testing.expectError( error.HeadingCapacityExceeded, inspect(&storage, "# A\n## B\n### C\n"), ); const second = try inspect(&storage, "# Other\n"); try std.testing.expectEqualStrings("Other", second.headings[0].text); storage.reset(); try std.testing.expectEqual(base, @intFromPtr(storage.bytes.ptr)); try std.testing.expectEqual(@as(usize, 2), storage.status().high_water_headings); try std.testing.expectEqual(@as(u64, 2), storage.status().rejected_source_count);}test "activated heading inspection performs no backing allocation" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_sealed"), null, null, null, null, null, null, ); } const heading_plan = try plan(heading_witness); const capacity = try document.Capacity.derive(heading_plan.exactLimits()); var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var storage = try document.Storage.init(counting.allocator(), heading_plan.exactLimits()); defer storage.deinit(counting.allocator()); storage.activate(); const allocations = counting.alloc_index; const resizes = counting.resize_index; const bytes = counting.allocated_bytes; counting.fail_index = allocations; counting.resize_fail_index = resizes; const doc = try inspect(&storage, heading_witness); defer storage.reset(); try std.testing.expectEqual(@as(usize, 2), doc.headings.len); try std.testing.expectEqual(@as(usize, 1), allocations); try std.testing.expectEqual(@as(usize, 0), resizes); try std.testing.expectEqual(capacity.storage_bytes, bytes); try std.testing.expectEqual(allocations, counting.alloc_index); try std.testing.expectEqual(resizes, counting.resize_index); try std.testing.expectEqual(bytes, counting.allocated_bytes);}test "heading storage owns text independently from the Markdown source" { var source = [_]u8{ '#', ' ', 'P', 'a', 'g', 'e', '\n' }; const heading_plan = try plan(&source); var storage = try document.Storage.init(std.testing.allocator, heading_plan.exactLimits()); defer storage.deinit(std.testing.allocator); storage.activate(); const doc = try inspect(&storage, &source); defer storage.reset(); @memset(&source, 'x'); try std.testing.expectEqualStrings("Page", doc.headings[0].text); try std.testing.expectEqualStrings("page", doc.headings[0].id);}test "Markdown rendering owns exact heading storage" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./document/root.zig").Storage, "zen_heading_consumer"), null, null, null, null, null, null, ); } const source = "# Page\n\n## Section\n"; const heading_plan = try plan(source); const capacity = try document.Capacity.derive(heading_plan.exactLimits()); var rendered = try render(std.testing.allocator, source, .{}); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqual(capacity.storage_bytes, rendered.document_storage.bytes.len); try std.testing.expectEqualStrings("Page", rendered.document.firstHeading().?.text);}test "markdown renders generated contents from following headings" { var rendered = try render( std.testing.allocator, "# Page\n\n" ++ "```zen-toc\n" ++ "scope: following\n" ++ "min-level: 2\n" ++ "max-level: 3\n" ++ "```\n\n" ++ "## First\n\n" ++ "### Detail\n\n" ++ "# Next page root\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<h1 id=\"page\">Page</h1>\n" ++ "<nav class=\"zen-contents zen-role-wide\" aria-label=\"Contents\">\n" ++ "<p>Contents:</p>\n" ++ "<ul>\n" ++ "<li class=\"zen-contents-level-2\"><a href=\"#first\">First</a></li>\n" ++ "<li class=\"zen-contents-level-3\" style=\"margin-left:calc(1 * 1.5rem)\"><a href=\"#detail\">Detail</a></li>\n" ++ "</ul>\n" ++ "</nav>\n" ++ "<h2 id=\"first\">First</h2>\n" ++ "<h3 id=\"detail\">Detail</h3>\n" ++ "<h1 id=\"next-page-root\">Next page root</h1>\n", rendered.html, );}test "markdown rejects invalid contents directives" { try std.testing.expectError(error.InvalidContentsDirective, render( std.testing.allocator, "```zen-toc\n" ++ "scope: nowhere\n" ++ "```\n", .{}, ));}test "markdown renders quiz fences with hidden answers" { var rendered = try render( std.testing.allocator, "```zen-quiz\n" ++ "? Which module owns the grammar?\n" ++ "- `markdown.zig`\n" ++ "* `quiz.zig`\n" ++ "! The typed data lives beside the renderer.\n" ++ "```\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<section class=\"zen-quiz\" aria-label=\"Quiz\">\n" ++ "<ol class=\"zen-quiz-questions\">\n" ++ "<li class=\"zen-quiz-question\">\n" ++ "<p class=\"zen-quiz-prompt\">Which module owns the grammar?</p>\n" ++ "<ul class=\"zen-quiz-options\">\n" ++ "<li><code>markdown.zig</code></li>\n" ++ "<li><code>quiz.zig</code></li>\n" ++ "</ul>\n" ++ "<details class=\"zen-quiz-answer\">\n" ++ "<summary>Reveal answer</summary>\n" ++ "<p class=\"zen-quiz-correct\"><strong><code>quiz.zig</code></strong></p>\n" ++ "<p class=\"zen-quiz-explanation\">The typed data lives beside the renderer.</p>\n" ++ "</details>\n" ++ "</li>\n" ++ "</ol>\n" ++ "</section>\n", rendered.html, );}test "markdown renders recall quiz questions without options" { var rendered = try render( std.testing.allocator, "```quiz\n" ++ "? What does the reveal block hold?\n" ++ "! The explanation.\n" ++ "```\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<section class=\"zen-quiz\" aria-label=\"Quiz\">\n" ++ "<ol class=\"zen-quiz-questions\">\n" ++ "<li class=\"zen-quiz-question\">\n" ++ "<p class=\"zen-quiz-prompt\">What does the reveal block hold?</p>\n" ++ "<details class=\"zen-quiz-answer\">\n" ++ "<summary>Reveal answer</summary>\n" ++ "<p class=\"zen-quiz-explanation\">The explanation.</p>\n" ++ "</details>\n" ++ "</li>\n" ++ "</ol>\n" ++ "</section>\n", rendered.html, );}test "markdown rejects invalid quiz fences" { try std.testing.expectError(error.InvalidQuizDirective, render( std.testing.allocator, "```zen-quiz\n" ++ "? A prompt with one lonely option\n" ++ "* only\n" ++ "```\n", .{}, ));}test "Markdown reuses one quiz region across fences" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./quiz/root.zig").Storage, "zen_quiz_consumer"), null, null, null, null, null, null, ); } const limits = quiz.Limits{ .max_questions = 1, .max_options = 2, .max_joined_text_bytes = "First answer continued".len, }; const capacity = try quiz.Capacity.derive(limits); var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var owner: QuizOwner = .{}; defer owner.deinit(counting.allocator()); const first = try owner.parse( counting.allocator(), "? First\n! First answer\ncontinued\n", limits, ); try std.testing.expectEqualStrings("First answer continued", first.questions[0].explanation); const base = @intFromPtr(owner.storage.?.bytes.ptr); owner.reset(); try std.testing.expectEqual(@as(usize, 1), counting.alloc_index); try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes); counting.fail_index = counting.alloc_index; counting.resize_fail_index = counting.resize_index; const second = try owner.parse( counting.allocator(), "? Second\n* Correct\n- Other\n", limits, ); try std.testing.expectEqualStrings("Second", second.questions[0].prompt); try std.testing.expectEqual(@as(usize, 2), second.questions[0].options.len); owner.reset(); try std.testing.expectEqual(base, @intFromPtr(owner.storage.?.bytes.ptr)); try std.testing.expectEqual(@as(usize, 1), counting.alloc_index); try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);}test "Markdown exposes quiz limits at the render boundary" { try std.testing.expectError(error.QuestionCapacityExceeded, render( std.testing.allocator, "```quiz\n? Question\n! Answer\n```\n", .{ .quiz_limits = .{ .max_questions = 0, .max_options = 0, .max_joined_text_bytes = 0, } }, ));}test "Markdown exposes heading limits at the render boundary" { try std.testing.expectError(error.HeadingCapacityExceeded, render( std.testing.allocator, "# Heading\n", .{ .heading_limits = .{ .max_headings = 0, .max_text_bytes = 0, .max_id_bytes = 0, } }, ));}test "Markdown exposes footnote limits at the render boundary" { const source = "[^one]: First\n continued\n"; try std.testing.expectError(error.FootnoteDefinitionCapacityExceeded, render( std.testing.allocator, source, .{ .footnote_limits = .{ .max_definitions = 0, .max_joined_text_bytes = 0, } }, )); try std.testing.expectError(error.FootnoteJoinedTextByteCapacityExceeded, render( std.testing.allocator, source, .{ .footnote_limits = .{ .max_definitions = 1, .max_joined_text_bytes = "First continued".len - 1, } }, ));}test "markdown renders explicit slide decks" { var rendered = try render( std.testing.allocator, "# Title\n\n" ++ "---\n\n" ++ "## Second\n\n" ++ "---\n", .{ .mode = .slides, .slides = .{ .runtime = false } }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<div class=\"zen-slides\" data-zen-slides>\n" ++ "<section class=\"zen-slide\" id=\"slide-1\" aria-label=\"Slide 1\">\n" ++ "<div class=\"zen-slide-frame\">\n" ++ "<h1 id=\"title\">Title</h1>\n" ++ "</div>\n" ++ "</section>\n" ++ "<section class=\"zen-slide\" id=\"slide-2\" aria-label=\"Slide 2\">\n" ++ "<div class=\"zen-slide-frame\">\n" ++ "<h2 id=\"second\">Second</h2>\n" ++ "</div>\n" ++ "</section>\n" ++ "</div>\n" ++ slides.print.stylesheet, rendered.html, );}test "markdown renders speaker notes in slide decks" { var rendered = try render( std.testing.allocator, "# Title\n\n" ++ "```notes\n" ++ "Remember the concrete example.\n" ++ "```\n", .{ .mode = .slides, .slides = .{ .runtime = false } }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<aside class=\"zen-speaker-notes\" data-zen-speaker-notes hidden>") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<p>Remember the concrete example.</p>") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "zen-code-block") == null);}test "markdown drops empty speaker notes in slide decks" { var rendered = try render( std.testing.allocator, "# Title\n\n" ++ "```notes\n" ++ "\n" ++ "```\n", .{ .mode = .slides, .slides = .{ .runtime = false } }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<aside class=\"zen-speaker-notes\"") == null);}test "markdown leaves notes fences as code outside slide decks" { var rendered = try render( std.testing.allocator, "```notes\n" ++ "ordinary note code\n" ++ "```\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "zen-speaker-notes") == null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "ordinary note code") != null);}test "markdown opens wide document figures at full size" { var rendered = try render( std.testing.allocator, "{.zen-slide-figure-wide}\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p class=\"zen-media-block zen-role-wide\">" ++ "<a href=\"/plot.svg\" target=\"_blank\" rel=\"noopener\" " ++ "aria-label=\"Open Plot at full size\">" ++ "<img src=\"/plot.svg\" alt=\"Plot\" " ++ "class=\"zen-slide-figure-wide\"></a></p>\n", rendered.html, );}test "markdown slide decks preserve media math code and diagrams" { var rendered = try render( std.testing.allocator, "# Media\n\n" ++ "{.zen-slide-figure-wide}\n\n" ++ "---\n\n" ++ "## Math and Code\n\n" ++ "$$ E = mc^2 $$\n\n" ++ "```zig\n" ++ "const x = 1 < 2;\n" ++ "```\n\n" ++ "---\n\n" ++ "```zen-diagram\n" ++ "{\"kind\":\"frame\",\"title\":\"Coin\",\"y_min\":0,\"y_max\":1}\n" ++ "{\"kind\":\"bar\",\"x\":\"heads\",\"y\":0.62,\"label\":\"heads\"}\n" ++ "```\n", .{ .mode = .slides, .diagram_width = 36, .diagram_height = 10 }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<section class=\"zen-slide\" id=\"slide-3\"") != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<p class=\"zen-media-block zen-role-reading\">" ++ "<img src=\"/plot.svg\" alt=\"Plot\" " ++ "class=\"zen-slide-figure-wide\"></p>", ) != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<a href=\"/plot.svg\"", ) == null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<math display=\"block\">") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "zen-code-block") != null); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<pre class=\"zen-diagram zen-role-wide\"><code>", ) != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "<hr>") == null);}test "markdown renders webm figures as looping videos with poster fallback" { var rendered = try render( std.testing.allocator, "{.zen-slide-figure-wide}\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p class=\"zen-media-block zen-role-wide\">" ++ "<video src=\"/assets/sph.webm\" poster=\"/assets/sph.webp\" " ++ "aria-label=\"Dam break\" autoplay loop muted playsinline " ++ "class=\"zen-slide-figure-wide\"></video></p>\n", rendered.html, );}test "markdown renders motion figures as static posters" { var rendered = try render( std.testing.allocator, "{.zen-slide-motion\n.zen-slide-figure-wide}\n", .{ .mode = .slides, .slides = .{ .runtime = false } }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<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\">", ) != null);}test "markdown renders a bounded widget selector on an image" { var rendered = try render( std.testing.allocator, "{.demo data-widget=\"accy-wos\"}\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p class=\"zen-media-block zen-role-reading\">" ++ "<img src=\"/assets/poster.svg\" alt=\"Accy demo\" " ++ "class=\"demo\" data-widget=\"accy-wos\"></p>\n", rendered.html, );}test "markdown renders lists blockquotes code fences and images" { var rendered = try render( std.testing.allocator, "- one\n" ++ "- two\n\n" ++ "> quoted\n" ++ "> text\n\n" ++ "```zig\n" ++ "const x = 1 < 2;\n" ++ "```\n\n" ++ "\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<ul>\n" ++ "<li>one</li>\n" ++ "<li>two</li>\n" ++ "</ul>\n" ++ "<blockquote><p>quoted text</p></blockquote>\n" ++ "<div class=\"zen-code-block zen-code-wrap zen-code-example " ++ "zen-role-reading\" data-language=\"zig\"><div class=\"zen-code-header\">" ++ "<span class=\"zen-code-language\">zig</span></div>" ++ "<pre class=\"zen-code\"><code class=\"language-zig\">" ++ "<span class=\"zen-code-line\"><span class=\"zen-code-line-number\" " ++ "aria-hidden=\"true\">1</span><span class=\"zen-code-line-source\">" ++ "<span class=\"zen-code-keyword\">const</span> x = " ++ "<span class=\"zen-code-number\">1</span> < " ++ "<span class=\"zen-code-number\">2</span>;</span></span>" ++ "</code></pre></div>\n" ++ "<p class=\"zen-media-block zen-role-reading\">" ++ "<img src=\"/logo.svg\" alt=\"Logo\"></p>\n", rendered.html, );}test "markdown renders continued list items and blockquote inline markup" { var rendered = try render( std.testing.allocator, "- first item wraps\n" ++ " onto the next line\n" ++ "- second _Item_\n\n" ++ "> **John R. Levine, _Linkers &\n" ++ "> Loaders_**\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<ul>\n" ++ "<li>first item wraps onto the next line</li>\n" ++ "<li>second <em>Item</em></li>\n" ++ "</ul>\n" ++ "<blockquote><p><strong>John R. Levine, <em>Linkers & Loaders</em></strong></p></blockquote>\n", rendered.html, );}test "markdown renders footnote references and definitions" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./footnote/root.zig").Storage, "zen_footnote_consumer"), null, null, null, null, null, null, ); } var rendered = try render( std.testing.allocator, "Alpha[^paper] beta[^paper] gamma[^block] missing[^none].\n\n" ++ "[^paper]: A [paper](/paper)\n" ++ " with `code`.\n" ++ "[^block]:\n" ++ " Block style.\n" ++ "[^unused]: Hidden.\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<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" ++ "<section class=\"footnotes\" aria-label=\"References\">\n" ++ "<ol>\n" ++ "<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\">↩</a></li>\n" ++ "<li id=\"fn-block\">Block style. <a class=\"footnote-backref\" href=\"#fnref-block\" aria-label=\"Back to reference\">↩</a></li>\n" ++ "</ol>\n" ++ "</section>\n", rendered.html, );}test "markdown keeps the first duplicate footnote definition" { var rendered = try render( std.testing.allocator, "Use[^same].\n\n" ++ "[^same]: First.\n" ++ "[^same]: Second\n" ++ " ignored continuation.\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "First.") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "Second") == null);}test "markdown renders inline and display math" { var rendered = try render( std.testing.allocator, "Euler saw $e^{i\\pi} + 1 = 0$ here.\n\n" ++ "$$\n" ++ "u(x) = \\mathbb{E}[u(x + R\\omega)]\n" ++ "$$\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p>Euler saw <math><mrow><msup><mi>e</mi><mrow><mi>i</mi><mi>π</mi></mrow></msup>" ++ "<mo>+</mo><mn>1</mn><mo>=</mo><mn>0</mn></mrow></math> here.</p>\n" ++ "<math display=\"block\"><mrow><mi>u</mi><mo stretchy=\"false\">(</mo><mi>x</mi><mo stretchy=\"false\">)</mo><mo>=</mo>" ++ "<mi>𝔼</mi><mo stretchy=\"false\">[</mo><mi>u</mi><mo stretchy=\"false\">(</mo><mi>x</mi><mo>+</mo><mi>R</mi><mi>ω</mi>" ++ "<mo stretchy=\"false\">)</mo><mo stretchy=\"false\">]</mo></mrow></math>\n", rendered.html, );}test "markdown renders single line display math" { var rendered = try render(std.testing.allocator, "$$ E = mc^2 $$\n", .{}); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<math display=\"block\"><mrow><mi>E</mi><mo>=</mo><mi>m</mi>" ++ "<msup><mi>c</mi><mn>2</mn></msup></mrow></math>\n", rendered.html, );}test "markdown renders display environments across lines" { var rendered = try render( std.testing.allocator, "$$\n" ++ "\\begin{aligned}\n" ++ "a &= b \\\\\n" ++ "&= c\n" ++ "\\end{aligned}\n" ++ "$$\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<math display=\"block\"><mtable displaystyle=\"true\" class=\"zen-aligned\">" ++ "<mtr><mtd><mi>a</mi></mtd><mtd><mo>=</mo><mi>b</mi></mtd></mtr>" ++ "<mtr><mtd></mtd><mtd><mo>=</mo><mi>c</mi></mtd></mtr>" ++ "</mtable></math>\n", rendered.html, );}test "markdown guards prose dollars and honors escapes" { var rendered = try render( std.testing.allocator, "It costs $5 and $10 to run, but \\$x\\$ stays literal.\n", .{}, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p>It costs $5 and $10 to run, but $x$ stays literal.</p>\n", rendered.html, );}test "markdown fails loudly on invalid math" { try std.testing.expectError(error.InvalidEquation, render( std.testing.allocator, "Broken $\\nonesuch$ command.\n", .{}, )); try std.testing.expectError(error.InvalidEquation, render( std.testing.allocator, "$$\nx = 1\n", .{}, ));}test "markdown names the offending equation" { var diag: Diagnostic = .{}; try std.testing.expectError(error.InvalidEquation, render( std.testing.allocator, "Broken $\\nonesuch$ command.\n", .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings("\\nonesuch", diag.equation()); try std.testing.expectEqualStrings("unknown command", diag.reason); try std.testing.expectEqual(@as(usize, 0), diag.offset); diag.reset(); try std.testing.expectError(error.InvalidEquation, render( std.testing.allocator, "$$\nu(x) = \\frac{1}\n$$\n", .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings("u(x) = \\frac{1}", diag.equation()); try std.testing.expectEqualStrings("missing argument", diag.reason); try std.testing.expectEqual(@as(usize, 15), diag.offset); diag.reset(); try std.testing.expectError(error.InvalidEquation, render( std.testing.allocator, "$$\nx = 1\n", .{ .diagnostic = &diag }, )); try std.testing.expectEqualStrings("x = 1", diag.equation()); try std.testing.expectEqualStrings("missing closing '$$'", diag.reason);}test "markdown leaves math inside code spans alone" { var rendered = try render(std.testing.allocator, "Use `$x$` verbatim.\n", .{}); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "<p>Use <code>$x$</code> verbatim.</p>\n", rendered.html, );}test "markdown renders diagram fences as ASCII" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./diagram/ascii/root.zig").RenderStorage, "zen_ascii_render_consumer"), null, null, null, null, null, null, ); } var rendered = try render( std.testing.allocator, "```zen-diagram\n" ++ "{\"kind\":\"frame\",\"title\":\"Coin\",\"y_min\":0,\"y_max\":1}\n" ++ "{\"kind\":\"bar\",\"x\":\"heads\",\"y\":0.62,\"label\":\"heads\"}\n" ++ "```\n", .{ .diagram_width = 36, .diagram_height = 10 }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(std.mem.indexOf( u8, rendered.html, "<pre class=\"zen-diagram zen-role-wide\"><code>", ) != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "Coin") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "#") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.html, "{\"kind\"") == null);}test "link reports locate a direct paragraph target" { const source = "See [the allocator](allocator.zig) for detail.\n"; var storage: [4]RenderedLink = undefined; var report: LinkReport = .{ .storage = &storage }; var rendered = try render( std.testing.allocator, source, .{ .link_report = &report }, ); defer rendered.deinit(std.testing.allocator); const links = report.links(); try std.testing.expectEqual(@as(usize, 1), links.len); try std.testing.expect(links[0].located); try std.testing.expect(links[0].contiguous); try std.testing.expect(!links[0].image); try std.testing.expectEqualStrings( "allocator.zig", source[links[0].target_start..links[0].target_end], ); try std.testing.expectEqualStrings( "[the allocator](allocator.zig)", source[links[0].start..links[0].end], );}test "link reports locate a target carried across joined source lines" { const source = "See [the\nallocator](allocator.zig) for detail.\n"; var storage: [4]RenderedLink = undefined; var report: LinkReport = .{ .storage = &storage }; var rendered = try render( std.testing.allocator, source, .{ .link_report = &report }, ); defer rendered.deinit(std.testing.allocator); const links = report.links(); try std.testing.expectEqual(@as(usize, 1), links.len); try std.testing.expect(links[0].located); try std.testing.expect(links[0].contiguous); try std.testing.expectEqualStrings( "allocator.zig", source[links[0].target_start..links[0].target_end], );}test "link reports emit a nested image after its enclosing link" { const source = "[](capacity/root.zig)\n"; var storage: [4]RenderedLink = undefined; var report: LinkReport = .{ .storage = &storage }; var rendered = try render( std.testing.allocator, source, .{ .link_report = &report }, ); defer rendered.deinit(std.testing.allocator); const links = report.links(); try std.testing.expectEqual(@as(usize, 2), links.len); try std.testing.expect(!links[0].image); try std.testing.expect(links[1].image); try std.testing.expectEqualStrings( "capacity/root.zig", source[links[0].target_start..links[0].target_end], ); try std.testing.expectEqualStrings( "badge.svg", source[links[1].target_start..links[1].target_end], ); try std.testing.expect(links[1].target_start < links[0].target_start);}test "link reports name the exhausted report capacity" { var storage: [1]RenderedLink = undefined; var report: LinkReport = .{ .storage = &storage }; var rendered = try render( std.testing.allocator, "[a](a.zig) and [b](b.zig)\n", .{ .link_report = &report }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expect(report.overflowed); try std.testing.expectEqual(@as(usize, 1), report.count);}test "link reports ignore targets inside fenced and inline code" { const source = "```\n[a](a.zig)\n```\n\nText `[b](b.zig)` text.\n"; var storage: [4]RenderedLink = undefined; var report: LinkReport = .{ .storage = &storage }; var rendered = try render( std.testing.allocator, source, .{ .link_report = &report }, ); defer rendered.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), report.links().len);}test "link reporting leaves ordinary rendered output unchanged" { const source = "# Title\n\nSee [one](one.zig) and .\n\n" ++ "- item [three](three.zig)\n- item two\n\n" ++ "> quoted [four](four.zig)\n\n" ++ "| head | head |\n| :--- | :--- |\n| [five](five.zig) | cell |\n"; var plain = try render(std.testing.allocator, source, .{}); defer plain.deinit(std.testing.allocator); var storage: [16]RenderedLink = undefined; var report: LinkReport = .{ .storage = &storage }; var reported = try render( std.testing.allocator, source, .{ .link_report = &report }, ); defer reported.deinit(std.testing.allocator); try std.testing.expectEqualStrings(plain.html, reported.html); try std.testing.expectEqual(@as(usize, 5), report.links().len);}test "link reports cover list, blockquote and table blocks" { const source = "- item [one](one.zig)\n\n" ++ "> quoted [two](two.zig)\n\n" ++ "| head |\n| :--- |\n| [three](three.zig) |\n"; var storage: [8]RenderedLink = undefined; var report: LinkReport = .{ .storage = &storage }; var rendered = try render( std.testing.allocator, source, .{ .link_report = &report }, ); defer rendered.deinit(std.testing.allocator); const links = report.links(); try std.testing.expectEqual(@as(usize, 3), links.len); for (links, [_][]const u8{ "one.zig", "two.zig", "three.zig" }) |link, expected| { try std.testing.expect(link.located); try std.testing.expect(link.contiguous); try std.testing.expectEqualStrings( expected, source[link.target_start..link.target_end], ); }}Source: lib/zen/src/root.zig:27
zig
/// Markdown inspection and owned HTML rendering.pub const markdown = @import("markdown.zig");Audit
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |