@tabnas/yaml 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/yaml.js ADDED
@@ -0,0 +1,2688 @@
1
+ "use strict";
2
+ /* Copyright (c) 2021-2025 Richard Rodger, MIT License */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.Yaml = void 0;
5
+ // The engine is the tabnas parser; jsonic supplies the relaxed-JSON
6
+ // grammar that the embedded grammar text is authored in.
7
+ const parser_1 = require("@tabnas/parser");
8
+ const jsonic_1 = require("@tabnas/jsonic");
9
+ // --- BEGIN EMBEDDED yaml-grammar.jsonic ---
10
+ const grammarText = `
11
+ # YAML Grammar Definition
12
+ # Parsed by a standard Tabnas instance and passed to tabnas.grammar()
13
+ # Function references (@ prefixed) are resolved against the refs map.
14
+ # State handlers (bo/ao/bc/ac) remain wired in code, since they use
15
+ # closures over per-parse state (anchors, pendingAnchors, etc.).
16
+
17
+ {
18
+ # Amend val rule: YAML indent/element-marker handling.
19
+ rule: val: open: {
20
+ alts: [
21
+ # Doc-frame markers between docs mean an empty value here; back up so
22
+ # the stream rule consumes the marker and starts the next document.
23
+ { s: '#DS' b: 1 a: '@val-set-null' g: yaml }
24
+ { s: '#DE' b: 1 a: '@val-set-null' g: yaml }
25
+ { s: '#DR' b: 1 a: '@val-set-null' g: yaml }
26
+ # Indent followed by content: push indent rule.
27
+ { s: '#IN' c: '@val-indent-deeper' p: indent a: '@val-set-in-from-o0' g: yaml }
28
+ # Same indent followed by element marker: list value at map level.
29
+ { s: ['#IN' '#EL'] c: '@val-indent-eq-parent' p: yamlBlockList a: '@val-set-in-from-o0' g: yaml }
30
+ # End of input means empty value.
31
+ { s: '#ZZ' b: 1 a: '@val-set-null' g: yaml }
32
+ # Same or lesser indent after a colon means empty value — backtrack.
33
+ { s: '#IN' b: 1 u: { yamlEmpty: true } g: yaml }
34
+ # This value is a list.
35
+ { s: '#EL' p: yamlBlockList a: '@val-set-el-in' g: yaml }
36
+ ]
37
+ inject: { append: false }
38
+ }
39
+ rule: val: close: {
40
+ alts: [
41
+ # Doc-frame markers terminate val; back up for the stream rule.
42
+ { s: '#DS' b: 1 g: yaml }
43
+ { s: '#DE' b: 1 g: yaml }
44
+ { s: '#DR' b: 1 g: yaml }
45
+ { s: '#IN' b: 1 g: yaml }
46
+ ]
47
+ inject: { append: false }
48
+ }
49
+
50
+ # Indent rule: start for block content at a given indent.
51
+ rule: indent: open: [
52
+ # Key pair => map.
53
+ { s: ['#KEY' '#CL'] p: map b: 2 g: yaml }
54
+ # Element marker => list.
55
+ { s: '#EL' p: list g: yaml }
56
+ # Plain value after indent (for nested scalars).
57
+ { s: '#KEY' a: '@indent-plain-value' g: yaml }
58
+ ]
59
+
60
+ # YAML block list: handles "- " sequences without consuming "[".
61
+ rule: yamlBlockList: open: [
62
+ # Element value is a key-value map: - key: val
63
+ { s: ['#KEY' '#CL'] p: yamlElemMap b: 2 a: '@set-map-in' g: yaml }
64
+ # Default: push to val for the element's value.
65
+ { p: val g: yaml }
66
+ ]
67
+ rule: yamlBlockList: close: [
68
+ # Doc-frame markers terminate list; back up for the stream rule.
69
+ { s: '#DS' b: 1 g: yaml }
70
+ { s: '#DE' b: 1 g: yaml }
71
+ { s: '#DR' b: 1 g: yaml }
72
+ # Indent followed by element marker: next element at same level.
73
+ { s: ['#IN' '#EL'] c: '@t0-eq-in' r: yamlBlockElem g: yaml }
74
+ # Same or lesser indent: close list.
75
+ { s: '#IN' c: '@t0-le-in' b: 1 g: yaml }
76
+ # Element marker at top level (no preceding newline).
77
+ { s: '#EL' r: yamlBlockElem g: yaml }
78
+ { s: '#ZZ' b: 1 g: yaml }
79
+ ]
80
+
81
+ # Subsequent elements in a yamlBlockList (via rotation).
82
+ rule: yamlBlockElem: open: [
83
+ { s: ['#KEY' '#CL'] p: yamlElemMap b: 2 a: '@set-map-in' g: yaml }
84
+ { p: val g: yaml }
85
+ ]
86
+ rule: yamlBlockElem: close: [
87
+ # Doc-frame markers terminate elem; back up for the stream rule.
88
+ { s: '#DS' b: 1 g: yaml }
89
+ { s: '#DE' b: 1 g: yaml }
90
+ { s: '#DR' b: 1 g: yaml }
91
+ { s: ['#IN' '#EL'] c: '@t0-eq-in' r: yamlBlockElem g: yaml }
92
+ { s: '#IN' c: '@t0-le-in' b: 1 g: yaml }
93
+ { s: '#EL' r: yamlBlockElem g: yaml }
94
+ { s: '#ZZ' b: 1 g: yaml }
95
+ ]
96
+
97
+ # Amend list rule: close on dedent or same-indent non-element.
98
+ rule: list: close: {
99
+ alts: [
100
+ # Doc-frame markers terminate list; back up for the stream rule.
101
+ { s: '#DS' b: 1 g: yaml }
102
+ { s: '#DE' b: 1 g: yaml }
103
+ { s: '#DR' b: 1 g: yaml }
104
+ { s: '#IN' c: '@t0-le-in' b: 1 g: yaml }
105
+ ]
106
+ inject: { append: false }
107
+ }
108
+
109
+ # Amend map rule: same-indent indent continues map with pair.
110
+ rule: map: open: {
111
+ alts: [
112
+ { s: '#IN' c: '@o0-eq-in' r: pair g: yaml }
113
+ ]
114
+ inject: { append: false }
115
+ }
116
+ rule: map: close: {
117
+ alts: [
118
+ # Doc-frame markers terminate map; back up for the stream rule.
119
+ { s: '#DS' b: 1 g: yaml }
120
+ { s: '#DE' b: 1 g: yaml }
121
+ { s: '#DR' b: 1 g: yaml }
122
+ { s: '#IN' c: '@t0-lt-in' b: 1 g: yaml }
123
+ ]
124
+ inject: { append: false }
125
+ }
126
+
127
+ # Amend pair rule: end of input ends pair; dedent closes, same-indent repeats.
128
+ # Also handle YAML flow-mapping shapes Tabnas doesn't have natively:
129
+ # - implicit null values: {a, b: c} — KEY followed directly by CA or CB
130
+ # - explicit-key marker: {? k : v} — leading #QM is consumed
131
+ rule: pair: open: {
132
+ alts: [
133
+ { s: ['#KEY' '#CA'] a: '@implicit-null-pair' b: 1 g: yaml }
134
+ { s: ['#KEY' '#CB'] a: '@implicit-null-pair' b: 1 g: yaml }
135
+ { s: ['#QM' '#KEY' '#CL'] p: val u: { pair: true } a: '@qm-pairkey' g: yaml }
136
+ { s: ['#QM' '#KEY' '#CA'] a: '@qm-implicit-null-pair' b: 1 g: yaml }
137
+ { s: ['#QM' '#KEY' '#CB'] a: '@qm-implicit-null-pair' b: 1 g: yaml }
138
+ { s: '#ZZ' b: 1 g: yaml }
139
+ ]
140
+ inject: { append: false }
141
+ }
142
+ rule: pair: close: {
143
+ alts: [
144
+ # Doc-frame markers terminate pair; back up for the stream rule.
145
+ { s: '#DS' b: 1 g: yaml }
146
+ { s: '#DE' b: 1 g: yaml }
147
+ { s: '#DR' b: 1 g: yaml }
148
+ { s: '#IN' c: '@t0-eq-in' r: pair g: yaml }
149
+ { s: '#IN' c: '@t0-lt-in' b: 1 g: yaml }
150
+ ]
151
+ inject: { append: false }
152
+ }
153
+
154
+ # yamlElemMap: "- key: val" patterns.
155
+ rule: yamlElemMap: open: [
156
+ { s: ['#KEY' '#CL'] p: val a: '@elem-key' g: yaml }
157
+ ]
158
+ rule: yamlElemMap: close: [
159
+ # Doc-frame markers terminate elem-map; back up for the stream rule.
160
+ { s: '#DS' b: 1 g: yaml }
161
+ { s: '#DE' b: 1 g: yaml }
162
+ { s: '#DR' b: 1 g: yaml }
163
+ { s: '#IN' c: '@t0-eq-map-in' r: yamlElemPair g: yaml }
164
+ { s: '#IN' b: 1 g: yaml }
165
+ { s: '#CA' b: 1 g: yaml }
166
+ { s: '#CS' b: 1 g: yaml }
167
+ { s: '#CB' b: 1 g: yaml }
168
+ { s: '#ZZ' g: yaml }
169
+ ]
170
+
171
+ # Additional pairs in a yamlElemMap.
172
+ rule: yamlElemPair: open: [
173
+ { s: ['#KEY' '#CL'] p: val a: '@elem-key' g: yaml }
174
+ ]
175
+ rule: yamlElemPair: close: [
176
+ # Doc-frame markers terminate elem-pair; back up for the stream rule.
177
+ { s: '#DS' b: 1 g: yaml }
178
+ { s: '#DE' b: 1 g: yaml }
179
+ { s: '#DR' b: 1 g: yaml }
180
+ { s: '#IN' c: '@t0-eq-map-in' r: yamlElemPair g: yaml }
181
+ { s: '#IN' b: 1 g: yaml }
182
+ { s: '#CA' b: 1 g: yaml }
183
+ { s: '#CS' b: 1 g: yaml }
184
+ { s: '#CB' b: 1 g: yaml }
185
+ { s: '#ZZ' g: yaml }
186
+ ]
187
+
188
+ # Amend elem rule for YAML sequences ("- key: val" at top level of [ ... ]).
189
+ # Also handle flow-sequence explicit-key entries: [? k : v] is a single-pair
190
+ # map element. Eat the leading #QM, then back up KEY+CL so yamlElemMap
191
+ # consumes them as a normal pair.
192
+ rule: elem: open: {
193
+ alts: [
194
+ { s: ['#KEY' '#CL'] p: yamlElemMap b: 2 a: '@set-map-in' g: yaml }
195
+ { s: ['#QM' '#KEY' '#CL'] p: yamlElemMap b: 2 a: '@set-map-in' g: yaml }
196
+ ]
197
+ inject: { append: false }
198
+ }
199
+ rule: elem: close: {
200
+ alts: [
201
+ # Doc-frame markers terminate elem; back up for the stream rule.
202
+ { s: '#DS' b: 1 g: yaml }
203
+ { s: '#DE' b: 1 g: yaml }
204
+ { s: '#DR' b: 1 g: yaml }
205
+ { s: ['#IN' '#EL'] c: '@t0-eq-in' r: elem g: yaml }
206
+ { s: '#IN' c: '@t0-eq-in' b: 1 g: yaml }
207
+ { s: '#IN' c: '@t0-lt-in' b: 1 g: yaml }
208
+ { s: '#EL' r: elem g: yaml }
209
+ ]
210
+ inject: { append: false }
211
+ }
212
+ }
213
+ `;
214
+ // --- END EMBEDDED yaml-grammar.jsonic ---
215
+ const Yaml = (tabnas, options) => {
216
+ // Guard against re-entry during options() re-application.
217
+ if (tabnas.__yamlInstalled)
218
+ return;
219
+ tabnas.__yamlInstalled = true;
220
+ // Human descriptions for YAML tokens, surfaced in railroad diagram legends
221
+ // (read off the live config by @tabnas/railroad).
222
+ tabnas.options({
223
+ config: {
224
+ modify: {
225
+ 'yaml-tokendesc': (cfg) => {
226
+ cfg.tokenDesc = Object.assign(cfg.tokenDesc || {}, {
227
+ '#DS': 'document start marker --- (column 0)',
228
+ '#DE': 'document end marker ... (column 0)',
229
+ '#DR': 'directive line, e.g. %YAML / %TAG (column 0)',
230
+ '#EL': 'block sequence item dash "- "',
231
+ '#IN': 'line indentation (count of leading spaces)',
232
+ '#QM': 'explicit-key marker ? in flow {? k : v }',
233
+ });
234
+ },
235
+ },
236
+ },
237
+ });
238
+ let TX = tabnas.token.TX;
239
+ let NR = tabnas.token.NR;
240
+ let ST = tabnas.token.ST;
241
+ let VL = tabnas.token.VL;
242
+ let CL = tabnas.token.CL;
243
+ let ZZ = tabnas.token.ZZ;
244
+ let IN = tabnas.token('#IN');
245
+ // Shared anchor storage for the plugin instance.
246
+ let anchors = {};
247
+ let pendingAnchors = [];
248
+ let pendingExplicitCL = false;
249
+ // Flag to tell the number matcher to skip, so text.check handles the value.
250
+ let skipNumberMatch = false;
251
+ // Queue for tokens that need to be emitted across multiple lex calls.
252
+ let pendingTokens = [];
253
+ // TAG directive handle mappings (e.g. %TAG !! tag:example.com/).
254
+ // When !! is redefined, built-in type conversion is skipped.
255
+ let tagHandles = {};
256
+ // Per-parse accumulators for the stream rule. Reset on first lex call.
257
+ let yamlStreamDocs = [];
258
+ let yamlStreamMeta = [];
259
+ let yamlStreamCurMeta = null;
260
+ // Incremental flow-depth cache for text.check (avoids O(n²) rescan).
261
+ let _flowDepth = 0;
262
+ let _flowScanPos = 0;
263
+ // Persistent quote state so multi-call scans handle quotes spanning slices.
264
+ let _inSingleQuote = false;
265
+ let _inDoubleQuote = false;
266
+ // Bring _flowDepth up to date with `upTo` by scanning lex.src incrementally.
267
+ // Skips quoted regions so embedded brackets don't mis-count the flow depth.
268
+ function updateFlowState(src, upTo) {
269
+ if (upTo < _flowScanPos) {
270
+ _flowDepth = 0;
271
+ _flowScanPos = 0;
272
+ _inSingleQuote = false;
273
+ _inDoubleQuote = false;
274
+ }
275
+ for (let fi = _flowScanPos; fi < upTo; fi++) {
276
+ let fc = src[fi];
277
+ if (_inDoubleQuote) {
278
+ if (fc === '\\')
279
+ fi++;
280
+ else if (fc === '"')
281
+ _inDoubleQuote = false;
282
+ continue;
283
+ }
284
+ if (_inSingleQuote) {
285
+ if (fc === "'") {
286
+ if (src[fi + 1] === "'")
287
+ fi++;
288
+ else
289
+ _inSingleQuote = false;
290
+ }
291
+ continue;
292
+ }
293
+ if (fc === '{' || fc === '[')
294
+ _flowDepth++;
295
+ else if (fc === '}' || fc === ']') {
296
+ if (_flowDepth > 0)
297
+ _flowDepth--;
298
+ }
299
+ else if (fc === '"') {
300
+ _inDoubleQuote = true;
301
+ }
302
+ else if (fc === "'") {
303
+ let pc = fi > 0 ? src.charCodeAt(fi - 1) : 0;
304
+ if (!((pc >= 65 && pc <= 90) || (pc >= 97 && pc <= 122) || (pc >= 48 && pc <= 57))) {
305
+ _inSingleQuote = true;
306
+ }
307
+ }
308
+ }
309
+ _flowScanPos = upTo;
310
+ }
311
+ tabnas.options({
312
+ fixed: {
313
+ token: {
314
+ // Single colon is not a YAML token, so remove.
315
+ '#CL': null,
316
+ }
317
+ },
318
+ // Colons can still end unquoted text (TX, lexer.textMatcher).
319
+ ender: ':',
320
+ // Remove all jsonic string chars — YAML handles quotes in yamlMatcher.
321
+ // Backtick is not a string delimiter in YAML.
322
+ string: {
323
+ chars: '',
324
+ },
325
+ // Skip number matching when yamlMatcher detected trailing text
326
+ // after a digit-starting value (e.g. "64 characters, hexadecimal.").
327
+ number: {
328
+ check: (_lex) => {
329
+ if (skipNumberMatch) {
330
+ skipNumberMatch = false;
331
+ return { done: true };
332
+ }
333
+ },
334
+ },
335
+ // Custom text check: consume to end of line (including spaces)
336
+ // for YAML plain scalar values.
337
+ text: {
338
+ check: (lex) => {
339
+ let pnt = lex.pnt;
340
+ let fwd = lex.fwd;
341
+ let ch = fwd[0];
342
+ // Block scalar: | or > (with optional chomping indicator)
343
+ if (ch === '|' || ch === '>') {
344
+ let fold = ch === '>';
345
+ let chomp = 'clip'; // default: single trailing newline
346
+ let explicitIndent = 0;
347
+ let idx = 1;
348
+ // Parse optional chomping and indentation indicators in either order.
349
+ // Valid: |, |+, |-, |2, |+2, |-2, |2+, |2-
350
+ for (let pi = 0; pi < 2; pi++) {
351
+ if (fwd[idx] === '+') {
352
+ chomp = 'keep';
353
+ idx++;
354
+ }
355
+ else if (fwd[idx] === '-') {
356
+ chomp = 'strip';
357
+ idx++;
358
+ }
359
+ else if (fwd[idx] >= '1' && fwd[idx] <= '9') {
360
+ explicitIndent = parseInt(fwd[idx]);
361
+ idx++;
362
+ }
363
+ }
364
+ // Must be followed by newline (possibly with trailing spaces/comment)
365
+ while (fwd[idx] === ' ')
366
+ idx++;
367
+ if (fwd[idx] === '#') {
368
+ while (idx < fwd.length && fwd[idx] !== '\n' && fwd[idx] !== '\r')
369
+ idx++;
370
+ }
371
+ if (fwd[idx] !== '\n' && fwd[idx] !== '\r' && fwd[idx] !== undefined) {
372
+ // Not a block scalar — fall through to normal text handling.
373
+ }
374
+ else {
375
+ // Skip the indicator line.
376
+ if (fwd[idx] === '\r')
377
+ idx++;
378
+ if (fwd[idx] === '\n')
379
+ idx++;
380
+ // Determine block indent from first content line,
381
+ // or use explicit indent indicator if provided.
382
+ let blockIndent = 0;
383
+ if (explicitIndent === 0) {
384
+ // Auto-detect: skip blank lines, find first content line.
385
+ let tempIdx = idx;
386
+ while (tempIdx < fwd.length) {
387
+ let lineSpaces = 0;
388
+ while (tempIdx + lineSpaces < fwd.length && fwd[tempIdx + lineSpaces] === ' ')
389
+ lineSpaces++;
390
+ let afterSpaces = tempIdx + lineSpaces;
391
+ if (afterSpaces >= fwd.length || fwd[afterSpaces] === '\n' || fwd[afterSpaces] === '\r') {
392
+ // Blank line — skip it.
393
+ tempIdx = afterSpaces;
394
+ if (fwd[tempIdx] === '\r')
395
+ tempIdx++;
396
+ if (fwd[tempIdx] === '\n')
397
+ tempIdx++;
398
+ continue;
399
+ }
400
+ blockIndent = lineSpaces;
401
+ break;
402
+ }
403
+ }
404
+ // Determine the indent of the line containing the block indicator.
405
+ // If blockIndent <= that indent, the block is empty (content must
406
+ // be more indented than the containing line). Exception: after ---,
407
+ // the containing indent is effectively -1 so blockIndent 0 is valid.
408
+ let containingIndent = 0;
409
+ let isDocStart = false;
410
+ {
411
+ let li = pnt.sI - 1;
412
+ while (li > 0 && lex.src[li - 1] !== '\n' && lex.src[li - 1] !== '\r')
413
+ li--;
414
+ let lineStart = li;
415
+ while (li < pnt.sI && lex.src[li] === ' ') {
416
+ containingIndent++;
417
+ li++;
418
+ }
419
+ // Check if this line starts with --- (document start marker).
420
+ if (lex.src[lineStart] === '-' && lex.src[lineStart + 1] === '-' && lex.src[lineStart + 2] === '-') {
421
+ isDocStart = true;
422
+ }
423
+ }
424
+ // Apply explicit indent relative to containing indent.
425
+ // Per YAML spec, the content indentation = block scalar's indent
426
+ // level + indicator value. The block scalar's indent level is the
427
+ // indent of the containing block (e.g., the mapping key), which
428
+ // may differ from the line's leading spaces (e.g., after "- ").
429
+ if (explicitIndent > 0) {
430
+ // Find the line containing the block indicator.
431
+ let li = pnt.sI - 1;
432
+ while (li > 0 && lex.src[li - 1] !== '\n' && lex.src[li - 1] !== '\r')
433
+ li--;
434
+ // li is now at the start of the line. Find the colon position.
435
+ let keyCol = containingIndent;
436
+ // Check if there's a colon on the SAME line as the block indicator.
437
+ let hasColonOnLine = false;
438
+ for (let ci = li + containingIndent; ci < pnt.sI; ci++) {
439
+ if (lex.src[ci] === ':' && (lex.src[ci + 1] === ' ' || lex.src[ci + 1] === '\t')) {
440
+ hasColonOnLine = true;
441
+ break;
442
+ }
443
+ }
444
+ if (hasColonOnLine) {
445
+ // Block indicator on same line as colon (e.g., "key: |2").
446
+ // Check for sequence indicators: each "- " adds to the effective indent.
447
+ let scanI = li + containingIndent;
448
+ while (scanI < pnt.sI && lex.src[scanI] === '-' &&
449
+ (lex.src[scanI + 1] === ' ' || lex.src[scanI + 1] === '\t')) {
450
+ keyCol += 2;
451
+ scanI += 2;
452
+ while (scanI < pnt.sI && lex.src[scanI] === ' ') {
453
+ keyCol++;
454
+ scanI++;
455
+ }
456
+ }
457
+ blockIndent = keyCol + explicitIndent;
458
+ }
459
+ else {
460
+ // Block indicator on its own line (e.g., after a tag on
461
+ // a separate line). Look backward to find the parent
462
+ // mapping key's indent by scanning previous lines for
463
+ // the colon that started this value context.
464
+ let parentIndent = 0;
465
+ let searchI = li - 1;
466
+ while (searchI > 0) {
467
+ // Find start of previous line.
468
+ if (lex.src[searchI] === '\n')
469
+ searchI--;
470
+ if (lex.src[searchI] === '\r')
471
+ searchI--;
472
+ let prevLineEnd = searchI + 1;
473
+ while (searchI > 0 && lex.src[searchI - 1] !== '\n' && lex.src[searchI - 1] !== '\r')
474
+ searchI--;
475
+ let prevLineStart = searchI;
476
+ // Check if this line has a colon (mapping key).
477
+ for (let ci = prevLineStart; ci < prevLineEnd; ci++) {
478
+ if (lex.src[ci] === ':' && (lex.src[ci + 1] === ' ' || lex.src[ci + 1] === '\t' ||
479
+ lex.src[ci + 1] === '\n' || lex.src[ci + 1] === '\r' || ci + 1 >= prevLineEnd)) {
480
+ // Found the parent key line. Get its indent.
481
+ parentIndent = 0;
482
+ let pi = prevLineStart;
483
+ while (pi < prevLineEnd && lex.src[pi] === ' ') {
484
+ parentIndent++;
485
+ pi++;
486
+ }
487
+ break;
488
+ }
489
+ }
490
+ break; // Only check the immediately preceding non-blank line.
491
+ }
492
+ blockIndent = parentIndent + explicitIndent;
493
+ // Update containingIndent to parent's indent so the
494
+ // "blockIndent <= containingIndent" check below works.
495
+ containingIndent = parentIndent;
496
+ }
497
+ }
498
+ if (blockIndent <= containingIndent && !isDocStart && idx < fwd.length) {
499
+ // Content is not indented enough — empty block scalar.
500
+ // For keep chomping, count trailing blank lines.
501
+ let val;
502
+ if (chomp === 'keep') {
503
+ let blankCount = 0;
504
+ let bi = idx;
505
+ while (bi < fwd.length) {
506
+ if (fwd[bi] === '\n') {
507
+ blankCount++;
508
+ bi++;
509
+ }
510
+ else if (fwd[bi] === '\r') {
511
+ bi++;
512
+ if (bi < fwd.length && fwd[bi] === '\n')
513
+ bi++;
514
+ blankCount++;
515
+ }
516
+ else
517
+ break;
518
+ }
519
+ val = '\n'.repeat(blankCount > 0 ? blankCount : 1);
520
+ idx = bi;
521
+ }
522
+ else {
523
+ val = chomp === 'strip' ? '' : '';
524
+ }
525
+ let src = fwd.substring(0, idx);
526
+ let tkn = lex.token('#TX', val, src, pnt);
527
+ pnt.sI += idx;
528
+ pnt.rI += 1;
529
+ pnt.cI = 0;
530
+ return { done: true, token: tkn };
531
+ }
532
+ // Collect indented lines.
533
+ let lines = [];
534
+ let pos = idx;
535
+ let rows = 1; // Already consumed one newline
536
+ let lastNewlinePos = idx; // Track position before last consumed newline
537
+ while (pos < fwd.length) {
538
+ // Check indent of current line.
539
+ let lineIndent = 0;
540
+ while (pos + lineIndent < fwd.length && fwd[pos + lineIndent] === ' ')
541
+ lineIndent++;
542
+ // Blank line (only whitespace before newline or end).
543
+ let afterSpaces = pos + lineIndent;
544
+ if (afterSpaces >= fwd.length || fwd[afterSpaces] === '\n' || fwd[afterSpaces] === '\r') {
545
+ // Preserve spaces beyond block indent on blank lines.
546
+ if (lineIndent > blockIndent) {
547
+ lines.push(fwd.substring(pos + blockIndent, afterSpaces));
548
+ }
549
+ else {
550
+ lines.push('');
551
+ }
552
+ lastNewlinePos = afterSpaces;
553
+ pos = afterSpaces;
554
+ if (fwd[pos] === '\r')
555
+ pos++;
556
+ if (fwd[pos] === '\n')
557
+ pos++;
558
+ rows++;
559
+ continue;
560
+ }
561
+ // Less indent means end of block.
562
+ if (lineIndent < blockIndent)
563
+ break;
564
+ // Stop at document markers (--- or ...) at indent 0.
565
+ if (lineIndent === 0 &&
566
+ ((fwd[pos] === '-' && fwd[pos + 1] === '-' && fwd[pos + 2] === '-' &&
567
+ (fwd[pos + 3] === '\n' || fwd[pos + 3] === '\r' || fwd[pos + 3] === ' ' || fwd[pos + 3] === undefined)) ||
568
+ (fwd[pos] === '.' && fwd[pos + 1] === '.' && fwd[pos + 2] === '.' &&
569
+ (fwd[pos + 3] === '\n' || fwd[pos + 3] === '\r' || fwd[pos + 3] === ' ' || fwd[pos + 3] === undefined))))
570
+ break;
571
+ // Consume the line content (strip block indent).
572
+ let lineStart = pos + blockIndent;
573
+ let lineEnd = lineStart;
574
+ while (lineEnd < fwd.length && fwd[lineEnd] !== '\n' && fwd[lineEnd] !== '\r')
575
+ lineEnd++;
576
+ lines.push(fwd.substring(lineStart, lineEnd));
577
+ lastNewlinePos = lineEnd;
578
+ pos = lineEnd;
579
+ if (fwd[pos] === '\r')
580
+ pos++;
581
+ if (fwd[pos] === '\n')
582
+ pos++;
583
+ rows++;
584
+ }
585
+ // Build the scalar value.
586
+ let val;
587
+ if (fold) {
588
+ // Folded: newlines between "normal" lines become spaces.
589
+ // Empty lines and "more indented" lines are preserved literally.
590
+ let result = '';
591
+ let prevWasNormal = false;
592
+ let pendingEmptyCount = 0;
593
+ for (let li = 0; li < lines.length; li++) {
594
+ let line = lines[li];
595
+ let isMore = line.length > 0 && (line[0] === ' ' || line[0] === '\t');
596
+ let isEmpty = line === '';
597
+ if (isEmpty) {
598
+ pendingEmptyCount++;
599
+ }
600
+ else if (isMore) {
601
+ // Flush pending empty lines, with paragraph-close if needed.
602
+ if (prevWasNormal && result.length > 0)
603
+ result += '\n';
604
+ for (let ei = 0; ei < pendingEmptyCount; ei++)
605
+ result += '\n';
606
+ pendingEmptyCount = 0;
607
+ // "More indented" — preserve with newlines around it.
608
+ if (result.length > 0 && result[result.length - 1] !== '\n') {
609
+ result += '\n';
610
+ }
611
+ result += line + '\n';
612
+ prevWasNormal = false;
613
+ }
614
+ else {
615
+ // Normal line.
616
+ if (pendingEmptyCount > 0) {
617
+ // Empty lines between content: emit them.
618
+ // The transition normal→empty→normal needs paragraph-close \n
619
+ // (which counts as the first empty line).
620
+ if (prevWasNormal && result.length > 0) {
621
+ // First empty line is the paragraph break.
622
+ result += '\n';
623
+ for (let ei = 1; ei < pendingEmptyCount; ei++)
624
+ result += '\n';
625
+ }
626
+ else {
627
+ for (let ei = 0; ei < pendingEmptyCount; ei++)
628
+ result += '\n';
629
+ }
630
+ pendingEmptyCount = 0;
631
+ }
632
+ if (prevWasNormal && result.length > 0 && result[result.length - 1] !== '\n') {
633
+ // Join with space (folding).
634
+ result += ' ';
635
+ }
636
+ result += line;
637
+ prevWasNormal = true;
638
+ }
639
+ }
640
+ // Flush trailing empty lines.
641
+ for (let ei = 0; ei < pendingEmptyCount; ei++)
642
+ result += '\n';
643
+ val = result;
644
+ }
645
+ else {
646
+ // Literal: preserve newlines.
647
+ val = lines.join('\n');
648
+ }
649
+ // Apply chomping.
650
+ if (lines.length === 0) {
651
+ // No content lines at all — result is empty string
652
+ // regardless of chomping.
653
+ val = '';
654
+ }
655
+ else if (chomp === 'strip') {
656
+ val = val.replace(/\n+$/, '');
657
+ }
658
+ else if (chomp === 'clip') {
659
+ val = val.replace(/\n+$/, '') + '\n';
660
+ }
661
+ else {
662
+ // keep: preserve all trailing newlines
663
+ val = val + '\n';
664
+ }
665
+ // If block ended because of less indent (more content follows
666
+ // that isn't a doc marker), don't consume the final newline —
667
+ // leave it for the yamlMatcher to emit #IN so the grammar can
668
+ // continue properly.
669
+ let endPos = pos;
670
+ let endRows = rows;
671
+ if (pos < fwd.length && pos > lastNewlinePos) {
672
+ // Check if the next content is a doc marker (--- or ...).
673
+ let nextLineIndent = 0;
674
+ let ni = pos;
675
+ while (ni < fwd.length && fwd[ni] === ' ') {
676
+ nextLineIndent++;
677
+ ni++;
678
+ }
679
+ let isDocMarker = nextLineIndent === 0 &&
680
+ ((fwd[ni] === '-' && fwd[ni + 1] === '-' && fwd[ni + 2] === '-') ||
681
+ (fwd[ni] === '.' && fwd[ni + 1] === '.' && fwd[ni + 2] === '.'));
682
+ if (!isDocMarker) {
683
+ // Regular content follows — back up to the newline position.
684
+ endPos = lastNewlinePos;
685
+ endRows = rows - 1;
686
+ }
687
+ }
688
+ let src = fwd.substring(0, endPos);
689
+ let tkn = lex.token('#TX', val, src, pnt);
690
+ pnt.sI += endPos;
691
+ pnt.rI += endRows;
692
+ pnt.cI = 0;
693
+ return { done: true, token: tkn };
694
+ }
695
+ }
696
+ // YAML tags: !!type value
697
+ if (ch === '!' && fwd[1] === '!') {
698
+ let tagEnd = 2;
699
+ while (tagEnd < fwd.length && fwd[tagEnd] !== ' ' && fwd[tagEnd] !== '\n' &&
700
+ fwd[tagEnd] !== '\r')
701
+ tagEnd++;
702
+ let tag = fwd.substring(2, tagEnd);
703
+ // For !!seq and !!map, these are handled in the yamlMatcher.
704
+ if (tag === 'seq' || tag === 'map') {
705
+ return null; // Don't advance pnt — let yamlMatcher handle it.
706
+ }
707
+ // For value tags, parse the value after the tag.
708
+ let valStart = tagEnd;
709
+ if (fwd[valStart] === ' ')
710
+ valStart++;
711
+ // Get the raw value string.
712
+ let rawVal = '';
713
+ let valEnd = valStart;
714
+ if (fwd[valStart] === '"' || fwd[valStart] === "'") {
715
+ // Quoted value — find matching close quote.
716
+ let q = fwd[valStart];
717
+ valEnd = valStart + 1;
718
+ while (valEnd < fwd.length && fwd[valEnd] !== q) {
719
+ if (fwd[valEnd] === '\\' && q === '"')
720
+ valEnd++; // Skip escape in double quotes.
721
+ valEnd++;
722
+ }
723
+ if (fwd[valEnd] === q)
724
+ valEnd++;
725
+ rawVal = fwd.substring(valStart + 1, valEnd - 1);
726
+ }
727
+ else {
728
+ // Unquoted value — to end of line, stopping at `: ` and ` #`.
729
+ while (valEnd < fwd.length && fwd[valEnd] !== '\n' && fwd[valEnd] !== '\r') {
730
+ if (fwd[valEnd] === ':' && (fwd[valEnd + 1] === ' ' || fwd[valEnd + 1] === '\n' ||
731
+ fwd[valEnd + 1] === '\r' || fwd[valEnd + 1] === undefined))
732
+ break;
733
+ if (fwd[valEnd] === ' ' && fwd[valEnd + 1] === '#')
734
+ break;
735
+ valEnd++;
736
+ }
737
+ rawVal = fwd.substring(valStart, valEnd).replace(/\s+$/, '');
738
+ }
739
+ // Apply tag conversion.
740
+ let result;
741
+ if (tag === 'str')
742
+ result = String(rawVal);
743
+ else if (tag === 'int')
744
+ result = parseInt(rawVal, 10);
745
+ else if (tag === 'float')
746
+ result = parseFloat(rawVal);
747
+ else if (tag === 'bool')
748
+ result = rawVal === 'true' || rawVal === 'True' || rawVal === 'TRUE';
749
+ else if (tag === 'null')
750
+ result = null;
751
+ else
752
+ result = rawVal; // Unknown tag — keep as string.
753
+ let src = fwd.substring(0, valEnd);
754
+ let tknTin = typeof result === 'string' ? '#TX' :
755
+ typeof result === 'number' ? '#NR' :
756
+ '#VL';
757
+ let tkn = lex.token(tknTin, result, src, pnt);
758
+ pnt.sI += valEnd;
759
+ pnt.cI += valEnd;
760
+ return { done: true, token: tkn };
761
+ }
762
+ // Don't apply text check for special chars or flow context.
763
+ // Also skip * and & which are YAML alias/anchor indicators.
764
+ if (ch === '{' || ch === '}' || ch === '[' || ch === ']' ||
765
+ ch === ',' || ch === '#' || ch === '\n' ||
766
+ ch === '\r' || ch === '"' || ch === "'" ||
767
+ ch === '*' || ch === '&' || ch === '!' ||
768
+ ch === undefined) {
769
+ return null;
770
+ }
771
+ // Colon only starts a key-value separator if followed by space/tab/newline/eof.
772
+ // Otherwise it can start a plain scalar (e.g. ::vector).
773
+ if (ch === ':' && (fwd[1] === ' ' || fwd[1] === '\t' || fwd[1] === '\n' ||
774
+ fwd[1] === '\r' || fwd[1] === undefined)) {
775
+ return null;
776
+ }
777
+ // Match text to end of line, stopping at `: `, `:\n`, ` #`, or newline.
778
+ // This handles YAML plain scalars with multiline continuation.
779
+ updateFlowState(lex.src, pnt.sI);
780
+ let inFlowCtx = _flowDepth > 0;
781
+ // Find key indent and determine context for multiline scalars.
782
+ let lineStart = pnt.sI;
783
+ while (lineStart > 0 && lex.src[lineStart - 1] !== '\n' && lex.src[lineStart - 1] !== '\r')
784
+ lineStart--;
785
+ // Current line indent (indent of the line where text starts).
786
+ let currentLineIndent = 0;
787
+ {
788
+ let ci = lineStart;
789
+ while (ci < pnt.sI && lex.src[ci] === ' ') {
790
+ currentLineIndent++;
791
+ ci++;
792
+ }
793
+ }
794
+ // Check if text is preceded by ": " on the same line (map value context).
795
+ let isMapValue = false;
796
+ {
797
+ let ci = pnt.sI - 1;
798
+ // Skip whitespace before text
799
+ while (ci >= lineStart && (lex.src[ci] === ' ' || lex.src[ci] === '\t'))
800
+ ci--;
801
+ if (ci >= lineStart && lex.src[ci] === ':')
802
+ isMapValue = true;
803
+ }
804
+ // For map values: continuation requires indent > key indent (parent line's indent).
805
+ // For standalone scalars: continuation requires indent >= current line indent.
806
+ let keyIndent = 0;
807
+ let prevLineStart = lineStart;
808
+ if (prevLineStart > 0) {
809
+ let pi = prevLineStart - 1;
810
+ if (pi >= 0 && lex.src[pi] === '\n')
811
+ pi--;
812
+ if (pi >= 0 && lex.src[pi] === '\r')
813
+ pi--;
814
+ while (pi > 0 && lex.src[pi - 1] !== '\n' && lex.src[pi - 1] !== '\r')
815
+ pi--;
816
+ while (pi < prevLineStart && lex.src[pi] === ' ') {
817
+ keyIndent++;
818
+ pi++;
819
+ }
820
+ }
821
+ // The minimum indent for continuation lines.
822
+ // For map values, continuation indent is based on the colon's line indent,
823
+ // not the previous line's indent (which may be a key continuation line).
824
+ let minContinuationIndent = isMapValue ? currentLineIndent + 1 : currentLineIndent;
825
+ let text = '';
826
+ let i = 0;
827
+ let totalConsumed = 0;
828
+ let rows = 0;
829
+ let scanLine = () => {
830
+ let line = '';
831
+ while (i < fwd.length) {
832
+ let c = fwd[i];
833
+ if (c === '\n' || c === '\r')
834
+ break;
835
+ if (c === ':' && (fwd[i + 1] === ' ' || fwd[i + 1] === '\t' || fwd[i + 1] === '\n' ||
836
+ fwd[i + 1] === '\r' || fwd[i + 1] === undefined))
837
+ break;
838
+ if ((c === ' ' || c === '\t') && fwd[i + 1] === '#')
839
+ break;
840
+ if (inFlowCtx && (c === ']' || c === '}'))
841
+ break;
842
+ if (c === ',' && inFlowCtx)
843
+ break;
844
+ line += c;
845
+ i++;
846
+ }
847
+ return line.replace(/\s+$/, '');
848
+ };
849
+ text = scanLine();
850
+ totalConsumed = i;
851
+ // Check for continuation lines (multiline plain scalars).
852
+ // Blank lines (whitespace-only) within a scalar become newlines.
853
+ while (i < fwd.length && (fwd[i] === '\n' || fwd[i] === '\r')) {
854
+ let nlPos = i;
855
+ // Count blank lines (lines with only whitespace).
856
+ let blankLines = 0;
857
+ while (i < fwd.length && (fwd[i] === '\n' || fwd[i] === '\r')) {
858
+ if (fwd[i] === '\r')
859
+ i++;
860
+ if (fwd[i] === '\n')
861
+ i++;
862
+ // Count indent of next line.
863
+ let li = 0;
864
+ while (i + li < fwd.length && (fwd[i + li] === ' ' || fwd[i + li] === '\t'))
865
+ li++;
866
+ if (i + li >= fwd.length || fwd[i + li] === '\n' || fwd[i + li] === '\r') {
867
+ // Blank line — count it and skip.
868
+ blankLines++;
869
+ i += li;
870
+ continue;
871
+ }
872
+ break;
873
+ }
874
+ // Count indent of the content line after blank lines.
875
+ let lineIndent = 0;
876
+ while (i < fwd.length && (fwd[i] === ' ' || fwd[i] === '\t')) {
877
+ lineIndent++;
878
+ i++;
879
+ }
880
+ // In flow context, continuation is allowed regardless of indent
881
+ // (as long as the next line doesn't start a flow indicator or comment).
882
+ // In block context, must be more indented than the key.
883
+ // Check for document markers (--- or ...) at column 0.
884
+ let isDocMarker = lineIndent === 0 &&
885
+ ((fwd[i] === '-' && fwd[i + 1] === '-' && fwd[i + 2] === '-' &&
886
+ (fwd[i + 3] === ' ' || fwd[i + 3] === '\t' || fwd[i + 3] === '\n' ||
887
+ fwd[i + 3] === '\r' || fwd[i + 3] === undefined)) ||
888
+ (fwd[i] === '.' && fwd[i + 1] === '.' && fwd[i + 2] === '.' &&
889
+ (fwd[i + 3] === ' ' || fwd[i + 3] === '\t' || fwd[i + 3] === '\n' ||
890
+ fwd[i + 3] === '\r' || fwd[i + 3] === undefined)));
891
+ // Check for sequence marker "- ". Only treat as a new sequence
892
+ // entry when the indent matches an enclosing sequence's level.
893
+ // Find the nearest "- " sequence marker preceding the text on
894
+ // the first line to determine the relevant sequence indent.
895
+ let isSeqMarker = false;
896
+ if (fwd[i] === '-' &&
897
+ (fwd[i + 1] === ' ' || fwd[i + 1] === '\t' || fwd[i + 1] === '\n' ||
898
+ fwd[i + 1] === '\r' || fwd[i + 1] === undefined)) {
899
+ // Determine the sequence indent from the first line's context.
900
+ // Look backward from pnt.sI to find "- " markers before the text.
901
+ let seqIndent = -1;
902
+ let si = pnt.sI - 1;
903
+ while (si >= lineStart) {
904
+ if (lex.src[si] === '-' && (lex.src[si + 1] === ' ' || lex.src[si + 1] === '\t')) {
905
+ seqIndent = si - lineStart;
906
+ break;
907
+ }
908
+ si--;
909
+ }
910
+ // isSeqMarker if the continuation "- " matches a known sequence
911
+ // indent, or if it's at the current line indent level.
912
+ isSeqMarker = (seqIndent >= 0 && lineIndent === seqIndent) ||
913
+ (seqIndent < 0 && lineIndent <= currentLineIndent);
914
+ }
915
+ let canContinue = inFlowCtx
916
+ ? (i < fwd.length && fwd[i] !== '\n' && fwd[i] !== '\r' &&
917
+ fwd[i] !== '#' && fwd[i] !== '{' && fwd[i] !== '}' &&
918
+ fwd[i] !== '[' && fwd[i] !== ']')
919
+ : (lineIndent >= minContinuationIndent && i < fwd.length &&
920
+ fwd[i] !== '\n' && fwd[i] !== '\r' && fwd[i] !== '#' &&
921
+ !isDocMarker && !isSeqMarker);
922
+ if (canContinue) {
923
+ // Check if this line is a key-value pair (contains ": ").
924
+ let peekJ = i;
925
+ let isKV = false;
926
+ while (peekJ < fwd.length && fwd[peekJ] !== '\n' && fwd[peekJ] !== '\r') {
927
+ if (fwd[peekJ] === ':' && (fwd[peekJ + 1] === ' ' || fwd[peekJ + 1] === '\t' ||
928
+ fwd[peekJ + 1] === '\n' || fwd[peekJ + 1] === '\r' ||
929
+ fwd[peekJ + 1] === undefined)) {
930
+ isKV = true;
931
+ break;
932
+ }
933
+ if (fwd[peekJ] === '}' || fwd[peekJ] === ']' || fwd[peekJ] === ',') {
934
+ break;
935
+ }
936
+ peekJ++;
937
+ }
938
+ if (!isKV || inFlowCtx) {
939
+ let contLine = scanLine();
940
+ if (contLine.length > 0) {
941
+ // Blank lines → newlines; single newline → space (folding).
942
+ if (blankLines > 0) {
943
+ for (let b = 0; b < blankLines; b++)
944
+ text += '\n';
945
+ }
946
+ else {
947
+ text += ' ';
948
+ }
949
+ text += contLine;
950
+ totalConsumed = i;
951
+ rows++;
952
+ continue;
953
+ }
954
+ }
955
+ }
956
+ // Not a continuation — revert to newline position.
957
+ i = nlPos;
958
+ break;
959
+ }
960
+ text = text.replace(/\s+$/, '');
961
+ if (text.length === 0)
962
+ return null;
963
+ // Check if this is a known YAML value.
964
+ let valMap = {
965
+ 'true': true, 'True': true, 'TRUE': true,
966
+ 'false': false, 'False': false, 'FALSE': false,
967
+ 'null': null, 'Null': null, 'NULL': null,
968
+ '~': null,
969
+ 'yes': true, 'Yes': true, 'YES': true,
970
+ 'no': false, 'No': false, 'NO': false,
971
+ 'on': true, 'On': true, 'ON': true,
972
+ 'off': false, 'Off': false, 'OFF': false,
973
+ '.inf': Infinity, '.Inf': Infinity, '.INF': Infinity,
974
+ '-.inf': -Infinity, '-.Inf': -Infinity, '-.INF': -Infinity,
975
+ '.nan': NaN, '.NaN': NaN, '.NAN': NaN,
976
+ };
977
+ if (text in valMap) {
978
+ let tkn = lex.token('#VL', valMap[text], text, pnt);
979
+ pnt.sI += text.length;
980
+ pnt.cI += text.length;
981
+ return { done: true, token: tkn };
982
+ }
983
+ // Check if it's a number.
984
+ let num = +text;
985
+ if (!isNaN(num) && text !== '') {
986
+ let tkn = lex.token('#NR', num, text, pnt);
987
+ pnt.sI += text.length;
988
+ pnt.cI += text.length;
989
+ return { done: true, token: tkn };
990
+ }
991
+ // Plain text — consume to end of meaningful content.
992
+ let src = fwd.substring(0, totalConsumed);
993
+ let tkn = lex.token('#TX', text, src, pnt);
994
+ pnt.sI += totalConsumed;
995
+ pnt.rI += rows;
996
+ pnt.cI += totalConsumed; // approximate
997
+ return { done: true, token: tkn };
998
+ },
999
+ },
1000
+ });
1001
+ // Register #EL token (not as a fixed token — we match it in yamlMatcher).
1002
+ let EL = tabnas.token('#EL');
1003
+ // Register #QM token: the YAML `?` explicit-key indicator inside flow
1004
+ // collections. Emitted by yamlMatcher only when in flow context and
1005
+ // followed by whitespace; consumed by pair/elem rule alts below.
1006
+ let QM = tabnas.token('#QM');
1007
+ // YAML document-frame tokens, emitted by yamlMatcher at column 0:
1008
+ // #DS document start: --- (with optional inline content following)
1009
+ // #DE document end: ...
1010
+ // #DR directive line: %YAML 1.2 / %TAG !! tag:...
1011
+ // The `stream` rule consumes them; rules apply directives and accumulate
1012
+ // each document's value into the result.
1013
+ let DS = tabnas.token('#DS');
1014
+ let DE = tabnas.token('#DE');
1015
+ let DR = tabnas.token('#DR');
1016
+ // Flow collection tokens.
1017
+ let CA = tabnas.token.CA; // comma
1018
+ let CS = tabnas.token.CS; // ]
1019
+ let CB = tabnas.token.CB; // }
1020
+ let OS = tabnas.token.OS; // [
1021
+ let OB = tabnas.token.OB; // {
1022
+ // All tokens that can start a value.
1023
+ let KEY = [TX, NR, ST, VL];
1024
+ // Add a custom lex matcher for YAML special cases.
1025
+ tabnas.options({
1026
+ lex: {
1027
+ match: {
1028
+ yaml: {
1029
+ order: 5e5,
1030
+ make: (_cfg, _opts) => {
1031
+ // Track Lex objects we've already initialised. Identity comparison
1032
+ // distinguishes parse invocations correctly even when the same
1033
+ // source string is parsed twice in a row — `lex.src !== cleanedSrc`
1034
+ // would skip the reset on the second call and pollute output with
1035
+ // state from the prior parse.
1036
+ const seenLex = new WeakSet();
1037
+ return function yamlMatcher(lex) {
1038
+ // First call of a new parse: reset per-parse state.
1039
+ // Document-frame syntax (--- / ... / %YAML / %TAG) is no longer
1040
+ // mutated out of lex.src here — it flows through as #DS / #DE /
1041
+ // #DR tokens consumed by the `stream` rule.
1042
+ if (!seenLex.has(lex)) {
1043
+ seenLex.add(lex);
1044
+ anchors = {};
1045
+ pendingAnchors = [];
1046
+ pendingExplicitCL = false;
1047
+ skipNumberMatch = false;
1048
+ pendingTokens = [];
1049
+ tagHandles = {};
1050
+ yamlStreamDocs = [];
1051
+ yamlStreamMeta = [];
1052
+ yamlStreamCurMeta = null;
1053
+ _flowDepth = 0;
1054
+ _flowScanPos = 0;
1055
+ _inSingleQuote = false;
1056
+ _inDoubleQuote = false;
1057
+ // Empty / whitespace-only / comments-only source: emit one
1058
+ // null #VL so the parser yields `null` rather than an error.
1059
+ let src = '' + lex.src;
1060
+ let stripped = src.replace(/^[ \t]*#[^\n]*(\n|$)/gm, '').trim();
1061
+ if (src.trim() === '' || stripped === '') {
1062
+ lex.pnt.len = 0;
1063
+ let tkn = lex.token('#VL', null, '', lex.pnt);
1064
+ lex.pnt.sI = 0;
1065
+ return tkn;
1066
+ }
1067
+ }
1068
+ // Drain any queued tokens first (from multi-token explicit keys).
1069
+ if (pendingTokens.length > 0) {
1070
+ return pendingTokens.shift();
1071
+ }
1072
+ let pnt = lex.pnt;
1073
+ let fwd = lex.fwd;
1074
+ // Loop to restart matching after consuming flow whitespace.
1075
+ yamlMatchLoop: while (true) {
1076
+ // Skip blank lines that contain only tabs (and maybe spaces).
1077
+ // YAML treats these as blank lines, but jsonic errors on bare tabs.
1078
+ if (fwd[0] === '\t' || fwd[0] === ' ') {
1079
+ let lineEnd = fwd.indexOf('\n');
1080
+ let lineContent = lineEnd >= 0 ? fwd.substring(0, lineEnd) : fwd;
1081
+ if (lineContent.indexOf('\t') >= 0 && /^[ \t]+$/.test(lineContent)) {
1082
+ let skip = lineEnd >= 0 ? lineEnd + 1 : lineContent.length;
1083
+ pnt.sI += skip;
1084
+ pnt.rI++;
1085
+ pnt.cI = 0;
1086
+ fwd = lex.refwd();
1087
+ continue yamlMatchLoop;
1088
+ }
1089
+ }
1090
+ // Emit pending CL from explicit key — must be before !!type handlers
1091
+ // so the CL token appears before the value token.
1092
+ if (pendingExplicitCL) {
1093
+ pendingExplicitCL = false;
1094
+ let tkn = lex.token('#CL', 1, ': ', lex.pnt);
1095
+ return tkn;
1096
+ }
1097
+ // YAML alias: *name — emit a VL token with alias name.
1098
+ // Resolution happens at grammar time (val.ac) since the anchor
1099
+ // may not be recorded yet due to lexer pre-fetching.
1100
+ if (fwd[0] === '*') {
1101
+ let nameEnd = 1;
1102
+ while (nameEnd < fwd.length && fwd[nameEnd] !== ' ' && fwd[nameEnd] !== '\t' &&
1103
+ fwd[nameEnd] !== '\n' && fwd[nameEnd] !== '\r' && fwd[nameEnd] !== ',' &&
1104
+ fwd[nameEnd] !== '{' && fwd[nameEnd] !== '}' && fwd[nameEnd] !== '[' &&
1105
+ fwd[nameEnd] !== ']') {
1106
+ // Colon terminates only when followed by space/tab (key-value separator).
1107
+ // Otherwise colon is a valid anchor-name character per YAML spec.
1108
+ if (fwd[nameEnd] === ':' &&
1109
+ (fwd[nameEnd + 1] === ' ' || fwd[nameEnd + 1] === '\t'))
1110
+ break;
1111
+ nameEnd++;
1112
+ }
1113
+ let name = fwd.substring(1, nameEnd);
1114
+ let src = fwd.substring(0, nameEnd);
1115
+ // Check if this alias is used as a map key (followed by ` :` or `:`).
1116
+ let afterAlias = nameEnd;
1117
+ while (afterAlias < fwd.length && (fwd[afterAlias] === ' ' || fwd[afterAlias] === '\t'))
1118
+ afterAlias++;
1119
+ let isKey = afterAlias < fwd.length && fwd[afterAlias] === ':' &&
1120
+ (fwd[afterAlias + 1] === ' ' || fwd[afterAlias + 1] === '\t' ||
1121
+ fwd[afterAlias + 1] === '\n' || fwd[afterAlias + 1] === '\r' ||
1122
+ fwd[afterAlias + 1] === undefined);
1123
+ if (isKey && anchors[name] !== undefined) {
1124
+ // Resolve alias immediately as a key string.
1125
+ let resolved = String(anchors[name]);
1126
+ let tkn = lex.token('#TX', resolved, src, lex.pnt);
1127
+ pnt.sI += nameEnd;
1128
+ pnt.cI += nameEnd;
1129
+ return tkn;
1130
+ }
1131
+ // Resolve alias immediately if anchor exists, since deferred
1132
+ // markers can be lost through Jsonic's rule processing.
1133
+ let tkn;
1134
+ if (anchors[name] !== undefined) {
1135
+ let val = anchors[name];
1136
+ if (typeof val === 'object' && val !== null) {
1137
+ val = JSON.parse(JSON.stringify(val));
1138
+ }
1139
+ let tin = typeof val === 'string' ? '#TX' :
1140
+ typeof val === 'number' ? '#NR' : '#VL';
1141
+ tkn = lex.token(tin, val, src, lex.pnt);
1142
+ }
1143
+ else {
1144
+ // Anchor not yet seen — store marker for deferred resolution.
1145
+ let marker = { __yamlAlias: name };
1146
+ tkn = lex.token('#VL', marker, src, lex.pnt);
1147
+ }
1148
+ pnt.sI += nameEnd;
1149
+ pnt.cI += nameEnd;
1150
+ return tkn;
1151
+ }
1152
+ // YAML anchor: &name — store value. Skip the anchor marker,
1153
+ // let the value be parsed, and record it post-parse via grammar rules.
1154
+ if (fwd[0] === '&') {
1155
+ let nameEnd = 1;
1156
+ while (nameEnd < fwd.length && fwd[nameEnd] !== ' ' && fwd[nameEnd] !== '\t' &&
1157
+ fwd[nameEnd] !== '\n' && fwd[nameEnd] !== '\r' && fwd[nameEnd] !== ',' &&
1158
+ fwd[nameEnd] !== '{' && fwd[nameEnd] !== '}' && fwd[nameEnd] !== '[' &&
1159
+ fwd[nameEnd] !== ']')
1160
+ nameEnd++;
1161
+ let anchorName = fwd.substring(1, nameEnd);
1162
+ let skip = nameEnd;
1163
+ if (fwd[skip] === ' ' || fwd[skip] === '\t')
1164
+ skip++;
1165
+ // Check if anchor is standalone (first content on its line).
1166
+ // Look backward to see if only whitespace precedes & on this line.
1167
+ let isStandalone = true;
1168
+ let anchorIndent = 0;
1169
+ {
1170
+ let bi = pnt.sI - 1;
1171
+ while (bi >= 0 && lex.src[bi] !== '\n' && lex.src[bi] !== '\r') {
1172
+ if (lex.src[bi] !== ' ' && lex.src[bi] !== '\t') {
1173
+ isStandalone = false;
1174
+ break;
1175
+ }
1176
+ anchorIndent++;
1177
+ bi--;
1178
+ }
1179
+ }
1180
+ pnt.sI += skip;
1181
+ pnt.cI += skip;
1182
+ // Determine if anchor is inline (content follows on same line)
1183
+ // or standalone (only newline follows).
1184
+ let anchorInline = !(isStandalone &&
1185
+ (lex.src[pnt.sI] === '\r' || lex.src[pnt.sI] === '\n' ||
1186
+ pnt.sI >= lex.src.length));
1187
+ // For inline anchors before scalar values, record the anchor
1188
+ // immediately so aliases in later pairs can resolve them.
1189
+ if (anchorInline) {
1190
+ let peek = lex.refwd();
1191
+ let pch = peek[0];
1192
+ if (pch !== '[' && pch !== '{' && pch !== '>' && pch !== '|' &&
1193
+ pch !== '\n' && pch !== '\r' && pch !== undefined) {
1194
+ let scalarVal;
1195
+ if (pch === '"') {
1196
+ let ei = 1;
1197
+ while (ei < peek.length && peek[ei] !== '"') {
1198
+ if (peek[ei] === '\\')
1199
+ ei++;
1200
+ ei++;
1201
+ }
1202
+ scalarVal = peek.substring(1, ei)
1203
+ .replace(/\\n/g, '\n').replace(/\\t/g, '\t')
1204
+ .replace(/\\\\/g, '\\').replace(/\\"/g, '"');
1205
+ }
1206
+ else if (pch === "'") {
1207
+ let ei = 1;
1208
+ while (ei < peek.length && peek[ei] !== "'") {
1209
+ if (peek[ei] === "'" && peek[ei + 1] === "'")
1210
+ ei++;
1211
+ ei++;
1212
+ }
1213
+ scalarVal = peek.substring(1, ei).replace(/''/g, "'");
1214
+ }
1215
+ else {
1216
+ let ei = 0;
1217
+ while (ei < peek.length && peek[ei] !== '\n' && peek[ei] !== '\r' &&
1218
+ peek[ei] !== ',' && peek[ei] !== '}' && peek[ei] !== ']') {
1219
+ if (peek[ei] === ':' && (peek[ei + 1] === ' ' || peek[ei + 1] === '\t' ||
1220
+ peek[ei + 1] === '\n' || peek[ei + 1] === '\r' ||
1221
+ peek[ei + 1] === undefined))
1222
+ break;
1223
+ if (peek[ei] === ' ' && peek[ei + 1] === '#')
1224
+ break;
1225
+ ei++;
1226
+ }
1227
+ let raw = peek.substring(0, ei).trim();
1228
+ if (raw.length > 0)
1229
+ scalarVal = raw;
1230
+ }
1231
+ if (scalarVal !== undefined) {
1232
+ anchors[anchorName] = scalarVal;
1233
+ }
1234
+ }
1235
+ }
1236
+ // Push pending anchor with inline flag.
1237
+ pendingAnchors.push({ name: anchorName, inline: anchorInline });
1238
+ // If anchor is standalone on its own line (followed by newline),
1239
+ // consume the newline and leading spaces so no extra IN token
1240
+ // is emitted. Only consume when next line indent >= anchor indent,
1241
+ // otherwise let the normal indent handler manage the transition.
1242
+ if (isStandalone &&
1243
+ (lex.src[pnt.sI] === '\r' || lex.src[pnt.sI] === '\n')) {
1244
+ let nl = pnt.sI;
1245
+ if (lex.src[nl] === '\r')
1246
+ nl++;
1247
+ if (lex.src[nl] === '\n')
1248
+ nl++;
1249
+ let spaces = 0;
1250
+ while (nl + spaces < lex.src.length && lex.src[nl + spaces] === ' ')
1251
+ spaces++;
1252
+ let nextCh = lex.src[nl + spaces];
1253
+ if (nextCh !== undefined && nextCh !== '\n' && nextCh !== '\r' &&
1254
+ spaces >= anchorIndent) {
1255
+ pnt.sI = nl + spaces;
1256
+ pnt.cI = spaces;
1257
+ pnt.rI++;
1258
+ }
1259
+ }
1260
+ // Update fwd and continue matching.
1261
+ fwd = lex.refwd();
1262
+ continue yamlMatchLoop;
1263
+ }
1264
+ // YAML directive line (%YAML, %TAG, %FOO) at column 0: emit a
1265
+ // #DR token whose val is the raw directive text. The stream rule
1266
+ // applies the directive (e.g. %TAG handle registration) at parse
1267
+ // time via the @apply-directive action.
1268
+ if ((pnt.sI === 0 || lex.src[pnt.sI - 1] === '\n' ||
1269
+ lex.src[pnt.sI - 1] === '\r') && fwd[0] === '%') {
1270
+ let pos = 0;
1271
+ while (pos < fwd.length && fwd[pos] !== '\n' && fwd[pos] !== '\r')
1272
+ pos++;
1273
+ let directiveSrc = fwd.substring(0, pos);
1274
+ pnt.sI += pos;
1275
+ pnt.cI += pos;
1276
+ let tkn = lex.token('#DR', directiveSrc, directiveSrc, lex.pnt);
1277
+ return tkn;
1278
+ }
1279
+ // YAML non-specific tag (! value) or local tag (!name value):
1280
+ // skip the tag and let the value be parsed normally.
1281
+ if (fwd[0] === '!' && fwd[1] !== '!' && fwd[1] !== undefined) {
1282
+ if (fwd[1] === ' ') {
1283
+ // Non-specific tag: ! value → treat value as string.
1284
+ let valStart = 2;
1285
+ let valEnd = valStart;
1286
+ while (valEnd < fwd.length && fwd[valEnd] !== '\n' && fwd[valEnd] !== '\r')
1287
+ valEnd++;
1288
+ let rawVal = fwd.substring(valStart, valEnd).replace(/\s+$/, '');
1289
+ let src = fwd.substring(0, valEnd);
1290
+ let tkn = lex.token('#TX', rawVal, src, lex.pnt);
1291
+ pnt.sI += valEnd;
1292
+ pnt.cI += valEnd;
1293
+ return tkn;
1294
+ }
1295
+ // Local tag: !name value → skip the tag, continue with value.
1296
+ let tagEnd = 1;
1297
+ while (tagEnd < fwd.length && fwd[tagEnd] !== ' ' && fwd[tagEnd] !== '\n' &&
1298
+ fwd[tagEnd] !== '\r')
1299
+ tagEnd++;
1300
+ if (fwd[tagEnd] === ' ')
1301
+ tagEnd++; // skip space after tag
1302
+ pnt.sI += tagEnd;
1303
+ pnt.cI += tagEnd;
1304
+ // If tag is standalone (followed by newline), consume the
1305
+ // newline and leading spaces so no extra #IN is emitted.
1306
+ if (pnt.sI < lex.src.length &&
1307
+ (lex.src[pnt.sI] === '\n' || lex.src[pnt.sI] === '\r')) {
1308
+ // Check if tag is standalone on its line.
1309
+ let tagStandalone = true;
1310
+ let tagLineIndent = 0;
1311
+ let bi = pnt.sI - tagEnd - 1;
1312
+ while (bi >= 0 && lex.src[bi] !== '\n' && lex.src[bi] !== '\r') {
1313
+ if (lex.src[bi] !== ' ' && lex.src[bi] !== '\t') {
1314
+ tagStandalone = false;
1315
+ break;
1316
+ }
1317
+ tagLineIndent++;
1318
+ bi--;
1319
+ }
1320
+ if (tagStandalone) {
1321
+ let nl = pnt.sI;
1322
+ if (lex.src[nl] === '\r')
1323
+ nl++;
1324
+ if (lex.src[nl] === '\n')
1325
+ nl++;
1326
+ let spaces = 0;
1327
+ while (nl + spaces < lex.src.length && lex.src[nl + spaces] === ' ')
1328
+ spaces++;
1329
+ pnt.sI = nl + spaces;
1330
+ pnt.cI = spaces;
1331
+ pnt.rI++;
1332
+ }
1333
+ }
1334
+ fwd = lex.refwd();
1335
+ // Restart matching to parse the value.
1336
+ continue yamlMatchLoop;
1337
+ }
1338
+ // Skip !!seq, !!map, !!omap, !!set, !!binary, etc. tags — just
1339
+ // consume and return undefined so the next lex cycle handles the
1340
+ // actual structure/value.
1341
+ if (fwd[0] === '!' && fwd[1] === '!' &&
1342
+ /^!!(seq|map|omap|set|pairs|binary|ordered|python\/[^\s]*)\b/.test(fwd)) {
1343
+ let skip = 2;
1344
+ while (skip < fwd.length && fwd[skip] !== ' ' && fwd[skip] !== '\n')
1345
+ skip++;
1346
+ while (skip < fwd.length && fwd[skip] === ' ')
1347
+ skip++;
1348
+ // Check if tag is standalone on its own line.
1349
+ let tagIndent = 0;
1350
+ {
1351
+ let bi = pnt.sI - 1;
1352
+ let standalone = true;
1353
+ while (bi >= 0 && lex.src[bi] !== '\n' && lex.src[bi] !== '\r') {
1354
+ if (lex.src[bi] !== ' ' && lex.src[bi] !== '\t') {
1355
+ standalone = false;
1356
+ break;
1357
+ }
1358
+ tagIndent++;
1359
+ bi--;
1360
+ }
1361
+ // If standalone and next line is at the same indent, consume
1362
+ // the newline so no extra IN token is emitted.
1363
+ if (standalone && skip < fwd.length &&
1364
+ (fwd[skip] === '\n' || fwd[skip] === '\r')) {
1365
+ let nl = skip;
1366
+ if (fwd[nl] === '\r')
1367
+ nl++;
1368
+ if (fwd[nl] === '\n')
1369
+ nl++;
1370
+ let spaces = 0;
1371
+ while (nl + spaces < fwd.length && fwd[nl + spaces] === ' ')
1372
+ spaces++;
1373
+ if (spaces >= tagIndent) {
1374
+ skip = nl + spaces;
1375
+ pnt.sI += skip;
1376
+ pnt.cI = spaces;
1377
+ pnt.rI++;
1378
+ fwd = lex.refwd();
1379
+ continue yamlMatchLoop;
1380
+ }
1381
+ }
1382
+ }
1383
+ pnt.sI += skip;
1384
+ pnt.cI += skip;
1385
+ // Don't return a token — let the next lex cycle see the actual value.
1386
+ fwd = lex.refwd();
1387
+ continue yamlMatchLoop;
1388
+ }
1389
+ // Handle other !!type tags (!!str, !!int, !!float, !!bool, !!null).
1390
+ // These apply a type to the following value. For !!str, the value
1391
+ // is always a string. For others, convert accordingly.
1392
+ if (fwd[0] === '!' && fwd[1] === '!') {
1393
+ let tagEnd = 2;
1394
+ while (tagEnd < fwd.length && fwd[tagEnd] !== ' ' && fwd[tagEnd] !== '\n' &&
1395
+ fwd[tagEnd] !== '\r' && fwd[tagEnd] !== ',' &&
1396
+ fwd[tagEnd] !== '}' && fwd[tagEnd] !== ']' &&
1397
+ fwd[tagEnd] !== ':')
1398
+ tagEnd++;
1399
+ let tag = fwd.substring(2, tagEnd);
1400
+ let valStart = tagEnd;
1401
+ if (fwd[valStart] === ' ')
1402
+ valStart++;
1403
+ let valEnd = valStart;
1404
+ // Skip and record anchor (&name) if present before value.
1405
+ let tagAnchorName = '';
1406
+ if (fwd[valStart] === '&') {
1407
+ let anchorEnd = valStart + 1;
1408
+ while (anchorEnd < fwd.length && fwd[anchorEnd] !== ' ' &&
1409
+ fwd[anchorEnd] !== '\n' && fwd[anchorEnd] !== '\r')
1410
+ anchorEnd++;
1411
+ tagAnchorName = fwd.substring(valStart + 1, anchorEnd);
1412
+ pendingAnchors.push({ name: tagAnchorName, inline: true });
1413
+ if (fwd[anchorEnd] === ' ')
1414
+ anchorEnd++;
1415
+ valStart = anchorEnd;
1416
+ valEnd = valStart;
1417
+ }
1418
+ // Check for quoted value.
1419
+ if (fwd[valStart] === '"' || fwd[valStart] === "'") {
1420
+ let q = fwd[valStart];
1421
+ valEnd = valStart + 1;
1422
+ while (valEnd < fwd.length && fwd[valEnd] !== q) {
1423
+ if (fwd[valEnd] === '\\' && q === '"')
1424
+ valEnd++;
1425
+ valEnd++;
1426
+ }
1427
+ if (fwd[valEnd] === q)
1428
+ valEnd++;
1429
+ let rawVal = fwd.substring(valStart + 1, valEnd - 1);
1430
+ let result = rawVal;
1431
+ if (!tagHandles['!!']) {
1432
+ if (tag === 'int')
1433
+ result = parseInt(rawVal, 10);
1434
+ else if (tag === 'float')
1435
+ result = parseFloat(rawVal);
1436
+ else if (tag === 'bool')
1437
+ result = rawVal === 'true' || rawVal === 'True' || rawVal === 'TRUE';
1438
+ else if (tag === 'null')
1439
+ result = null;
1440
+ }
1441
+ if (tagAnchorName)
1442
+ anchors[tagAnchorName] = result;
1443
+ let tknTin = typeof result === 'string' ? '#TX' :
1444
+ typeof result === 'number' ? '#NR' : '#VL';
1445
+ let tkn = lex.token(tknTin, result, fwd.substring(0, valEnd), lex.pnt);
1446
+ pnt.sI += valEnd;
1447
+ pnt.cI += valEnd;
1448
+ return tkn;
1449
+ }
1450
+ // If value is on next line (tag followed by newline with
1451
+ // indented content), skip the tag and let the next lex cycle
1452
+ // handle the value. If end-of-source or next line is not
1453
+ // indented content, fall through to produce default value.
1454
+ if ((fwd[valStart] === '\n' || fwd[valStart] === '\r') &&
1455
+ valStart < fwd.length - 1) {
1456
+ // Tag followed by newline — skip the tag and let the
1457
+ // next lex cycle handle the value on the following line.
1458
+ let nl = valStart;
1459
+ if (fwd[nl] === '\r')
1460
+ nl++;
1461
+ if (fwd[nl] === '\n')
1462
+ nl++;
1463
+ pnt.sI += nl;
1464
+ pnt.cI = 0;
1465
+ pnt.rI++;
1466
+ fwd = lex.refwd();
1467
+ continue yamlMatchLoop;
1468
+ }
1469
+ // Unquoted: stop at `: `, ` #`, newline, flow indicators.
1470
+ while (valEnd < fwd.length && fwd[valEnd] !== '\n' && fwd[valEnd] !== '\r' &&
1471
+ fwd[valEnd] !== ',' && fwd[valEnd] !== '}' && fwd[valEnd] !== ']') {
1472
+ if (fwd[valEnd] === ':' && (fwd[valEnd + 1] === ' ' || fwd[valEnd + 1] === '\n' ||
1473
+ fwd[valEnd + 1] === '\r' || fwd[valEnd + 1] === undefined))
1474
+ break;
1475
+ if (fwd[valEnd] === ' ' && fwd[valEnd + 1] === '#')
1476
+ break;
1477
+ valEnd++;
1478
+ }
1479
+ let rawVal = fwd.substring(valStart, valEnd).replace(/\s+$/, '');
1480
+ let result = rawVal;
1481
+ // Only apply built-in type conversion when !! has not been
1482
+ // redefined by a %TAG directive. Custom tag handles mean
1483
+ // !!type is a user-defined tag, not a YAML core type.
1484
+ if (!tagHandles['!!']) {
1485
+ if (tag === 'str')
1486
+ result = String(rawVal);
1487
+ else if (tag === 'int')
1488
+ result = parseInt(rawVal, 10);
1489
+ else if (tag === 'float')
1490
+ result = parseFloat(rawVal);
1491
+ else if (tag === 'bool')
1492
+ result = rawVal === 'true' || rawVal === 'True' || rawVal === 'TRUE';
1493
+ else if (tag === 'null')
1494
+ result = null;
1495
+ }
1496
+ if (tagAnchorName)
1497
+ anchors[tagAnchorName] = result;
1498
+ // Use #ST for empty strings (jsonic handles #ST better than
1499
+ // empty #TX in flow context), #NR for numbers, #VL for null.
1500
+ let tknTin = (typeof result === 'string' && result === '') ? '#ST' :
1501
+ typeof result === 'string' ? '#TX' :
1502
+ typeof result === 'number' ? '#NR' : '#VL';
1503
+ let tkn = lex.token(tknTin, result, fwd.substring(0, valEnd), lex.pnt);
1504
+ pnt.sI += valEnd;
1505
+ pnt.cI += valEnd;
1506
+ return tkn;
1507
+ }
1508
+ // Flow-context `?` explicit-key marker: emit a #QM token so
1509
+ // pair/elem rule alts can handle it. Block-context `?` falls
1510
+ // through to the heavyweight handler below.
1511
+ if (fwd[0] === '?' && (fwd[1] === ' ' || fwd[1] === '\t')) {
1512
+ updateFlowState(lex.src, pnt.sI);
1513
+ if (_flowDepth > 0) {
1514
+ let tkn = lex.token('#QM', undefined, '?', lex.pnt);
1515
+ pnt.sI += 1;
1516
+ pnt.cI += 1;
1517
+ return tkn;
1518
+ }
1519
+ }
1520
+ // YAML explicit key indicator: ? key\n: value
1521
+ // Handles: ? key (with null value if no : follows)
1522
+ // ? key\n: value
1523
+ // ? key\n# comment\n: value
1524
+ // ? key1\n? key2 (consecutive explicit keys with null values)
1525
+ if (fwd[0] === '?' && (fwd[1] === ' ' || fwd[1] === '\t' ||
1526
+ fwd[1] === '\n' || fwd[1] === '\r' || fwd[1] === undefined)) {
1527
+ let start = (fwd[1] === ' ' || fwd[1] === '\t') ? 2 : 1;
1528
+ // Collect key text (may be multiline via continuation).
1529
+ let keyEnd = start;
1530
+ let key = '';
1531
+ // First line of key.
1532
+ while (keyEnd < fwd.length && fwd[keyEnd] !== '\n' && fwd[keyEnd] !== '\r') {
1533
+ if (fwd[keyEnd] === ' ' && fwd[keyEnd + 1] === '#')
1534
+ break; // comment
1535
+ keyEnd++;
1536
+ }
1537
+ key = fwd.substring(start, keyEnd).replace(/\s+$/, '');
1538
+ // Strip !!type tags from explicit keys and apply conversion.
1539
+ let explicitKeyTag = '';
1540
+ let tagMatch = key.match(/^!!(\w+)\s+(.*)$/);
1541
+ if (tagMatch) {
1542
+ explicitKeyTag = tagMatch[1];
1543
+ key = tagMatch[2];
1544
+ }
1545
+ let consumed = keyEnd;
1546
+ // Track position before consuming newline (for !hasValue case).
1547
+ let beforeNewline = consumed;
1548
+ // Skip comment at end of key line.
1549
+ while (consumed < fwd.length && fwd[consumed] !== '\n' && fwd[consumed] !== '\r')
1550
+ consumed++;
1551
+ beforeNewline = consumed;
1552
+ // Consume newline after key line.
1553
+ if (consumed < fwd.length && fwd[consumed] === '\r')
1554
+ consumed++;
1555
+ if (consumed < fwd.length && fwd[consumed] === '\n')
1556
+ consumed++;
1557
+ // Check for multiline key (continuation lines indented more than ?).
1558
+ let qIndent = 0;
1559
+ {
1560
+ let li = pnt.sI;
1561
+ while (li > 0 && lex.src[li - 1] !== '\n' && lex.src[li - 1] !== '\r')
1562
+ li--;
1563
+ while (li < pnt.sI && lex.src[li] === ' ') {
1564
+ qIndent++;
1565
+ li++;
1566
+ }
1567
+ }
1568
+ // Count extra rows consumed (for multiline keys).
1569
+ let extraRows = 0;
1570
+ // Handle block scalar keys (| or >).
1571
+ let blockScalarMatch = key.match(/^([|>])([+-]?)([0-9]?)$/);
1572
+ if (blockScalarMatch) {
1573
+ let isFolded = blockScalarMatch[1] === '>';
1574
+ let chomp = blockScalarMatch[2] || '';
1575
+ let explicitIndent = blockScalarMatch[3] ? parseInt(blockScalarMatch[3]) : 0;
1576
+ // Collect block scalar content lines.
1577
+ let blockLines = [];
1578
+ let contentIndent = 0;
1579
+ while (consumed < fwd.length) {
1580
+ let lineIndent = 0;
1581
+ while (consumed + lineIndent < fwd.length && fwd[consumed + lineIndent] === ' ')
1582
+ lineIndent++;
1583
+ let afterSpaces = consumed + lineIndent;
1584
+ // Empty line or line with only spaces.
1585
+ if (afterSpaces >= fwd.length || fwd[afterSpaces] === '\n' || fwd[afterSpaces] === '\r') {
1586
+ blockLines.push('');
1587
+ consumed = afterSpaces;
1588
+ if (consumed < fwd.length && fwd[consumed] === '\r')
1589
+ consumed++;
1590
+ if (consumed < fwd.length && fwd[consumed] === '\n')
1591
+ consumed++;
1592
+ extraRows++;
1593
+ continue;
1594
+ }
1595
+ // Determine content indent from first non-empty line.
1596
+ if (contentIndent === 0) {
1597
+ contentIndent = explicitIndent > 0 ? qIndent + explicitIndent : lineIndent;
1598
+ }
1599
+ // Line must be indented more than ? to be content.
1600
+ if (lineIndent < contentIndent)
1601
+ break;
1602
+ // Collect line content.
1603
+ let lineEnd = afterSpaces;
1604
+ while (lineEnd < fwd.length && fwd[lineEnd] !== '\n' && fwd[lineEnd] !== '\r')
1605
+ lineEnd++;
1606
+ blockLines.push(fwd.substring(consumed + contentIndent, lineEnd));
1607
+ consumed = lineEnd;
1608
+ if (consumed < fwd.length && fwd[consumed] === '\r')
1609
+ consumed++;
1610
+ if (consumed < fwd.length && fwd[consumed] === '\n')
1611
+ consumed++;
1612
+ extraRows++;
1613
+ }
1614
+ // Apply chomping.
1615
+ // Remove trailing empty lines for non-keep.
1616
+ if (chomp !== '+') {
1617
+ while (blockLines.length > 0 && blockLines[blockLines.length - 1] === '')
1618
+ blockLines.pop();
1619
+ }
1620
+ if (isFolded) {
1621
+ key = blockLines.join(' ') + '\n';
1622
+ }
1623
+ else {
1624
+ key = blockLines.join('\n') + '\n';
1625
+ }
1626
+ if (chomp === '-') {
1627
+ key = key.replace(/\n$/, '');
1628
+ }
1629
+ }
1630
+ else {
1631
+ // Scan continuation lines for key (plain scalar multiline).
1632
+ while (consumed < fwd.length) {
1633
+ // Skip comment lines.
1634
+ let lineIndent = 0;
1635
+ while (consumed + lineIndent < fwd.length && fwd[consumed + lineIndent] === ' ')
1636
+ lineIndent++;
1637
+ let afterSpaces = consumed + lineIndent;
1638
+ if (afterSpaces < fwd.length && fwd[afterSpaces] === '#') {
1639
+ // Comment line — skip it.
1640
+ while (afterSpaces < fwd.length && fwd[afterSpaces] !== '\n' && fwd[afterSpaces] !== '\r')
1641
+ afterSpaces++;
1642
+ beforeNewline = afterSpaces;
1643
+ if (afterSpaces < fwd.length && fwd[afterSpaces] === '\r')
1644
+ afterSpaces++;
1645
+ if (afterSpaces < fwd.length && fwd[afterSpaces] === '\n')
1646
+ afterSpaces++;
1647
+ extraRows++;
1648
+ consumed = afterSpaces;
1649
+ continue;
1650
+ }
1651
+ // Check if this is a continuation of the key (indented more than ?).
1652
+ if (lineIndent > qIndent && fwd[afterSpaces] !== ':' &&
1653
+ fwd[afterSpaces] !== '?' && fwd[afterSpaces] !== '-') {
1654
+ // Continuation line for multiline key.
1655
+ let contEnd = afterSpaces;
1656
+ while (contEnd < fwd.length && fwd[contEnd] !== '\n' && fwd[contEnd] !== '\r') {
1657
+ if (fwd[contEnd] === ' ' && fwd[contEnd + 1] === '#')
1658
+ break;
1659
+ contEnd++;
1660
+ }
1661
+ let contText = fwd.substring(afterSpaces, contEnd).replace(/\s+$/, '');
1662
+ if (contText.length > 0) {
1663
+ key += ' ' + contText;
1664
+ }
1665
+ consumed = contEnd;
1666
+ beforeNewline = consumed;
1667
+ if (consumed < fwd.length && fwd[consumed] === '\r')
1668
+ consumed++;
1669
+ if (consumed < fwd.length && fwd[consumed] === '\n')
1670
+ consumed++;
1671
+ extraRows++;
1672
+ continue;
1673
+ }
1674
+ break;
1675
+ }
1676
+ }
1677
+ // Now check if the next non-comment line starts with `:`.
1678
+ let hasValue = false;
1679
+ let valConsumed = consumed;
1680
+ {
1681
+ let ci = consumed;
1682
+ // Skip leading spaces on the next line.
1683
+ while (ci < fwd.length && fwd[ci] === ' ')
1684
+ ci++;
1685
+ if (ci < fwd.length && fwd[ci] === ':' &&
1686
+ (fwd[ci + 1] === ' ' || fwd[ci + 1] === '\t' || fwd[ci + 1] === '\n' ||
1687
+ fwd[ci + 1] === '\r' || fwd[ci + 1] === undefined)) {
1688
+ // Found `: ` — this key has a value.
1689
+ hasValue = true;
1690
+ valConsumed = ci + 1;
1691
+ if (fwd[valConsumed] === ' ' || fwd[valConsumed] === '\t')
1692
+ valConsumed++;
1693
+ }
1694
+ }
1695
+ let src = fwd.substring(0, hasValue ? consumed : keyEnd);
1696
+ if (hasValue) {
1697
+ pnt.sI += valConsumed;
1698
+ pnt.rI += 1 + extraRows;
1699
+ let indent = valConsumed - consumed;
1700
+ pnt.cI = indent + 1;
1701
+ // Check if there's inline content after `: ` on the same line
1702
+ // that looks like a block mapping or sequence (needs #IN context).
1703
+ let nextCh = fwd[valConsumed];
1704
+ let hasInlineContent = nextCh !== undefined &&
1705
+ nextCh !== '\n' && nextCh !== '\r';
1706
+ let needsIndent = false;
1707
+ if (hasInlineContent) {
1708
+ let isQuotedOrFlowOrTag = nextCh === '"' || nextCh === "'" ||
1709
+ nextCh === '[' || nextCh === '{' || nextCh === '!';
1710
+ if (!isQuotedOrFlowOrTag) {
1711
+ // Scan line for mapping key indicator (`: ` or `:` at EOL).
1712
+ let le = valConsumed;
1713
+ while (le < fwd.length && fwd[le] !== '\n' && fwd[le] !== '\r')
1714
+ le++;
1715
+ for (let ri = valConsumed; ri < le; ri++) {
1716
+ if (fwd[ri] === ':') {
1717
+ let nc = fwd[ri + 1];
1718
+ if (nc === ' ' || nc === '\t' || nc === '\n' ||
1719
+ nc === '\r' || nc === undefined || ri + 1 === le) {
1720
+ needsIndent = true;
1721
+ break;
1722
+ }
1723
+ }
1724
+ }
1725
+ // Also check for sequence indicator (`- `).
1726
+ if (!needsIndent && nextCh === '-' &&
1727
+ (fwd[valConsumed + 1] === ' ' || fwd[valConsumed + 1] === '\t')) {
1728
+ needsIndent = true;
1729
+ }
1730
+ }
1731
+ }
1732
+ if (needsIndent) {
1733
+ // Block mapping/sequence inline (e.g., `: get:\n v: 1`).
1734
+ // Emit CL then IN to establish indent context.
1735
+ let clTkn = lex.token('#CL', 1, ': ', lex.pnt);
1736
+ let inTkn = lex.token('#IN', indent, '', lex.pnt);
1737
+ pendingTokens.push(clTkn, inTkn);
1738
+ }
1739
+ else {
1740
+ // Simple scalar or value on next line.
1741
+ // Just emit CL; the newline handler will emit IN if needed.
1742
+ pendingExplicitCL = true;
1743
+ }
1744
+ }
1745
+ else {
1746
+ // No `:` follows — don't consume past newline so the
1747
+ // normal newline→#IN handler can emit indent for map continuation.
1748
+ pnt.sI += beforeNewline;
1749
+ pnt.cI += beforeNewline;
1750
+ // Emit KEY, CL, null as queued tokens.
1751
+ let clTkn = lex.token('#CL', 1, ': ', lex.pnt);
1752
+ let vlTkn = lex.token('#VL', null, '', lex.pnt);
1753
+ pendingTokens.push(clTkn, vlTkn);
1754
+ }
1755
+ let tkn = lex.token('#TX', key, src, lex.pnt);
1756
+ return tkn;
1757
+ }
1758
+ // YAML document-frame markers at column 0:
1759
+ // --- → emit #DS (document start)
1760
+ // ... → emit #DE (document end)
1761
+ // The handler also consumes trailing whitespace, optional `#`
1762
+ // comment, and the newline ending the marker line — so the next
1763
+ // matcher call lands directly on the next document's content
1764
+ // (no spurious #IN gets emitted between #DS and the content).
1765
+ // Inline content on the same line as the marker (--- foo) is
1766
+ // left in place for the next call.
1767
+ if ((pnt.sI === 0 || lex.src[pnt.sI - 1] === '\n' ||
1768
+ lex.src[pnt.sI - 1] === '\r') &&
1769
+ ((fwd[0] === '-' && fwd[1] === '-' && fwd[2] === '-' &&
1770
+ (fwd[3] === '\n' || fwd[3] === '\r' ||
1771
+ fwd[3] === ' ' || fwd[3] === '\t' || fwd[3] === undefined)) ||
1772
+ (fwd[0] === '.' && fwd[1] === '.' && fwd[2] === '.' &&
1773
+ (fwd[3] === '\n' || fwd[3] === '\r' ||
1774
+ fwd[3] === ' ' || fwd[3] === '\t' || fwd[3] === undefined)))) {
1775
+ let isEnd = fwd[0] === '.';
1776
+ let pos = 3;
1777
+ while (pos < fwd.length && (fwd[pos] === ' ' || fwd[pos] === '\t'))
1778
+ pos++;
1779
+ let hasInline = pos < fwd.length &&
1780
+ fwd[pos] !== '\n' && fwd[pos] !== '\r' && fwd[pos] !== '#';
1781
+ if (!hasInline) {
1782
+ // Skip a trailing comment, then the line terminator.
1783
+ while (pos < fwd.length && fwd[pos] !== '\n' && fwd[pos] !== '\r')
1784
+ pos++;
1785
+ if (fwd[pos] === '\r')
1786
+ pos++;
1787
+ if (fwd[pos] === '\n') {
1788
+ pos++;
1789
+ pnt.rI++;
1790
+ }
1791
+ pnt.cI = 1; // column 1 at start of next line
1792
+ }
1793
+ else {
1794
+ pnt.cI += pos;
1795
+ }
1796
+ pnt.sI += pos;
1797
+ let tkn = lex.token(isEnd ? '#DE' : '#DS', undefined, fwd.substring(0, 3), lex.pnt);
1798
+ return tkn;
1799
+ }
1800
+ // Non-specific tag.
1801
+ if (fwd[0] === '!' && fwd[1] === ' ') {
1802
+ let valStart = 2;
1803
+ let valEnd = valStart;
1804
+ while (valEnd < fwd.length && fwd[valEnd] !== '\n' && fwd[valEnd] !== '\r')
1805
+ valEnd++;
1806
+ let rawVal = fwd.substring(valStart, valEnd).replace(/\s+$/, '');
1807
+ let src = fwd.substring(0, valEnd);
1808
+ let tkn = lex.token('#TX', rawVal, src, lex.pnt);
1809
+ pnt.sI += valEnd;
1810
+ pnt.cI += valEnd;
1811
+ return tkn;
1812
+ }
1813
+ // Anchor after ---.
1814
+ if (fwd[0] === '&') {
1815
+ let nameEnd = 1;
1816
+ while (nameEnd < fwd.length && fwd[nameEnd] !== ' ' && fwd[nameEnd] !== '\t' &&
1817
+ fwd[nameEnd] !== '\n' && fwd[nameEnd] !== '\r' && fwd[nameEnd] !== ',' &&
1818
+ fwd[nameEnd] !== '{' && fwd[nameEnd] !== '}' && fwd[nameEnd] !== '[' &&
1819
+ fwd[nameEnd] !== ']')
1820
+ nameEnd++;
1821
+ let anchorName = fwd.substring(1, nameEnd);
1822
+ let skip = nameEnd;
1823
+ if (fwd[skip] === ' ')
1824
+ skip++;
1825
+ pnt.sI += skip;
1826
+ pnt.cI += skip;
1827
+ pendingAnchors.push({ name: anchorName, inline: true });
1828
+ fwd = lex.refwd();
1829
+ }
1830
+ // YAML double-quoted string: backslash escapes + multiline folding.
1831
+ if (fwd[0] === '"') {
1832
+ let i = 1;
1833
+ let val = '';
1834
+ let escapedUpTo = 0; // val chars up to this index are from escapes (non-trimmable)
1835
+ while (i < fwd.length && fwd[i] !== '"') {
1836
+ if (fwd[i] === '\\') {
1837
+ i++;
1838
+ let esc = fwd[i];
1839
+ if (esc === 'n') {
1840
+ val += '\n';
1841
+ i++;
1842
+ escapedUpTo = val.length;
1843
+ }
1844
+ else if (esc === 't') {
1845
+ val += '\t';
1846
+ i++;
1847
+ escapedUpTo = val.length;
1848
+ }
1849
+ else if (esc === 'r') {
1850
+ val += '\r';
1851
+ i++;
1852
+ escapedUpTo = val.length;
1853
+ }
1854
+ else if (esc === '"') {
1855
+ val += '"';
1856
+ i++;
1857
+ escapedUpTo = val.length;
1858
+ }
1859
+ else if (esc === '\\') {
1860
+ val += '\\';
1861
+ i++;
1862
+ escapedUpTo = val.length;
1863
+ }
1864
+ else if (esc === '/') {
1865
+ val += '/';
1866
+ i++;
1867
+ escapedUpTo = val.length;
1868
+ }
1869
+ else if (esc === 'b') {
1870
+ val += '\b';
1871
+ i++;
1872
+ escapedUpTo = val.length;
1873
+ }
1874
+ else if (esc === 'f') {
1875
+ val += '\f';
1876
+ i++;
1877
+ escapedUpTo = val.length;
1878
+ }
1879
+ else if (esc === 'a') {
1880
+ val += '\x07';
1881
+ i++;
1882
+ escapedUpTo = val.length;
1883
+ }
1884
+ else if (esc === 'e') {
1885
+ val += '\x1b';
1886
+ i++;
1887
+ escapedUpTo = val.length;
1888
+ }
1889
+ else if (esc === 'v') {
1890
+ val += '\v';
1891
+ i++;
1892
+ escapedUpTo = val.length;
1893
+ }
1894
+ else if (esc === '0') {
1895
+ val += '\0';
1896
+ i++;
1897
+ escapedUpTo = val.length;
1898
+ }
1899
+ else if (esc === '\t') {
1900
+ val += '\t';
1901
+ i++;
1902
+ escapedUpTo = val.length;
1903
+ }
1904
+ else if (esc === ' ') {
1905
+ val += ' ';
1906
+ i++;
1907
+ escapedUpTo = val.length;
1908
+ }
1909
+ else if (esc === '_') {
1910
+ val += '\u00a0';
1911
+ i++;
1912
+ escapedUpTo = val.length;
1913
+ }
1914
+ else if (esc === 'N') {
1915
+ val += '\u0085';
1916
+ i++;
1917
+ escapedUpTo = val.length;
1918
+ }
1919
+ else if (esc === 'L') {
1920
+ val += '\u2028';
1921
+ i++;
1922
+ escapedUpTo = val.length;
1923
+ }
1924
+ else if (esc === 'P') {
1925
+ val += '\u2029';
1926
+ i++;
1927
+ escapedUpTo = val.length;
1928
+ }
1929
+ else if (esc === 'x') {
1930
+ val += String.fromCharCode(parseInt(fwd.substring(i + 1, i + 3), 16));
1931
+ i += 3;
1932
+ escapedUpTo = val.length;
1933
+ }
1934
+ else if (esc === 'u') {
1935
+ val += String.fromCharCode(parseInt(fwd.substring(i + 1, i + 5), 16));
1936
+ i += 5;
1937
+ escapedUpTo = val.length;
1938
+ }
1939
+ else if (esc === 'U') {
1940
+ val += String.fromCodePoint(parseInt(fwd.substring(i + 1, i + 9), 16));
1941
+ i += 9;
1942
+ escapedUpTo = val.length;
1943
+ }
1944
+ else if (esc === '\n' || esc === '\r') {
1945
+ // Escaped newline: line continuation (join directly).
1946
+ if (esc === '\r' && fwd[i + 1] === '\n')
1947
+ i++;
1948
+ i++;
1949
+ // Skip leading whitespace on next line.
1950
+ while (i < fwd.length && (fwd[i] === ' ' || fwd[i] === '\t'))
1951
+ i++;
1952
+ }
1953
+ else {
1954
+ val += esc;
1955
+ i++;
1956
+ }
1957
+ }
1958
+ else if (fwd[i] === '\n' || fwd[i] === '\r') {
1959
+ // Flow scalar line folding for double-quoted strings.
1960
+ // Only trim trailing whitespace that was NOT from escape sequences.
1961
+ let trimTo = val.length;
1962
+ while (trimTo > escapedUpTo && (val[trimTo - 1] === ' ' || val[trimTo - 1] === '\t'))
1963
+ trimTo--;
1964
+ val = val.substring(0, trimTo);
1965
+ let emptyLines = 0;
1966
+ while (i < fwd.length && (fwd[i] === '\n' || fwd[i] === '\r')) {
1967
+ if (fwd[i] === '\r')
1968
+ i++;
1969
+ if (fwd[i] === '\n')
1970
+ i++;
1971
+ emptyLines++;
1972
+ while (i < fwd.length && (fwd[i] === ' ' || fwd[i] === '\t'))
1973
+ i++;
1974
+ }
1975
+ if (emptyLines > 1) {
1976
+ for (let e = 1; e < emptyLines; e++)
1977
+ val += '\n';
1978
+ }
1979
+ else {
1980
+ val += ' ';
1981
+ }
1982
+ }
1983
+ else {
1984
+ val += fwd[i];
1985
+ i++;
1986
+ }
1987
+ }
1988
+ if (fwd[i] === '"')
1989
+ i++;
1990
+ let src = fwd.substring(0, i);
1991
+ let tkn = lex.token('#ST', val, src, lex.pnt);
1992
+ pnt.sI += i;
1993
+ pnt.cI += i;
1994
+ return tkn;
1995
+ }
1996
+ // YAML single-quoted string: no backslash escape processing.
1997
+ // Only escape is '' (two single quotes) → literal single quote.
1998
+ // Newlines are folded: single newline → space, empty lines → \n.
1999
+ if (fwd[0] === "'") {
2000
+ let i = 1;
2001
+ let val = '';
2002
+ while (i < fwd.length) {
2003
+ if (fwd[i] === "'") {
2004
+ if (fwd[i + 1] === "'") {
2005
+ // Escaped single quote.
2006
+ val += "'";
2007
+ i += 2;
2008
+ }
2009
+ else {
2010
+ // End of string.
2011
+ i++;
2012
+ break;
2013
+ }
2014
+ }
2015
+ else if (fwd[i] === '\n' || fwd[i] === '\r') {
2016
+ // Flow scalar line folding.
2017
+ // Trim trailing whitespace from current content.
2018
+ val = val.replace(/[ \t]+$/, '');
2019
+ // Count empty lines (newlines with only whitespace).
2020
+ let emptyLines = 0;
2021
+ while (i < fwd.length && (fwd[i] === '\n' || fwd[i] === '\r')) {
2022
+ if (fwd[i] === '\r')
2023
+ i++;
2024
+ if (fwd[i] === '\n')
2025
+ i++;
2026
+ emptyLines++;
2027
+ // Skip leading whitespace on next line.
2028
+ while (i < fwd.length && (fwd[i] === ' ' || fwd[i] === '\t'))
2029
+ i++;
2030
+ }
2031
+ if (emptyLines > 1) {
2032
+ // Each extra empty line becomes a \n.
2033
+ for (let e = 1; e < emptyLines; e++)
2034
+ val += '\n';
2035
+ }
2036
+ else {
2037
+ // Single newline → space (folding).
2038
+ val += ' ';
2039
+ }
2040
+ }
2041
+ else {
2042
+ val += fwd[i];
2043
+ i++;
2044
+ }
2045
+ }
2046
+ let src = fwd.substring(0, i);
2047
+ let tkn = lex.token('#ST', val, src, lex.pnt);
2048
+ pnt.sI += i;
2049
+ pnt.cI += i;
2050
+ return tkn;
2051
+ }
2052
+ // Plain scalars starting with digits but containing colons (e.g. 20:03:20),
2053
+ // trailing commas (e.g. 12,), or non-numeric text after a space
2054
+ // (e.g. "64 characters, hexadecimal.") must be captured before
2055
+ // jsonic's number matcher grabs just the digits.
2056
+ if (fwd[0] >= '0' && fwd[0] <= '9') {
2057
+ updateFlowState(lex.src, pnt.sI);
2058
+ let inFlow = _flowDepth > 0;
2059
+ let hasEmbeddedColon = false;
2060
+ let hasTrailingText = false;
2061
+ let hasTrailingComma = false;
2062
+ let pi = 1;
2063
+ while (pi < fwd.length && fwd[pi] !== '\n' && fwd[pi] !== '\r') {
2064
+ if (fwd[pi] === ':' && fwd[pi + 1] !== ' ' && fwd[pi + 1] !== '\t' &&
2065
+ fwd[pi + 1] !== '\n' && fwd[pi + 1] !== '\r' && fwd[pi + 1] !== undefined) {
2066
+ hasEmbeddedColon = true;
2067
+ break;
2068
+ }
2069
+ // Trailing comma at end of line means plain scalar in block
2070
+ // context (e.g. "12,"). In flow context commas are always
2071
+ // separators, so don't treat the digits as a plain scalar.
2072
+ if (fwd[pi] === ',') {
2073
+ let ci = pi + 1;
2074
+ while (ci < fwd.length && (fwd[ci] === ' ' || fwd[ci] === '\t'))
2075
+ ci++;
2076
+ if (!inFlow && (ci >= fwd.length || fwd[ci] === '\n' || fwd[ci] === '\r')) {
2077
+ hasTrailingComma = true;
2078
+ }
2079
+ break;
2080
+ }
2081
+ if (fwd[pi] === ' ' || fwd[pi] === '\t') {
2082
+ // Check if after the space there are non-separator characters,
2083
+ // meaning this is a plain scalar like "64 characters, hexadecimal."
2084
+ // not a standalone number.
2085
+ let si = pi;
2086
+ while (si < fwd.length && (fwd[si] === ' ' || fwd[si] === '\t'))
2087
+ si++;
2088
+ if (si < fwd.length && fwd[si] !== '\n' && fwd[si] !== '\r' &&
2089
+ fwd[si] !== '#' && fwd[si] !== ':' && fwd[si] !== undefined) {
2090
+ // Check it's not ": " (key-value separator).
2091
+ if (!(fwd[si] === ':' && (fwd[si + 1] === ' ' || fwd[si + 1] === '\t' ||
2092
+ fwd[si + 1] === '\n' || fwd[si + 1] === '\r' || fwd[si + 1] === undefined))) {
2093
+ hasTrailingText = true;
2094
+ }
2095
+ }
2096
+ break;
2097
+ }
2098
+ pi++;
2099
+ }
2100
+ if (hasEmbeddedColon || hasTrailingComma) {
2101
+ // Scan to end of plain scalar token (space, tab, newline, eof).
2102
+ let end = 0;
2103
+ while (end < fwd.length && fwd[end] !== ' ' && fwd[end] !== '\t' &&
2104
+ fwd[end] !== '\n' && fwd[end] !== '\r')
2105
+ end++;
2106
+ let text = fwd.substring(0, end);
2107
+ let tkn = lex.token('#TX', text, text, lex.pnt);
2108
+ pnt.sI += end;
2109
+ pnt.cI += end;
2110
+ return tkn;
2111
+ }
2112
+ if (hasTrailingText) {
2113
+ // Flag that the number matcher should skip this value,
2114
+ // so the text.check handler can process it as a plain
2115
+ // scalar (including multiline continuation support).
2116
+ skipNumberMatch = true;
2117
+ return null;
2118
+ }
2119
+ }
2120
+ // YAML element marker: "- " or "-\t" or "-\n" or "-" at end.
2121
+ if (fwd[0] === '-' && (fwd[1] === ' ' || fwd[1] === '\t' || fwd[1] === '\n' ||
2122
+ fwd[1] === '\r' || fwd[1] === undefined)) {
2123
+ let tkn = lex.token('#EL', undefined, '- ', lex.pnt);
2124
+ pnt.sI += 1;
2125
+ pnt.cI += 1;
2126
+ // Consume the space/tab after dash if present.
2127
+ if (fwd[1] === ' ' || fwd[1] === '\t') {
2128
+ pnt.sI += 1;
2129
+ pnt.cI += 1;
2130
+ }
2131
+ return tkn;
2132
+ }
2133
+ // Yaml colons are ': ', ':\t', ':<newline>', ':' at end of input,
2134
+ // or ':' in flow context (JSON-compatible, e.g. {"key":value}).
2135
+ // In flow context, detect by checking if the previous non-whitespace
2136
+ // token was a quoted string followed immediately by ':'.
2137
+ let isFlowColon = false;
2138
+ if (fwd[0] === ':' && fwd[1] !== ' ' && fwd[1] !== '\t' &&
2139
+ fwd[1] !== '\n' && fwd[1] !== '\r' && fwd[1] !== undefined) {
2140
+ // JSON-compatible flow colon: only when preceded by a quoted string.
2141
+ // Skip whitespace/newlines and any intervening line comments
2142
+ // (`# ...` to end-of-line) so e.g. `"foo" # c\n :bar` works.
2143
+ let prevI = pnt.sI - 1;
2144
+ while (prevI >= 0) {
2145
+ let pc = lex.src[prevI];
2146
+ if (pc === ' ' || pc === '\t' || pc === '\n' || pc === '\r') {
2147
+ prevI--;
2148
+ continue;
2149
+ }
2150
+ // If on a line whose `#` is preceded by whitespace, that's a
2151
+ // comment — jump to before the `#` and keep walking back.
2152
+ let lineStart = prevI;
2153
+ while (lineStart > 0 && lex.src[lineStart - 1] !== '\n' &&
2154
+ lex.src[lineStart - 1] !== '\r')
2155
+ lineStart--;
2156
+ let hashAt = -1;
2157
+ for (let li = lineStart; li <= prevI; li++) {
2158
+ if (lex.src[li] === '#' &&
2159
+ (li === lineStart || lex.src[li - 1] === ' ' ||
2160
+ lex.src[li - 1] === '\t')) {
2161
+ hashAt = li;
2162
+ break;
2163
+ }
2164
+ }
2165
+ if (hashAt >= 0) {
2166
+ prevI = hashAt - 1;
2167
+ continue;
2168
+ }
2169
+ break;
2170
+ }
2171
+ if (prevI >= 0 && (lex.src[prevI] === '"' || lex.src[prevI] === "'")) {
2172
+ isFlowColon = true;
2173
+ }
2174
+ }
2175
+ if (fwd[0] === ':' && (fwd[1] === ' ' || fwd[1] === '\t' || fwd[1] === '\n' ||
2176
+ fwd[1] === '\r' || fwd[1] === undefined || isFlowColon)) {
2177
+ let tkn = lex.token('#CL', 1, ': ', lex.pnt);
2178
+ pnt.sI += 1;
2179
+ if (fwd[1] === ' ' || fwd[1] === '\t') {
2180
+ pnt.cI += 2;
2181
+ }
2182
+ else if (fwd[1] === '\n' || fwd[1] === '\r') {
2183
+ // Don't consume newline — leave for #IN.
2184
+ }
2185
+ else {
2186
+ // End of input after colon.
2187
+ pnt.cI += 1;
2188
+ }
2189
+ return tkn;
2190
+ }
2191
+ // Match any newline — YAML indentation is significant.
2192
+ // In flow context, newlines are just whitespace — don't emit #IN.
2193
+ if (fwd[0] === '\n' || fwd[0] === '\r') {
2194
+ updateFlowState(lex.src, pnt.sI);
2195
+ if (_flowDepth > 0) {
2196
+ // Inside flow collection — consume whitespace, don't emit #IN.
2197
+ let pos = 0;
2198
+ while (pos < fwd.length &&
2199
+ (fwd[pos] === '\n' || fwd[pos] === '\r' || fwd[pos] === ' ' || fwd[pos] === '\t')) {
2200
+ pos++;
2201
+ }
2202
+ // Also skip comment lines inside flow collections.
2203
+ if (pos < fwd.length && fwd[pos] === '#') {
2204
+ while (pos < fwd.length && fwd[pos] !== '\n' && fwd[pos] !== '\r')
2205
+ pos++;
2206
+ }
2207
+ pnt.sI += pos;
2208
+ pnt.cI = 0;
2209
+ // Re-run yamlMatcher from new position.
2210
+ fwd = lex.refwd();
2211
+ continue yamlMatchLoop;
2212
+ }
2213
+ }
2214
+ // Must catch all newlines before the default line/space matchers.
2215
+ if (fwd[0] === '\n' || fwd[0] === '\r') {
2216
+ // Consume all blank lines and comment-only lines,
2217
+ // finding the last meaningful indent.
2218
+ let pos = 0;
2219
+ let spaces = 0;
2220
+ let rows = 0;
2221
+ while (pos < fwd.length) {
2222
+ // Match \r\n or \n
2223
+ if (fwd[pos] === '\r' && fwd[pos + 1] === '\n') {
2224
+ pos += 2;
2225
+ rows++;
2226
+ }
2227
+ else if (fwd[pos] === '\n') {
2228
+ pos += 1;
2229
+ rows++;
2230
+ }
2231
+ else {
2232
+ break;
2233
+ }
2234
+ // Count spaces after this newline.
2235
+ spaces = 0;
2236
+ while (pos < fwd.length && fwd[pos] === ' ') {
2237
+ pos++;
2238
+ spaces++;
2239
+ }
2240
+ // If the line is a comment-only line, consume it too.
2241
+ if (fwd[pos] === '#') {
2242
+ while (pos < fwd.length && fwd[pos] !== '\n' && fwd[pos] !== '\r')
2243
+ pos++;
2244
+ continue;
2245
+ }
2246
+ // If the line is whitespace-only (tabs and/or spaces),
2247
+ // treat it as a blank line and continue.
2248
+ if (fwd[pos] === '\t') {
2249
+ let tp = pos;
2250
+ while (tp < fwd.length && (fwd[tp] === ' ' || fwd[tp] === '\t'))
2251
+ tp++;
2252
+ if (tp >= fwd.length || fwd[tp] === '\n' || fwd[tp] === '\r') {
2253
+ pos = tp;
2254
+ continue;
2255
+ }
2256
+ }
2257
+ // If the line is an anchor-only line (&name with nothing after),
2258
+ // consume it (record the anchor) and continue to the next line
2259
+ // so the indent is determined by actual content.
2260
+ if (fwd[pos] === '&') {
2261
+ let ae = pos + 1;
2262
+ while (ae < fwd.length && fwd[ae] !== ' ' && fwd[ae] !== '\t' &&
2263
+ fwd[ae] !== '\n' && fwd[ae] !== '\r')
2264
+ ae++;
2265
+ let afterAnchor = ae;
2266
+ while (afterAnchor < fwd.length &&
2267
+ (fwd[afterAnchor] === ' ' || fwd[afterAnchor] === '\t'))
2268
+ afterAnchor++;
2269
+ if (afterAnchor >= fwd.length || fwd[afterAnchor] === '\n' ||
2270
+ fwd[afterAnchor] === '\r' || fwd[afterAnchor] === '#') {
2271
+ pendingAnchors.push({ name: fwd.substring(pos + 1, ae), inline: false });
2272
+ // Skip to end of line (including any comment).
2273
+ while (afterAnchor < fwd.length &&
2274
+ fwd[afterAnchor] !== '\n' && fwd[afterAnchor] !== '\r')
2275
+ afterAnchor++;
2276
+ pos = afterAnchor;
2277
+ continue;
2278
+ }
2279
+ }
2280
+ }
2281
+ // If we consumed everything (trailing newlines), advance and emit #ZZ.
2282
+ if (pos >= fwd.length) {
2283
+ pnt.sI += pos;
2284
+ pnt.rI += rows;
2285
+ pnt.cI = spaces + 1;
2286
+ let tkn = lex.token('#ZZ', undefined, '', lex.pnt);
2287
+ pnt.end = tkn;
2288
+ return tkn;
2289
+ }
2290
+ // If the next line is a document-frame marker (--- / ...) at
2291
+ // column 0, advance to it without emitting #IN. The next
2292
+ // matcher call will emit the corresponding #DS / #DE token.
2293
+ if (spaces === 0 &&
2294
+ ((fwd[pos] === '-' && fwd[pos + 1] === '-' && fwd[pos + 2] === '-' &&
2295
+ (fwd[pos + 3] === '\n' || fwd[pos + 3] === '\r' ||
2296
+ fwd[pos + 3] === ' ' || fwd[pos + 3] === '\t' ||
2297
+ fwd[pos + 3] === undefined)) ||
2298
+ (fwd[pos] === '.' && fwd[pos + 1] === '.' && fwd[pos + 2] === '.' &&
2299
+ (fwd[pos + 3] === '\n' || fwd[pos + 3] === '\r' ||
2300
+ fwd[pos + 3] === ' ' || fwd[pos + 3] === '\t' ||
2301
+ fwd[pos + 3] === undefined)))) {
2302
+ pnt.sI += pos;
2303
+ pnt.rI += rows;
2304
+ pnt.cI = 0;
2305
+ fwd = lex.refwd();
2306
+ continue yamlMatchLoop;
2307
+ }
2308
+ // Likewise if the next line is a directive (%YAML / %TAG):
2309
+ // advance and let the next call emit #DR.
2310
+ if (spaces === 0 && fwd[pos] === '%') {
2311
+ pnt.sI += pos;
2312
+ pnt.rI += rows;
2313
+ pnt.cI = 0;
2314
+ fwd = lex.refwd();
2315
+ continue yamlMatchLoop;
2316
+ }
2317
+ // Skip #IN when the next content is a flow indicator or quoted
2318
+ // string at column 0 — there's no block to indent into. Match
2319
+ // the previous behavior of the inline `--- foo` handler.
2320
+ if (spaces === 0 &&
2321
+ (fwd[pos] === '{' || fwd[pos] === '[' ||
2322
+ fwd[pos] === '"' || fwd[pos] === "'")) {
2323
+ pnt.sI += pos;
2324
+ pnt.rI += rows;
2325
+ pnt.cI = 0;
2326
+ fwd = lex.refwd();
2327
+ continue yamlMatchLoop;
2328
+ }
2329
+ // Emit #IN with val = indent level of the last non-blank line.
2330
+ let src = fwd.substring(0, pos);
2331
+ let tkn = lex.token('#IN', spaces, src, lex.pnt);
2332
+ pnt.sI += pos;
2333
+ pnt.rI += rows;
2334
+ pnt.cI = spaces + 1;
2335
+ return tkn;
2336
+ }
2337
+ break; // End of yamlMatchLoop
2338
+ } // end while(true) yamlMatchLoop
2339
+ };
2340
+ }
2341
+ }
2342
+ }
2343
+ }
2344
+ });
2345
+ // Extract a key value from a token, resolving aliases.
2346
+ function extractKey(rule, tkn = rule.o0) {
2347
+ if (VL === tkn.tin && tkn.val && typeof tkn.val === 'object' && tkn.val.__yamlAlias) {
2348
+ // Alias used as key — resolve to anchor value.
2349
+ let name = tkn.val.__yamlAlias;
2350
+ return anchors[name] !== undefined ? anchors[name] : '*' + name;
2351
+ }
2352
+ return ST === tkn.tin || TX === tkn.tin ? tkn.val : tkn.src;
2353
+ }
2354
+ // Function refs used by the declarative grammar (yaml-grammar.jsonic).
2355
+ const refs = {
2356
+ '@val-indent-deeper': (rule, ctx) => {
2357
+ let parentIn = rule.k.yamlIn;
2358
+ let listIn = rule.k.yamlListIn;
2359
+ if (listIn != null && ctx.t0.val <= listIn)
2360
+ return false;
2361
+ return parentIn == null || ctx.t0.val > parentIn;
2362
+ },
2363
+ '@val-indent-eq-parent': (rule, ctx) => {
2364
+ let parentIn = rule.k.yamlIn;
2365
+ return parentIn != null && ctx.t0.val === parentIn;
2366
+ },
2367
+ '@val-set-in-from-o0': (rule) => { rule.n.in = rule.o0.val; },
2368
+ '@val-set-null': (rule) => { rule.node = null; },
2369
+ '@val-set-el-in': (rule) => { rule.n.in = rule.o0.cI - 1; },
2370
+ '@indent-plain-value': (rule) => {
2371
+ rule.node = ST === rule.o0.tin || TX === rule.o0.tin
2372
+ ? rule.o0.val : rule.o0.src;
2373
+ },
2374
+ '@set-map-in': (rule) => { rule.k.yamlMapIn = rule.n.in + 2; },
2375
+ '@t0-eq-in': (rule, ctx) => ctx.t0.val === rule.n.in,
2376
+ '@t0-le-in': (rule, ctx) => ctx.t0.val <= rule.n.in,
2377
+ '@t0-lt-in': (rule, ctx) => ctx.t0.val < rule.n.in,
2378
+ '@o0-eq-in': (rule) => rule.o0.val === rule.n.in,
2379
+ '@t0-eq-map-in': (rule, ctx) => ctx.t0.val === rule.k.yamlMapIn,
2380
+ '@elem-key': (rule) => { rule.u.key = extractKey(rule); },
2381
+ '@implicit-null-pair': (rule) => {
2382
+ let key = extractKey(rule);
2383
+ rule.u.key = key;
2384
+ rule.node[key] = null;
2385
+ },
2386
+ // Same as @pairkey, but the KEY is at o1 (after the leading #QM).
2387
+ '@qm-pairkey': (rule) => { rule.u.key = extractKey(rule, rule.o1); },
2388
+ '@qm-implicit-null-pair': (rule) => {
2389
+ let key = extractKey(rule, rule.o1);
2390
+ rule.u.key = key;
2391
+ rule.node[key] = null;
2392
+ },
2393
+ };
2394
+ // Parse the embedded grammar text and install declarative rules.
2395
+ const grammarDef = new parser_1.Tabnas().use(jsonic_1.jsonic).parse(grammarText);
2396
+ grammarDef.ref = refs;
2397
+ tabnas.grammar(grammarDef);
2398
+ // ===== State handlers (bo/ao/bc/ac) — kept in code for closure capture =====
2399
+ // val rule: claim pending anchors (ao), handle empty (bc),
2400
+ // resolve aliases and record anchors (ac).
2401
+ tabnas.rule('val', (rulespec) => {
2402
+ rulespec.ao((rule) => {
2403
+ if (pendingAnchors.length > 0) {
2404
+ rule.u.yamlAnchors = [...pendingAnchors];
2405
+ rule.u.yamlAnchorOpenNode = rule.node;
2406
+ pendingAnchors.length = 0;
2407
+ }
2408
+ });
2409
+ rulespec.bc((rule) => {
2410
+ if (rule.u.yamlEmpty) {
2411
+ rule.node = undefined;
2412
+ }
2413
+ });
2414
+ rulespec.ac((rule) => {
2415
+ // Resolve alias markers to actual values.
2416
+ if (rule.node && typeof rule.node === 'object' &&
2417
+ rule.node.__yamlAlias) {
2418
+ let name = rule.node.__yamlAlias;
2419
+ let val = anchors[name];
2420
+ if (typeof val === 'object' && val !== null) {
2421
+ rule.node = JSON.parse(JSON.stringify(val));
2422
+ }
2423
+ else {
2424
+ rule.node = val;
2425
+ }
2426
+ }
2427
+ // Record anchors only if this val claimed them.
2428
+ if (rule.u.yamlAnchors) {
2429
+ for (let anchor of rule.u.yamlAnchors) {
2430
+ if (anchor.inline &&
2431
+ rule.u.yamlAnchorOpenNode != null &&
2432
+ typeof rule.u.yamlAnchorOpenNode !== 'object' &&
2433
+ typeof rule.node === 'object' && rule.node !== null) {
2434
+ continue;
2435
+ }
2436
+ let val = rule.node;
2437
+ if (typeof val === 'object' && val !== null) {
2438
+ val = JSON.parse(JSON.stringify(val));
2439
+ }
2440
+ anchors[anchor.name] = val;
2441
+ }
2442
+ }
2443
+ });
2444
+ });
2445
+ // indent rule: propagate child node up on close.
2446
+ tabnas.rule('indent', (rulespec) => {
2447
+ rulespec.bc((rule) => {
2448
+ if (undefined !== rule.child.node) {
2449
+ rule.node = rule.child.node;
2450
+ }
2451
+ });
2452
+ });
2453
+ // yamlBlockList rule: init array and push child nodes.
2454
+ tabnas.rule('yamlBlockList', (rulespec) => {
2455
+ rulespec.bo((rule) => {
2456
+ rule.node = [];
2457
+ rule.k.yamlBlockArr = rule.node;
2458
+ rule.k.yamlListIn = rule.n.in;
2459
+ });
2460
+ rulespec.bc((rule) => {
2461
+ let val = rule.child.node !== undefined ? rule.child.node : null;
2462
+ rule.k.yamlBlockArr.push(val);
2463
+ });
2464
+ });
2465
+ // yamlBlockElem rule: reuse shared array, push child nodes.
2466
+ tabnas.rule('yamlBlockElem', (rulespec) => {
2467
+ rulespec.bo((rule) => {
2468
+ rule.node = rule.k.yamlBlockArr;
2469
+ });
2470
+ rulespec.bc((rule) => {
2471
+ let val = rule.child.node !== undefined ? rule.child.node : null;
2472
+ rule.k.yamlBlockArr.push(val);
2473
+ });
2474
+ });
2475
+ // list rule: propagate list indent so val can check nesting depth, and
2476
+ // OWN the node-append phase for YAML block sequences.
2477
+ //
2478
+ // jsonic's @list-bo only allocates the array when the list is explicit
2479
+ // (`[` -> @array$) or a top-level implicit comma/space list
2480
+ // (prev.u.implist). A YAML block sequence reaches `list` a third way —
2481
+ // the indent rule's `#EL` alt does `p: list` with no `#OS` and no
2482
+ // implist — so neither builder runs and r.node stays the inherited
2483
+ // parent container (a map/pair value, or undefined). jsonic's
2484
+ // @elem-bc/replace then does a bare r.node.push(...) and throws
2485
+ // ("r.node.push is not a function") / silently drops elements in Go.
2486
+ // Allocate the array here so the push lands in a real list. For a flow
2487
+ // `[...]` list the subsequent `#OS` open alt's @array$ re-allocates an
2488
+ // (info-marked) array before any element rule is pushed, so this is a
2489
+ // harmless pre-seed in that case.
2490
+ tabnas.rule('list', (rulespec) => {
2491
+ rulespec.bo((rule) => {
2492
+ rule.k.yamlListIn = rule.n.in;
2493
+ // OWN the node-append phase for an indented YAML block sequence.
2494
+ //
2495
+ // jsonic's @array$ only allocates the list's array on the flow `[`
2496
+ // (`#OS`) open alt; @list-bo only allocates for a top-level implicit
2497
+ // comma/space list (prev.u.implist). A block sequence nested deeper
2498
+ // than its map key reaches `list` a third way — the indent rule's
2499
+ // `#EL` alt does `p: list` with no `#OS` — so neither builder runs
2500
+ // and r.node stays the inherited parent container (the map). jsonic's
2501
+ // @elem-bc/replace then does a bare r.node.push(...) on that map and
2502
+ // throws ("r.node.push is not a function") in TS / silently drops
2503
+ // every element in Go. Allocate the array here so the push lands in a
2504
+ // real list. Only the indent path needs this: a flow `[...]` list is
2505
+ // pushed by `val` (parent=val) and gets its array from @array$, so it
2506
+ // is left untouched.
2507
+ if (rule.parent && 'indent' === rule.parent.name) {
2508
+ rule.node = [];
2509
+ }
2510
+ });
2511
+ });
2512
+ // ===== stream rule: top-level YAML document collector =====
2513
+ // The stream rule replaces `val` as the parser's start rule. It consumes
2514
+ // doc-frame tokens (#DS, #DE, #DR) emitted by yamlMatcher, pushes a fresh
2515
+ // `val` rule for each document's content, and accumulates the results.
2516
+ // Final shape:
2517
+ // - 0 docs (empty source) → undefined
2518
+ // - 1 doc → the single value
2519
+ // - >1 docs → array of values
2520
+ const ensureCurMeta = () => {
2521
+ if (!yamlStreamCurMeta) {
2522
+ yamlStreamCurMeta = { directives: [], explicit: false, ended: false };
2523
+ }
2524
+ };
2525
+ const flushCurMeta = (ended) => {
2526
+ ensureCurMeta();
2527
+ yamlStreamCurMeta.ended = ended || yamlStreamCurMeta.ended;
2528
+ yamlStreamMeta.push(yamlStreamCurMeta);
2529
+ yamlStreamCurMeta = null;
2530
+ };
2531
+ const accumChildDoc = (rule) => {
2532
+ if (rule.child && rule.child.node !== undefined) {
2533
+ yamlStreamDocs.push(rule.child.node);
2534
+ }
2535
+ else {
2536
+ yamlStreamDocs.push(null);
2537
+ }
2538
+ // The matched close-phase token tells us whether this doc ended
2539
+ // explicitly with `...`.
2540
+ flushCurMeta(rule.c0 != null && rule.c0.tin === DE);
2541
+ };
2542
+ const finalizeStream = (rule, ctx) => {
2543
+ if (rule.child && rule.child.node !== undefined) {
2544
+ yamlStreamDocs.push(rule.child.node);
2545
+ flushCurMeta(false);
2546
+ }
2547
+ else if (yamlStreamCurMeta != null) {
2548
+ // The final document was explicitly opened (a `---` / `%TAG`
2549
+ // directive started a doc, recorded in yamlStreamCurMeta) but its
2550
+ // value coalesced to undefined — a bare `---` at end-of-stream, or a
2551
+ // trailing empty doc in `---\n---\n---`. jsonic's val-close treats a
2552
+ // deliberate `@val-set-null` as undefined (typeof null === 'object'
2553
+ // fails its primitive-value check), so the empty doc's null is lost
2554
+ // here; restore it the same way accumChildDoc / pushEmptyDoc force a
2555
+ // null for the non-final empty docs. Without an open doc (empty or
2556
+ // comment-only source) yamlStreamCurMeta stays null and the stream
2557
+ // correctly finalizes to undefined.
2558
+ yamlStreamDocs.push(null);
2559
+ flushCurMeta(false);
2560
+ }
2561
+ let content;
2562
+ if (yamlStreamDocs.length === 0) {
2563
+ content = undefined;
2564
+ }
2565
+ else if (yamlStreamDocs.length === 1) {
2566
+ content = yamlStreamDocs[0];
2567
+ }
2568
+ else {
2569
+ content = yamlStreamDocs.slice();
2570
+ }
2571
+ let result = content;
2572
+ if (options.meta) {
2573
+ let meta;
2574
+ if (yamlStreamMeta.length === 0) {
2575
+ meta = undefined;
2576
+ }
2577
+ else if (yamlStreamMeta.length === 1) {
2578
+ meta = yamlStreamMeta[0];
2579
+ }
2580
+ else {
2581
+ meta = yamlStreamMeta.slice();
2582
+ }
2583
+ result = { meta, content };
2584
+ }
2585
+ rule.node = result;
2586
+ // Rotation via `r: stream` creates a chain; ctx.root() is the original
2587
+ // stream the parser hands back. Write the result there.
2588
+ ctx.root().node = result;
2589
+ };
2590
+ const applyDirective = (rule) => {
2591
+ let src = rule.o0.src || '';
2592
+ let m = src.match(/^%TAG\s+(\S+)\s+(\S+)/);
2593
+ if (m)
2594
+ tagHandles[m[1]] = m[2];
2595
+ ensureCurMeta();
2596
+ yamlStreamCurMeta.directives.push(src);
2597
+ };
2598
+ const markExplicit = (_rule) => {
2599
+ ensureCurMeta();
2600
+ yamlStreamCurMeta.explicit = true;
2601
+ };
2602
+ const pushEmptyDoc = (_rule) => {
2603
+ yamlStreamDocs.push(null);
2604
+ flushCurMeta(true);
2605
+ };
2606
+ tabnas.rule('stream', (rs) => {
2607
+ rs.open([
2608
+ // Consume directive line; rotate to stream to look for the next token.
2609
+ { s: '#DR', a: applyDirective, r: 'stream', g: 'yaml' },
2610
+ // Explicit doc start: push val for the document content.
2611
+ { s: '#DS', a: markExplicit, p: 'val', g: 'yaml' },
2612
+ // ... before any content: count as empty doc, look for more.
2613
+ { s: '#DE', a: pushEmptyDoc, r: 'stream', g: 'yaml' },
2614
+ // Empty source: end immediately (stream.close will run).
2615
+ { s: '#ZZ', b: 1, g: 'yaml' },
2616
+ // Implicit first doc.
2617
+ { p: 'val', g: 'yaml' },
2618
+ ]);
2619
+ rs.close([
2620
+ // End of input: accumulate last doc, finalize result shape.
2621
+ { s: '#ZZ', a: finalizeStream, g: 'yaml' },
2622
+ // Directive between docs: accumulate previous doc, apply, continue.
2623
+ { s: '#DR', a: (r) => { accumChildDoc(r); applyDirective(r); },
2624
+ r: 'stream', g: 'yaml' },
2625
+ // ... terminator: accumulate, look for next doc.
2626
+ { s: '#DE', a: accumChildDoc, r: 'stream', g: 'yaml' },
2627
+ // --- start of next doc (back up so stream.open consumes it).
2628
+ { s: '#DS', b: 1, a: accumChildDoc, r: 'stream', g: 'yaml' },
2629
+ ]);
2630
+ });
2631
+ // Configure jsonic to start parsing with `stream` instead of `val`.
2632
+ tabnas.options({ rule: { start: 'stream' } });
2633
+ // map rule: default indent and merge-key handling.
2634
+ tabnas.rule('map', (rulespec) => {
2635
+ rulespec.bo((rule) => {
2636
+ if (null == rule.n.in) {
2637
+ rule.n.in = 0;
2638
+ }
2639
+ rule.k.yamlIn = rule.n.in;
2640
+ });
2641
+ rulespec.ac((rule) => {
2642
+ if (rule.node && typeof rule.node === 'object' && '<<' in rule.node) {
2643
+ let mergeVal = rule.node['<<'];
2644
+ delete rule.node['<<'];
2645
+ if (Array.isArray(mergeVal)) {
2646
+ for (let m of mergeVal) {
2647
+ if (typeof m === 'object' && m !== null && !Array.isArray(m)) {
2648
+ for (let k of Object.keys(m)) {
2649
+ if (!(k in rule.node))
2650
+ rule.node[k] = m[k];
2651
+ }
2652
+ }
2653
+ }
2654
+ }
2655
+ else if (typeof mergeVal === 'object' && mergeVal !== null) {
2656
+ for (let k of Object.keys(mergeVal)) {
2657
+ if (!(k in rule.node))
2658
+ rule.node[k] = mergeVal[k];
2659
+ }
2660
+ }
2661
+ }
2662
+ });
2663
+ });
2664
+ // yamlElemMap rule: init map and store pairs.
2665
+ tabnas.rule('yamlElemMap', (rulespec) => {
2666
+ rulespec.bo((rule) => {
2667
+ rule.node = Object.create(null);
2668
+ });
2669
+ rulespec.bc((rule) => {
2670
+ if (rule.u.key != null) {
2671
+ rule.node[rule.u.key] = rule.child.node;
2672
+ }
2673
+ });
2674
+ });
2675
+ // yamlElemPair rule: store pair into shared map node.
2676
+ tabnas.rule('yamlElemPair', (rulespec) => {
2677
+ rulespec.bc((rule) => {
2678
+ if (rule.u.key != null) {
2679
+ rule.node[rule.u.key] = rule.child.node;
2680
+ }
2681
+ });
2682
+ });
2683
+ };
2684
+ exports.Yaml = Yaml;
2685
+ Yaml.defaults = {
2686
+ meta: false,
2687
+ };
2688
+ //# sourceMappingURL=yaml.js.map