fig-sys 3.4.0

FFI bindings and native library for fig (the comment-preserving JSON/YAML/TOML/… config engine). Used by the `fig` 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
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//! The declared half of the `Language` interface — what a format supplies
//! about its own syntax, rather than what `Editor` infers by testing which
//! language it was instantiated with.
//!
//! `language.zig`'s `validate` states the four-declaration contract (`Type`,
//! `default_type`, `parse`, `print`). That was never the whole of it: the rest
//! lived as `if (Language == Toml)` / `== Plist` / `!= NestedText` branches
//! inside `editor.zig`, discoverable only by reading them. The types here are
//! where that unwritten half becomes writable — a format declares `syntax`,
//! `caps`, `name` and `extensions`, and the generic engine reads them instead
//! of asking what it is. See `docs/proposals/language-interface.md`.
//!
//! This module is the PARAMETER half of that interface — the answers a format
//! can give as a value. The other half is operations it takes over outright,
//! which are declared as hooks in the "Editing hooks" block of each
//! `<lang>/<lang>.zig` and dispatched by `@hasDecl` from `editor.zig`; they
//! need no types here, because a hook's signature is fixed by the `Editor`
//! method it overrides.
//!
//! It also holds the shape of a format-registry entry, `Dialect`, since each
//! language now declares its own dialects (`Language.dialects`) and
//! `language.zig` only assembles them.
//!
//! This module is deliberately a LEAF: it imports nothing, not even `std`.
//! `language.zig` re-exports these types and every `<lang>/<lang>.zig` imports
//! them, so anything pulled in here would be pulled into all eleven language
//! modules — and an import back to `language.zig` (which imports each of them
//! in turn) would make the manifest's own types depend on the languages that
//! declare them.

/// Which leading-comment syntax a language uses, so the owned-comment scan in
/// delete/move (`editor/splice.zig`'s `commentBlockStart`) recognizes the right marker.
///
/// Distinct from `Comments.line`: this selects the *scanner*, which is
/// per-language and comptime, while the marker is per-dialect and may be null
/// where the scanner still has a sensible answer. Plain JSON has no comments,
/// but `.slashes` is harmless there since no `//` line can exist.
pub const CommentStyle = enum {
    /// YAML, TOML, fig, dotenv, `.properties`, NestedText.
    hash,
    /// JSON/JSONC/JSON5 and ZON (which follows Zig). The only style whose
    /// scanner also walks multi-line `/* ... */` blocks as a unit.
    slashes,
    /// INI. Its printer accepts a leading `#` on read but always WRITES `;`,
    /// so `;` is the marker the editor's own inserts and scans use.
    semicolon,
    /// plist's `<!-- ... -->`.
    xml_comment,
};

/// A format's whole comment surface: which scanner walks an owned comment
/// block, and the markers the editor writes and strips.
///
/// One field on `Syntax` rather than three, because the three answers coincide
/// for most formats and restating the same marker three times reads as
/// redundancy rather than as the three independent questions it is. The two
/// presets below are exactly the "all three agree" case; a format whose
/// answers diverge — INI, NestedText, plist — writes the literal out, and so
/// does JSON, whose marker varies by dialect while its scanner does not.
pub const Comments = struct {
    /// Selects the owned-comment-block scanner. See `CommentStyle`. Never
    /// null, and never redundant with `line`: the scanner is per-language
    /// while a marker is per-dialect, so strict JSON declares `.slashes`
    /// alongside a null marker — unobservable there, since no `//` line can
    /// exist for the scanner to find.
    style: CommentStyle,

    /// The own-line (leading) comment marker, or null when the dialect has
    /// none to write — strict JSON, where the comment ops return
    /// `CommentsUnsupported`, and plist, whose `<!-- ... -->` is a delimiter
    /// pair with no leader (it hooks all six comment ops instead).
    line: ?[]const u8,

    /// The marker for a same-line TRAILING comment specifically, or null when
    /// the format has no such syntax.
    ///
    /// Distinct from `line` because INI and NestedText have real, safe leading
    /// comments but no trailing ones: a `;`/`#` after a value on the SAME line
    /// is literal value text, not a comment (see `ini/parser.zig`'s "a value
    /// runs to end of line" and `nestedtext/parser.zig`'s "rest-of-line values
    /// are 100% literal", and both printers, which render a "trailing" comment
    /// as its own line immediately after the entry). Splicing one in anyway
    /// would silently corrupt the value on reread, so trailing ops are refused
    /// there.
    trailing: ?[]const u8,

    /// `#` throughout — YAML, TOML, fig, dotenv, `.properties`.
    pub const hash: Comments = .{ .style = .hash, .line = "#", .trailing = "#" };

    /// `//` throughout — ZON, which follows Zig.
    pub const slashes: Comments = .{ .style = .slashes, .line = "//", .trailing = "//" };
};

