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