twig-sys 2.5.1

FFI bindings and native library for Twig (the Djot/Markdown/HTML/XML document engine). Used by the `twig-doc` crate.
Documentation
//! The CLI's view of the format registry: the `-i`/`-o` vocabulary and the
//! diagnostics printed when one doesn't resolve.
//!
//! The registry itself — the parser/renderer/serializer/syntax adapters, and
//! `Format` itself — moved to `twig.format` (`src/format.zig`), because the C
//! ABI needs the same table and cannot import the CLI; it used to keep a second
//! hand-maintained copy of the parse adapters instead. What's left here is the
//! part that is genuinely about being a command-line tool: which words `-o`
//! accepts, and what to print at a human when they typo one.
//!
//! The core names are re-exported below so the rest of the CLI keeps saying
//! `format.entryFor` / `format.InputFormat` and needn't care where they live.

const std = @import("std");
const Writer = std.Io.Writer;

const twig = @import("twig");

// ── re-exports from the shared registry ────────────────────────────────────

/// Every language Twig can PARSE — the `-i`/`--input` vocabulary. Named
/// `InputFormat` here (against `twig.Format`) because in CLI terms it is
/// specifically what `-i` accepts, as opposed to `-o`'s `OutputMode`.
pub const InputFormat = twig.format.Format;
pub const FormatEntry = twig.format.Entry;
pub const ParsedDoc = twig.format.ParsedDoc;
pub const ParseConfig = twig.format.ParseConfig;
pub const registry = twig.format.registry;
pub const entryFor = twig.format.entryFor;
pub const parseFormatName = twig.format.parseFormatName;
pub const detectFromExtension = twig.format.detectFromExtension;

// ── the CLI's own vocabulary ───────────────────────────────────────────────

/// What `-o`/`--output` selects: not a source language like `InputFormat`, but
/// one of the three ways `convert` can render a parsed document. `html` is the
/// default; `ast` and `canonical` are documented on `actions.zig`'s `runConvert`.
pub const OutputMode = enum { html, ast, canonical };

pub fn parseOutputMode(name: []const u8) ?OutputMode {
    return std.meta.stringToEnum(OutputMode, name);
}

/// What `-o`/`--output` resolved to: `mode` is always one of the three
/// `OutputMode`s, plus (only ever set alongside `.canonical`) an explicit TARGET
/// language when `-o` named one directly (e.g. `-o djot`) rather than the
/// literal word `canonical` — which means "serialize back to whatever `-i` was",
/// same format in, same format out. `-o <format-name>` is `.canonical` plus "but
/// serialize as `<format-name>` even if that's not the input format", the
/// cross-format `convert` path (`actions.zig`'s `convertSource`).
pub const OutputTarget = struct {
    mode: OutputMode,
    format: ?InputFormat = null,
};

/// Parse an `-o`/`--output` value against both vocabularies it accepts:
/// `OutputMode`'s own names (`html`, `ast`, `canonical`) first, then every
/// registry entry's format name/aliases (`djot`/`dj`, `markdown`/`md`, `xml`) as
/// a request to convert TO that format. Returns `null` when `name` matches
/// neither, so the caller can print a diagnostic listing both.
pub fn parseOutputTarget(name: []const u8) ?OutputTarget {
    if (parseOutputMode(name)) |mode| return .{ .mode = mode };
    if (parseFormatName(name)) |fmt| return .{ .mode = .canonical, .format = fmt };
    return null;
}

/// Write a "supported input formats" list to `w` — the same list an unknown
/// `-i`/`--input` name or an undetectable extension both point the user at
/// (mirrors fig's `main.zig` inline loop over `types.Format`'s fields).
pub fn printSupportedInputFormats(w: *Writer) Writer.Error!void {
    try w.writeAll("supported input formats:\n");
    for (&registry) |*e| {
        try w.print("  - {s}", .{@tagName(e.id)});
        for (e.aliases) |alias| try w.print(" ({s})", .{alias});
        try w.writeByte('\n');
    }
}

/// Errors `resolveInputFormat` can produce, beyond the write failures its own
/// diagnostics can hit. Folded into `args.zig`'s `ArgError` via `||`.
pub const ResolveInputFormatError = Writer.Error || error{
    /// `-` (stdin) was given as the file with no `-i`/`--input` override — there
    /// is no extension to infer from, so the caller MUST say what it is.
    StdinRequiresInputFormat,
    /// A real file path was given, but its extension is missing or matches no
    /// registry entry, and no `-i`/`--input` override was given either.
    UnknownExtension,
};

/// Resolve the definitive `InputFormat` for `convert`/`identify`: an explicit
/// `override` (from `-i`/`--input`) always wins; otherwise infer from
/// `file_path`'s extension; otherwise fail with a diagnostic written to `stderr`
/// (mirrors fig's `ArgError.UnsupportedFileFormat` handling in `main.zig`, but
/// resolved eagerly here in one place instead of deferred to every action).
/// `file_path == "-"` (stdin) skips extension inference entirely, since there is
/// no path to infer from.
pub fn resolveInputFormat(stderr: *Writer, file_path: []const u8, override: ?InputFormat) ResolveInputFormatError!InputFormat {
    if (override) |f| return f;

    if (std.mem.eql(u8, file_path, "-")) {
        try stderr.writeAll("error: reading from stdin ('-') requires an explicit --input/-i format (there is no file extension to infer one from).\n");
        try stderr.flush();
        return error.StdinRequiresInputFormat;
    }

    if (detectFromExtension(file_path)) |f| return f;

    try stderr.print("error: could not infer an input format for '{s}' from its extension.\n", .{file_path});
    try stderr.writeAll("Try using --input/-i <format> to specify one explicitly.\n");
    try printSupportedInputFormats(stderr);
    try stderr.flush();
    return error.UnknownExtension;
}