libyay 1.0.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.
Files changed (3) hide show
  1. package/README.md +868 -0
  2. package/package.json +23 -0
  3. package/yay.js +2113 -0
package/yay.js ADDED
@@ -0,0 +1,2113 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Parses a YAY document string and returns the corresponding JavaScript value.
5
+ * @param {string} source - UTF-8 YAY document
6
+ * @param {string} [filename] - Optional filename for error messages
7
+ * @returns {unknown} - Parsed value (null, bigint, number, boolean, string, Array, object, Uint8Array)
8
+ */
9
+ function parseYay(source, filename) {
10
+ const ctx = { filename: filename || undefined };
11
+ const lines = scan(source, ctx);
12
+ const tokens = outlineLex(lines);
13
+ return parseRoot(tokens, ctx);
14
+ }
15
+
16
+ function locSuffix(ctx, line, col) {
17
+ if (!ctx.filename) return "";
18
+ const oneBasedLine = line + 1;
19
+ const oneBasedCol = col + 1;
20
+ return (
21
+ " at " + oneBasedLine + ":" + oneBasedCol + " of <" + ctx.filename + ">"
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Check whether a code point is allowed in a YAY document.
27
+ * @param {number} cp
28
+ * @returns {boolean}
29
+ */
30
+ function isAllowedCodePoint(cp) {
31
+ return (
32
+ cp === 0x000a ||
33
+ (0x0020 <= cp && cp <= 0x007e) ||
34
+ (0x00a0 <= cp && cp <= 0xd7ff) ||
35
+ (0xe000 <= cp && cp <= 0xfffd && !(0xfdd0 <= cp && cp <= 0xfdef)) ||
36
+ (0x10000 <= cp && cp <= 0x10ffff && (cp & 0xffff) < 0xfffe)
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Strip inline comments from a string.
42
+ * Returns the value part (trimmed) without the comment.
43
+ * @param {string} s
44
+ * @returns {string}
45
+ */
46
+ function stripInlineComment(s) {
47
+ let inDouble = false;
48
+ let inSingle = false;
49
+ let escape = false;
50
+
51
+ for (let i = 0; i < s.length; i++) {
52
+ const c = s[i];
53
+ if (escape) {
54
+ escape = false;
55
+ continue;
56
+ }
57
+ if (c === "\\") {
58
+ escape = true;
59
+ continue;
60
+ }
61
+ if (c === '"' && !inSingle) {
62
+ inDouble = !inDouble;
63
+ } else if (c === "'" && !inDouble) {
64
+ inSingle = !inSingle;
65
+ } else if (c === "#" && !inDouble && !inSingle) {
66
+ return s.slice(0, i).trimEnd();
67
+ }
68
+ }
69
+ return s;
70
+ }
71
+
72
+ /**
73
+ * @typedef {Object} ParseContext
74
+ * @property {string=} filename
75
+ */
76
+
77
+ /**
78
+ * @typedef {Object} ScanLine
79
+ * @property {string} line
80
+ * @property {number} indent
81
+ * @property {string} leader
82
+ * @property {number} lineNum
83
+ */
84
+ /**
85
+ * @typedef {Object} Token
86
+ * @property {'start'|'stop'|'text'|'break'} type
87
+ * @property {string} text
88
+ * @property {number=} indent
89
+ * @property {number=} lineNum
90
+ * @property {number=} col
91
+ */
92
+
93
+ // --- Scanner: source -> lines with { line, indent, leader, lineNum } ---
94
+
95
+ /**
96
+ * @param {string} source
97
+ * @param {ParseContext} ctx
98
+ * @returns {ScanLine[]}
99
+ */
100
+ function scan(source, ctx = {}) {
101
+ if (source.length >= 1 && source.charCodeAt(0) === 0xfeff) {
102
+ throw new Error("Illegal BOM" + locSuffix(ctx, 0, 0));
103
+ }
104
+ // Validate all code points.
105
+ {
106
+ let line = 0;
107
+ let col = 0;
108
+ for (let i = 0; i < source.length; i++) {
109
+ const c = source.charCodeAt(i);
110
+ let cp = c;
111
+ // Decode surrogate pairs to get the actual code point.
112
+ if (c >= 0xd800 && c <= 0xdbff) {
113
+ if (
114
+ i + 1 >= source.length ||
115
+ source.charCodeAt(i + 1) < 0xdc00 ||
116
+ source.charCodeAt(i + 1) > 0xdfff
117
+ ) {
118
+ throw new Error("Illegal surrogate" + locSuffix(ctx, line, col));
119
+ }
120
+ cp =
121
+ (c - 0xd800) * 0x400 + (source.charCodeAt(i + 1) - 0xdc00) + 0x10000;
122
+ if (!isAllowedCodePoint(cp)) {
123
+ throw new Error(
124
+ "Forbidden code point U+" +
125
+ cp.toString(16).toUpperCase().padStart(4, "0") +
126
+ locSuffix(ctx, line, col),
127
+ );
128
+ }
129
+ col++;
130
+ i++;
131
+ continue;
132
+ }
133
+ if (c >= 0xdc00 && c <= 0xdfff) {
134
+ throw new Error("Illegal surrogate" + locSuffix(ctx, line, col));
135
+ }
136
+ if (!isAllowedCodePoint(cp)) {
137
+ if (cp === 0x09) {
138
+ throw new Error(
139
+ "Tab not allowed (use spaces)" + locSuffix(ctx, line, col),
140
+ );
141
+ }
142
+ throw new Error(
143
+ "Forbidden code point U+" +
144
+ cp.toString(16).toUpperCase().padStart(4, "0") +
145
+ locSuffix(ctx, line, col),
146
+ );
147
+ }
148
+ if (cp === 0x0a) {
149
+ line++;
150
+ col = 0;
151
+ } else {
152
+ col++;
153
+ }
154
+ }
155
+ }
156
+ const lines = [];
157
+ const lineStrings = source.split(/\n/);
158
+ for (let i = 0; i < lineStrings.length; i++) {
159
+ const lineStr = lineStrings[i];
160
+ if (lineStr.length > 0 && lineStr.charCodeAt(lineStr.length - 1) === 0x20) {
161
+ throw new Error(
162
+ "Unexpected trailing space" + locSuffix(ctx, i, lineStr.length - 1),
163
+ );
164
+ }
165
+ let indent = 0;
166
+ while (indent < lineStr.length && lineStr[indent] === " ") {
167
+ indent++;
168
+ }
169
+ const rest = lineStr.slice(indent);
170
+ if (rest.startsWith("#") && indent === 0) {
171
+ continue; // comment only at column 0
172
+ }
173
+ // leader identifies list/bytes syntax while preserving the line payload.
174
+ let leader = "";
175
+ let line = rest;
176
+ if (rest.startsWith("- ")) {
177
+ leader = "-";
178
+ line = rest.slice(2);
179
+ } else if (rest === "-") {
180
+ // Bare "-" without space is invalid - must be "- " followed by value
181
+ throw new Error(
182
+ 'Expected space after "-"' + locSuffix(ctx, i, indent + 1),
183
+ );
184
+ } else if (rest.match(/^-\.?\d/)) {
185
+ leader = "";
186
+ line = rest;
187
+ } else if (rest === "-infinity") {
188
+ leader = "";
189
+ line = rest;
190
+ } else if (
191
+ rest.length >= 2 &&
192
+ rest[0] === "-" &&
193
+ rest[1] !== " " &&
194
+ rest[1] !== "." &&
195
+ !/^\d/.test(rest[1])
196
+ ) {
197
+ // Compact list syntax (-value without space) is not allowed
198
+ throw new Error(
199
+ 'Expected space after "-"' + locSuffix(ctx, i, indent + 1),
200
+ );
201
+ } else if (
202
+ rest === "*" ||
203
+ (rest.length >= 2 && rest[0] === "*" && rest[1] === " ")
204
+ ) {
205
+ throw new Error('Unexpected character "*"' + locSuffix(ctx, i, indent));
206
+ }
207
+ lines.push({ line, indent, leader, lineNum: i });
208
+ }
209
+ return lines;
210
+ }
211
+
212
+ // --- Outline Lexer: lines -> tokens { type, text, indent?, lineNum?, col? } ---
213
+
214
+ /**
215
+ * @param {ScanLine[]} lines
216
+ * @returns {Token[]}
217
+ */
218
+ function outlineLex(lines) {
219
+ const tokens = [];
220
+ let stack = [0];
221
+ let top = 0;
222
+ let broken = false;
223
+ for (const { line, indent, leader, lineNum } of lines) {
224
+ // Close blocks on dedent.
225
+ while (indent < top) {
226
+ tokens.push({ type: "stop", text: "" });
227
+ stack.pop();
228
+ top = stack[stack.length - 1];
229
+ }
230
+ if (leader.length > 0 && indent > top) {
231
+ tokens.push({
232
+ type: "start",
233
+ text: leader,
234
+ indent,
235
+ lineNum,
236
+ col: indent,
237
+ });
238
+ stack.push(indent);
239
+ top = indent;
240
+ broken = false;
241
+ } else if (leader.length > 0 && indent === top) {
242
+ tokens.push({ type: "stop", text: "" });
243
+ tokens.push({
244
+ type: "start",
245
+ text: leader,
246
+ indent,
247
+ lineNum,
248
+ col: indent,
249
+ });
250
+ broken = false;
251
+ }
252
+ if (line.length > 0) {
253
+ tokens.push({ type: "text", text: line, indent, lineNum, col: indent });
254
+ broken = false;
255
+ } else if (!broken) {
256
+ tokens.push({ type: "break", text: "", lineNum, col: indent });
257
+ broken = true;
258
+ }
259
+ }
260
+ while (stack.length > 1) {
261
+ tokens.push({ type: "stop", text: "" });
262
+ stack.pop();
263
+ }
264
+ return tokens;
265
+ }
266
+
267
+ // --- Value parser: tokens -> value ---
268
+
269
+ /**
270
+ * @param {Token[]} tokens
271
+ * @param {ParseContext} ctx
272
+ * @returns {unknown}
273
+ */
274
+ function parseRoot(tokens, ctx = {}) {
275
+ let i = 0;
276
+ while (
277
+ i < tokens.length &&
278
+ (tokens[i].type === "stop" || tokens[i].type === "break")
279
+ )
280
+ i++;
281
+ if (i >= tokens.length) {
282
+ throw new Error(
283
+ "No value found in document" +
284
+ (ctx.filename ? " <" + ctx.filename + ">" : ""),
285
+ );
286
+ }
287
+ const t = tokens[i];
288
+ if (t.type === "text" && (t.indent ?? 0) > 0) {
289
+ const line = (t.lineNum ?? 0) + 1;
290
+ throw new Error(
291
+ "Unexpected indent" +
292
+ (ctx.filename ? " at " + line + ":1 of <" + ctx.filename + ">" : ""),
293
+ );
294
+ }
295
+ if (
296
+ t.type === "text" &&
297
+ findKeyColonOutsideQuotes(t.text) >= 0 &&
298
+ (t.indent ?? 0) === 0 &&
299
+ !t.text.startsWith("{")
300
+ ) {
301
+ const [value, next] = parseRootObject(tokens, i, ctx);
302
+ return ensureAtEnd(value, tokens, next, ctx);
303
+ }
304
+ const [value, next] = parseValue(tokens, i, ctx);
305
+ return ensureAtEnd(value, tokens, next, ctx);
306
+ }
307
+
308
+ function ensureAtEnd(value, tokens, i, ctx = {}) {
309
+ let j = i;
310
+ while (
311
+ j < tokens.length &&
312
+ (tokens[j].type === "stop" || tokens[j].type === "break")
313
+ )
314
+ j++;
315
+ if (j < tokens.length) {
316
+ const t = tokens[j];
317
+ const line = (t.lineNum ?? 0) + 1;
318
+ const col = (t.col ?? 0) + 1;
319
+ throw new Error(
320
+ "Unexpected extra content" +
321
+ (ctx.filename
322
+ ? " at " + line + ":" + col + " of <" + ctx.filename + ">"
323
+ : ""),
324
+ );
325
+ }
326
+ return value;
327
+ }
328
+
329
+ /**
330
+ * @param {Token[]} tokens
331
+ * @param {number} i
332
+ * @param {ParseContext} ctx
333
+ * @returns {[unknown, number]}
334
+ */
335
+ function parseValue(tokens, i, ctx = {}) {
336
+ const t = tokens[i];
337
+ if (t.type === "text") {
338
+ if (t.text.startsWith(" ")) {
339
+ const line = (t.lineNum ?? 0) + 1;
340
+ const col = (t.col ?? 0) + 1;
341
+ throw new Error(
342
+ "Unexpected leading space" +
343
+ (ctx.filename
344
+ ? " at " + line + ":" + col + " of <" + ctx.filename + ">"
345
+ : ""),
346
+ );
347
+ }
348
+ if (t.text === "$") {
349
+ const line = (t.lineNum ?? 0) + 1;
350
+ const col = (t.col ?? 0) + 1;
351
+ throw new Error(
352
+ 'Unexpected character "$"' +
353
+ (ctx.filename
354
+ ? " at " + line + ":" + col + " of <" + ctx.filename + ">"
355
+ : ""),
356
+ );
357
+ }
358
+ }
359
+ if (t.type === "start") {
360
+ if (t.text === "-") {
361
+ return parseListArray(tokens, i, ctx);
362
+ }
363
+ }
364
+ if (t.type === "text") {
365
+ const raw = t.text;
366
+ const s = raw;
367
+ const sCol = t.col ?? 0;
368
+ if (s === "null") return [null, i + 1];
369
+ if (s === "true") return [true, i + 1];
370
+ if (s === "false") return [false, i + 1];
371
+ if (s === "nan") return [NaN, i + 1];
372
+ if (s === "infinity") return [Infinity, i + 1];
373
+ if (s === "-infinity") return [-Infinity, i + 1];
374
+ const num = parseNumber(s, ctx, t.lineNum ?? 0, sCol);
375
+ if (num !== undefined) return [num, i + 1];
376
+ if (s === "`" || (s.startsWith("`") && s.length >= 2 && s[1] === " ")) {
377
+ const firstLine = s.length > 2 ? s.slice(2) : "";
378
+ // Use token's indent as base - block string content must be indented more
379
+ return parseBlockStringWithIndent(
380
+ tokens,
381
+ i,
382
+ firstLine,
383
+ false,
384
+ t.indent ?? 0,
385
+ );
386
+ }
387
+ if (
388
+ (s.startsWith('"') && s.length > 1) ||
389
+ (s.startsWith("'") && s.length > 1)
390
+ ) {
391
+ return [parseQuotedString(s, ctx, t.lineNum ?? 0, sCol), i + 1];
392
+ }
393
+ if (s.startsWith("[")) {
394
+ // Inline arrays must close on the same line.
395
+ if (!s.endsWith("]")) {
396
+ throw new Error(
397
+ "Unexpected newline in inline array" +
398
+ locSuffix(ctx, t.lineNum ?? 0, sCol),
399
+ );
400
+ }
401
+ validateInlineArrayWhitespace(s, ctx, t.lineNum ?? 0, sCol);
402
+ return [parseInlineArray(s, ctx, t.lineNum ?? 0, sCol), i + 1];
403
+ }
404
+ if (s.startsWith(">")) {
405
+ let firstLine = s.slice(1);
406
+ if (firstLine.startsWith(" ")) firstLine = firstLine.slice(1);
407
+ if (firstLine.length === 0) {
408
+ throw new Error("Expected hex or comment in hex block");
409
+ }
410
+ return parseBlockBytes(tokens, i, ctx, firstLine, t.indent ?? 0);
411
+ }
412
+ if (s.startsWith("{")) {
413
+ // Inline objects must close on the same line.
414
+ if (!s.includes("}")) {
415
+ throw new Error(
416
+ "Unexpected newline in inline object" +
417
+ locSuffix(ctx, t.lineNum ?? 0, sCol),
418
+ );
419
+ }
420
+ const inlineObj = parseInlineObject(s, ctx, t.lineNum ?? 0, sCol);
421
+ if (inlineObj !== null) return [inlineObj, i + 1];
422
+ }
423
+ if (s.startsWith("<") && s.includes(">"))
424
+ return [parseAngleBytes(s, ctx, t.lineNum, t.col), i + 1];
425
+ if (s.startsWith("<")) {
426
+ throw new Error(
427
+ "Unmatched angle bracket" + locSuffix(ctx, t.lineNum ?? 0, sCol),
428
+ );
429
+ }
430
+ const keyValue = splitKeyValue(s, sCol, ctx, t.lineNum ?? 0);
431
+ if (keyValue) {
432
+ const { key, valuePart, valueCol } = keyValue;
433
+ if (valuePart === "" && key.length > 0) {
434
+ return parseObjectOrNamedArray(tokens, i, key, ctx);
435
+ }
436
+ // Note: "key: <" without closing ">" is invalid - inline byte arrays must be closed on the same line
437
+ if (key.length > 0) {
438
+ const value =
439
+ valuePart === ""
440
+ ? undefined
441
+ : parseScalar(valuePart, ctx, t.lineNum ?? 0, valueCol);
442
+ return [{ [key]: value }, i + 1];
443
+ }
444
+ }
445
+ return [parseScalar(s, ctx, t.lineNum ?? 0, sCol), i + 1];
446
+ }
447
+ return [undefined, i + 1];
448
+ }
449
+
450
+ /**
451
+ * @param {string} s
452
+ * @param {number} sCol
453
+ * @param {ParseContext} ctx
454
+ * @param {number} lineNum
455
+ * @param {number|undefined} inlineCol - column of opening brace for inline objects
456
+ * @returns {{key: string, valuePart: string, valueCol: number}|null}
457
+ */
458
+ function splitKeyValue(s, sCol, ctx, lineNum, inlineCol) {
459
+ const colonIdx = findKeyColonOutsideQuotes(s);
460
+ if (colonIdx < 0) return null;
461
+ const keyRaw = s.slice(0, colonIdx);
462
+ if (keyRaw.endsWith(" ")) {
463
+ const col = sCol + Math.max(0, keyRaw.length - 1);
464
+ throw new Error(
465
+ 'Unexpected space before ":"' + locSuffix(ctx, lineNum, col),
466
+ );
467
+ }
468
+ let key = keyRaw;
469
+ if (keyRaw.startsWith('"') || keyRaw.startsWith("'")) {
470
+ const quote = keyRaw[0];
471
+ if (keyRaw.length < 2 || keyRaw[keyRaw.length - 1] !== quote) {
472
+ const col = sCol + Math.max(0, keyRaw.length - 1);
473
+ throw new Error("Unterminated string" + locSuffix(ctx, lineNum, col));
474
+ }
475
+ key =
476
+ quote === '"'
477
+ ? parseQuotedString(keyRaw, ctx, lineNum, sCol)
478
+ : keyRaw.slice(1, -1);
479
+ } else {
480
+ if (keyRaw.length === 0) {
481
+ throw new Error("Missing key" + locSuffix(ctx, lineNum, sCol + colonIdx));
482
+ }
483
+ for (let i = 0; i < keyRaw.length; i++) {
484
+ const c = keyRaw[i];
485
+ const isAlpha = (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
486
+ const isDigit = c >= "0" && c <= "9";
487
+ const isUnderscore = c === "_";
488
+ const isHyphen = c === "-";
489
+ if (!isAlpha && !isDigit && !isUnderscore && !isHyphen) {
490
+ // First character invalid = "Invalid key", subsequent = "Invalid key character"
491
+ const errMsg = i === 0 ? "Invalid key" : "Invalid key character";
492
+ // For inline objects, report column of opening brace; otherwise report character position
493
+ const errCol =
494
+ i === 0 && inlineCol !== undefined ? inlineCol : sCol + i;
495
+ throw new Error(errMsg + locSuffix(ctx, lineNum, errCol));
496
+ }
497
+ }
498
+ }
499
+ const valueSlice = s.slice(colonIdx + 1);
500
+ let valuePart = valueSlice;
501
+ let valueCol = sCol + colonIdx + 1;
502
+ if (valueSlice.length > 0 && !valueSlice.startsWith(" ")) {
503
+ throw new Error(
504
+ 'Expected space after ":"' + locSuffix(ctx, lineNum, sCol + colonIdx),
505
+ );
506
+ }
507
+ if (valueSlice.startsWith(" ")) {
508
+ if (valueSlice.startsWith(" ")) {
509
+ throw new Error(
510
+ 'Unexpected space after ":"' +
511
+ locSuffix(ctx, lineNum, sCol + colonIdx + 2),
512
+ );
513
+ }
514
+ valuePart = valueSlice.slice(1);
515
+ valueCol = sCol + colonIdx + 2;
516
+ }
517
+ return { key, valuePart, valueCol };
518
+ }
519
+
520
+ /**
521
+ * @param {string} s
522
+ * @returns {number}
523
+ */
524
+ function findKeyColonOutsideQuotes(s) {
525
+ let inSingle = false;
526
+ let inDouble = false;
527
+ let escape = false;
528
+ for (let i = 0; i < s.length; i++) {
529
+ const ch = s[i];
530
+ if (escape) {
531
+ escape = false;
532
+ continue;
533
+ }
534
+ if (inSingle) {
535
+ if (ch === "\\") {
536
+ escape = true;
537
+ } else if (ch === "'") {
538
+ inSingle = false;
539
+ }
540
+ continue;
541
+ }
542
+ if (inDouble) {
543
+ if (ch === "\\") {
544
+ escape = true;
545
+ } else if (ch === '"') {
546
+ inDouble = false;
547
+ }
548
+ continue;
549
+ }
550
+ if (ch === "'") {
551
+ inSingle = true;
552
+ continue;
553
+ }
554
+ if (ch === '"') {
555
+ inDouble = true;
556
+ continue;
557
+ }
558
+ if (ch === ":") return i;
559
+ }
560
+ return -1;
561
+ }
562
+
563
+ /**
564
+ * @param {string} valuePart
565
+ * @param {string} leader
566
+ * @returns {boolean}
567
+ */
568
+ function isPropertyBlockLeaderOnly(valuePart, leader) {
569
+ if (valuePart === leader) return true;
570
+ if (!valuePart.startsWith(leader)) return false;
571
+ let i = 1;
572
+ while (i < valuePart.length && valuePart[i] === " ") i++;
573
+ if (i >= valuePart.length) return true;
574
+ return valuePart[i] === "#";
575
+ }
576
+
577
+ /**
578
+ * @param {string} s
579
+ * @param {ParseContext} ctx
580
+ * @param {number} lineNum
581
+ * @param {number} col
582
+ * @returns {Record<string, unknown>|null}
583
+ */
584
+ function parseInlineObject(s, ctx = {}, lineNum = 0, col = 0) {
585
+ if (!s.startsWith("{")) return null;
586
+ if (!s.includes("}")) {
587
+ return null;
588
+ }
589
+ if (!s.endsWith("}")) {
590
+ throw new Error(
591
+ "Unexpected inline object content" + locSuffix(ctx, lineNum, col),
592
+ );
593
+ }
594
+ if (s === "{}") return {};
595
+ if (s[1] === " ") {
596
+ throw new Error(
597
+ 'Unexpected space after "{"' + locSuffix(ctx, lineNum, col + 1),
598
+ );
599
+ }
600
+ if (s[s.length - 2] === " ") {
601
+ throw new Error(
602
+ 'Unexpected space before "}"' +
603
+ locSuffix(ctx, lineNum, col + s.length - 2),
604
+ );
605
+ }
606
+ const body = s.slice(1, -1);
607
+ const parts = [];
608
+ let start = 0;
609
+ let inSingle = false;
610
+ let inDouble = false;
611
+ let escape = false;
612
+ let braceDepth = 0;
613
+ let bracketDepth = 0;
614
+ for (let i = 0; i < body.length; i++) {
615
+ const ch = body[i];
616
+ if (escape) {
617
+ escape = false;
618
+ continue;
619
+ }
620
+ if (inSingle) {
621
+ if (ch === "\\") {
622
+ escape = true;
623
+ } else if (ch === "'") {
624
+ inSingle = false;
625
+ }
626
+ continue;
627
+ }
628
+ if (inDouble) {
629
+ if (ch === "\\") {
630
+ escape = true;
631
+ } else if (ch === '"') {
632
+ inDouble = false;
633
+ }
634
+ continue;
635
+ }
636
+ if (ch === "'") {
637
+ inSingle = true;
638
+ continue;
639
+ }
640
+ if (ch === '"') {
641
+ inDouble = true;
642
+ continue;
643
+ }
644
+ if (ch === "{") {
645
+ braceDepth++;
646
+ continue;
647
+ }
648
+ if (ch === "}") {
649
+ if (braceDepth > 0) braceDepth--;
650
+ continue;
651
+ }
652
+ if (ch === "[") {
653
+ bracketDepth++;
654
+ continue;
655
+ }
656
+ if (ch === "]") {
657
+ if (bracketDepth > 0) bracketDepth--;
658
+ continue;
659
+ }
660
+ if (ch === "," && braceDepth === 0 && bracketDepth === 0) {
661
+ if (i > 0 && body[i - 1] === " ") {
662
+ throw new Error(
663
+ 'Unexpected space before ","' +
664
+ locSuffix(ctx, lineNum, col + 1 + i - 1),
665
+ );
666
+ }
667
+ if (i + 1 >= body.length || body[i + 1] !== " ") {
668
+ throw new Error(
669
+ 'Expected space after ","' + locSuffix(ctx, lineNum, col + 1 + i),
670
+ );
671
+ }
672
+ parts.push({ text: body.slice(start, i), start });
673
+ start = i + 2;
674
+ }
675
+ }
676
+ parts.push({ text: body.slice(start), start });
677
+ const obj = {};
678
+ for (const part of parts) {
679
+ if (part.text.length === 0) {
680
+ throw new Error("Missing key" + locSuffix(ctx, lineNum, col + 1));
681
+ }
682
+ const partCol = col + 1 + part.start;
683
+ const keyValue = splitKeyValue(part.text, partCol, ctx, lineNum, col);
684
+ if (!keyValue) {
685
+ throw new Error(
686
+ "Expected colon after key" + locSuffix(ctx, lineNum, col),
687
+ );
688
+ }
689
+ const { key, valuePart, valueCol } = keyValue;
690
+ if (valuePart === "") {
691
+ throw new Error("Missing value" + locSuffix(ctx, lineNum, valueCol));
692
+ }
693
+ obj[key] = parseScalar(valuePart, ctx, lineNum, valueCol);
694
+ }
695
+ return obj;
696
+ }
697
+
698
+ /**
699
+ * @param {string} s
700
+ * @param {ParseContext} ctx
701
+ * @param {number} lineNum
702
+ * @param {number} col
703
+ * @returns {unknown}
704
+ */
705
+ function parseScalar(s, ctx = {}, lineNum = 0, col = 0) {
706
+ // Strip inline comments first
707
+ s = stripInlineComment(s);
708
+
709
+ if (s === "null") return null;
710
+ if (s === "true") return true;
711
+ if (s === "false") return false;
712
+ if (s === "nan") return NaN;
713
+ if (s === "infinity") return Infinity;
714
+ if (s === "-infinity") return -Infinity;
715
+ const num = parseNumber(s, ctx, lineNum, col);
716
+ if (num !== undefined) return num;
717
+ if (s.startsWith('"')) {
718
+ if (!s.endsWith('"') || s.length < 2) {
719
+ throw new Error(
720
+ "Unterminated string" + locSuffix(ctx, lineNum, col + s.length),
721
+ );
722
+ }
723
+ return parseQuotedString(s, ctx, lineNum, col);
724
+ }
725
+ if (s.startsWith("'")) {
726
+ if (!s.endsWith("'") || s.length < 2) {
727
+ throw new Error(
728
+ "Unterminated string" + locSuffix(ctx, lineNum, col + s.length),
729
+ );
730
+ }
731
+ return s.slice(1, -1);
732
+ }
733
+ if (s.startsWith("[")) return parseInlineArray(s, ctx, lineNum, col);
734
+ if (s.startsWith("{")) return parseInlineObject(s, ctx, lineNum, col);
735
+ if (s.startsWith("<")) return parseAngleBytes(s, ctx, lineNum, col);
736
+ // Bare words are not valid - strings must be quoted
737
+ const firstChar = s.charAt(0) || "?";
738
+ throw new Error(
739
+ `Unexpected character "${firstChar}"` + locSuffix(ctx, lineNum, col),
740
+ );
741
+ }
742
+
743
+ /**
744
+ * @param {string} s
745
+ * @returns {number|bigint|undefined}
746
+ */
747
+ /**
748
+ * @param {string} s
749
+ * @param {ParseContext} ctx
750
+ * @param {number} lineNum
751
+ * @param {number} col
752
+ * @returns {number|bigint|undefined}
753
+ */
754
+ function parseNumber(s, ctx = {}, lineNum = 0, col = 0) {
755
+ // Check for uppercase E in exponent (must be lowercase)
756
+ // Only check if this looks like a number (starts with digit, minus, or dot)
757
+ const firstNonSpace = s.replace(/^ */, "")[0];
758
+ if (
759
+ (firstNonSpace >= "0" && firstNonSpace <= "9") ||
760
+ firstNonSpace === "-" ||
761
+ firstNonSpace === "."
762
+ ) {
763
+ const eIdx = s.indexOf("E");
764
+ if (eIdx >= 0) {
765
+ throw new Error(
766
+ "Uppercase exponent (use lowercase 'e')" +
767
+ locSuffix(ctx, lineNum, col + eIdx),
768
+ );
769
+ }
770
+ }
771
+
772
+ let hasDigit = false;
773
+ let hasExponent = false;
774
+ for (let i = 0; i < s.length; i++) {
775
+ const c = s[i];
776
+ if (c === " ") continue;
777
+ if (c >= "0" && c <= "9") {
778
+ hasDigit = true;
779
+ continue;
780
+ }
781
+ if (c === ".") continue;
782
+ if (c === "-" && i === 0) continue;
783
+ // Allow 'e' for exponent notation (E already rejected above)
784
+ if (c === "e" && hasDigit && !hasExponent) {
785
+ hasExponent = true;
786
+ continue;
787
+ }
788
+ // Allow +/- after exponent
789
+ if ((c === "+" || c === "-") && hasExponent) {
790
+ const prev = i > 0 ? s[i - 1] : "";
791
+ if (prev === "e") continue;
792
+ }
793
+ // Not a numeric candidate.
794
+ return undefined;
795
+ }
796
+ if (!hasDigit) return undefined;
797
+ for (let i = 0; i < s.length; i++) {
798
+ if (s[i] !== " ") continue;
799
+ const prev = i > 0 ? s[i - 1] : "";
800
+ const next = i + 1 < s.length ? s[i + 1] : "";
801
+ const isDigitPrev = prev >= "0" && prev <= "9";
802
+ const isDigitNext = next >= "0" && next <= "9";
803
+ if (!(isDigitPrev && isDigitNext)) {
804
+ throw new Error(
805
+ "Unexpected space in number" + locSuffix(ctx, lineNum, col + i),
806
+ );
807
+ }
808
+ }
809
+ const compact = s.replace(/ /g, "");
810
+ if (/^-?\d+$/.test(compact)) return BigInt(compact);
811
+ // Float patterns: with decimal point, or with exponent, or both
812
+ if (
813
+ /^-?\d*\.\d*([eE][+-]?\d+)?$/.test(compact) ||
814
+ /^-?\d+[eE][+-]?\d+$/.test(compact)
815
+ ) {
816
+ const n = Number(compact);
817
+ if (!isNaN(n)) return n;
818
+ }
819
+ return undefined;
820
+ }
821
+
822
+ /**
823
+ * @param {string} s
824
+ * @param {ParseContext} ctx
825
+ * @param {number} lineNum
826
+ * @param {number} col
827
+ * @returns {string}
828
+ */
829
+ function parseQuotedString(s, ctx = {}, lineNum = 0, col = 0) {
830
+ if (s.startsWith('"')) return parseJsonQuotedString(s, ctx, lineNum, col);
831
+ if (s.startsWith("'")) return s.slice(1, -1);
832
+ return s;
833
+ }
834
+
835
+ /**
836
+ * Minimal JSON string parser for deterministic errors.
837
+ * @param {string} s
838
+ * @param {ParseContext} ctx
839
+ * @param {number} lineNum
840
+ * @param {number} col
841
+ * @returns {string}
842
+ */
843
+ function parseJsonQuotedString(s, ctx = {}, lineNum = 0, col = 0) {
844
+ if (s.length < 2 || s[0] !== '"') return s;
845
+ if (s[s.length - 1] !== '"') {
846
+ const index = Math.max(0, s.length - 1);
847
+ throw new Error(
848
+ "Unterminated string" + locSuffix(ctx, lineNum, col + index),
849
+ );
850
+ }
851
+ let out = "";
852
+ for (let i = 1; i < s.length - 1; i++) {
853
+ const ch = s[i];
854
+ if (ch === "\\") {
855
+ // Escapes follow JSON rules; report the offending escape character.
856
+ if (i + 1 >= s.length - 1) {
857
+ throw new Error(
858
+ "Bad escaped character" + locSuffix(ctx, lineNum, col + i + 1),
859
+ );
860
+ }
861
+ const esc = s[i + 1];
862
+ switch (esc) {
863
+ case '"':
864
+ out += '"';
865
+ i++;
866
+ break;
867
+ case "\\":
868
+ out += "\\";
869
+ i++;
870
+ break;
871
+ case "/":
872
+ out += "/";
873
+ i++;
874
+ break;
875
+ case "b":
876
+ out += "\b";
877
+ i++;
878
+ break;
879
+ case "f":
880
+ out += "\f";
881
+ i++;
882
+ break;
883
+ case "n":
884
+ out += "\n";
885
+ i++;
886
+ break;
887
+ case "r":
888
+ out += "\r";
889
+ i++;
890
+ break;
891
+ case "t":
892
+ out += "\t";
893
+ i++;
894
+ break;
895
+ case "u": {
896
+ // YAY uses \u{XXXXXX} syntax (variable-length with braces)
897
+ const braceStart = i + 2;
898
+ const uCol = col + i + 1; // Column of 'u' for "Bad escaped character"
899
+ const braceCol = col + braceStart; // Column of '{' for other errors
900
+ if (braceStart >= s.length - 1 || s[braceStart] !== "{") {
901
+ // Old-style \uXXXX syntax is not supported
902
+ throw new Error(
903
+ "Bad escaped character" + locSuffix(ctx, lineNum, uCol),
904
+ );
905
+ }
906
+ // Find closing brace
907
+ let braceEnd = braceStart + 1;
908
+ while (braceEnd < s.length - 1 && s[braceEnd] !== "}") {
909
+ braceEnd++;
910
+ }
911
+ if (braceEnd >= s.length - 1 || s[braceEnd] !== "}") {
912
+ throw new Error(
913
+ "Bad Unicode escape" + locSuffix(ctx, lineNum, braceCol),
914
+ );
915
+ }
916
+ const hexStart = braceStart + 1;
917
+ if (hexStart === braceEnd) {
918
+ throw new Error(
919
+ "Bad Unicode escape" + locSuffix(ctx, lineNum, braceCol),
920
+ );
921
+ }
922
+ // Check for too many hex digits (max 6)
923
+ if (braceEnd - hexStart > 6) {
924
+ throw new Error(
925
+ "Bad Unicode escape" + locSuffix(ctx, lineNum, braceCol),
926
+ );
927
+ }
928
+ let hex = "";
929
+ for (let j = hexStart; j < braceEnd; j++) {
930
+ const c = s[j];
931
+ if (!/[0-9a-fA-F]/.test(c)) {
932
+ throw new Error(
933
+ "Bad Unicode escape" + locSuffix(ctx, lineNum, braceCol),
934
+ );
935
+ }
936
+ hex += c;
937
+ }
938
+ const code = parseInt(hex, 16);
939
+ if (code >= 0xd800 && code <= 0xdfff) {
940
+ throw new Error(
941
+ "Illegal surrogate" + locSuffix(ctx, lineNum, braceCol),
942
+ );
943
+ }
944
+ if (code > 0x10ffff) {
945
+ throw new Error(
946
+ "Unicode code point out of range" +
947
+ locSuffix(ctx, lineNum, braceCol),
948
+ );
949
+ }
950
+ out += String.fromCodePoint(code);
951
+ i = braceEnd; // Loop will increment to braceEnd + 1
952
+ break;
953
+ }
954
+ default:
955
+ throw new Error(
956
+ "Bad escaped character" + locSuffix(ctx, lineNum, col + i + 1),
957
+ );
958
+ }
959
+ } else {
960
+ // Unescaped control characters are illegal in JSON strings.
961
+ const code = ch.charCodeAt(0);
962
+ if (code < 0x20) {
963
+ throw new Error(
964
+ "Bad character in string" + locSuffix(ctx, lineNum, col + i),
965
+ );
966
+ }
967
+ out += ch;
968
+ }
969
+ }
970
+ return out;
971
+ }
972
+
973
+ /**
974
+ * @param {string} s
975
+ * @param {ParseContext} ctx
976
+ * @param {number} lineNum
977
+ * @param {number} col
978
+ * @returns {unknown[]}
979
+ */
980
+ function parseInlineArray(s, ctx = {}, lineNum = 0, col = 0) {
981
+ if (!s.startsWith("[")) return [];
982
+ if (!s.endsWith("]")) {
983
+ throw new Error("Unterminated inline array" + locSuffix(ctx, lineNum, col));
984
+ }
985
+ if (s === "[]") return [];
986
+ if (s[1] === " ") {
987
+ throw new Error(
988
+ 'Unexpected space after "["' + locSuffix(ctx, lineNum, col + 1),
989
+ );
990
+ }
991
+ if (s[s.length - 2] === " ") {
992
+ throw new Error(
993
+ 'Unexpected space before "]"' +
994
+ locSuffix(ctx, lineNum, col + s.length - 2),
995
+ );
996
+ }
997
+ const body = s.slice(1, -1);
998
+ const parts = [];
999
+ let start = 0;
1000
+ let inSingle = false;
1001
+ let inDouble = false;
1002
+ let escape = false;
1003
+ let braceDepth = 0;
1004
+ let bracketDepth = 0;
1005
+ let angleDepth = 0;
1006
+ for (let i = 0; i < body.length; i++) {
1007
+ const ch = body[i];
1008
+ if (escape) {
1009
+ escape = false;
1010
+ continue;
1011
+ }
1012
+ if (inSingle) {
1013
+ if (ch === "\\") {
1014
+ escape = true;
1015
+ } else if (ch === "'") {
1016
+ inSingle = false;
1017
+ }
1018
+ continue;
1019
+ }
1020
+ if (inDouble) {
1021
+ if (ch === "\\") {
1022
+ escape = true;
1023
+ } else if (ch === '"') {
1024
+ inDouble = false;
1025
+ }
1026
+ continue;
1027
+ }
1028
+ if (ch === "'") {
1029
+ inSingle = true;
1030
+ continue;
1031
+ }
1032
+ if (ch === '"') {
1033
+ inDouble = true;
1034
+ continue;
1035
+ }
1036
+ if (ch === "{") {
1037
+ braceDepth++;
1038
+ continue;
1039
+ }
1040
+ if (ch === "}") {
1041
+ if (braceDepth > 0) braceDepth--;
1042
+ continue;
1043
+ }
1044
+ if (ch === "[") {
1045
+ bracketDepth++;
1046
+ continue;
1047
+ }
1048
+ if (ch === "]") {
1049
+ if (bracketDepth > 0) bracketDepth--;
1050
+ continue;
1051
+ }
1052
+ if (ch === "<") {
1053
+ angleDepth++;
1054
+ continue;
1055
+ }
1056
+ if (ch === ">") {
1057
+ if (angleDepth > 0) angleDepth--;
1058
+ continue;
1059
+ }
1060
+ if (
1061
+ ch === "," &&
1062
+ braceDepth === 0 &&
1063
+ bracketDepth === 0 &&
1064
+ angleDepth === 0
1065
+ ) {
1066
+ if (i > 0 && body[i - 1] === " ") {
1067
+ throw new Error(
1068
+ 'Unexpected space before ","' +
1069
+ locSuffix(ctx, lineNum, col + 1 + i - 1),
1070
+ );
1071
+ }
1072
+ if (i + 1 >= body.length || body[i + 1] !== " ") {
1073
+ throw new Error(
1074
+ 'Expected space after ","' + locSuffix(ctx, lineNum, col + 1 + i),
1075
+ );
1076
+ }
1077
+ parts.push({ text: body.slice(start, i), start });
1078
+ start = i + 2;
1079
+ }
1080
+ }
1081
+ parts.push({ text: body.slice(start), start });
1082
+ const arr = [];
1083
+ for (const part of parts) {
1084
+ if (part.text.length === 0) {
1085
+ throw new Error(
1086
+ "Missing array element" + locSuffix(ctx, lineNum, col + 1 + part.start),
1087
+ );
1088
+ }
1089
+ const partCol = col + 1 + part.start;
1090
+ arr.push(parseScalar(part.text, ctx, lineNum, partCol));
1091
+ }
1092
+ return arr;
1093
+ }
1094
+
1095
+ /**
1096
+ * @param {string} s
1097
+ * @param {ParseContext} ctx
1098
+ * @param {number} lineNum
1099
+ * @param {number} col
1100
+ */
1101
+ function validateInlineArrayWhitespace(s, ctx = {}, lineNum = 0, col = 0) {
1102
+ let inSingle = false;
1103
+ let inDouble = false;
1104
+ let escape = false;
1105
+ let depth = 0;
1106
+ for (let i = 0; i < s.length; i++) {
1107
+ const ch = s[i];
1108
+ if (escape) {
1109
+ escape = false;
1110
+ continue;
1111
+ }
1112
+ if (inSingle) {
1113
+ if (ch === "\\") {
1114
+ escape = true;
1115
+ } else if (ch === "'") {
1116
+ inSingle = false;
1117
+ }
1118
+ continue;
1119
+ }
1120
+ if (inDouble) {
1121
+ if (ch === "\\") {
1122
+ escape = true;
1123
+ } else if (ch === '"') {
1124
+ inDouble = false;
1125
+ }
1126
+ continue;
1127
+ }
1128
+ if (ch === "'") {
1129
+ inSingle = true;
1130
+ continue;
1131
+ }
1132
+ if (ch === '"') {
1133
+ inDouble = true;
1134
+ continue;
1135
+ }
1136
+ if (ch === "[") {
1137
+ depth++;
1138
+ if (i + 1 < s.length && s[i + 1] === " ") {
1139
+ throw new Error(
1140
+ 'Unexpected space after "["' + locSuffix(ctx, lineNum, col + i + 1),
1141
+ );
1142
+ }
1143
+ continue;
1144
+ }
1145
+ if (ch === "]") {
1146
+ if (i > 0 && s[i - 1] === " ") {
1147
+ throw new Error(
1148
+ 'Unexpected space before "]"' + locSuffix(ctx, lineNum, col + i - 1),
1149
+ );
1150
+ }
1151
+ if (depth > 0) depth--;
1152
+ continue;
1153
+ }
1154
+ if (ch === ",") {
1155
+ if (i > 0 && s[i - 1] === " ") {
1156
+ throw new Error(
1157
+ 'Unexpected space before ","' + locSuffix(ctx, lineNum, col + i - 1),
1158
+ );
1159
+ }
1160
+ if (i + 1 < s.length && s[i + 1] !== " " && s[i + 1] !== "]") {
1161
+ let lookaheadDepth = depth;
1162
+ let inS = false;
1163
+ let inD = false;
1164
+ let esc = false;
1165
+ let nextIsClosingWithSpace = false;
1166
+ for (let j = i + 1; j < s.length; j++) {
1167
+ const cj = s[j];
1168
+ if (esc) {
1169
+ esc = false;
1170
+ continue;
1171
+ }
1172
+ if (inS) {
1173
+ if (cj === "\\") esc = true;
1174
+ else if (cj === "'") inS = false;
1175
+ continue;
1176
+ }
1177
+ if (inD) {
1178
+ if (cj === "\\") esc = true;
1179
+ else if (cj === '"') inD = false;
1180
+ continue;
1181
+ }
1182
+ if (cj === "'") {
1183
+ inS = true;
1184
+ continue;
1185
+ }
1186
+ if (cj === '"') {
1187
+ inD = true;
1188
+ continue;
1189
+ }
1190
+ if (cj === "[") {
1191
+ lookaheadDepth++;
1192
+ continue;
1193
+ }
1194
+ if (cj === "]") {
1195
+ if (lookaheadDepth === depth) {
1196
+ nextIsClosingWithSpace = j > 0 && s[j - 1] === " ";
1197
+ break;
1198
+ }
1199
+ if (lookaheadDepth > 0) lookaheadDepth--;
1200
+ continue;
1201
+ }
1202
+ if (cj === "," && lookaheadDepth === depth) {
1203
+ break;
1204
+ }
1205
+ }
1206
+ if (!nextIsClosingWithSpace) {
1207
+ throw new Error(
1208
+ 'Expected space after ","' + locSuffix(ctx, lineNum, col + i),
1209
+ );
1210
+ }
1211
+ }
1212
+ if (i + 2 < s.length && s[i + 1] === " " && s[i + 2] === " ") {
1213
+ throw new Error(
1214
+ 'Unexpected space after ","' + locSuffix(ctx, lineNum, col + i + 2),
1215
+ );
1216
+ }
1217
+ }
1218
+ }
1219
+ }
1220
+ /**
1221
+ * @param {string} s
1222
+ * @param {ParseContext} ctx
1223
+ * @param {number} lineNum
1224
+ * @param {number} col
1225
+ * @returns {Uint8Array}
1226
+ */
1227
+ function parseAngleBytes(s, ctx = {}, lineNum = 0, col = 0) {
1228
+ if (!s.endsWith(">")) {
1229
+ throw new Error("Unmatched angle bracket" + locSuffix(ctx, lineNum, col));
1230
+ }
1231
+ if (s === "<>") return new Uint8Array(0);
1232
+ if (s.length > 2 && s[1] === " ") {
1233
+ throw new Error(
1234
+ 'Unexpected space after "<"' + locSuffix(ctx, lineNum, col + 1),
1235
+ );
1236
+ }
1237
+ if (s.length > 2 && s[s.length - 2] === " ") {
1238
+ throw new Error(
1239
+ 'Unexpected space before ">"' +
1240
+ locSuffix(ctx, lineNum, col + s.length - 2),
1241
+ );
1242
+ }
1243
+ const inner = s.slice(1, -1);
1244
+ // Check for uppercase hex digits
1245
+ for (let i = 0; i < inner.length; i++) {
1246
+ const c = inner[i];
1247
+ if (c >= "A" && c <= "F") {
1248
+ throw new Error(
1249
+ "Uppercase hex digit (use lowercase)" +
1250
+ locSuffix(ctx, lineNum, col + 1 + i),
1251
+ );
1252
+ }
1253
+ }
1254
+ const hex = inner.replace(/\s/g, "");
1255
+ if (hex.length % 2 !== 0)
1256
+ throw new Error(
1257
+ "Odd number of hex digits in byte literal" + locSuffix(ctx, lineNum, col),
1258
+ );
1259
+ if (!/^[0-9a-f]*$/.test(hex))
1260
+ throw new Error("Invalid hex digit" + locSuffix(ctx, lineNum, col));
1261
+ return Uint8Array.fromHex
1262
+ ? Uint8Array.fromHex(hex)
1263
+ : hexToUint8Array(hex, ctx, lineNum, col);
1264
+ }
1265
+
1266
+ function hexToUint8Array(hex, ctx, lineNum, col) {
1267
+ const bytes = new Uint8Array(hex.length / 2);
1268
+ for (let j = 0; j < bytes.length; j++) {
1269
+ const pair = hex.slice(j * 2, j * 2 + 2);
1270
+ const val = parseInt(pair, 16);
1271
+ if (isNaN(val)) {
1272
+ throw new Error("Invalid hex digit" + locSuffix(ctx, lineNum, col));
1273
+ }
1274
+ bytes[j] = val;
1275
+ }
1276
+ return bytes;
1277
+ }
1278
+
1279
+ /**
1280
+ * @param {Token[]} tokens
1281
+ * @param {number} i
1282
+ * @param {ParseContext} ctx
1283
+ * @returns {[unknown[], number]}
1284
+ */
1285
+ function parseListArray(tokens, i, ctx = {}, minIndent = -1) {
1286
+ const arr = [];
1287
+ while (
1288
+ i < tokens.length &&
1289
+ tokens[i].type === "start" &&
1290
+ tokens[i].text === "-"
1291
+ ) {
1292
+ const listIndent = tokens[i].indent ?? 0;
1293
+ // Stop if we encounter a list item at a lower indent than expected
1294
+ if (minIndent >= 0 && listIndent < minIndent) break;
1295
+ i++;
1296
+ while (i < tokens.length && tokens[i].type === "break") i++;
1297
+ if (i >= tokens.length) break;
1298
+ const next = tokens[i];
1299
+ if (
1300
+ next.type === "text" &&
1301
+ next.text === "" &&
1302
+ i + 1 < tokens.length &&
1303
+ tokens[i + 1].type === "start" &&
1304
+ tokens[i + 1].text === "-"
1305
+ ) {
1306
+ const [value, j] = parseListArray(tokens, i + 1, ctx);
1307
+ arr.push(value);
1308
+ i = j;
1309
+ } else if (next.type === "start" && next.text === "-") {
1310
+ const [value, j] = parseListArray(tokens, i, ctx);
1311
+ arr.push(value);
1312
+ i = j;
1313
+ } else if (
1314
+ next.type === "text" &&
1315
+ (next.indent ?? 0) >= listIndent &&
1316
+ isInlineBullet(next.text)
1317
+ ) {
1318
+ // Inline bullet list inside a multiline list item.
1319
+ const group = [];
1320
+ let j = i;
1321
+ for (;;) {
1322
+ if (
1323
+ j < tokens.length &&
1324
+ tokens[j].type === "text" &&
1325
+ (tokens[j].indent ?? 0) >= listIndent &&
1326
+ isInlineBullet(tokens[j].text)
1327
+ ) {
1328
+ const valStr = parseInlineBulletValue(
1329
+ tokens[j].text,
1330
+ ctx,
1331
+ tokens[j].lineNum ?? 0,
1332
+ tokens[j].col ?? 0,
1333
+ );
1334
+ group.push(
1335
+ parseNestedInlineBullet(
1336
+ valStr,
1337
+ ctx,
1338
+ tokens[j].lineNum ?? 0,
1339
+ (tokens[j].col ?? 0) + 2,
1340
+ ),
1341
+ );
1342
+ j++;
1343
+ } else if (
1344
+ j < tokens.length &&
1345
+ tokens[j].type === "start" &&
1346
+ tokens[j].text === "-" &&
1347
+ (tokens[j].indent ?? 0) > listIndent &&
1348
+ j + 1 < tokens.length &&
1349
+ tokens[j + 1].type === "text" &&
1350
+ isInlineBullet(tokens[j + 1].text)
1351
+ ) {
1352
+ const valStr = parseInlineBulletValue(
1353
+ tokens[j + 1].text,
1354
+ ctx,
1355
+ tokens[j + 1].lineNum ?? 0,
1356
+ tokens[j + 1].col ?? 0,
1357
+ );
1358
+ group.push(
1359
+ parseNestedInlineBullet(
1360
+ valStr,
1361
+ ctx,
1362
+ tokens[j + 1].lineNum ?? 0,
1363
+ (tokens[j + 1].col ?? 0) + 2,
1364
+ ),
1365
+ );
1366
+ j += 2;
1367
+ } else {
1368
+ break;
1369
+ }
1370
+ }
1371
+ // If next token is start(-) at deeper indent, same list continues with nested bullets (e.g. "- - a" then " - b").
1372
+ while (
1373
+ j < tokens.length &&
1374
+ tokens[j].type === "start" &&
1375
+ tokens[j].text === "-" &&
1376
+ (tokens[j].indent ?? 0) > listIndent
1377
+ ) {
1378
+ j++;
1379
+ while (j < tokens.length && tokens[j].type === "break") j++;
1380
+ if (j >= tokens.length) break;
1381
+ const [subVal, nextJ] = parseValue(tokens, j, ctx);
1382
+ group.push(subVal);
1383
+ j = nextJ;
1384
+ while (j < tokens.length && tokens[j].type === "stop") j++;
1385
+ }
1386
+ arr.push(group);
1387
+ i = j;
1388
+ } else if (
1389
+ next.type === "text" &&
1390
+ findKeyColonOutsideQuotes(next.text) >= 0
1391
+ ) {
1392
+ const nextIndent = next.indent ?? 0;
1393
+ const inlineIndent = nextIndent === listIndent ? nextIndent : undefined;
1394
+ const baseIndent =
1395
+ inlineIndent !== undefined ? nextIndent + 2 : nextIndent;
1396
+ const [obj, nextIndex] = parseObjectBlock(
1397
+ tokens,
1398
+ i,
1399
+ baseIndent,
1400
+ ctx,
1401
+ inlineIndent,
1402
+ );
1403
+ arr.push(obj);
1404
+ i = nextIndex;
1405
+ } else if (next.type === "text" || next.type === "start") {
1406
+ const [value, j] = parseValue(tokens, i, ctx);
1407
+ let k = j;
1408
+ while (k < tokens.length && tokens[k].type === "break") k++;
1409
+ const afterBreak = k < tokens.length ? tokens[k] : null;
1410
+ if (
1411
+ afterBreak &&
1412
+ afterBreak.type === "start" &&
1413
+ afterBreak.text === "-" &&
1414
+ (afterBreak.indent ?? 0) > listIndent
1415
+ ) {
1416
+ const group = [value];
1417
+ i = k;
1418
+ while (
1419
+ i < tokens.length &&
1420
+ tokens[i].type === "start" &&
1421
+ tokens[i].text === "-" &&
1422
+ (tokens[i].indent ?? 0) > listIndent
1423
+ ) {
1424
+ i++;
1425
+ while (i < tokens.length && tokens[i].type === "break") i++;
1426
+ if (i >= tokens.length) break;
1427
+ const [subVal, nextI] = parseValue(tokens, i, ctx);
1428
+ group.push(subVal);
1429
+ i = nextI;
1430
+ while (i < tokens.length && tokens[i].type === "stop") i++;
1431
+ }
1432
+ arr.push(group);
1433
+ } else {
1434
+ arr.push(value);
1435
+ i = j;
1436
+ }
1437
+ } else {
1438
+ i++;
1439
+ }
1440
+ // Skip stops and breaks between items
1441
+ while (
1442
+ i < tokens.length &&
1443
+ (tokens[i].type === "stop" || tokens[i].type === "break")
1444
+ )
1445
+ i++;
1446
+ }
1447
+ return [arr, i];
1448
+ }
1449
+
1450
+ /**
1451
+ * Parse a block string with an optional base indent constraint.
1452
+ * @param {Token[]} tokens
1453
+ * @param {number} i
1454
+ * @param {string|undefined} firstLine
1455
+ * @param {boolean} inPropertyContext
1456
+ * @param {number} baseIndent - If >= 0, stop collecting when indent <= baseIndent
1457
+ * @returns {[string, number]}
1458
+ */
1459
+ function parseBlockStringWithIndent(
1460
+ tokens,
1461
+ i,
1462
+ firstLine,
1463
+ inPropertyContext,
1464
+ baseIndent,
1465
+ ) {
1466
+ const lines = [];
1467
+ if (firstLine !== undefined) {
1468
+ lines.push(firstLine);
1469
+ i++;
1470
+ } else {
1471
+ i++;
1472
+ }
1473
+ // Collect continuation lines with their indent so we can strip minimum indent (for property block strings).
1474
+ const continuationLines = [];
1475
+ while (
1476
+ i < tokens.length &&
1477
+ (tokens[i].type === "text" || tokens[i].type === "break")
1478
+ ) {
1479
+ if (tokens[i].type === "break") {
1480
+ continuationLines.push({ indent: undefined, text: "" });
1481
+ i++;
1482
+ } else {
1483
+ // If we have a base indent constraint, stop when we see a line at or below that indent
1484
+ if (baseIndent >= 0 && (tokens[i].indent ?? 0) <= baseIndent) {
1485
+ break;
1486
+ }
1487
+ continuationLines.push({
1488
+ indent: tokens[i].indent ?? 0,
1489
+ text: tokens[i].text,
1490
+ });
1491
+ i++;
1492
+ }
1493
+ }
1494
+ const minIndent = continuationLines
1495
+ .filter((x) => x.indent !== undefined)
1496
+ .reduce((min, x) => (x.indent < min ? x.indent : min), Infinity);
1497
+ const effectiveMin = minIndent === Infinity ? 0 : minIndent;
1498
+ for (const { indent, text } of continuationLines) {
1499
+ if (indent === undefined) {
1500
+ lines.push("");
1501
+ } else {
1502
+ // Token text is already after indent; add back (indent - minIndent) spaces for relative indent.
1503
+ const extraSpaces = indent - effectiveMin;
1504
+ lines.push((extraSpaces > 0 ? " ".repeat(extraSpaces) : "") + text);
1505
+ }
1506
+ }
1507
+ // Trim leading and trailing empty lines; then one leading newline and one trailing newline.
1508
+ let start = 0;
1509
+ while (start < lines.length && lines[start] === "") start++;
1510
+ let end = lines.length;
1511
+ while (end > start && lines[end - 1] === "") end--;
1512
+ const trimmed = lines.slice(start, end);
1513
+ // When block starts with quote on its own line (firstLine === ''), output has a leading newline.
1514
+ // But NOT in property context.
1515
+ const leadingNewline =
1516
+ firstLine === "" && trimmed.length > 0 && !inPropertyContext;
1517
+ const body =
1518
+ (leadingNewline ? "\n" : "") +
1519
+ trimmed.join("\n") +
1520
+ (trimmed.length > 0 ? "\n" : "");
1521
+ // Empty block strings are not allowed - use "" for empty string
1522
+ if (body === "") {
1523
+ throw new Error(
1524
+ 'Empty block string not allowed (use "" or "\\n" explicitly)',
1525
+ );
1526
+ }
1527
+ return [body, i];
1528
+ }
1529
+
1530
+ /**
1531
+ * Parse concatenated quoted strings (multiple quoted strings on consecutive lines).
1532
+ * Returns null if there's only one string (single string on new line is invalid).
1533
+ * @param {Token[]} tokens
1534
+ * @param {number} i
1535
+ * @param {number} baseIndent
1536
+ * @param {ParseContext} ctx
1537
+ * @returns {[string, number] | null}
1538
+ */
1539
+ function parseConcatenatedStrings(tokens, i, baseIndent, ctx = {}) {
1540
+ const parts = [];
1541
+ const startI = i;
1542
+
1543
+ while (i < tokens.length) {
1544
+ const t = tokens[i];
1545
+
1546
+ if (t.type === "break" || t.type === "stop") {
1547
+ i++;
1548
+ continue;
1549
+ }
1550
+
1551
+ if (t.type !== "text" || (t.indent ?? 0) < baseIndent) {
1552
+ break;
1553
+ }
1554
+
1555
+ const trimmed = t.text.trim();
1556
+
1557
+ // Check if this line is a quoted string
1558
+ const isDoubleQuoted =
1559
+ trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2;
1560
+ const isSingleQuoted =
1561
+ trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2;
1562
+
1563
+ if (!isDoubleQuoted && !isSingleQuoted) {
1564
+ break;
1565
+ }
1566
+
1567
+ // Parse the quoted string
1568
+ const parsed = parseQuotedString(trimmed, ctx, t.lineNum ?? 0, t.col ?? 0);
1569
+ parts.push(parsed);
1570
+ i++;
1571
+ }
1572
+
1573
+ // Require at least 2 strings for concatenation
1574
+ // A single string on a new line is invalid (use inline syntax instead)
1575
+ if (parts.length < 2) {
1576
+ return null;
1577
+ }
1578
+
1579
+ return [parts.join(""), i];
1580
+ }
1581
+
1582
+ /**
1583
+ * @param {string} text
1584
+ * @returns {boolean}
1585
+ */
1586
+ function isInlineBullet(text) {
1587
+ let i = 0;
1588
+ while (i < text.length && text[i] === " ") i++;
1589
+ return (
1590
+ i < text.length &&
1591
+ text[i] === "-" &&
1592
+ i + 1 < text.length &&
1593
+ text[i + 1] === " "
1594
+ );
1595
+ }
1596
+
1597
+ /**
1598
+ * @param {string} text
1599
+ * @param {ParseContext} ctx
1600
+ * @param {number} lineNum
1601
+ * @param {number} col
1602
+ * @returns {string}
1603
+ */
1604
+ function parseInlineBulletValue(text, ctx = {}, lineNum = 0, col = 0) {
1605
+ let i = 0;
1606
+ while (i < text.length && text[i] === " ") i++;
1607
+ if (i >= text.length || text[i] !== "-") return "";
1608
+ const dashIndex = i;
1609
+ const afterDash = dashIndex + 1;
1610
+ if (afterDash >= text.length || text[afterDash] !== " ") return "";
1611
+ if (afterDash + 1 < text.length && text[afterDash + 1] === " ") {
1612
+ throw new Error(
1613
+ 'Unexpected space after "-"' +
1614
+ locSuffix(ctx, lineNum, col + afterDash + 1),
1615
+ );
1616
+ }
1617
+ return text.slice(afterDash + 1);
1618
+ }
1619
+
1620
+ /**
1621
+ * Recursively parse an inline bullet value, handling nested "- " prefixes.
1622
+ * Returns the parsed value (could be a nested array or a scalar).
1623
+ */
1624
+ function parseNestedInlineBullet(text, ctx = {}, lineNum = 0, col = 0) {
1625
+ // Check if the text itself is another inline bullet
1626
+ if (isInlineBullet(text)) {
1627
+ const innerText = parseInlineBulletValue(text, ctx, lineNum, col);
1628
+ const innerValue = parseNestedInlineBullet(
1629
+ innerText,
1630
+ ctx,
1631
+ lineNum,
1632
+ col + 2,
1633
+ );
1634
+ return [innerValue];
1635
+ }
1636
+ // Otherwise, parse as a scalar
1637
+ return parseScalar(text, ctx, lineNum, col);
1638
+ }
1639
+
1640
+ // Note: parseMultilineBytes for '*' syntax was removed as dead code.
1641
+ // The scanner rejects '*' syntax, so this function was unreachable.
1642
+
1643
+ /**
1644
+ * @param {Token[]} tokens
1645
+ * @param {number} i
1646
+ * @param {ParseContext} ctx
1647
+ * @param {string} firstLineRaw
1648
+ * @param {number} baseIndent
1649
+ * @returns {[Uint8Array, number]}
1650
+ */
1651
+ function parseBlockBytes(tokens, i, ctx = {}, firstLineRaw, baseIndent) {
1652
+ const startToken = tokens[i];
1653
+ const lineNum = startToken.lineNum ?? 0;
1654
+ const col = startToken.col ?? 0;
1655
+ let hex = "";
1656
+ if (firstLineRaw !== undefined) {
1657
+ hex += firstLineRaw.replace(/#.*$/, "").replace(/\s/g, "").toLowerCase();
1658
+ i++;
1659
+ } else {
1660
+ i++;
1661
+ }
1662
+ while (i < tokens.length) {
1663
+ const t = tokens[i];
1664
+ if (t.type === "break") {
1665
+ i++;
1666
+ continue;
1667
+ }
1668
+ if (t.type === "text" && (t.indent ?? 0) > baseIndent) {
1669
+ hex += t.text.replace(/#.*$/, "").replace(/\s/g, "").toLowerCase();
1670
+ i++;
1671
+ continue;
1672
+ }
1673
+ break;
1674
+ }
1675
+ if (hex.length % 2 !== 0)
1676
+ throw new Error(
1677
+ "Odd number of hex digits in byte literal" + locSuffix(ctx, lineNum, col),
1678
+ );
1679
+ if (!/^[0-9a-f]*$/.test(hex))
1680
+ throw new Error("Invalid hex digit" + locSuffix(ctx, lineNum, col));
1681
+ const result = Uint8Array.fromHex
1682
+ ? Uint8Array.fromHex(hex)
1683
+ : hexToUint8Array(hex, ctx, lineNum, col);
1684
+ return [result, i];
1685
+ }
1686
+
1687
+ // Note: parseMultilineAngleBytes was removed as dead code.
1688
+ // The "< hex" syntax (without closing ">") is invalid - inline byte arrays must be closed on the same line.
1689
+
1690
+ /**
1691
+ * @param {Token[]} tokens
1692
+ * @param {number} i
1693
+ * @param {string} key
1694
+ * @param {ParseContext} ctx
1695
+ * @returns {[Record<string, unknown>, number]}
1696
+ */
1697
+ function parseObjectOrNamedArray(tokens, i, key, ctx = {}) {
1698
+ const keyToken = tokens[i];
1699
+ const keyValue = splitKeyValue(
1700
+ keyToken.text,
1701
+ keyToken.col ?? 0,
1702
+ ctx,
1703
+ keyToken.lineNum ?? 0,
1704
+ );
1705
+ i++;
1706
+ while (
1707
+ i < tokens.length &&
1708
+ (tokens[i].type === "break" || tokens[i].type === "stop")
1709
+ )
1710
+ i++;
1711
+ const baseIndent = i < tokens.length ? (tokens[i].indent ?? 0) : 0;
1712
+ const first = i < tokens.length ? tokens[i] : null;
1713
+ // Check for empty property with no nested content
1714
+ if (
1715
+ !first ||
1716
+ (first.type === "text" && (first.indent ?? 0) <= (keyToken.indent ?? 0))
1717
+ ) {
1718
+ // Check if the next token is a sibling property (same indent) or parent (lower indent)
1719
+ // If so, this property has no value which is invalid
1720
+ if (
1721
+ !first ||
1722
+ (first.type === "text" &&
1723
+ splitKeyValue(first.text, first.col ?? 0, ctx, first.lineNum ?? 0))
1724
+ ) {
1725
+ const col = (keyToken.col ?? 0) + key.length + 1;
1726
+ throw new Error(
1727
+ "Expected value after property" +
1728
+ locSuffix(ctx, keyToken.lineNum ?? 0, col),
1729
+ );
1730
+ }
1731
+ }
1732
+ if (first && first.type === "start" && first.text === "-") {
1733
+ const [arr] = parseListArray(tokens, i);
1734
+ return [{ [key]: arr }, skipToNextKey(tokens, i, baseIndent)];
1735
+ }
1736
+ // Note: '*' syntax for multiline bytes is rejected by the scanner
1737
+ // Note: "key: <" without closing ">" is invalid - inline byte arrays must be closed on the same line
1738
+ if (first && first.type === "text" && first.text === '"') {
1739
+ const [body, next] = parseBlockStringWithIndent(
1740
+ tokens,
1741
+ i,
1742
+ undefined,
1743
+ false,
1744
+ -1,
1745
+ );
1746
+ return [{ [key]: body }, next];
1747
+ }
1748
+ // Reject block string leader on separate line - must be on same line as key
1749
+ if (first && first.type === "text" && first.text.trim() === "`") {
1750
+ throw new Error(
1751
+ "Unexpected indent" + locSuffix(ctx, first.lineNum ?? 0, 0),
1752
+ );
1753
+ }
1754
+ // Concatenated quoted strings (multiple quoted strings on consecutive lines)
1755
+ if (first && first.type === "text") {
1756
+ const trimmed = first.text.trim();
1757
+ if (
1758
+ (trimmed.startsWith('"') &&
1759
+ trimmed.endsWith('"') &&
1760
+ trimmed.length >= 2) ||
1761
+ (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2)
1762
+ ) {
1763
+ const result = parseConcatenatedStrings(tokens, i, baseIndent, ctx);
1764
+ if (result !== null) {
1765
+ const [concatStr, next] = result;
1766
+ return [{ [key]: concatStr }, next];
1767
+ }
1768
+ // Single string on new line is invalid - fall through to error
1769
+ throw new Error(
1770
+ "Unexpected indent" + locSuffix(ctx, first.lineNum ?? 0, 0),
1771
+ );
1772
+ }
1773
+ }
1774
+ const obj = {};
1775
+ while (i < tokens.length) {
1776
+ const t = tokens[i];
1777
+ if (t.type === "stop") {
1778
+ i++;
1779
+ continue;
1780
+ }
1781
+ if (t.type === "text") {
1782
+ const s = t.text;
1783
+ // Reject inline values on separate line (they look like keys starting with special chars)
1784
+ if (s.startsWith("{") || s.startsWith("[") || s.startsWith("<")) {
1785
+ throw new Error(
1786
+ "Unexpected indent" + locSuffix(ctx, t.lineNum ?? 0, 0),
1787
+ );
1788
+ }
1789
+ const keyValue = splitKeyValue(s, t.col ?? 0, ctx, t.lineNum ?? 0);
1790
+ if (keyValue) {
1791
+ const k = keyValue.key;
1792
+ const vPart = keyValue.valuePart;
1793
+ if ((t.indent ?? 0) < baseIndent) break;
1794
+ if (k && (t.indent ?? 0) <= baseIndent && obj.hasOwnProperty(k)) break;
1795
+ if (k && (t.indent ?? 0) <= baseIndent && !obj.hasOwnProperty(k)) {
1796
+ if (vPart === "{}") {
1797
+ obj[k] = {};
1798
+ i++;
1799
+ } else if (vPart.startsWith(">")) {
1800
+ // Block bytes in property context
1801
+ if (!isPropertyBlockLeaderOnly(vPart, ">")) {
1802
+ throw new Error(
1803
+ "Expected newline after block leader in property",
1804
+ );
1805
+ }
1806
+ const [bytes, next] = parseBlockBytes(
1807
+ tokens,
1808
+ i,
1809
+ ctx,
1810
+ "",
1811
+ t.indent ?? 0,
1812
+ );
1813
+ obj[k] = bytes;
1814
+ i = next;
1815
+ } else if (vPart.trim() === "`") {
1816
+ // Block string in property context: backtick alone on line
1817
+ const [body, next] = parseBlockStringWithIndent(
1818
+ tokens,
1819
+ i,
1820
+ "",
1821
+ true,
1822
+ t.indent ?? 0,
1823
+ );
1824
+ obj[k] = body;
1825
+ i = next;
1826
+ } else if (vPart === "") {
1827
+ i++;
1828
+ while (i < tokens.length && tokens[i].type === "break") i++;
1829
+ const nextT = tokens[i];
1830
+ // Note: '*' syntax for multiline bytes is rejected by the scanner
1831
+ if (nextT && nextT.type === "text" && nextT.text === '"') {
1832
+ const [body, next] = parseBlockStringWithIndent(
1833
+ tokens,
1834
+ i,
1835
+ undefined,
1836
+ false,
1837
+ -1,
1838
+ );
1839
+ obj[k] = body;
1840
+ i = next;
1841
+ } else if (nextT && nextT.type === "start" && nextT.text === "-") {
1842
+ const [arr, next] = parseListArray(tokens, i);
1843
+ obj[k] = arr;
1844
+ i = next;
1845
+ } else if (
1846
+ nextT &&
1847
+ nextT.type === "text" &&
1848
+ (nextT.indent ?? 0) > (t.indent ?? 0)
1849
+ ) {
1850
+ const [child, next] = parseObjectBlock(
1851
+ tokens,
1852
+ i,
1853
+ nextT.indent ?? 0,
1854
+ ctx,
1855
+ );
1856
+ obj[k] = child;
1857
+ i = next;
1858
+ } else {
1859
+ // Empty property with no nested content is invalid
1860
+ throw new Error(
1861
+ "Expected value after property" +
1862
+ locSuffix(ctx, t.lineNum ?? 0, (t.col ?? 0) + k.length + 1),
1863
+ );
1864
+ }
1865
+ } else {
1866
+ // Inline value (scalar, array, object, bytes)
1867
+ obj[k] = parseScalar(vPart, ctx, t.lineNum ?? 0, keyValue.valueCol);
1868
+ i++;
1869
+ }
1870
+ } else {
1871
+ i++;
1872
+ }
1873
+ } else {
1874
+ // Text without colon in nested object context is invalid
1875
+ // (e.g., inline array/object/bytes/string on separate line)
1876
+ throw new Error(
1877
+ "Unexpected indent" + locSuffix(ctx, t.lineNum ?? 0, 0),
1878
+ );
1879
+ }
1880
+ } else {
1881
+ i++;
1882
+ }
1883
+ }
1884
+ return [{ [key]: Object.keys(obj).length ? obj : undefined }, i];
1885
+ }
1886
+
1887
+ function skipToNextKey(tokens, i, baseIndent) {
1888
+ while (
1889
+ i < tokens.length &&
1890
+ tokens[i].type !== "stop" &&
1891
+ (tokens[i].indent ?? 0) > baseIndent
1892
+ )
1893
+ i++;
1894
+ while (i < tokens.length && tokens[i].type === "stop") i++;
1895
+ return i;
1896
+ }
1897
+
1898
+ /**
1899
+ * @param {Token[]} tokens
1900
+ * @param {number} i
1901
+ * @param {number} baseIndent
1902
+ * @param {ParseContext} ctx
1903
+ * @param {number=} inlineIndent
1904
+ * @returns {[Record<string, unknown>, number]}
1905
+ */
1906
+ function parseObjectBlock(tokens, i, baseIndent, ctx = {}, inlineIndent) {
1907
+ const obj = {};
1908
+ let firstText = true;
1909
+ while (i < tokens.length) {
1910
+ const t = tokens[i];
1911
+ if (inlineIndent !== undefined) {
1912
+ if (t.type === "stop") break;
1913
+ if (t.type === "start" && (t.indent ?? 0) <= inlineIndent) break;
1914
+ }
1915
+ if (t.type === "stop") {
1916
+ i++;
1917
+ continue;
1918
+ }
1919
+ if (t.type !== "text") {
1920
+ i++;
1921
+ continue;
1922
+ }
1923
+ let indent = t.indent ?? 0;
1924
+ if (firstText && inlineIndent !== undefined && indent === inlineIndent) {
1925
+ indent = baseIndent;
1926
+ }
1927
+ firstText = false;
1928
+ if (indent < baseIndent) break;
1929
+ if (indent > baseIndent) {
1930
+ i++;
1931
+ continue;
1932
+ }
1933
+ const keyValue = splitKeyValue(t.text, t.col ?? 0, ctx, t.lineNum ?? 0);
1934
+ if (!keyValue) {
1935
+ i++;
1936
+ continue;
1937
+ }
1938
+ const k = keyValue.key;
1939
+ const vPart = keyValue.valuePart;
1940
+ // Note: "key: <" without closing ">" is invalid - inline byte arrays must be closed on the same line
1941
+ if (vPart.startsWith(">")) {
1942
+ if (!isPropertyBlockLeaderOnly(vPart, ">")) {
1943
+ throw new Error("Expected newline after block leader in property");
1944
+ }
1945
+ const [bytes, j] = parseBlockBytes(tokens, i, ctx, "", baseIndent);
1946
+ obj[k] = bytes;
1947
+ i = j;
1948
+ continue;
1949
+ }
1950
+ if (vPart === "{}") {
1951
+ obj[k] = {};
1952
+ i++;
1953
+ continue;
1954
+ }
1955
+ // Block string in property context: backtick alone on line
1956
+ if (vPart.trim() === "`") {
1957
+ const [body, next] = parseBlockStringWithIndent(
1958
+ tokens,
1959
+ i,
1960
+ "",
1961
+ true,
1962
+ t.indent ?? 0,
1963
+ );
1964
+ obj[k] = body;
1965
+ i = next;
1966
+ continue;
1967
+ }
1968
+ if (vPart === "") {
1969
+ i++;
1970
+ while (
1971
+ i < tokens.length &&
1972
+ (tokens[i].type === "break" || tokens[i].type === "stop")
1973
+ )
1974
+ i++;
1975
+ const nextT = tokens[i];
1976
+ if (nextT && nextT.type === "start" && nextT.text === "-") {
1977
+ // Pass baseIndent as minIndent so nested arrays stop at the object's level
1978
+ const [arr, next] = parseListArray(tokens, i, ctx, baseIndent);
1979
+ obj[k] = arr;
1980
+ i = next;
1981
+ continue;
1982
+ }
1983
+ // Note: "key: <" without closing ">" is invalid - inline byte arrays must be closed on the same line
1984
+ // Note: A line with just '"' is not valid - block strings use backtick (`)
1985
+ if (nextT && nextT.type === "text" && (nextT.indent ?? 0) > baseIndent) {
1986
+ const [child, next] = parseObjectBlock(
1987
+ tokens,
1988
+ i,
1989
+ nextT.indent ?? 0,
1990
+ ctx,
1991
+ );
1992
+ obj[k] = child;
1993
+ i = next;
1994
+ continue;
1995
+ }
1996
+ // Empty property with no nested content is handled by parseObjectOrNamedArray
1997
+ // which throws an error before we reach here
1998
+ continue;
1999
+ }
2000
+ // Inline value (scalar, array, object, bytes)
2001
+ obj[k] = parseScalar(vPart, ctx, t.lineNum ?? 0, keyValue.valueCol);
2002
+ i++;
2003
+ }
2004
+ return [obj, i];
2005
+ }
2006
+
2007
+ // Root as object (multiple key: value lines at indent 0)
2008
+ /**
2009
+ * @param {Token[]} tokens
2010
+ * @param {number} i
2011
+ * @param {ParseContext} ctx
2012
+ * @returns {[Record<string, unknown>, number]}
2013
+ */
2014
+ function parseRootObject(tokens, i, ctx = {}) {
2015
+ const obj = {};
2016
+ const baseIndent = 0;
2017
+ while (i < tokens.length) {
2018
+ const t = tokens[i];
2019
+ if (t.type === "stop") {
2020
+ i++;
2021
+ continue;
2022
+ }
2023
+ if (t.type === "text") {
2024
+ const s = t.text;
2025
+ const keyValue = splitKeyValue(s, t.col ?? 0, ctx, t.lineNum ?? 0);
2026
+ if (keyValue && (t.indent ?? 0) === baseIndent) {
2027
+ const k = keyValue.key;
2028
+ const vPart = keyValue.valuePart;
2029
+ // Note: "key: <" without closing ">" is invalid - inline byte arrays must be closed on the same line
2030
+ if (vPart.startsWith(">")) {
2031
+ if (!isPropertyBlockLeaderOnly(vPart, ">")) {
2032
+ throw new Error("Expected newline after block leader in property");
2033
+ }
2034
+ const [bytes, j] = parseBlockBytes(tokens, i, ctx, "", baseIndent);
2035
+ obj[k] = bytes;
2036
+ i = j;
2037
+ } else if (vPart === "{}") {
2038
+ obj[k] = {};
2039
+ i++;
2040
+ // Note: "key: \"" is an unterminated string error, not a block string
2041
+ // Block strings use backtick (`) not double-quote (")
2042
+ } else if (vPart.startsWith("`")) {
2043
+ if (!isPropertyBlockLeaderOnly(vPart, "`")) {
2044
+ throw new Error("Expected newline after block leader in property");
2045
+ }
2046
+ i++;
2047
+ while (
2048
+ i < tokens.length &&
2049
+ (tokens[i].type === "break" || tokens[i].type === "stop")
2050
+ )
2051
+ i++;
2052
+ const nextT = tokens[i];
2053
+ if (nextT && nextT.type === "text" && nextT.text === "`") {
2054
+ // Empty block string (just opening and closing backticks) is not allowed
2055
+ throw new Error(
2056
+ 'Empty block string not allowed (use "" or "\\n" explicitly)',
2057
+ );
2058
+ } else {
2059
+ const withIndent = [];
2060
+ while (
2061
+ i < tokens.length &&
2062
+ ((tokens[i].type === "text" &&
2063
+ (tokens[i].indent ?? 0) > baseIndent) ||
2064
+ tokens[i].type === "break")
2065
+ ) {
2066
+ if (tokens[i].type === "break") {
2067
+ withIndent.push({ indent: undefined, text: "" });
2068
+ i++;
2069
+ } else {
2070
+ withIndent.push({
2071
+ indent: tokens[i].indent ?? 0,
2072
+ text: tokens[i].text,
2073
+ });
2074
+ i++;
2075
+ }
2076
+ }
2077
+ const minIndent = withIndent
2078
+ .filter((x) => x.indent !== undefined)
2079
+ .reduce((min, x) => (x.indent < min ? x.indent : min), Infinity);
2080
+ const effectiveMin = minIndent === Infinity ? 0 : minIndent;
2081
+ const bodyLines = withIndent.map(({ indent, text }) =>
2082
+ indent === undefined
2083
+ ? ""
2084
+ : (indent - effectiveMin > 0
2085
+ ? " ".repeat(indent - effectiveMin)
2086
+ : "") + text,
2087
+ );
2088
+ let endLine = bodyLines.length;
2089
+ while (endLine > 0 && bodyLines[endLine - 1] === "") endLine--;
2090
+ const trimmedLines = bodyLines.slice(0, endLine);
2091
+ obj[k] =
2092
+ trimmedLines.join("\n") + (trimmedLines.length > 0 ? "\n" : "");
2093
+ }
2094
+ } else if (vPart === "") {
2095
+ const [valueObj, next] = parseObjectOrNamedArray(tokens, i, k, ctx);
2096
+ obj[k] = valueObj[k];
2097
+ i = next;
2098
+ } else {
2099
+ // Inline value (scalar, array, object, bytes)
2100
+ obj[k] = parseScalar(vPart, ctx, t.lineNum ?? 0, keyValue.valueCol);
2101
+ i++;
2102
+ }
2103
+ } else {
2104
+ i++;
2105
+ }
2106
+ } else {
2107
+ i++;
2108
+ }
2109
+ }
2110
+ return [obj, i];
2111
+ }
2112
+
2113
+ export { parseYay };