/// How a logical mapping key renders as this format's key syntax on the `set`
/// insert path (`editor.formatInsertKey`).
///
/// A logical key is plain text (`b`, `has space`); what reaches the source
/// depends on the format's key grammar, and the splice is reparsed under it.
pub const KeyStyle = enum {
    /// Spliced as-is — YAML, TOML, fig, INI, dotenv, `.properties`,
    /// NestedText, plist. The same thing `insertKey`'s other callers do.
    verbatim,
    /// Quoted and escaped as a JSON string (`b` -> `"b"`). Required by strict
    /// JSON and harmless in JSONC/JSON5.
    json_quoted,
    /// ZON's struct-field syntax, which always carries a leading `.`
    /// (`b` -> `.b`, quoted as `.@"has space"` when not a bare identifier).
    zon_field,
};

/// What `fig` can do with a format, as declared by the format itself.
///
/// The single source: `c_api.fig_format_capabilities` reads these bits rather
/// than restating them, so the C ABI cannot disagree with the format about what
/// the format can do. It used to be hand-maintained in both places, and drifted
/// silently in both directions — see the proposal's §7 and §12.
///
/// The MAPPING between the two is not spelled out anywhere either: `FigFormat`
/// is per-dialect (json/jsonc/json5 are three ABI values over this one
/// `Language`) while `caps` is per-language, and the format registry's `Lang`
/// field is the bridge — `fig_format_capabilities` looks the entry up by member
/// name and reads the bits off the language it names.
pub const Caps = struct {
    /// `parse` accepts this format. True for every language in tree.
    read: bool = true,
    /// `Editor(Language)` is instantiated for this format. False for XML,
    /// which has a reader and a writer but no in-place editor yet.
    edit: bool = false,
    /// `print` can write this format.
    serialize: bool = false,
    /// What the lossless `$fig` envelope pass (`lossless.zig`) may assume
    /// about this format's value model on OUTPUT, or null when the format
    /// takes no envelope at all.
    ///
    /// Non-null says: when `--lossless` targets this format, wrap every
    /// scalar kind NOT marked native in `NativeKinds` in a `$fig` envelope so
    /// a later run can rebuild it, and leave the marked kinds bare. Only the
    /// four typed formats with a real value model and a mapping to carry the
    /// envelope in — JSON, YAML, TOML, ZON — declare one.
    ///
    /// Null says: never encode an envelope into this format (envelopes in
    /// its INPUT are still decoded). Two distinct reasons collapse into the
    /// one answer, deliberately, because the pass has one behaviour for both:
    ///
    ///   * fig and canonical spell every kind directly, so an envelope would
    ///     preserve nothing a plain print does not.
    ///   * XML, INI, dotenv, `.properties`, plist and NestedText have no typed
    ///     scalar envelope of their own — their printers already reduce the
    ///     value to text, so a mapping-shaped envelope would be no more
    ///     recoverable than the degraded scalar it replaced.
    ///
    /// A field on `Caps` rather than its own `Language` declaration because it
    /// IS a capability — "can fig round-trip a value through this format
    /// without loss, and which values need help" — and because the seven
    /// null answers then cost nothing to state: the default is the
    /// conservative one. It sits on the LANGUAGE (json/jsonc/json5 share it),
    /// which is why JSON5's native `Infinity`/`NaN` are still enveloped: the
    /// declaration is per-language and JSON's is the strict dialect's.
    lossless: ?NativeKinds = null,

    /// How many levels of mapping nesting this format can represent, or
    /// null for a format with no depth limit at all (every typed format).
    /// INI holds a root mapping plus one level of `[section]`s (1); dotenv
    /// and `.properties` are flat — the root mapping itself, nothing nested
    /// under it (0).
    ///
    /// A non-null value also says the format holds no sequence anywhere and
    /// no `null`: the three flat formats share that shape, and
    /// `flat_strip.zig` — the lossy pass that drops what such a format
    /// cannot hold before printing — and `diagnostics.zig`'s matching
    /// warning read this one field for the depth and take the rest as
    /// given. A future shallow format with sequences would need a second
    /// field, not a different reading of this one.
    max_mapping_depth: ?u8 = null,
};

