twig-sys 3.10.0

FFI bindings and native library for Twig (the Djot/Markdown/HTML/XML document engine). Used by the `twig-doc` crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
//! A `Syntax` written down as JSON, and read back.
//!
//! ── Why ─────────────────────────────────────────────────────────────────────
//! A runtime language that authors has to hand core the table its gestures
//! read, and it cannot hand over a Zig struct. So `Syntax` crosses the same
//! boundary the node table does (`ast/table.zig`), as data, and the compiled
//! formats cross it first: every compiled table, encoded and decoded, is the
//! table it was. That identity is what makes the encoding a contract rather
//! than a description of one.
//!
//! ── The shape ──────────────────────────────────────────────────────────────
//! One object, keyed by `Syntax`'s field names, which are already the
//! snake_case a description is written in. The encoding is DERIVED from the
//! struct rather than written beside it, so a field added to `Syntax` is in
//! the encoding the moment it exists, and the identity test fails until it
//! round-trips:
//!
//!   * a byte string is a JSON string; a single byte (`heading_marker`,
//!     `code_fence.char`) is a one-byte string, since `"#"` is what a person
//!     means and `35` is not;
//!   * an enum is its tag name, a flag a bool, a count an integer;
//!   * an `EnumArray` is an object keyed by the enum's tag names — a table of
//!     optionals names only the keys it spells, a table of values names all;
//!   * a nested struct is an object, and a list of them an array;
//!   * a member left at its default is omitted, and a member omitted takes its
//!     default, so `{}` is `syntax.none` and a description states only what
//!     it spells.
//!
//! The three renderers are functions, and a function does not cross. What
//! crosses is which ones the language answers itself, as `renderers`: any of
//! `render_text`, `render_block` and `spells_autolink`. A table that states
//! `text_escapes` renders literals by that alphabet — `renderTextByAlphabet`,
//! which core has — so it names no `render_text`, exactly as a compiled table
//! that points at that function states nothing else. Binding the named ones
//! to something callable is the caller's: the identity test binds the
//! compiled functions back, and a runtime language binds calls to itself.
//!
//! Decoding does not validate — `Syntax.validate` does, once the renderers
//! are bound, since two of its rules are about them. Decoding refuses what is
//! not a `Syntax` at all: an unknown key, a value of the wrong type, a byte
//! that is not one byte, a missing member that has no default. A refusal
//! fills a `Problem` with the key path at fault.

const std = @import("std");
const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;
const Stringify = std.json.Stringify;

const syntax = @import("syntax.zig");
const Syntax = syntax.Syntax;

/// Which renderers a table's language answers itself — the part of a
/// `Syntax` that crosses as names rather than values.
pub const Renderers = struct {
    render_text: bool = false,
    render_block: bool = false,
    spells_autolink: bool = false,

    /// What `s` answers itself: every renderer it carries, except the
    /// alphabet renderer, which its `text_escapes` already says.
    pub fn of(s: *const Syntax) Renderers {
        return .{
            .render_text = s.renderText != null and s.renderText != &syntax.renderTextByAlphabet,
            .render_block = s.renderBlock != null,
            .spells_autolink = s.spellsAutolink != null,
        };
    }
};

/// A decoded table: every spelling, the alphabet renderer bound where an
/// alphabet is stated, and the other renderers named but unbound.
pub const Decoded = struct {
    syntax: Syntax,
    renderers: Renderers,
};

/// Why a description's `syntax` was refused: the key path at fault
/// (`container_spelling.block_quote.marker`), and what is wrong with it.
pub const Problem = struct {
    path: []const u8 = "",
    what: []const u8 = "",
};

pub const Options = struct {
    pretty: bool = true,
};

// ── Encoding ────────────────────────────────────────────────────────────────

