twig-sys 2.5.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
//! Builder — bottom-up, allocation-owning construction of an `AST`. Mirrors
//! fig's `src/ast/builder.zig`: this file IS the `Builder` type (re-exported
//! as `AST.Builder`), ids are array indices, siblings link via
//! `next_sibling`, and every string handed to the builder is *copied* into
//! `owned_strings` so a finished `AST` never borrows the caller's buffers.
//!
//! This is the *simple*, batch-children API (give it a kind and an already-
//! built slice of child ids). A source-driven parser typically needs a more
//! incremental, container-stack style of construction instead (open a node
//! before its children exist, attach them one at a time, possibly rewrite
//! its `Kind` once more is known at close time — e.g. a list's `tight` flag).
//! Rather than generalize this API to cover that (and risk making the common
//! case awkward), `languages/djot/parser.zig` manages its own flat node
//! arrays directly during the build and only produces `AST` values at the
//! boundary — the same division of labor fig's `languages/json/parser.zig`
//! uses relative to this file.

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

const AST = @import("ast.zig");
const Node = AST.Node;
const Span = @import("../span.zig");

allocator: Allocator,
nodes: std.ArrayList(Node) = .empty,
owned_strings: std.ArrayList([]const u8) = .empty,
attrs: std.ArrayList(AST.Attrs) = .empty,

pub fn init(allocator: Allocator) Builder {
    return .{ .allocator = allocator };
}

pub fn deinit(self: *Builder) void {
    for (self.owned_strings.items) |s| self.allocator.free(s);
    self.owned_strings.deinit(self.allocator);
    self.nodes.deinit(self.allocator);
    for (self.attrs.items) |a| {
        self.allocator.free(a.entries);
    }
    self.attrs.deinit(self.allocator);
}

/// Add a node with no children (yet) and the given kind, copying any string
/// payload the kind carries into owned storage. Pair with `setChildren` (via
/// `addContainer`, or directly) to give it children afterward.
pub fn addNode(self: *Builder, kind: Node.Kind) Allocator.Error!Node.Id {
    const id: Node.Id = @intCast(self.nodes.items.len);
    try self.nodes.append(self.allocator, .{
        .id = id,
        .kind = try self.dupeKind(kind),
    });
    return id;
}

/// Add a childless node. An alias for `addNode` — use whichever name reads
/// better at the call site.
pub fn addLeaf(self: *Builder, kind: Node.Kind) Allocator.Error!Node.Id {
    return self.addNode(kind);
}

/// Add a node with the given kind and children (already-built ids, in
/// order). Empty `ids` is fine, yielding a node with no children.
pub fn addContainer(self: *Builder, kind: Node.Kind, ids: []const Node.Id) Allocator.Error!Node.Id {
    const id = try self.addNode(kind);
    self.setChildren(id, ids);
    return id;
}

/// Chain `ids` as `parent`'s children (`parent.first_child = ids[0]`, each
/// linked to the next via `next_sibling`). The ids must already be in
/// `nodes`. Replaces any children `parent` had before.
pub fn setChildren(self: *Builder, parent: Node.Id, ids: []const Node.Id) void {
    self.nodes.items[parent].first_child = if (ids.len == 0) null else ids[0];
    if (ids.len == 0) return;
    for (ids[0 .. ids.len - 1], ids[1..]) |cur, nxt| {
        self.nodes.items[cur].next_sibling = nxt;
    }
    self.nodes.items[ids[ids.len - 1]].next_sibling = null;
}

pub fn setSpan(self: *Builder, id: Node.Id, span: Span) void {
    self.nodes.items[id].span = span;
}

/// Set the interior (between-the-delimiters) span of a container node — see
/// `Node.content_span`'s doc comment for the contract. Left `null` when
/// never called, which is always a correct (if less informative) value.
pub fn setContentSpan(self: *Builder, id: Node.Id, span: Span) void {
    self.nodes.items[id].content_span = span;
}

