twig-sys 2.2.3

FFI bindings and native library for Twig (the Djot/Markdown/HTML/XML document engine). Used by the `twig-doc` crate.
Documentation
//! Markdown: the entry point for this language module, mirroring
//! `languages/djot/djot.zig`'s / `languages/xml/xml.zig`'s role for their
//! formats. Wires the block parser (`block.zig`, which delegates inline
//! content to `inline.zig`) into one `parse` call, and aggregates every
//! sibling file's `test {}` blocks (the fig/djot/xml convention).
//!
//! ── Scope: this is Phase 2 of 3 ─────────────────────────────────────────
//! Twig's Markdown support targets CommonMark 0.31.2
//! (https://spec.commonmark.org/0.31.2/), built in three phases:
//!   - Phase 1 (done): block structure (headings, lists, block quotes, code
//!     blocks, HTML blocks, thematic breaks, link reference definitions)
//!     plus a minimal inline subset (plain text, backslash escapes, entity/
//!     numeric character references, code spans, soft/hard breaks).
//!   - Phase 2 (this): the rest of CommonMark's inline grammar — emphasis/
//!     strong (the delimiter-run algorithm), links, images, autolinks, raw
//!     inline HTML — resolved AT PARSE TIME against the `link_references`
//!     table Phase 1 populates (unlike djot, Markdown has no render-time
//!     reference table: a resolved reference link is emitted as a `link`
//!     node with `destination` already set and `reference == null`). See
//!     `block.zig` and `inline.zig`'s module doc comments for the precise
//!     boundary and documented simplifications/approximations.
//!   - Phase 3 (later): GFM extensions and other options `ParseOptions`
//!     already declares (tables, strikethrough, task lists, footnotes,
//!     definition lists, frontmatter, math), plus GFM's *extended*
//!     autolinks (bare `www.`/`http` URLs in text, as opposed to Phase 2's
//!     CommonMark-core `<scheme:...>` form).
//! Do not read the presence of `ParseOptions` fields as "already
//! implemented" — see that file's doc comment.
//!
//! ── Rendering ────────────────────────────────────────────────────────────
//! `Markdown.parse` targets the same shared `AST` every other language
//! module does. For everything EXCEPT footnotes, that means MD->HTML can go
//! straight through the generic printer, `Html.serialize`/`Html.serializeAlloc`
//! (`languages/html/serializer.zig`), the same way `languages/html/conformance.zig`
//! proves it works for djot: Phase 2 resolves every reference link/image at
//! PARSE time, so (unlike djot's `Html.Context.references`) `Document
//! .link_references` never needs to be consulted by the renderer at all.
//! Footnotes (`self.options.footnotes`) are the one exception — like djot's
//! footnotes, they're resolved/numbered/backlinked entirely at RENDER time
//! (see `Document.footnotes`'s doc comment below), which the shared printer
//! only does when handed an `Html.Context` — so `Markdown.parse` output
//! should be rendered via THIS package's own `html.zig` (`Markdown.html
//! .render`/`.renderAlloc`, mirroring `Djot.html`), not the bare generic
//! printer, whenever footnotes might be in play (`cli/format.zig`'s
//! registry does exactly this).
//!
//! ── `Document` ───────────────────────────────────────────────────────────
//! Like djot (and unlike XML, which needs no side tables), Markdown needs a
//! wrapper around the shared `AST`: link reference definitions
//! (`[label]: url "title"`) are parsed and stripped out of the block stream
//! by `block.zig` (see its module doc comment), but their labels only
//! become useful once Phase 2 resolves `link`/`image` nodes against them —
//! so, exactly like djot's `Document.references`, they're carried
//! alongside the `AST` rather than folded into it. Footnote definitions
//! (`Document.footnotes`) are carried the same way, for the render-time
//! reason described above.

const std = @import("std");
const Allocator = std.mem.Allocator;

const block = @import("block.zig");
const inline_mod = @import("inline.zig");

pub const AST = @import("../../ast/ast.zig");
pub const ParseOptions = @import("options.zig");
pub const html = @import("html.zig");
pub const serializer = @import("serializer.zig");

pub const Parser = block.Parser;