pub fn encode(writer: *Writer, s: *const Syntax, options: Options) Writer.Error!void {
    var w: Stringify = .{ .writer = writer, .options = .{ .whitespace = if (options.pretty) .indent_2 else .minified } };
    try w.beginObject();
    try writeMembers(&w, Syntax, s.*);
    const r = Renderers.of(s);
    if (r.render_text or r.render_block or r.spells_autolink) {
        try w.objectField("renderers");
        try w.beginArray();
        inline for (std.meta.fields(Renderers)) |f| {
            if (@field(r, f.name)) try w.write(f.name);
        }
        try w.endArray();
    }
    try w.endObject();
}

pub fn encodeAlloc(allocator: Allocator, s: *const Syntax, options: Options) Allocator.Error![]u8 {
    var out: Writer.Allocating = .init(allocator);
    defer out.deinit();
    encode(&out.writer, s, options) catch return error.OutOfMemory;
    return out.toOwnedSlice();
}

/// Whether `T` is a function pointer, or an optional one — a renderer, which
/// the encoding names rather than carries.
fn isFunction(comptime T: type) bool {
    return switch (@typeInfo(T)) {
        .optional => |o| isFunction(o.child),
        .pointer => |p| @typeInfo(p.child) == .@"fn",
        else => false,
    };
}

fn isEnumArray(comptime T: type) bool {
    return @typeInfo(T) == .@"struct" and @hasDecl(T, "Indexer") and @hasDecl(T, "Value") and @hasField(T, "values");
}

fn writeMembers(w: *Stringify, comptime T: type, v: T) Writer.Error!void {
    inline for (std.meta.fields(T)) |f| {
        if (comptime isFunction(f.type)) continue;
        const value = @field(v, f.name);
        const at_default = if (f.defaultValue()) |d| eqlValue(f.type, value, d) else false;
        if (!at_default) {
            try w.objectField(f.name);
            try writeValue(w, f.type, value);
        }
    }
}

fn writeValue(w: *Stringify, comptime T: type, v: T) Writer.Error!void {
    if (T == u8) return w.write(&[_]u8{v});
    if (comptime isEnumArray(T)) {
        try w.beginObject();
        for (std.enums.values(T.Key)) |k| {
            const item = v.get(k);
            if (@typeInfo(T.Value) == .optional and item == null) continue;
            try w.objectField(@tagName(k));
            try writeValue(w, T.Value, item);
        }
        return w.endObject();
    }
    switch (@typeInfo(T)) {
        .bool, .int => try w.write(v),
        .@"enum" => try w.write(@tagName(v)),
        .optional => |o| if (v) |inner| try writeValue(w, o.child, inner) else try w.write(null),
        .pointer => |p| {
            if (p.child == u8) return w.write(v);
            try w.beginArray();
            for (v) |item| try writeValue(w, p.child, item);
            try w.endArray();
        },
        .@"struct" => {
            try w.beginObject();
            try writeMembers(w, T, v);
            try w.endObject();
        },
        else => @compileError("no JSON spelling for " ++ @typeName(T)),
    }
}

// ── Equality ────────────────────────────────────────────────────────────────

/// Two tables spell the same: every byte string equal by content, every
/// renderer the same function. What the identity is stated in.
pub fn eql(a: *const Syntax, b: *const Syntax) bool {
    return eqlValue(Syntax, a.*, b.*);
}

fn eqlValue(comptime T: type, a: T, b: T) bool {
    if (comptime isEnumArray(T)) {
        for (std.enums.values(T.Key)) |k| {
            if (!eqlValue(T.Value, a.get(k), b.get(k))) return false;
        }
        return true;
    }
    return switch (@typeInfo(T)) {
        .bool, .int, .@"enum" => a == b,
        .optional => |o| if (a) |x| (if (b) |y| eqlValue(o.child, x, y) else false) else b == null,
        .pointer => |p| blk: {
            if (@typeInfo(p.child) == .@"fn") break :blk a == b;
            if (a.len != b.len) break :blk false;
            for (a, b) |x, y| {
                if (!eqlValue(p.child, x, y)) break :blk false;
            }
            break :blk true;
        },
        .@"struct" => blk: {
            inline for (std.meta.fields(T)) |f| {
                if (!eqlValue(f.type, @field(a, f.name), @field(b, f.name))) break :blk false;
            }
            break :blk true;
        },
        else => @compileError("no equality for " ++ @typeName(T)),
    };
}