/// Attach `attrs` to `id` (copying its strings into owned storage),
/// replacing any attributes previously set. Passing an empty `Attrs` clears
/// the attachment (`node.attrs` goes back to `null`) without growing the
/// side-table.
pub fn setAttrs(self: *Builder, id: Node.Id, attrs: AST.Attrs) Allocator.Error!void {
    if (attrs.isEmpty()) {
        self.nodes.items[id].attrs = null;
        return;
    }
    const entries = try self.allocator.alloc(AST.KeyVal, attrs.entries.len);
    errdefer self.allocator.free(entries);
    for (attrs.entries, entries) |src, *dst| {
        dst.* = .{
            .key = try self.dupe(src.key),
            // `null` (a bare attribute, e.g. HTML `disabled`) stays null.
            .value = if (src.value) |v| try self.dupe(v) else null,
        };
    }

    const idx: u32 = @intCast(self.attrs.items.len);
    try self.attrs.append(self.allocator, .{ .entries = entries });
    self.nodes.items[id].attrs = idx;
}

/// Copy the entire tree of `src` into this builder, shifting every node's
/// `span`/`content_span` right by `offset` source bytes, and return the
/// builder id that now holds `src.root`. Strings and attributes are copied
/// into the builder's owned storage, and the child/sibling linkage is rebuilt
/// against the builder's own ids.
///
/// `offset` is where `src`'s source text sits inside the builder's document,
/// so a subtree parsed from an *extracted slice* — e.g. a Markdown HTML block
/// handed to `languages/html/parser.zig` — ends up addressing the true outer
/// source rather than the slice. Pass `0` when `src` was parsed from the same
/// buffer this builder is spanning.
///
/// The returned id is `src.root` remapped; a caller that wants to drop a
/// synthetic wrapper (an HTML parse's `doc` root) walks its `first_child`
/// chain instead of appending the returned id directly.
pub fn graftAst(self: *Builder, src: *const AST, offset: usize) Allocator.Error!Node.Id {
    // Nodes are cloned in id order, so `src` id `i` lands at `base + i`; every
    // child/sibling reference is therefore just its old id plus `base`.
    const base: Node.Id = @intCast(self.nodes.items.len);
    for (src.nodes) |node| {
        const id = try self.addNode(node.kind);
        const dst = &self.nodes.items[id];
        dst.first_child = if (node.first_child) |c| base + c else null;
        dst.next_sibling = if (node.next_sibling) |s| base + s else null;
        dst.span = .{ .start = node.span.start + offset, .end = node.span.end + offset };
        if (node.content_span) |cs|
            dst.content_span = .{ .start = cs.start + offset, .end = cs.end + offset };
        // `setAttrs` copies the entries' strings; it touches only `attrs`, so
        // the `dst` node pointer stays valid across it.
        if (node.attrs) |ai| try self.setAttrs(id, src.attrs[ai]);
    }
    return base + src.root;
}

/// Freeze the builder into an owned `AST` rooted at `root`. The builder is
/// left empty, so a subsequent `deinit` is harmless.
pub fn finish(self: *Builder, root: Node.Id) Allocator.Error!AST {
    const nodes = try self.nodes.toOwnedSlice(self.allocator);
    self.nodes = .empty;
    const owned_strings = try self.owned_strings.toOwnedSlice(self.allocator);
    self.owned_strings = .empty;
    const attrs = try self.attrs.toOwnedSlice(self.allocator);
    self.attrs = .empty;
    return .{
        .allocator = self.allocator,
        .owned_strings = owned_strings,
        .root = root,
        .nodes = nodes,
        .attrs = attrs,
    };
}