/// The scalar kinds a format spells natively, beyond the core four every
/// serialize format has (boolean, string, number, and the two containers).
/// Read by `lossless.zig`, whose `needsEnvelope` is exactly "the kind is one
/// of these and the format did not mark it".
///
/// One field per kind the envelope can carry: `null`, plus one per
/// `AST.Node.Kind.Extended.ExtKind` member, named identically. This module is
/// a leaf and cannot name the AST's enum, so the correspondence is a
/// comptime pin in `lossless.zig` (both directions) rather than a type: a new
/// `ExtKind` fails the build until a field for it exists here, and a field
/// with no `ExtKind` behind it fails the same way. Every field defaults to
/// false — a format declares what it holds, and an omission is "envelope it",
/// which is always lossless if sometimes unidiomatic.
pub const NativeKinds = struct {
    /// A bare `null`. Every typed format but TOML has one; TOML's absence is
    /// the one kind the lossy path (`Lossless.lossyStrip`) DROPS rather than
    /// degrades, since there is no string to collapse it to.
    null: bool = false,
    /// The four RFC-3339-derived TOML datetimes.
    offset_datetime: bool = false,
    local_datetime: bool = false,
    local_date: bool = false,
    local_time: bool = false,
    /// ZON's `.name` and `'c'` literals.
    enum_literal: bool = false,
    char_literal: bool = false,
    /// A non-finite float (`inf`/`nan`, JSON5's `Infinity`/`NaN`).
    number_special: bool = false,
    /// plist's `<date>` and `<data>`.
    plist_date: bool = false,
    plist_data: bool = false,
};

/// How a format spells itself when it is EMBEDDED in a host document — the
/// four openers `embed.zig` writes, and the tags/MIMEs it accepts on read.
///
/// Plain data, and deliberately so: this module is a leaf (see the header),
/// and these are strings a format knows about itself, not behaviour. They sit
/// here rather than on `Language` because only four of the eleven formats have
/// an embedded spelling at all — a `Language` decl would either be optional
/// (and so invisible to `Decls`' closed set) or a lie for the other seven. The
/// registry entry in `language.zig` carries `?EmbedSpellings`, and null is the
/// statement that the format has no embedded form.
///
/// These fields ARE `embed.zig`'s spelling tables: its four literal builders
/// (`fencedLiteral`/`frontmatterLiteral`/`scriptLiteral`/`codeLiteral`) and its
/// two resolvers (`formatFromLangTag`/`formatFromScriptMime`) are one
/// `inline`-over-the-registry each, so a value changed here changes the bytes
/// a host document is written with and the spellings it is read back from.
pub const EmbedSpellings = struct {
    /// The ```` ```<tag> ```` info-string this format writes for a fenced
    /// block, WITHOUT the backticks — `embed.zig`'s `fencedLiteral` is
    /// ```` "```" ++ fence_tag ````. Also the canonical spelling
    /// `formatFromLangTag` resolves.
    fence_tag: []const u8,

    /// Extra `<tag>` spellings `formatFromLangTag` accepts for this format on
    /// READ but never writes — `yml` for YAML, `figl` for fig. Matched
    /// case-insensitively, like the canonical tag.
    fence_aliases: []const []const u8 = &.{},

    /// The WHOLE `---<lang>` frontmatter opener, not just the tag: YAML's is a
    /// bare `---` (the ecosystem default — an untagged frontmatter block IS
    /// YAML), while every other format tags it. The one field here that is a
    /// literal rather than a token, because that asymmetry has no token to
    /// carry it. `frontmatterLiteral` emits it verbatim.
    frontmatter: []const u8,

    /// The `type` attribute an `html_script` block is written with —
    /// `scriptLiteral` is `<script type="` ++ script_mime ++ `">`. Also
    /// `formatFromScriptMime`'s canonical arm.
    script_mime: []const u8,

    /// Extra `type` MIMEs `formatFromScriptMime` accepts on READ but never
    /// writes — `application/x-yaml`/`text/yaml`, `application/ld+json`,
    /// `application/fig`. Matched case-insensitively.
    script_mime_aliases: []const []const u8 = &.{},

    /// The `class` token an `html_code` block is written with —
    /// `codeLiteral` is `<pre><code class="` ++ code_class ++ `">`. Note fig's
    /// is `language-figl` while its fence tag is `fig`: the two spellings
    /// genuinely differ. On READ the class token's `language-`/`lang-`
    /// suffix is resolved by `formatFromLangTag`, so `fence_aliases` covers
    /// reading and this covers writing.
    code_class: []const u8,
};