// ── Decoding ────────────────────────────────────────────────────────────────

pub const Error = error{ InvalidSyntax, OutOfMemory };

/// Read a `Syntax` from its JSON value. Strings are copied into `arena`,
/// which owns the result. The alphabet renderer is bound where `text_escapes`
/// is stated; the renderers `renderers` names are left null for the caller.
pub fn fromValue(arena: Allocator, value: std.json.Value, problem: *Problem) Error!Decoded {
    var r: Reader = .{ .arena = arena, .problem = problem };
    const obj = switch (value) {
        .object => |o| o,
        else => return r.fail("", "is not an object"),
    };
    var decoded: Decoded = .{ .syntax = try r.members(Syntax, obj, "", &.{"renderers"}), .renderers = .{} };
    if (obj.get("renderers")) |list| {
        const items = switch (list) {
            .array => |a| a.items,
            else => return r.fail("renderers", "is not an array"),
        };
        for (items) |item| {
            const name = switch (item) {
                .string => |n| n,
                else => return r.fail("renderers", "holds a value that is not a renderer's name"),
            };
            inline for (std.meta.fields(Renderers)) |f| {
                if (std.mem.eql(u8, name, f.name)) {
                    @field(decoded.renderers, f.name) = true;
                    break;
                }
            } else return r.fail("renderers", "names a renderer that is not render_text, render_block or spells_autolink");
        }
    }
    if (decoded.syntax.text_escapes != null) decoded.syntax.renderText = &syntax.renderTextByAlphabet;
    return decoded;
}

/// `fromValue` over JSON text.
pub fn decode(arena: Allocator, text: []const u8, problem: *Problem) Error!Decoded {
    const value = std.json.parseFromSliceLeaky(std.json.Value, arena, text, .{}) catch |err| switch (err) {
        error.OutOfMemory => return error.OutOfMemory,
        else => {
            problem.* = .{ .what = "is not JSON" };
            return error.InvalidSyntax;
        },
    };
    return fromValue(arena, value, problem);
}