/// A non-owning `AST` over the builder's current nodes, rooted at `root`. The
/// returned AST *borrows* the builder's storage (`nodes`, `owned_strings`,
/// `attrs`): it is valid only while the builder lives and stays unmodified, and
/// must NOT be `deinit`ed — the builder owns the memory. Use it to serialize,
/// render, or query an in-progress build without consuming it; use `finish`
/// when you want an owned `AST` instead. Mirrors fig's `Builder.view`.
///
/// `root` must be a valid id (`< nodes.items.len`); callers that accept an id
/// from outside should bounds-check first (the C ABI does).
pub fn view(self: *const Builder, root: Node.Id) AST {
    return .{
        .allocator = self.allocator,
        .owned_strings = self.owned_strings.items,
        .root = root,
        .nodes = self.nodes.items,
        .attrs = self.attrs.items,
    };
}

// ── internals ────────────────────────────────────────────────────────────

/// Take ownership of an already-allocated string. Freed, not leaked, if
/// registration fails.
fn own(self: *Builder, owned: []const u8) Allocator.Error![]const u8 {
    errdefer self.allocator.free(owned);
    try self.owned_strings.append(self.allocator, owned);
    return owned;
}

fn dupe(self: *Builder, s: []const u8) Allocator.Error![]const u8 {
    return self.own(try self.allocator.dupe(u8, s));
}

/// Copy every string payload a `Kind` carries into owned storage, returning
/// the equivalent `Kind` pointing at the copies. Kinds with no string
/// payload (most container kinds) pass through unchanged. This is the single
/// place that needs a new arm whenever `Kind` grows a string-bearing variant.
fn dupeKind(self: *Builder, kind: Node.Kind) Allocator.Error!Node.Kind {
    return switch (kind) {
        .code_block => |v| .{ .code_block = .{
            .lang = if (v.lang) |l| try self.dupe(l) else null,
            .text = try self.dupe(v.text),
        } },
        .raw_block => |v| .{ .raw_block = .{ .format = try self.dupe(v.format), .text = try self.dupe(v.text) } },
        .metadata => |v| .{ .metadata = .{ .lang = try self.dupe(v.lang), .text = try self.dupe(v.text) } },
        .footnote => |v| .{ .footnote = .{ .label = try self.dupe(v.label) } },
        .reference => |v| .{ .reference = .{ .label = try self.dupe(v.label), .destination = try self.dupe(v.destination) } },
        .str => |v| .{ .str = try self.dupe(v) },
        .symb => |v| .{ .symb = try self.dupe(v) },
        .verbatim => |v| .{ .verbatim = try self.dupe(v) },
        .raw_inline => |v| .{ .raw_inline = .{ .format = try self.dupe(v.format), .text = try self.dupe(v.text) } },
        .inline_math => |v| .{ .inline_math = try self.dupe(v) },
        .display_math => |v| .{ .display_math = try self.dupe(v) },
        .url => |v| .{ .url = try self.dupe(v) },
        .email => |v| .{ .email = try self.dupe(v) },
        .footnote_reference => |v| .{ .footnote_reference = try self.dupe(v) },
        .smart_punctuation => |v| .{ .smart_punctuation = .{ .kind = v.kind, .text = try self.dupe(v.text) } },
        .link => |v| .{ .link = .{
            .destination = if (v.destination) |d| try self.dupe(d) else null,
            .reference = if (v.reference) |r| try self.dupe(r) else null,
        } },
        .image => |v| .{ .image = .{
            .destination = if (v.destination) |d| try self.dupe(d) else null,
            .reference = if (v.reference) |r| try self.dupe(r) else null,
        } },
        .directive => |v| .{ .directive = .{ .form = v.form, .name = try self.dupe(v.name) } },
        .element => |v| .{ .element = .{ .name = try self.dupe(v.name) } },
        .comment => |v| .{ .comment = try self.dupe(v) },
        .doctype => |v| .{ .doctype = try self.dupe(v) },
        .processing_instruction => |v| .{ .processing_instruction = .{
            .target = try self.dupe(v.target),
            .data = try self.dupe(v.data),
        } },
        .cdata => |v| .{ .cdata = try self.dupe(v) },
        else => kind,
    };
}