/// A parsed Markdown document: the language-neutral `AST` plus the
/// label -> `reference`-node table `inline.zig`'s link/image resolution
/// consults DURING parsing (`block.parse` calls `inline_mod.parseInline`
/// with this same map before `Document` is ever constructed — see
/// `block.zig`'s `resolvePendingInline`). Kept on `Document` after parsing
/// mainly for provenance/introspection (e.g. future editor tooling that
/// wants to see where a definition came from); unlike `Djot.Document
/// .references`, the shared HTML printer never needs to consult it, since
/// every resolvable `link`/`image` node already carries its resolved
/// `destination` by the time parsing finishes. Mirrors `Djot.Document`'s
/// general shape — see that type's doc comment for why side tables live
/// here rather than on the shared `AST` (XML/HTML have nothing like them).
pub const Document = struct {
    ast: AST,

    /// The options this document was parsed with. Retained so RENDERING can
    /// recover the dialect (`ParseOptions.dialect`) without the caller having
    /// to supply the flavor a second time — `html.zig` maps it to the shared
    /// printer's conventions. Every field defaults, so a `Document` assembled
    /// from a bare AST rather than by `parse` (`serializer.zig`'s
    /// `serializeAstAlloc`) simply gets twig's default flavor, which is what
    /// that path wants: it serializes back to Markdown source and never
    /// consults a render convention.
    options: ParseOptions = .{},

    /// Label (normalized: trimmed, internal whitespace collapsed, ASCII
    /// lowercased — see `block.zig`'s `normalizeLabel`) -> the `reference`
    /// node holding that link reference definition's destination (and, as
    /// a `title` attribute when present, its title). These `reference`
    /// nodes are NOT attached anywhere in `ast`'s tree: they're pure
    /// side-table entries that `inline.zig` resolves by label at PARSE
    /// time (never at render time, and never rendered in place).
    ///
    /// This map's KEYS are slices of `ast.owned_strings` (each key is the
    /// same string as its `reference` node's own `.label` field, not a
    /// separate copy — see `block.zig`'s `tryParseLinkRefDef`), so `deinit`
    /// only needs to free the map structure itself.
    link_references: std.StringHashMapUnmanaged(AST.Node.Id) = .empty,

    /// Label (normalized, same rule as `link_references`) -> the `footnote`
    /// definition node with that label (`block.zig`'s `[^label]: ...`
    /// handling, gated on `self.options.footnotes`). Unlike
    /// `link_references`, the shared HTML printer DOES need to consult this
    /// one: a `footnote_reference`'s label, like djot's, is resolved,
    /// numbered, and backlinked entirely at RENDER time (djot.js-style),
    /// never at parse time — see `html.zig` (this package's thin adapter
    /// over the shared printer's `Html.Context`, mirroring
    /// `Djot.Document.footnotes`/`djot/html.zig` exactly) and this file's
    /// module doc comment.
    ///
    /// This map's KEYS are slices of `ast.owned_strings` (the same string as
    /// its `footnote` node's own `.label` field), so `deinit` only needs to
    /// free the map structure itself — same story as `link_references`.
    footnotes: std.StringHashMapUnmanaged(AST.Node.Id) = .empty,

    pub fn deinit(self: *Document) void {
        self.link_references.deinit(self.ast.allocator);
        self.footnotes.deinit(self.ast.allocator);
        self.ast.deinit();
    }
};

/// Parse `source` (Markdown/CommonMark text) into a `Document`. The
/// returned document is fully self-contained (its AST owns copies of every
/// string it needs) and must be freed with `doc.deinit()`.
pub fn parse(allocator: Allocator, source: []const u8, options: ParseOptions) Allocator.Error!Document {
    const result = try block.parse(allocator, source, options);
    return .{
        .ast = result.ast,
        .options = options,
        .link_references = result.link_references,
        .footnotes = result.footnotes,
    };
}

/// Whether `angled` — a whole `<…>` run, brackets included — spells a
/// CommonMark autolink (an absolute URI or an email) rather than raw HTML or
/// literal text. The inline scanner's own two recognizers, asked in the order
/// the scanner itself dispatches them, so a caller choosing how to spell a
/// destination gets the answer a reparse will give. Unlike djot's classifier
/// this needs the brackets: CommonMark's grammar is defined over the whole
/// `<…>` run, and the recognizers report where it ends.
///
/// Nothing this accepts can be misread as an HTML block when it lands alone in
/// a paragraph: every accepted form has a `:` or `@` inside the first token,
/// and no HTML block start condition admits either in a tag name.
pub fn spellsAutolink(angled: []const u8) bool {
    if (angled.len < 2 or angled[0] != '<') return false;
    if (inline_mod.scanAutolinkUri(angled, 0)) |end| if (end == angled.len) return true;
    if (inline_mod.scanAutolinkEmail(angled, 0)) |end| if (end == angled.len) return true;
    return false;
}

test {
    _ = @import("entities.zig");
    _ = @import("attributes.zig");
    _ = inline_mod;
    _ = block;
    _ = @import("html.zig");
    _ = @import("conformance.zig");
    _ = @import("gfm_conformance.zig");
}

const testing = std.testing;

test "parse produces a doc with a paragraph" {
    var doc = try parse(testing.allocator, "hello world\n", .{});
    defer doc.deinit();

    const ast = doc.ast;
    try testing.expect(ast.nodes[ast.root].kind == .doc);
    const para_id = ast.nodes[ast.root].first_child orelse return error.TestExpectedNonNull;
    try testing.expect(ast.nodes[para_id].kind == .para);
}

test "headings are flat -- no section wrapper, no auto id (unlike djot)" {
    var doc = try parse(testing.allocator, "# Hello World\n\npara\n", .{});
    defer doc.deinit();

    const ast = doc.ast;
    const heading_id = ast.nodes[ast.root].first_child orelse return error.TestExpectedNonNull;
    try testing.expect(ast.nodes[heading_id].kind == .heading);
    try testing.expectEqual(@as(u32, 1), ast.nodes[heading_id].kind.heading.level);
    try testing.expect(ast.attrsOf(heading_id).isEmpty());

    const para_id = ast.nodes[heading_id].next_sibling orelse return error.TestExpectedNonNull;
    try testing.expect(ast.nodes[para_id].kind == .para);
}