/// `Lang.Type` when `Lang` is a language, `void` when it is the gated-out
/// placeholder.
///
/// A `-D<lang>=false` build resolves that language to `void` in
/// `language.zig`, and `void` has no `.Type` — so a field naming one directly
/// fails to compile in exactly the builds the flag exists to produce. Routing
/// the type through here keeps every dependent shape (a registry `Dialect`,
/// the CLI's `Spec`) identical in every build: the gated-out field becomes a
/// zero-bit `void` that nothing reads, because every consumer already sits
/// behind the same `build_options` test.
pub fn DialectOf(comptime Lang: type) type {
    return if (Lang == void) void else Lang.Type;
}

/// `Lang.default_type`, or the `void` value when `Lang` is gated out.
pub fn defaultDialect(comptime Lang: type) DialectOf(Lang) {
    return if (Lang == void) {} else Lang.default_type;
}

/// How a format takes the caller's edit text, which decides what the fix is
/// when the text turns out not to fit. The semantic `cli/diag_report.zig`'s
/// `spliceStyle` states (and which `cli/edit_ops.zig` acts on), declared once
/// per dialect beside everything else about it.
pub const SpliceStyle = enum {
    /// Spliced in verbatim as source, so a string value needs its own quotes —
    /// YAML, TOML, ZON, fig.
    literal,
    /// Wrapped as a JSON string first (`edit_ops.jsonifyEdit`), so `"`/`\` in
    /// the text are escaped rather than taken as syntax — the JSON family.
    json_string,
    /// Written as raw characters, so only the format's own separators can
    /// break it — INI, dotenv, `.properties`, XML, plist, NestedText. (plist
    /// and NestedText *render* the text rather than splicing it; XML has no
    /// in-place editor at all, so no edit text ever reaches it.)
    raw,
};

/// One `--spec <version>` string and the dialect it selects. The element type
/// of `Dialect.specs`, generic over the language so a gated-out one collapses
/// to a `void` dialect and the registry still compiles (and still lists the
/// version STRINGS, which are build-invariant — `resolveSpec` rejects them
/// for a gated-out language rather than not knowing them).
pub fn SpecName(comptime Lang: type) type {
    return struct {
        /// The accepted `--spec` text, matched exactly. Several map to one
        /// dialect (`1.0` and `1.0.0` both select TOML 1.0).
        name: []const u8,
        dialect: DialectOf(Lang),
    };
}