test "Builder constructs a small tree bottom-up" {
    const testing = std.testing;
    var b = Builder.init(testing.allocator);
    defer b.deinit();

    const s1 = try b.addLeaf(.{ .str = "hello " });
    const em_text = try b.addLeaf(.{ .str = "world" });
    const em = try b.addContainer(.emph, &.{em_text});
    const para = try b.addContainer(.para, &.{ s1, em });

    var ast = try b.finish(para);
    defer ast.deinit();

    try testing.expectEqual(para, ast.root);
    try testing.expectEqualStrings("hello ", ast.nodes[s1].kind.str);
    try testing.expect(ast.nodes[para].kind == .para);
    try testing.expectEqual(@as(?Node.Id, em), ast.nodes[s1].next_sibling);
}

test "setAttrs copies attribute strings into owned storage" {
    const testing = std.testing;
    var b = Builder.init(testing.allocator);
    defer b.deinit();

    var class_buf = [_]u8{ 'w', 'a', 'r', 'n' };
    const id = try b.addLeaf(.{ .str = "x" });
    try b.setAttrs(id, .{ .entries = &.{ .{ .key = "class", .value = class_buf[0..] }, .{ .key = "id", .value = "y" } } });
    class_buf[0] = 'Z'; // mutate the source buffer; the copy must be unaffected

    var ast = try b.finish(id);
    defer ast.deinit();

    const attrs = ast.attrsOf(id);
    try testing.expectEqualStrings("warn", attrs.get("class").?);
    try testing.expectEqualStrings("y", attrs.get("id").?);
}

test "setAttrs keeps a bare (null-value) attribute bare" {
    const testing = std.testing;
    var b = Builder.init(testing.allocator);
    defer b.deinit();

    const id = try b.addLeaf(.{ .element = .{ .name = "input" } });
    try b.setAttrs(id, .{ .entries = &.{ .{ .key = "disabled", .value = null }, .{ .key = "type", .value = "checkbox" } } });

    var ast = try b.finish(id);
    defer ast.deinit();

    const attrs = ast.attrsOf(id);
    try testing.expectEqual(@as(?[]const u8, null), attrs.find("disabled").?.value);
    try testing.expectEqualStrings("checkbox", attrs.get("type").?);
}

test "dupeKind copies generic-markup string payloads into owned storage" {
    const testing = std.testing;
    var b = Builder.init(testing.allocator);
    defer b.deinit();

    // Mutable source buffers: mutating them after `addLeaf` proves the
    // builder copied rather than aliased (same trick as the setAttrs test).
    var name_buf = "svg:rect".*;
    var comment_buf = " todo ".*;
    var doctype_buf = "html".*;
    var target_buf = "xml".*;
    var data_buf = "version=\"1.0\"".*;
    var cdata_buf = "a < b".*;

    const el = try b.addLeaf(.{ .element = .{ .name = name_buf[0..] } });
    const cm = try b.addLeaf(.{ .comment = comment_buf[0..] });
    const dt = try b.addLeaf(.{ .doctype = doctype_buf[0..] });
    const pi = try b.addLeaf(.{ .processing_instruction = .{ .target = target_buf[0..], .data = data_buf[0..] } });
    const cd = try b.addLeaf(.{ .cdata = cdata_buf[0..] });
    const root = try b.addContainer(.doc, &.{ el, cm, dt, pi, cd });

    name_buf[0] = 'X';
    comment_buf[0] = 'X';
    doctype_buf[0] = 'X';
    target_buf[0] = 'X';
    data_buf[0] = 'X';
    cdata_buf[0] = 'X';

    var ast = try b.finish(root);
    defer ast.deinit();

    try testing.expectEqualStrings("svg:rect", ast.nodes[el].kind.element.name);
    try testing.expectEqualStrings(" todo ", ast.nodes[cm].kind.comment);
    try testing.expectEqualStrings("html", ast.nodes[dt].kind.doctype);
    try testing.expectEqualStrings("xml", ast.nodes[pi].kind.processing_instruction.target);
    try testing.expectEqualStrings("version=\"1.0\"", ast.nodes[pi].kind.processing_instruction.data);
    try testing.expectEqualStrings("a < b", ast.nodes[cd].kind.cdata);
    // Not aliasing the (now-mutated) inputs also means distinct pointers.
    try testing.expect(ast.nodes[el].kind.element.name.ptr != &name_buf);
}

