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.
- package/README.md +868 -0
- package/package.json +23 -0
- 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 };
|