/// One user-facing dialect of a language: everything about it that is not
/// the language module itself. A language declares its own as
/// `Language.dialects: []const Dialect(Language)` — one entry for most, three
/// for JSON (json/jsonc/json5 share one `Language`) — and `language.zig`
/// assembles the format registry from those tables, in language order.
///
/// Generic over the language so the `void` protocol survives (see
/// `DialectOf`): in the registry a gated-out language's entries are
/// `Dialect(void)`, still present with their names, ABI values and spellings,
/// so every enum derived from the registry is build-invariant. A language's
/// own table is always `Dialect(Language)` — it is the registry that lifts
/// the entries to the gated type.
pub fn Dialect(comptime L: type) type {
    return struct {
        /// The member name this dialect has in every derived enum, and —
        /// upper-cased — the `FIG_FORMAT_<NAME>` suffix in fig.h. Sentinel-
        /// terminated because a reified enum's field names must be. The
        /// entry that selects the language's `default_type` must be named
        /// `Language.name` (`validate` checks); the others are the language's
        /// business (JSON's `jsonc`/`json5`).
        name: [:0]const u8,

        /// The language this dialect is a dialect OF; `void` when that
        /// language is gated out of this build. Every consumer must test this
        /// FIRST — it is the gate, and reading any other `Lang`-derived field
        /// past a `void` is a compile error, which is the point.
        Lang: type = L,

        /// The `Lang.Type` value this dialect selects. Defaults to the
        /// language's own default; only the JSON trio overrides it.
        dialect: DialectOf(L) = defaultDialect(L),

        /// The `FigFormat` value in the C ABI. FROZEN: a released value can
        /// never change or be reused, so a new dialect takes the next unused
        /// integer (which is why these run 1,2,7 down the JSON family — JSON5
        /// arrived after XML). `zig build abi-check` compares these against
        /// fig.h's `FIG_FORMAT_*` enumerators in both directions, and
        /// `language.zig` refuses a duplicate.
        abi_value: c_int,

        /// Where this dialect sits in `Language.detect`'s probe order, or
        /// null for a dialect `detect` never sniffs (and which is then no
        /// member of `Detected`) — `jsonc` alone today, since it overlaps
        /// json/json5 on almost all input.
        ///
        /// The order is one argument about grammar overlap — strictest first,
        /// so a permissive grammar cannot claim what a stricter one would
        /// have accepted — and each row carries its own place in it with the
        /// reasoning beside the number. Ranks are unique across the registry
        /// (`language.zig` refuses a duplicate) and need not be contiguous;
        /// `language.zig` pins the resulting sequence in a test, so a new
        /// format choosing a rank cannot reorder the existing ones unnoticed.
        /// Every language must give at least one of its dialects a rank.
        sniff_rank: ?u8 = null,

        /// Whether `deserialize.Format` covers it — the typed
        /// struct-deserialization entry points, which today reach five of the
        /// thirteen dialects.
        deserializable: bool = false,

        /// How this dialect takes spliced edit text. See `SpliceStyle`.
        splice: SpliceStyle,

        /// The document `set` seeds when the target file does not exist yet
        /// (and what `Embed.initRegion` writes into a freshly created region),
        /// or null for a format that refuses to be created from scratch.
        ///
        /// An empty string is NOT the same statement as null: it means an
        /// empty file already parses as an empty root mapping, so the first
        /// key can just be inserted into it.
        empty_doc_seed: ?[]const u8,

        /// The `Lang.Printer` declaration that writes a whole document in this
        /// dialect, and the one that writes a single node. Two names rather
        /// than one because the JSON family shares a printer and separates its
        /// dialects by entry point (`print`/`printc`/`print5`), and YAML's
        /// document printer is `printWith`.
        ///
        /// These ARE the serializer's dispatch: `ast/serialize_options.zig`
        /// calls `@field(d.Lang.Printer, d.print_name)(writer, ast, options)`
        /// (and the `print_node_name` twin) for every entry, so a wrong name
        /// here is a compile error rather than a wrong output. There is no
        /// separate fragment name: `serializeFragmentWith` uses `print_name`
        /// for every dialect but fig, whose `printFragment` takes an explicit
        /// arm there for the reason documented on that function.
        print_name: [:0]const u8 = "print",
        print_node_name: [:0]const u8 = "printNode",

        /// The `--spec` strings this dialect accepts and what each selects.
        /// Empty for the eleven dialects with a single grammar. See
        /// `cli/parse_dispatch.zig`'s `resolveSpec`, whose behaviour a
        /// comptime assert beside it pins against the registry.
        specs: []const SpecName(L) = &.{},

        /// How this format spells itself inside a host document, or null when
        /// it has no embedded form (`Embed.InnerFormat` is REIFIED from
        /// exactly the entries where this is non-null). `embed.zig` builds
        /// every fence, frontmatter marker, `<script type>` and `<code class>`
        /// it writes — and every tag/MIME it accepts on read — out of these
        /// fields, so they are the spelling, not a description of it.
        embed: ?EmbedSpellings = null,
    };
}

