twig-sys 3.7.0

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 `labels.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, highlight), 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) `Document.labels.references` never needs to
//! be consulted by the renderer at all. Footnotes (`ParseOptions.footnotes`)
//! are the one exception — like djot's footnotes, they're
//! resolved/numbered/backlinked entirely at RENDER time against
//! `Document.labels.footnotes`, 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 (`format.zig`'s registry does exactly this).
//!
//! ── `Document` ───────────────────────────────────────────────────────────
//! `parse` returns the shared `Document` (`src/document.zig`). Link reference
//! definitions (`[label]: url "title"`) are parsed and stripped out of the
//! block stream by `block.zig` (see its module doc comment), and their labels
//! land in `Document.labels.references`, where Phase 2 resolves `link`/`image`
//! nodes against them; footnote definitions land in `labels.footnotes` for the
//! render-time reason above. Neither kind of definition is attached in the
//! tree — they are resolved by label, never rendered in place.

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");
const Document = @import("../../document.zig");
pub const ParseOptions = @import("options.zig");
pub const highlight = @import("highlight.zig");
pub const html = @import("html.zig");
pub const serializer = @import("serializer.zig");

pub const Parser = block.Parser;

/// Parse `source` (Markdown/CommonMark text) into a `Document`. The returned
/// document borrows `source` and owns everything else (its AST holds copies
/// of every string it needs); free it with `doc.deinit()`.
///
/// `options` decides what parses; it is not recorded on the result. The one
/// option rendering needs — `options.dialect`, for GFM's HTML conventions —
/// is passed to `html.render` by whoever holds the preset (`format.zig`'s
/// registry, where each Markdown dialect is a row and `ParsedDoc.format`
/// says which).
pub fn parse(allocator: Allocator, source: []const u8, options: ParseOptions) Allocator.Error!Document {
    return block.parse(allocator, source, options);
}

/// 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("highlight.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);
}

/// Small documents exercising the shared engine contract; not a conformance corpus.
/// These are the default flavor's; each dialect row declares its own below,
/// in the grammar it actually reads.
pub const samples: []const []const u8 = &.{
    "",
    "A paragraph with *emphasis* and **strong** text.\n",
    "# Heading\n\n> Quote\n\n- first\n- second\n",
    "- [x] done\n- [ ] pending\n",
    "``` zig\nconst x = 1;\n```\n",
};

/// Strict CommonMark's harness samples: only what the spec itself spells, so
/// the row round-trips without leaning on an extension it does not read.
pub const commonmark_samples: []const []const u8 = &.{
    "",
    "A paragraph with *emphasis* and **strong** text.\n",
    "# Heading\n\n> Quote\n\n- first\n- second\n",
    "Setext\n======\n\n    indented code\n",
};

/// GFM's harness samples: the spec's constructs plus the four GFM
/// extensions, so a table and a strikethrough have to survive the row's own
/// reparse.
pub const gfm_samples: []const []const u8 = &.{
    "",
    "A paragraph with ~~struck~~ and **strong** text.\n",
    "| a | b |\n| --- | ---: |\n| 1 | 2 |\n",
    "- [x] done\n- [ ] pending\n\nSee https://example.com now.\n",
};