const Reader = struct {
    arena: Allocator,
    problem: *Problem,

    fn fail(self: *Reader, path: []const u8, what: []const u8) Error {
        self.problem.* = .{ .path = path, .what = what };
        return error.InvalidSyntax;
    }

    fn join(self: *Reader, path: []const u8, key: []const u8) Error![]const u8 {
        if (path.len == 0) return self.arena.dupe(u8, key);
        return std.fmt.allocPrint(self.arena, "{s}.{s}", .{ path, key });
    }

    fn members(
        self: *Reader,
        comptime T: type,
        obj: std.json.ObjectMap,
        path: []const u8,
        comptime ignore: []const []const u8,
    ) Error!T {
        var out: T = undefined;
        inline for (std.meta.fields(T)) |f| {
            if (comptime isFunction(f.type)) {
                @field(out, f.name) = f.defaultValue().?;
            } else if (obj.get(f.name)) |v| {
                @field(out, f.name) = try self.value(f.type, v, try self.join(path, f.name));
            } else if (f.defaultValue()) |d| {
                @field(out, f.name) = d;
            } else {
                return self.fail(try self.join(path, f.name), "is missing, and has no default");
            }
        }
        for (obj.keys()) |key| {
            if (!isMember(T, key, ignore)) return self.fail(try self.join(path, key), "is not a member of this table");
        }
        return out;
    }

    fn isMember(comptime T: type, key: []const u8, comptime ignore: []const []const u8) bool {
        inline for (ignore) |name| {
            if (std.mem.eql(u8, key, name)) return true;
        }
        inline for (std.meta.fields(T)) |f| {
            if (comptime isFunction(f.type)) continue;
            if (std.mem.eql(u8, key, f.name)) return true;
        }
        return false;
    }

    fn value(self: *Reader, comptime T: type, v: std.json.Value, path: []const u8) Error!T {
        if (T == u8) {
            const s = switch (v) {
                .string => |s| s,
                else => return self.fail(path, "is not a one-byte string"),
            };
            if (s.len != 1) return self.fail(path, "is not a one-byte string");
            return s[0];
        }
        if (comptime isEnumArray(T)) {
            const obj = switch (v) {
                .object => |o| o,
                else => return self.fail(path, "is not an object"),
            };
            const optional = @typeInfo(T.Value) == .optional;
            var out: T = undefined;
            for (std.enums.values(T.Key)) |k| {
                if (obj.get(@tagName(k))) |item| {
                    out.set(k, try self.value(T.Value, item, try self.join(path, @tagName(k))));
                } else if (optional) {
                    out.set(k, null);
                } else {
                    return self.fail(try self.join(path, @tagName(k)), "is missing");
                }
            }
            var keys = obj.iterator();
            while (keys.next()) |entry| {
                if (std.meta.stringToEnum(T.Key, entry.key_ptr.*) == null)
                    return self.fail(try self.join(path, entry.key_ptr.*), "is not a key of this table");
            }
            return out;
        }
        switch (@typeInfo(T)) {
            .bool => return switch (v) {
                .bool => |b| b,
                else => self.fail(path, "is not a bool"),
            },
            .int => return switch (v) {
                .integer => |i| std.math.cast(T, i) orelse self.fail(path, "is out of range"),
                else => self.fail(path, "is not an integer"),
            },
            .@"enum" => {
                const s = switch (v) {
                    .string => |s| s,
                    else => return self.fail(path, "is not a name"),
                };
                return std.meta.stringToEnum(T, s) orelse self.fail(path, "names no value this member takes");
            },
            .optional => |o| return switch (v) {
                .null => null,
                else => try self.value(o.child, v, path),
            },
            .pointer => |p| {
                if (p.child == u8) {
                    const s = switch (v) {
                        .string => |s| s,
                        else => return self.fail(path, "is not a string"),
                    };
                    return self.arena.dupe(u8, s);
                }
                const items = switch (v) {
                    .array => |a| a.items,
                    else => return self.fail(path, "is not an array"),
                };
                const out = try self.arena.alloc(p.child, items.len);
                for (items, out, 0..) |item, *slot, i| {
                    slot.* = try self.value(p.child, item, try std.fmt.allocPrint(self.arena, "{s}[{d}]", .{ path, i }));
                }
                return out;
            },
            .@"struct" => {
                const obj = switch (v) {
                    .object => |o| o,
                    else => return self.fail(path, "is not an object"),
                };
                return self.members(T, obj, path, &.{});
            },
            else => @compileError("no JSON reading for " ++ @typeName(T)),
        }
    }
};

// ── Tests ───────────────────────────────────────────────────────────────────

const testing = std.testing;
const format = @import("format.zig");
const markdown_syntax = @import("languages/markdown/syntax.zig");

/// `s` encoded, decoded, and given back its own renderers is `s`.
fn expectIdentity(s: *const Syntax) !void {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const text = try encodeAlloc(arena, s, .{});
    errdefer std.debug.print("\n--- syntax ---\n{s}\n", .{text});
    var problem: Problem = .{};
    var decoded = decode(arena, text, &problem) catch |err| {
        std.debug.print("\nrefused: {s}: {s}\n", .{ problem.path, problem.what });
        return err;
    };
    try testing.expectEqual(Renderers.of(s), decoded.renderers);
    if (decoded.renderers.render_text) decoded.syntax.renderText = s.renderText;
    if (decoded.renderers.render_block) decoded.syntax.renderBlock = s.renderBlock;
    if (decoded.renderers.spells_autolink) decoded.syntax.spellsAutolink = s.spellsAutolink;
    try testing.expect(eql(s, &decoded.syntax));
    try decoded.syntax.validate(null);
    // And the encoding is a fixed point: nothing the decode filled in by
    // default is written back differently.
    const again = try encodeAlloc(arena, &decoded.syntax, .{});
    try testing.expectEqualStrings(text, again);
}