test "graftAst copies a foreign tree, shifting spans and re-linking children" {
    const testing = std.testing;

    // A donor AST parsed from a slice that sits at offset 100 in some larger
    // document — its spans are slice-relative (0-based).
    var donor = Builder.init(testing.allocator);
    const inner = try donor.addLeaf(.{ .str = "hi" });
    donor.setSpan(inner, Span.init(3, 5));
    const el = try donor.addContainer(.{ .element = .{ .name = "b" } }, &.{inner});
    donor.setSpan(el, Span.init(0, 8));
    try donor.setAttrs(el, .{ .entries = &.{.{ .key = "id", .value = "x" }} });
    var donor_ast = try donor.finish(el);
    defer donor_ast.deinit();

    // Graft it into a host builder as a child of a paragraph, shifted by 100.
    var host = Builder.init(testing.allocator);
    defer host.deinit();
    const grafted = try host.graftAst(&donor_ast, 100);
    const para = try host.addContainer(.para, &.{grafted});

    var ast = try host.finish(para);
    defer ast.deinit();

    // The grafted root is reachable, kept its kind/attrs, and its span shifted.
    const b_el = ast.nodes[ast.root].first_child.?;
    try testing.expectEqualStrings("b", ast.nodes[b_el].kind.element.name);
    try testing.expectEqualStrings("x", ast.attrsOf(b_el).get("id").?);
    try testing.expect(ast.nodes[b_el].span.eql(Span.init(100, 108)));
    // Child linkage was rebuilt against host ids, and its span shifted too.
    const str = ast.nodes[b_el].first_child.?;
    try testing.expectEqualStrings("hi", ast.nodes[str].kind.str);
    try testing.expect(ast.nodes[str].span.eql(Span.init(103, 105)));
}

test "view borrows the in-progress build without consuming it" {
    const testing = std.testing;
    var b = Builder.init(testing.allocator);
    defer b.deinit(); // `view` does not consume, so the builder must still free.

    const hello = try b.addLeaf(.{ .str = "hi" });
    const para = try b.addContainer(.para, &.{hello});

    // A borrowed view sees the current tree; it must NOT be deinit'd.
    const v = b.view(para);
    try testing.expectEqual(para, v.root);
    try testing.expect(v.nodes[para].kind == .para);
    try testing.expectEqualStrings("hi", v.nodes[hello].kind.str);

    // The builder is still live: we can keep adding, and a fresh view reflects it.
    const world = try b.addLeaf(.{ .str = " world" });
    const doc = try b.addContainer(.doc, &.{ para, world });
    const v2 = b.view(doc);
    try testing.expectEqual(doc, v2.root);
    var it = v2.children(doc);
    try testing.expectEqual(para, it.next().?.id);
    try testing.expectEqual(world, it.next().?.id);
    try testing.expectEqual(@as(?*const Node, null), it.next());
}

test "content_span defaults to null and is set via setContentSpan" {
    const testing = std.testing;
    var b = Builder.init(testing.allocator);
    defer b.deinit();

    const text = try b.addLeaf(.{ .str = "abc" });
    const el = try b.addContainer(.{ .element = .{ .name = "div" } }, &.{text});
    b.setSpan(el, Span.init(0, 24));
    b.setContentSpan(el, Span.init(13, 16));

    var ast = try b.finish(el);
    defer ast.deinit();

    try testing.expectEqual(@as(?Span, null), ast.nodes[text].content_span);
    try testing.expect(ast.nodes[el].content_span.?.eql(Span.init(13, 16)));
}