/// Everything the generic splice engine needs to know about a format's
/// surface syntax, indexed by dialect.
///
/// Obtained as `Language.syntax(t)` rather than as a constant because
/// `comments.line` genuinely varies by dialect: strict JSON has no comment
/// syntax while JSONC and JSON5 do, and the splice is reparsed under whichever
/// dialect the editor is holding. Making the whole struct a function of `Type`
/// keeps that question in one place instead of scattering per-field `fn(Type)`
/// types across the struct, and is where a TOML 1.0/1.1 or YAML 1.1/1.2.2
/// *editing* divergence would land if one ever appears.
///
/// The cost is a runtime switch where there used to be a comptime constant,
/// and it stops there: every consumer is an `appendSlice` call or an argument
/// to `commentBlockStart`/`entryBlockStart`. Nothing downstream needs a
/// comptime value — no array lengths, no `++`, no switch prongs.
pub const Syntax = struct {
    // ==================
    // COMMENTS
    // ==================

    /// This format's comment scanner and markers. See `Comments`.
    comments: Comments,

    // ==================
    // ENTRIES
    // ==================

    /// The mapping key/value separator spliced by the generic flow-entry
    /// insert helpers (`insertFlowMapEntry`/`insertFlowEntry`) and by
    /// `writeMapValue`'s block-insert path — or null for a format that owns
    /// every one of those paths itself and so has no answer to give.
    ///
    /// This is the separator the GENERIC engine writes, which need not be the
    /// separator the format's printer writes: ZON's struct-field syntax is
    /// ` = `, dotenv/`.properties` print a bare `=` with no surrounding
    /// spaces, INI always pads it. See each `printer.zig`.
    ///
    /// The null is not "no separator" — it is "not the generic engine's
    /// question". fig and TOML spell an entry `key = value` but hook
    /// `insertKey` and decide the separator from the source there (fig's flow
    /// objects are `=`-mode or `:`-mode and may not mix, so there is no one
    /// answer to declare); plist's entries are a pair of sibling ELEMENTS and
    /// NestedText's are `key:` lines its own helper writes. All four used to
    /// declare `": "` — a value no code read, and for fig one its own parser
    /// rejects (`FigFlowBareKeyColon`). `language.validate` requires a null
    /// here to come with an `insertKey` hook, which is what makes every
    /// consumer below unreachable; `editor.Editor.kvSep` is where that is
    /// cashed in.
    kv_sep: ?[]const u8,

    /// How a logical key renders into this format's key syntax. See `KeyStyle`.
    key_style: KeyStyle = .verbatim,

    /// A sigil each key carries in the source that its AST key span EXCLUDES
    /// — ZON's leading `.`, whose span starts at the bare identifier.
    ///
    /// A flow-mapping entry delete backs up over it so the splice carries
    /// `.name` as a unit rather than stranding a bare `.` next to a survivor.
    key_sigil: ?u8 = null,

    // ==================
    // SHAPES
    // ==================

    /// The empty-mapping seed `set` splices to auto-vivify a missing ancestor,
    /// or null for a format that cannot vivify at all.
    ///
    /// Three distinct answers, and the null is not a degenerate case:
    ///
    ///   * Most formats use the flow `{}` literal, which each accepts as an
    ///     empty mapping value (JSON object, TOML inline table, fig flow map).
    ///     ZON spells it `.{}`. The dotted-key formats (fig/TOML) deliberately
    ///     keep `{}`: there the flow chain is the idiomatic intermediate form,
    ///     and `fig fmt` canonicalizes `a = { b = { c = v }}` to `a.b.c = v`.
    ///
    ///   * YAML seeds with NOTHING (`""`) — a bare `key:`, i.e. a null value.
    ///     Both spellings are valid YAML for "no entries yet" but they are not
    ///     interchangeable as a SEED: a flow `{}` can only ever be extended
    ///     with flow members, so every block-spelled value landing under a
    ///     vivified ancestor had to be refused (`BlockValueIntoFlow`). A null
    ///     value has the opposite property — `insertKey` promotes it to a real
    ///     block mapping (`promoteNullToMapping`), which takes block and inline
    ///     values alike — so `set(a.b.c, 1)` produces the block containers YAML
    ///     is normally written in.
    ///
    ///   * INI, plist and NestedText declare null: they have no literal
    ///     spelling for "an empty nested mapping" that the generic seed could
    ///     use. INI's case is the sharpest — `{}` there is a two-character
    ///     STRING value, not a container, so seeding with it would write a
    ///     nonsense `section = {}` root key. This is an ABSENCE of a syntax,
    ///     which is why it collapses into this field rather than standing as
    ///     a separate "can vivify" flag: `empty_map_literal` has exactly one
    ///     consumer, inside `set`'s vivify branch, so null and "excluded from
    ///     vivify" are the same statement.
    empty_map_literal: ?[]const u8,

    /// Whether a BLOCK (non-flow) sequence can be edited in place.
    ///
    /// False for TOML alone: a non-flow TOML sequence is an array-of-tables,
    /// which `appendContainerToSeq` handles instead, and TOML has no block
    /// scalar array. Append/prepend/remove/reorder all refuse with
    /// `NotAnInlineArray` when this is false.
    block_seq_editable: bool = true,

    /// Whether a single line of the form `k: v` is a block MAPPING entry
    /// rather than scalar text — the one value shape that cannot be told
    /// apart by sniffing, so it is settled by the language's own parser.
    ///
    /// True for YAML alone: it is the only editable format whose block mapping
    /// has a single-line spelling reaching these splice paths. The flat
    /// formats (dotenv/`.properties`/INI) route through the same code, and
    /// there a `k: v` value is genuinely just scalar text — so they must keep
    /// splicing it inline, and declare false.
    single_line_block_mapping: bool = false,

    /// Whether a bare `key: value` document form exists — a keyless top-level
    /// mapping, as in YAML and JSON5.
    ///
    /// False for ZON alone, which has no such form: a null value, root or
    /// nested, promotes in place to a flow `.{ key = value }` container built
    /// from `flow_map_open`/`flow_map_close`, so `promoteNullToMapping` needs
    /// no root-versus-descend distinction there.
    bare_document_mapping: bool = true,

    /// The flow-mapping delimiters, used when promoting a null in a format
    /// with no `bare_document_mapping`. ZON's opener is `.{`.
    flow_map_open: []const u8 = "{",
    flow_map_close: []const u8 = "}",

    /// Whether a line's prefix is STRUCTURAL rather than whitespace.
    ///
    /// True for fig alone. Its `#`-only comment lines need the same `>`
    /// marker-run prefix as the line they anchor above — comment depth is
    /// load-bearing for attachment (see `fig/DESIGN.md`, "Comments") — so the
    /// "indent" a new comment copies is the raw byte range from the line start
    /// to the node's span, not the leading whitespace. `firstNonSpace` would
    /// stop at the `>` and yield bare whitespace, dropping the markers
    /// entirely. `span.start` already sits just past that prefix for every fig
    /// node (see `TNode.span` in `fig/parser.zig`), so slicing back to the line
    /// start recovers it exactly. Every other language's prefix is pure
    /// whitespace, where `firstNonSpace` and `span.start` agree anyway.
    structural_indent: bool = false,

    /// Declares this a SECTION format — one whose logical containers are
    /// assembled from lines scattered through the source (a TOML `[table]`,
    /// an INI `[section]`, a fig block container) — and names what the format
    /// calls such a container. Null for every format whose containers are
    /// contiguous, which is what the default says.
    ///
    /// Three things hang off a non-null value, all in `editor.zig`:
    ///
    ///   * the parser is expected to fill `Document.node_regions` with the
    ///     header lines of every such container (see that field), which is
    ///     what the generic whole-container ops and the line-splice guards
    ///     read — nothing here can check that the parser does, so the two
    ///     are a pair by contract rather than by `validate`;
    ///   * the generic `deleteContainer`/`moveContainer`/`reorderContainers`
    ///     are live, and refuse at comptime for a null;
    ///   * the engine's refusals are spelled in this vocabulary
    ///     (`CannotDeleteTable` for `.table`, `CannotDeleteSection` for
    ///     `.section`, …), so a format's own words survive in its errors.
    ///
    /// A VALUE rather than a hook because the engine's rule is the same for
    /// all three — "a section node cannot be line-spliced; use the container
    /// op" — and only the noun in the error differs.
    section_noun: ?SectionNoun = null,
};

/// What a section format calls its scattered container — the one word that
/// differs between the three formats' otherwise identical refusals. See
/// `Syntax.section_noun`.
pub const SectionNoun = enum {
    /// TOML: `NotATable`, `CannotDeleteTable`, `CannotReplaceTable`,
    /// `CannotMoveTable`, `CannotReorderTables`.
    table,
    /// INI: `NotAContainer`, `CannotDeleteSection`, `CannotReplaceSection`,
    /// `CannotMoveSection`, `CannotReorderSections`.
    section,
    /// fig: `NotAContainer`, `CannotDeleteContainer`,
    /// `CannotReplaceContainer`, `CannotMoveContainer`,
    /// `CannotReorderContainers`.
    container,
};