test "syntax json: every compiled table is its own encoding" {
    for (format.registry) |entry| {
        errdefer std.debug.print("\n{s}: syntax identity\n", .{@tagName(entry.id)});
        try expectIdentity(entry.syntax);
    }
    // Every table Markdown is authored by, not only the registry's default:
    // the palette, the directives and the math alphabet live only in these.
    for (markdown_syntax.every) |*t| try expectIdentity(t);
}

test "syntax json: the empty object is the table that spells nothing" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    var problem: Problem = .{};
    const decoded = try decode(arena_state.allocator(), "{}", &problem);
    try testing.expect(eql(&syntax.none, &decoded.syntax));
    const text = try encodeAlloc(arena_state.allocator(), &syntax.none, .{ .pretty = false });
    try testing.expectEqualStrings("{}", text);
}

test "syntax json: a description states only what it spells" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    var problem: Problem = .{};
    const decoded = try decode(arena_state.allocator(),
        \\{"inline_delims":{"strong":{"open":"*","close":"*"},"mark":{"open":"==","close":"==","authorable":false}},
        \\ "heading_marker":"=","code_fence":{"char":"~"},
        \\ "text_escapes":"*\\","block_start_escapes":"=",
        \\ "table_spelling":{"bar":"|","delim_pad":" ","delim":{"default":"---","left":":--","right":"--:","center":":-:"}},
        \\ "renderers":["render_block"]}
    , &problem);
    const s = decoded.syntax;
    try testing.expectEqualStrings("*", s.inline_delims.get(.strong).?.open);
    try testing.expect(s.inline_delims.get(.strong).?.authorable);
    try testing.expect(!s.inline_delims.get(.mark).?.authorable);
    try testing.expect(s.inline_delims.get(.emph) == null);
    try testing.expectEqual(@as(?u8, '='), s.heading_marker);
    try testing.expectEqual(@as(u8, '~'), s.code_fence.?.char);
    try testing.expectEqual(@as(usize, 3), s.code_fence.?.min);
    try testing.expectEqualStrings(" ", s.table_spelling.?.pad);
    try testing.expect(s.renderText == &syntax.renderTextByAlphabet);
    try testing.expect(decoded.renderers.render_block and !decoded.renderers.render_text);
    try testing.expect(s.renderBlock == null);
}

test "syntax json: what is not a Syntax is refused at its path" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const cases = [_]struct { text: []const u8, path: []const u8 }{
        .{ .text = "{\"heading_markr\":\"#\"}", .path = "heading_markr" },
        .{ .text = "{\"heading_marker\":\"##\"}", .path = "heading_marker" },
        .{ .text = "{\"inline_delims\":{\"bold\":{\"open\":\"*\",\"close\":\"*\"}}}", .path = "inline_delims.bold" },
        .{ .text = "{\"inline_delims\":{\"strong\":{\"open\":\"*\"}}}", .path = "inline_delims.strong.close" },
        .{ .text = "{\"attr_spelling\":{\"quoting\":\"sometimes\"}}", .path = "attr_spelling.quoting" },
        .{ .text = "{\"table_spelling\":{\"bar\":\"|\",\"delim_pad\":\"\",\"delim\":{\"default\":\"---\"}}}", .path = "table_spelling.delim.left" },
        .{ .text = "{\"mark_colors\":{\"attr_key\":\"c\",\"colors\":[{\"name\":\"red\"}]}}", .path = "mark_colors.colors[0].prefix" },
        .{ .text = "{\"renderers\":[\"render_html\"]}", .path = "renderers" },
        .{ .text = "[]", .path = "" },
    };
    for (cases) |c| {
        var problem: Problem = .{};
        errdefer std.debug.print("\ncase: {s}\n", .{c.text});
        try testing.expectError(error.InvalidSyntax, decode(arena, c.text, &problem));
        try testing.expectEqualStrings(c.path, problem.path);
    }
}