@o3co/ts.hocon 1.1.0 → 1.3.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.ja.md +14 -6
- package/README.md +22 -6
- package/dist/index.cjs +601 -204
- package/dist/index.js +601 -204
- package/dist/zod.cjs +601 -204
- package/dist/zod.js +601 -204
- package/package.json +4 -4
package/dist/zod.cjs
CHANGED
|
@@ -39,6 +39,27 @@ module.exports = __toCommonJS(zod_exports);
|
|
|
39
39
|
|
|
40
40
|
// src/coerce.ts
|
|
41
41
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
42
|
+
function isHoconWs(cp) {
|
|
43
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
44
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
45
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
46
|
+
if (cp === 5760) return true;
|
|
47
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
48
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
49
|
+
if (cp === 12288) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function trimHoconWs(s) {
|
|
53
|
+
let start = 0;
|
|
54
|
+
let end = s.length;
|
|
55
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
56
|
+
start++;
|
|
57
|
+
}
|
|
58
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
59
|
+
end--;
|
|
60
|
+
}
|
|
61
|
+
return s.slice(start, end);
|
|
62
|
+
}
|
|
42
63
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
43
64
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
44
65
|
function coerceBoolean(value) {
|
|
@@ -84,22 +105,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
84
105
|
d: 864e5
|
|
85
106
|
};
|
|
86
107
|
function parseDuration(value, outputUnit = "ms") {
|
|
87
|
-
const trimmed = value
|
|
108
|
+
const trimmed = trimHoconWs(value);
|
|
88
109
|
let i = 0;
|
|
89
110
|
while (i < trimmed.length) {
|
|
90
111
|
const ch = trimmed[i];
|
|
91
|
-
if (
|
|
112
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
92
117
|
i++;
|
|
93
118
|
}
|
|
94
119
|
if (i === 0) return NaN;
|
|
95
120
|
const num = Number(trimmed.slice(0, i));
|
|
96
121
|
if (Number.isNaN(num)) return NaN;
|
|
97
|
-
const unit = trimmed.slice(i)
|
|
122
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
123
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
124
|
+
if (divisor === void 0) return NaN;
|
|
125
|
+
if (unit === "") {
|
|
126
|
+
return num / divisor;
|
|
127
|
+
}
|
|
98
128
|
const mult = DURATION_UNITS[unit];
|
|
99
129
|
if (mult === void 0) return NaN;
|
|
100
130
|
const ms = num * mult;
|
|
101
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
102
|
-
if (divisor === void 0) return NaN;
|
|
103
131
|
return ms / divisor;
|
|
104
132
|
}
|
|
105
133
|
var BYTE_UNITS = {
|
|
@@ -130,6 +158,22 @@ var BYTE_UNITS = {
|
|
|
130
158
|
TiB: 1099511627776,
|
|
131
159
|
tebibyte: 1099511627776,
|
|
132
160
|
tebibytes: 1099511627776,
|
|
161
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
162
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
163
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
164
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
165
|
+
K: 1024,
|
|
166
|
+
k: 1024,
|
|
167
|
+
M: 1024 ** 2,
|
|
168
|
+
m: 1024 ** 2,
|
|
169
|
+
G: 1024 ** 3,
|
|
170
|
+
g: 1024 ** 3,
|
|
171
|
+
T: 1024 ** 4,
|
|
172
|
+
t: 1024 ** 4,
|
|
173
|
+
P: 1024 ** 5,
|
|
174
|
+
p: 1024 ** 5,
|
|
175
|
+
E: 1024 ** 6,
|
|
176
|
+
e: 1024 ** 6,
|
|
133
177
|
// lowercase short-form aliases
|
|
134
178
|
b: 1,
|
|
135
179
|
kb: 1e3,
|
|
@@ -153,25 +197,39 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
153
197
|
TiB: 1099511627776
|
|
154
198
|
};
|
|
155
199
|
function parseBytes(value, outputUnit = "B") {
|
|
156
|
-
const trimmed = value
|
|
200
|
+
const trimmed = trimHoconWs(value);
|
|
157
201
|
let i = 0;
|
|
158
202
|
while (i < trimmed.length) {
|
|
159
203
|
const ch = trimmed[i];
|
|
160
|
-
if (
|
|
204
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
161
209
|
i++;
|
|
162
210
|
}
|
|
163
211
|
if (i === 0) return NaN;
|
|
164
212
|
const num = Number(trimmed.slice(0, i));
|
|
165
213
|
if (Number.isNaN(num)) return NaN;
|
|
166
|
-
const unit = trimmed.slice(i)
|
|
214
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
215
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
216
|
+
if (divisor === void 0) return NaN;
|
|
217
|
+
if (unit === "") {
|
|
218
|
+
const bytes2 = Math.trunc(num);
|
|
219
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
220
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
221
|
+
}
|
|
222
|
+
return bytes2 / divisor;
|
|
223
|
+
}
|
|
167
224
|
let mult = BYTE_UNITS[unit];
|
|
168
225
|
if (mult === void 0) {
|
|
169
226
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
170
227
|
}
|
|
171
228
|
if (mult === void 0) return NaN;
|
|
172
229
|
const bytes = num * mult;
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
231
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
232
|
+
}
|
|
175
233
|
const result = bytes / divisor;
|
|
176
234
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
177
235
|
}
|
|
@@ -205,6 +263,20 @@ var ConfigError = class extends Error {
|
|
|
205
263
|
};
|
|
206
264
|
|
|
207
265
|
// src/internal/lexer/lexer.ts
|
|
266
|
+
function isHoconWhitespace(ch) {
|
|
267
|
+
const cp = ch.codePointAt(0) ?? -1;
|
|
268
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
269
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
270
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
271
|
+
if (cp === 5760) return true;
|
|
272
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
273
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
274
|
+
if (cp === 12288) return true;
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
function isHoconNewline(ch) {
|
|
278
|
+
return ch === "\n";
|
|
279
|
+
}
|
|
208
280
|
var SINGLE_CHAR_TOKENS = {
|
|
209
281
|
"{": "lbrace",
|
|
210
282
|
"}": "rbrace",
|
|
@@ -227,18 +299,18 @@ var Lexer = class {
|
|
|
227
299
|
while (this.pos < this.input.length) {
|
|
228
300
|
const sl = this.line, sc = this.col;
|
|
229
301
|
const ch = this.peek();
|
|
230
|
-
if (ch
|
|
231
|
-
this.advance();
|
|
232
|
-
this.hadSpace = true;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (ch === "\n") {
|
|
302
|
+
if (isHoconNewline(ch)) {
|
|
236
303
|
this.advance();
|
|
237
304
|
if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
|
|
238
305
|
this.push("newline", "\n", sl, sc);
|
|
239
306
|
}
|
|
240
307
|
continue;
|
|
241
308
|
}
|
|
309
|
+
if (isHoconWhitespace(ch)) {
|
|
310
|
+
this.advance();
|
|
311
|
+
this.hadSpace = true;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
242
314
|
if (ch === "/" && this.peek(1) === "/") {
|
|
243
315
|
while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
|
|
244
316
|
this.hadSpace = true;
|
|
@@ -268,16 +340,16 @@ var Lexer = class {
|
|
|
268
340
|
if (ch === "$" && this.peek(1) === "{") {
|
|
269
341
|
this.advance();
|
|
270
342
|
this.advance();
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.
|
|
343
|
+
const payload = this.parseSubstBody(sl, sc);
|
|
344
|
+
const value = payload.segments.map((s) => {
|
|
345
|
+
const t = s.text;
|
|
346
|
+
if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
|
|
347
|
+
const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
348
|
+
return `"${escaped}"`;
|
|
349
|
+
}
|
|
350
|
+
return t;
|
|
351
|
+
}).join(".");
|
|
352
|
+
this.pushSubst(payload, value, sl, sc);
|
|
281
353
|
continue;
|
|
282
354
|
}
|
|
283
355
|
if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
|
|
@@ -312,58 +384,7 @@ var Lexer = class {
|
|
|
312
384
|
}
|
|
313
385
|
if (ch === '"') {
|
|
314
386
|
this.advance();
|
|
315
|
-
|
|
316
|
-
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
317
|
-
if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
|
|
318
|
-
if (this.peek() === "\\") {
|
|
319
|
-
this.advance();
|
|
320
|
-
const esc = this.advance();
|
|
321
|
-
switch (esc) {
|
|
322
|
-
case "n":
|
|
323
|
-
value += "\n";
|
|
324
|
-
break;
|
|
325
|
-
case "t":
|
|
326
|
-
value += " ";
|
|
327
|
-
break;
|
|
328
|
-
case "r":
|
|
329
|
-
value += "\r";
|
|
330
|
-
break;
|
|
331
|
-
case '"':
|
|
332
|
-
value += '"';
|
|
333
|
-
break;
|
|
334
|
-
case "\\":
|
|
335
|
-
value += "\\";
|
|
336
|
-
break;
|
|
337
|
-
case "/":
|
|
338
|
-
value += "/";
|
|
339
|
-
break;
|
|
340
|
-
case "b":
|
|
341
|
-
value += "\b";
|
|
342
|
-
break;
|
|
343
|
-
case "f":
|
|
344
|
-
value += "\f";
|
|
345
|
-
break;
|
|
346
|
-
case "u": {
|
|
347
|
-
if (this.pos + 4 > this.input.length) {
|
|
348
|
-
throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
|
|
349
|
-
}
|
|
350
|
-
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
351
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
352
|
-
throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
|
|
353
|
-
}
|
|
354
|
-
value += String.fromCharCode(parseInt(hex, 16));
|
|
355
|
-
for (let i = 0; i < 4; i++) this.advance();
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
default:
|
|
359
|
-
throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
|
|
360
|
-
}
|
|
361
|
-
} else {
|
|
362
|
-
value += this.advance();
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
|
|
366
|
-
this.advance();
|
|
387
|
+
const value = this.readQuotedStringBody(sl, sc);
|
|
367
388
|
this.push("string", value, sl, sc, true);
|
|
368
389
|
continue;
|
|
369
390
|
}
|
|
@@ -397,22 +418,255 @@ var Lexer = class {
|
|
|
397
418
|
this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
|
|
398
419
|
this.hadSpace = false;
|
|
399
420
|
}
|
|
421
|
+
pushSubst(payload, value, l, c) {
|
|
422
|
+
this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
|
|
423
|
+
this.hadSpace = false;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Read body of a quoted string. Opening `"` must already be consumed.
|
|
427
|
+
* Returns decoded string. Throws ParseError on unterminated/invalid-escape.
|
|
428
|
+
* openLine/openCol are the position of the opening `"` for error reporting.
|
|
429
|
+
*/
|
|
430
|
+
readQuotedStringBody(openLine, openCol) {
|
|
431
|
+
let value = "";
|
|
432
|
+
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
433
|
+
if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
|
|
434
|
+
if (this.peek() === "\\") {
|
|
435
|
+
const escCol = this.col;
|
|
436
|
+
this.advance();
|
|
437
|
+
if (this.pos >= this.input.length) {
|
|
438
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
439
|
+
}
|
|
440
|
+
const esc = this.advance();
|
|
441
|
+
switch (esc) {
|
|
442
|
+
case "n":
|
|
443
|
+
value += "\n";
|
|
444
|
+
break;
|
|
445
|
+
case "t":
|
|
446
|
+
value += " ";
|
|
447
|
+
break;
|
|
448
|
+
case "r":
|
|
449
|
+
value += "\r";
|
|
450
|
+
break;
|
|
451
|
+
case '"':
|
|
452
|
+
value += '"';
|
|
453
|
+
break;
|
|
454
|
+
case "\\":
|
|
455
|
+
value += "\\";
|
|
456
|
+
break;
|
|
457
|
+
case "/":
|
|
458
|
+
value += "/";
|
|
459
|
+
break;
|
|
460
|
+
case "b":
|
|
461
|
+
value += "\b";
|
|
462
|
+
break;
|
|
463
|
+
case "f":
|
|
464
|
+
value += "\f";
|
|
465
|
+
break;
|
|
466
|
+
case "u": {
|
|
467
|
+
if (this.pos + 4 > this.input.length) {
|
|
468
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
469
|
+
}
|
|
470
|
+
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
471
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
472
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
473
|
+
}
|
|
474
|
+
const code = parseInt(hex, 16);
|
|
475
|
+
value += String.fromCharCode(code);
|
|
476
|
+
for (let i = 0; i < 4; i++) this.advance();
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
default:
|
|
480
|
+
throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
value += this.advance();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (this.pos >= this.input.length || this.peek() !== '"') {
|
|
487
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
488
|
+
}
|
|
489
|
+
this.advance();
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
494
|
+
* Implements Appendix A state machine from the cross-language spec.
|
|
495
|
+
*
|
|
496
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
497
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
498
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
499
|
+
*/
|
|
500
|
+
parseSubstBody(startLine, startCol) {
|
|
501
|
+
let optional = false;
|
|
502
|
+
if (this.peek() === "?") {
|
|
503
|
+
this.advance();
|
|
504
|
+
optional = true;
|
|
505
|
+
}
|
|
506
|
+
let curText = "";
|
|
507
|
+
let curStarted = false;
|
|
508
|
+
let curLine = 0;
|
|
509
|
+
let curCol = 0;
|
|
510
|
+
let pendingWs = "";
|
|
511
|
+
const segments = [];
|
|
512
|
+
let lastDot = null;
|
|
513
|
+
let listSuffix = false;
|
|
514
|
+
while (true) {
|
|
515
|
+
if (this.pos >= this.input.length) {
|
|
516
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
517
|
+
}
|
|
518
|
+
const ch = this.peek();
|
|
519
|
+
if (ch === "}") {
|
|
520
|
+
this.advance();
|
|
521
|
+
pendingWs = "";
|
|
522
|
+
break;
|
|
523
|
+
} else if (ch === "[") {
|
|
524
|
+
if (!curStarted) {
|
|
525
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
526
|
+
}
|
|
527
|
+
for (const w of pendingWs) {
|
|
528
|
+
if (w !== " " && w !== " ") {
|
|
529
|
+
throw new ParseError(
|
|
530
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
531
|
+
startLine,
|
|
532
|
+
this.col
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
537
|
+
pendingWs = "";
|
|
538
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
539
|
+
break;
|
|
540
|
+
} else if (ch === '"') {
|
|
541
|
+
const qLine = startLine;
|
|
542
|
+
const qCol = this.col;
|
|
543
|
+
if (curStarted) {
|
|
544
|
+
curText += pendingWs;
|
|
545
|
+
}
|
|
546
|
+
pendingWs = "";
|
|
547
|
+
this.advance();
|
|
548
|
+
const decoded = this.readQuotedStringBody(qLine, qCol);
|
|
549
|
+
curText += decoded;
|
|
550
|
+
if (!curStarted) {
|
|
551
|
+
curLine = qLine;
|
|
552
|
+
curCol = qCol;
|
|
553
|
+
curStarted = true;
|
|
554
|
+
}
|
|
555
|
+
} else if (isUnquotedSubstChar(ch)) {
|
|
556
|
+
if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
|
|
557
|
+
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
558
|
+
throw new ParseError(
|
|
559
|
+
`unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
560
|
+
startLine,
|
|
561
|
+
this.col
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
const uCol = this.col;
|
|
565
|
+
if (curStarted) {
|
|
566
|
+
curText += pendingWs;
|
|
567
|
+
}
|
|
568
|
+
pendingWs = "";
|
|
569
|
+
if (!curStarted) {
|
|
570
|
+
curLine = startLine;
|
|
571
|
+
curCol = uCol;
|
|
572
|
+
curStarted = true;
|
|
573
|
+
}
|
|
574
|
+
while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
|
|
575
|
+
curText += this.advance();
|
|
576
|
+
}
|
|
577
|
+
} else if (ch === ".") {
|
|
578
|
+
const dotCol = this.col;
|
|
579
|
+
pendingWs = "";
|
|
580
|
+
if (!curStarted) {
|
|
581
|
+
throw new ParseError("empty segment in path", startLine, dotCol);
|
|
582
|
+
}
|
|
583
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
584
|
+
curText = "";
|
|
585
|
+
curStarted = false;
|
|
586
|
+
curLine = 0;
|
|
587
|
+
curCol = 0;
|
|
588
|
+
lastDot = [startLine, dotCol];
|
|
589
|
+
this.advance();
|
|
590
|
+
} else if (isHoconNewline(ch)) {
|
|
591
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
592
|
+
} else if (isHoconWhitespace(ch)) {
|
|
593
|
+
pendingWs += ch;
|
|
594
|
+
this.advance();
|
|
595
|
+
} else {
|
|
596
|
+
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (!listSuffix) {
|
|
600
|
+
if (curStarted) {
|
|
601
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
602
|
+
} else if (segments.length === 0) {
|
|
603
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
604
|
+
} else {
|
|
605
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
606
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return { segments, optional, listSuffix };
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
613
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
614
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
615
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
616
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
617
|
+
*/
|
|
618
|
+
parseListSuffix(startLine) {
|
|
619
|
+
this.advance();
|
|
620
|
+
const afterBracket = this.peek();
|
|
621
|
+
if (afterBracket !== "]") {
|
|
622
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
623
|
+
throw new ParseError(
|
|
624
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
625
|
+
startLine,
|
|
626
|
+
this.col
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
this.advance();
|
|
630
|
+
if (this.peek() !== "}") {
|
|
631
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
632
|
+
throw new ParseError(
|
|
633
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
634
|
+
startLine,
|
|
635
|
+
this.col
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
this.advance();
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
400
641
|
};
|
|
401
642
|
function tokenize(input) {
|
|
402
643
|
return new Lexer(input).tokenize();
|
|
403
644
|
}
|
|
645
|
+
function isUnquotedSubstChar(ch) {
|
|
646
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
647
|
+
if ('"\\'.includes(ch)) return false;
|
|
648
|
+
if ("{}[]".includes(ch)) return false;
|
|
649
|
+
if (":=,+#`^?!@*&$.".includes(ch)) return false;
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
function isDecimalDigit(ch) {
|
|
653
|
+
return ch >= "0" && ch <= "9";
|
|
654
|
+
}
|
|
404
655
|
function isUnquotedStart(ch) {
|
|
405
|
-
|
|
656
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
657
|
+
if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
|
|
658
|
+
return true;
|
|
406
659
|
}
|
|
407
660
|
function isUnquotedContinue(ch, nextFn) {
|
|
408
|
-
if (ch === "" ||
|
|
661
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
662
|
+
if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
|
|
409
663
|
if (ch === "+" && nextFn() === "=") return false;
|
|
410
664
|
if (ch === "/" && nextFn() === "/") return false;
|
|
411
665
|
return true;
|
|
412
666
|
}
|
|
413
667
|
|
|
414
668
|
// src/internal/parser/parser.ts
|
|
415
|
-
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
|
|
669
|
+
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
|
|
416
670
|
var Parser = class {
|
|
417
671
|
constructor(tokens) {
|
|
418
672
|
this.tokens = tokens;
|
|
@@ -470,6 +724,14 @@ var Parser = class {
|
|
|
470
724
|
const t = this.peek();
|
|
471
725
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
472
726
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
727
|
+
const next = this.peek(1);
|
|
728
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
729
|
+
throw new ParseError(
|
|
730
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
731
|
+
t.line,
|
|
732
|
+
t.col
|
|
733
|
+
);
|
|
734
|
+
}
|
|
473
735
|
this.advance();
|
|
474
736
|
fields.push(this.parseInclude());
|
|
475
737
|
this.skip("newline");
|
|
@@ -478,7 +740,14 @@ var Parser = class {
|
|
|
478
740
|
continue;
|
|
479
741
|
}
|
|
480
742
|
const keyPos = this.currentPos();
|
|
481
|
-
const key = this.parseKey();
|
|
743
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
744
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
745
|
+
throw new ParseError(
|
|
746
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
747
|
+
keyPos.line,
|
|
748
|
+
keyPos.col
|
|
749
|
+
);
|
|
750
|
+
}
|
|
482
751
|
this.skip("newline");
|
|
483
752
|
let append = false;
|
|
484
753
|
const sep = this.peek();
|
|
@@ -508,18 +777,31 @@ var Parser = class {
|
|
|
508
777
|
parseKey() {
|
|
509
778
|
const segments = [];
|
|
510
779
|
let trailingDot = false;
|
|
780
|
+
let firstWasQuoted = false;
|
|
511
781
|
while (true) {
|
|
512
782
|
const t = this.peek();
|
|
513
783
|
if (t.kind === "string") {
|
|
514
784
|
this.advance();
|
|
785
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
515
786
|
segments.push(t.value);
|
|
516
787
|
trailingDot = false;
|
|
517
788
|
} else if (t.kind === "unquoted") {
|
|
518
789
|
this.advance();
|
|
519
|
-
const
|
|
790
|
+
const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
|
|
791
|
+
const parts = raw.split(".");
|
|
520
792
|
const filtered = parts.filter((s) => s.length > 0);
|
|
793
|
+
for (const seg of filtered) {
|
|
794
|
+
if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
|
|
795
|
+
const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
|
|
796
|
+
throw new ParseError(
|
|
797
|
+
`unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
|
|
798
|
+
t.line,
|
|
799
|
+
t.col
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
521
803
|
segments.push(...filtered);
|
|
522
|
-
trailingDot =
|
|
804
|
+
trailingDot = raw.endsWith(".");
|
|
523
805
|
} else {
|
|
524
806
|
if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
|
|
525
807
|
break;
|
|
@@ -531,9 +813,13 @@ var Parser = class {
|
|
|
531
813
|
trailingDot = true;
|
|
532
814
|
continue;
|
|
533
815
|
}
|
|
816
|
+
if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
|
|
817
|
+
trailingDot = true;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
534
820
|
break;
|
|
535
821
|
}
|
|
536
|
-
return segments;
|
|
822
|
+
return { segments, firstWasQuoted };
|
|
537
823
|
}
|
|
538
824
|
parseInclude() {
|
|
539
825
|
const p = this.currentPos();
|
|
@@ -606,9 +892,13 @@ var Parser = class {
|
|
|
606
892
|
} else if (t.kind === "lbracket") {
|
|
607
893
|
this.advance();
|
|
608
894
|
node = this.parseArray();
|
|
609
|
-
} else if (t.kind === "subst"
|
|
895
|
+
} else if (t.kind === "subst") {
|
|
610
896
|
this.advance();
|
|
611
|
-
|
|
897
|
+
if (!t.subst) {
|
|
898
|
+
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
899
|
+
}
|
|
900
|
+
const payload = t.subst;
|
|
901
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
612
902
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
613
903
|
this.advance();
|
|
614
904
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -659,6 +949,20 @@ function parseTokens(tokens) {
|
|
|
659
949
|
return new Parser(tokens).parse();
|
|
660
950
|
}
|
|
661
951
|
|
|
952
|
+
// src/internal/parser/empty-check.ts
|
|
953
|
+
function hasContentTokens(tokens) {
|
|
954
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
955
|
+
}
|
|
956
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
957
|
+
if (!hasContentTokens(tokens)) {
|
|
958
|
+
throw new ParseError(
|
|
959
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
960
|
+
1,
|
|
961
|
+
1
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
662
966
|
// src/internal/resolver/types.ts
|
|
663
967
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
664
968
|
function isSubst(v) {
|
|
@@ -678,64 +982,20 @@ function makeResObj() {
|
|
|
678
982
|
}
|
|
679
983
|
|
|
680
984
|
// src/internal/resolver/utils.ts
|
|
681
|
-
function parseSubstPath(raw) {
|
|
682
|
-
const segments = [];
|
|
683
|
-
let i = 0;
|
|
684
|
-
while (i < raw.length) {
|
|
685
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
686
|
-
if (i >= raw.length) break;
|
|
687
|
-
if (raw[i] === '"') {
|
|
688
|
-
i++;
|
|
689
|
-
let seg = "";
|
|
690
|
-
while (i < raw.length && raw[i] !== '"') {
|
|
691
|
-
if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
|
|
692
|
-
seg += raw[i + 1];
|
|
693
|
-
i += 2;
|
|
694
|
-
} else {
|
|
695
|
-
seg += raw[i];
|
|
696
|
-
i++;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
if (i < raw.length) i++;
|
|
700
|
-
segments.push(seg);
|
|
701
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
702
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
703
|
-
} else if (raw[i] === ".") {
|
|
704
|
-
segments.push("");
|
|
705
|
-
i++;
|
|
706
|
-
} else {
|
|
707
|
-
let seg = "";
|
|
708
|
-
while (i < raw.length && raw[i] !== ".") {
|
|
709
|
-
seg += raw[i];
|
|
710
|
-
i++;
|
|
711
|
-
}
|
|
712
|
-
segments.push(seg.trim());
|
|
713
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
return segments;
|
|
717
|
-
}
|
|
718
985
|
function segmentsToKey(segments) {
|
|
719
986
|
return segments.map((s) => {
|
|
720
|
-
|
|
721
|
-
|
|
987
|
+
const t = s.text;
|
|
988
|
+
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
989
|
+
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
722
990
|
}
|
|
723
|
-
return
|
|
991
|
+
return t;
|
|
724
992
|
}).join(".");
|
|
725
993
|
}
|
|
726
994
|
function lookupPath(root, segments) {
|
|
727
995
|
const [head, ...tail] = segments;
|
|
728
|
-
if (head === void 0
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (val2 === void 0) return void 0;
|
|
732
|
-
if (tail.length === 0) return val2;
|
|
733
|
-
if (isResObj(val2)) return lookupPath(val2, tail);
|
|
734
|
-
return void 0;
|
|
735
|
-
}
|
|
736
|
-
return void 0;
|
|
737
|
-
}
|
|
738
|
-
const val = root.fields.get(head);
|
|
996
|
+
if (head === void 0) return void 0;
|
|
997
|
+
const key = head.text;
|
|
998
|
+
const val = root.fields.get(key);
|
|
739
999
|
if (val === void 0) return void 0;
|
|
740
1000
|
if (tail.length === 0) return val;
|
|
741
1001
|
if (isResObj(val)) return lookupPath(val, tail);
|
|
@@ -744,7 +1004,7 @@ function lookupPath(root, segments) {
|
|
|
744
1004
|
function lookupResObj(root, segments) {
|
|
745
1005
|
let cur = root;
|
|
746
1006
|
for (const seg of segments) {
|
|
747
|
-
const val = cur.fields.get(seg);
|
|
1007
|
+
const val = cur.fields.get(seg.text);
|
|
748
1008
|
if (val === void 0 || !isResObj(val)) return void 0;
|
|
749
1009
|
cur = val;
|
|
750
1010
|
}
|
|
@@ -804,6 +1064,7 @@ var nodePath = __toESM(require("path"), 1);
|
|
|
804
1064
|
// src/internal/properties/properties.ts
|
|
805
1065
|
function parseProperties(input) {
|
|
806
1066
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1067
|
+
const pairs = [];
|
|
807
1068
|
for (const line of input.split("\n")) {
|
|
808
1069
|
const trimmed = line.trim();
|
|
809
1070
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -812,6 +1073,10 @@ function parseProperties(input) {
|
|
|
812
1073
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
813
1074
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
814
1075
|
if (key === "") continue;
|
|
1076
|
+
pairs.push([key, value]);
|
|
1077
|
+
}
|
|
1078
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1079
|
+
for (const [key, value] of pairs) {
|
|
815
1080
|
setNested(root, key.split("."), value);
|
|
816
1081
|
}
|
|
817
1082
|
return root;
|
|
@@ -835,6 +1100,7 @@ function setNested(obj, segments, value) {
|
|
|
835
1100
|
}
|
|
836
1101
|
const last = segments[segments.length - 1];
|
|
837
1102
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1103
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
838
1104
|
current[last] = value;
|
|
839
1105
|
}
|
|
840
1106
|
function propertiesToHoconValue(input) {
|
|
@@ -948,7 +1214,9 @@ var IncludeLoader = class {
|
|
|
948
1214
|
if (candidate.endsWith(".properties")) {
|
|
949
1215
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
950
1216
|
}
|
|
951
|
-
const
|
|
1217
|
+
const tokens = tokenize(content);
|
|
1218
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1219
|
+
const ast = parseTokens(tokens);
|
|
952
1220
|
return this.onBuildResObj(ast, {
|
|
953
1221
|
env,
|
|
954
1222
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -972,7 +1240,9 @@ var IncludeLoader = class {
|
|
|
972
1240
|
if (candidate.endsWith(".properties")) {
|
|
973
1241
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
974
1242
|
}
|
|
975
|
-
const
|
|
1243
|
+
const tokens = tokenize(content);
|
|
1244
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1245
|
+
const ast = parseTokens(tokens);
|
|
976
1246
|
return this.onBuildResObjAsync(ast, {
|
|
977
1247
|
env,
|
|
978
1248
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1021,7 +1291,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1021
1291
|
return;
|
|
1022
1292
|
}
|
|
1023
1293
|
const [head, ...tail] = field.key;
|
|
1024
|
-
if (
|
|
1294
|
+
if (head === void 0) return;
|
|
1025
1295
|
if (tail.length > 0) {
|
|
1026
1296
|
const syntheticAst = {
|
|
1027
1297
|
kind: "object",
|
|
@@ -1063,7 +1333,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1063
1333
|
return;
|
|
1064
1334
|
}
|
|
1065
1335
|
const [head, ...tail] = field.key;
|
|
1066
|
-
if (
|
|
1336
|
+
if (head === void 0) return;
|
|
1067
1337
|
if (tail.length > 0) {
|
|
1068
1338
|
const syntheticAst = {
|
|
1069
1339
|
kind: "object",
|
|
@@ -1109,9 +1379,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1109
1379
|
return inner;
|
|
1110
1380
|
}
|
|
1111
1381
|
case "subst":
|
|
1112
|
-
return { _kind: "subst-placeholder", segments:
|
|
1382
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1113
1383
|
case "concat":
|
|
1114
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1384
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1115
1385
|
case "include":
|
|
1116
1386
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1117
1387
|
}
|
|
@@ -1133,13 +1403,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1133
1403
|
case "object":
|
|
1134
1404
|
return await this.buildAsync(ast, pathPrefix);
|
|
1135
1405
|
case "subst":
|
|
1136
|
-
return { _kind: "subst-placeholder", segments:
|
|
1406
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1137
1407
|
case "concat": {
|
|
1138
1408
|
const nodes = [];
|
|
1139
1409
|
for (const n of ast.nodes) {
|
|
1140
1410
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1141
1411
|
}
|
|
1142
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1412
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1143
1413
|
}
|
|
1144
1414
|
case "include":
|
|
1145
1415
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1148,7 +1418,8 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1148
1418
|
// ---- Relativize substitution paths for nested includes ----
|
|
1149
1419
|
relativizeSubstPaths(val, prefixSegments) {
|
|
1150
1420
|
if (isSubst(val)) {
|
|
1151
|
-
|
|
1421
|
+
const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
|
|
1422
|
+
val.segments = [...prefixAsSegments, ...val.segments];
|
|
1152
1423
|
val.prefixLen += prefixSegments.length;
|
|
1153
1424
|
return;
|
|
1154
1425
|
}
|
|
@@ -1184,6 +1455,23 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1184
1455
|
}
|
|
1185
1456
|
};
|
|
1186
1457
|
|
|
1458
|
+
// src/value/numeric-array.ts
|
|
1459
|
+
function numericObjectToArray(value) {
|
|
1460
|
+
if (value.kind !== "object") return null;
|
|
1461
|
+
if (value.fields.size === 0) return null;
|
|
1462
|
+
const canonicalInt = /^(0|[1-9][0-9]*)$/;
|
|
1463
|
+
const eligible = [];
|
|
1464
|
+
for (const [key, val] of value.fields) {
|
|
1465
|
+
if (!canonicalInt.test(key)) continue;
|
|
1466
|
+
const n = Number(key);
|
|
1467
|
+
if (!Number.isInteger(n) || n > 2147483647) continue;
|
|
1468
|
+
eligible.push({ n, v: val });
|
|
1469
|
+
}
|
|
1470
|
+
if (eligible.length === 0) return null;
|
|
1471
|
+
eligible.sort((a, b) => a.n - b.n);
|
|
1472
|
+
return eligible.map((e) => e.v);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1187
1475
|
// src/internal/resolver/substitution-resolver.ts
|
|
1188
1476
|
var SubstitutionResolver = class {
|
|
1189
1477
|
constructor(root, opts) {
|
|
@@ -1191,6 +1479,19 @@ var SubstitutionResolver = class {
|
|
|
1191
1479
|
this.opts = opts;
|
|
1192
1480
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1193
1481
|
this.cache = /* @__PURE__ */ new Map();
|
|
1482
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1483
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1484
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1485
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1486
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1487
|
+
//
|
|
1488
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1489
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1490
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1491
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1492
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1493
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1494
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1194
1495
|
}
|
|
1195
1496
|
resolve() {
|
|
1196
1497
|
return this.resolveResObj(this.root);
|
|
@@ -1229,7 +1530,14 @@ var SubstitutionResolver = class {
|
|
|
1229
1530
|
}
|
|
1230
1531
|
resolveVal(v, scope) {
|
|
1231
1532
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1232
|
-
if (isConcat(v))
|
|
1533
|
+
if (isConcat(v)) {
|
|
1534
|
+
this.resolvingConcats.add(v);
|
|
1535
|
+
try {
|
|
1536
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1537
|
+
} finally {
|
|
1538
|
+
this.resolvingConcats.delete(v);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1233
1541
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1234
1542
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1235
1543
|
const hv = v;
|
|
@@ -1243,23 +1551,20 @@ var SubstitutionResolver = class {
|
|
|
1243
1551
|
}
|
|
1244
1552
|
return hv;
|
|
1245
1553
|
}
|
|
1246
|
-
segmentsEqual(a, b) {
|
|
1247
|
-
return a.length === b.length && a.every((seg, i) => seg === b[i]);
|
|
1248
|
-
}
|
|
1249
1554
|
resolveSubst(s, scope) {
|
|
1250
|
-
const key = segmentsToKey(s.segments);
|
|
1555
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1251
1556
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1252
1557
|
if (this.resolving.has(key)) {
|
|
1253
1558
|
let prior;
|
|
1254
|
-
if (s.
|
|
1255
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1559
|
+
if (s.segments.length > 1) {
|
|
1560
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1256
1561
|
const parentScope = lookupResObj(
|
|
1257
1562
|
this.root,
|
|
1258
1563
|
s.segments.slice(0, s.segments.length - 1)
|
|
1259
1564
|
);
|
|
1260
1565
|
prior = parentScope?.priorValues.get(leafSeg);
|
|
1261
1566
|
} else {
|
|
1262
|
-
const rootSeg = s.segments[0] ?? "";
|
|
1567
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1263
1568
|
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1264
1569
|
}
|
|
1265
1570
|
if (prior !== void 0) {
|
|
@@ -1283,34 +1588,36 @@ var SubstitutionResolver = class {
|
|
|
1283
1588
|
try {
|
|
1284
1589
|
const found = lookupPath(this.root, s.segments);
|
|
1285
1590
|
if (found !== void 0) {
|
|
1286
|
-
if (
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
prior = parentScope?.priorValues.get(leafSeg);
|
|
1299
|
-
} else {
|
|
1300
|
-
const rootSeg = s.segments[0] ?? "";
|
|
1301
|
-
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1302
|
-
}
|
|
1303
|
-
if (prior !== void 0) {
|
|
1304
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1305
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1306
|
-
return result2;
|
|
1307
|
-
}
|
|
1591
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1592
|
+
let prior;
|
|
1593
|
+
if (s.segments.length > 1) {
|
|
1594
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1595
|
+
const parentScope = lookupResObj(
|
|
1596
|
+
this.root,
|
|
1597
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1598
|
+
);
|
|
1599
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1600
|
+
} else {
|
|
1601
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1602
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1308
1603
|
}
|
|
1604
|
+
if (prior !== void 0) {
|
|
1605
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1606
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1607
|
+
return result2;
|
|
1608
|
+
}
|
|
1609
|
+
if (s.optional) return void 0;
|
|
1610
|
+
throw new ResolveError(
|
|
1611
|
+
`could not resolve substitution: \${${key}}`,
|
|
1612
|
+
key,
|
|
1613
|
+
s.line,
|
|
1614
|
+
s.col
|
|
1615
|
+
);
|
|
1309
1616
|
}
|
|
1310
1617
|
let result = this.resolveVal(found, scope);
|
|
1311
1618
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
1312
1619
|
if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
|
|
1313
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1620
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1314
1621
|
let prior;
|
|
1315
1622
|
if (s.prefixLen > 0) {
|
|
1316
1623
|
const parentScope = lookupResObj(
|
|
@@ -1334,8 +1641,23 @@ var SubstitutionResolver = class {
|
|
|
1334
1641
|
if (result !== void 0) this.cache.set(key, result);
|
|
1335
1642
|
return result;
|
|
1336
1643
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1644
|
+
if (s.listSuffix) {
|
|
1645
|
+
const result = this.resolveEnvList(s);
|
|
1646
|
+
if (result !== void 0) {
|
|
1647
|
+
this.cache.set(key, result);
|
|
1648
|
+
return result;
|
|
1649
|
+
}
|
|
1650
|
+
if (s.optional) return void 0;
|
|
1651
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1652
|
+
throw new ResolveError(
|
|
1653
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1654
|
+
key,
|
|
1655
|
+
s.line,
|
|
1656
|
+
s.col
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1660
|
+
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1339
1661
|
if (envVal !== void 0) {
|
|
1340
1662
|
const result = { kind: "scalar", raw: envVal, valueType: "string" };
|
|
1341
1663
|
this.cache.set(key, result);
|
|
@@ -1352,37 +1674,106 @@ var SubstitutionResolver = class {
|
|
|
1352
1674
|
this.resolving.delete(key);
|
|
1353
1675
|
}
|
|
1354
1676
|
}
|
|
1355
|
-
|
|
1677
|
+
/**
|
|
1678
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1679
|
+
*
|
|
1680
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1681
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1682
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1683
|
+
*
|
|
1684
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1685
|
+
* none do (caller applies optional/required handling).
|
|
1686
|
+
*/
|
|
1687
|
+
resolveEnvList(s) {
|
|
1688
|
+
const bases = this.candidateBases(s);
|
|
1689
|
+
for (const base of bases) {
|
|
1690
|
+
const key0 = base + "_0";
|
|
1691
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1692
|
+
const items = [];
|
|
1693
|
+
for (let i = 0; ; i++) {
|
|
1694
|
+
const k = base + "_" + i;
|
|
1695
|
+
if (!(k in this.opts.env)) break;
|
|
1696
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1697
|
+
}
|
|
1698
|
+
return { kind: "array", items };
|
|
1699
|
+
}
|
|
1700
|
+
return void 0;
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1704
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1705
|
+
*/
|
|
1706
|
+
candidateBases(s) {
|
|
1707
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1708
|
+
if (s.prefixLen === 0) return [full];
|
|
1709
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1710
|
+
return [full, bare];
|
|
1711
|
+
}
|
|
1712
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1356
1713
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1357
1714
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1358
1715
|
if (resolved.length === 1) return resolved[0];
|
|
1359
1716
|
const nonSep = resolved.filter((v) => !separatorValues.has(v));
|
|
1360
|
-
if (nonSep.length
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
}
|
|
1717
|
+
if (nonSep.length === 0) {
|
|
1718
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1719
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1720
|
+
}
|
|
1721
|
+
const joinPair = (left, right) => {
|
|
1722
|
+
if (left.kind === "object" && right.kind === "object") {
|
|
1723
|
+
return deepMergeHoconValues(left, right);
|
|
1724
|
+
}
|
|
1725
|
+
if (left.kind === "array" && right.kind === "object") {
|
|
1726
|
+
const converted = numericObjectToArray(right);
|
|
1727
|
+
if (converted !== null) {
|
|
1728
|
+
return { kind: "array", items: [...left.items, ...converted] };
|
|
1372
1729
|
}
|
|
1730
|
+
throw new ResolveError(
|
|
1731
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1732
|
+
"",
|
|
1733
|
+
line,
|
|
1734
|
+
col
|
|
1735
|
+
);
|
|
1373
1736
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1737
|
+
if (left.kind === "object" && right.kind === "array") {
|
|
1738
|
+
const converted = numericObjectToArray(left);
|
|
1739
|
+
if (converted !== null) {
|
|
1740
|
+
return { kind: "array", items: [...converted, ...right.items] };
|
|
1741
|
+
}
|
|
1742
|
+
throw new ResolveError(
|
|
1743
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1744
|
+
"",
|
|
1745
|
+
line,
|
|
1746
|
+
col
|
|
1747
|
+
);
|
|
1381
1748
|
}
|
|
1382
|
-
|
|
1749
|
+
if (left.kind === "array" && right.kind === "array") {
|
|
1750
|
+
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1751
|
+
}
|
|
1752
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1753
|
+
throw new ResolveError(
|
|
1754
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1755
|
+
"",
|
|
1756
|
+
line,
|
|
1757
|
+
col
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1761
|
+
throw new ResolveError(
|
|
1762
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1763
|
+
"",
|
|
1764
|
+
line,
|
|
1765
|
+
col
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1769
|
+
};
|
|
1770
|
+
const [head, ...tail] = nonSep;
|
|
1771
|
+
const folded = tail.reduce(joinPair, head);
|
|
1772
|
+
if (folded.kind === "scalar") {
|
|
1773
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1774
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1383
1775
|
}
|
|
1384
|
-
|
|
1385
|
-
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1776
|
+
return folded;
|
|
1386
1777
|
}
|
|
1387
1778
|
resolveAppend(a, scope) {
|
|
1388
1779
|
const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
|
|
@@ -1441,6 +1832,7 @@ var Config = class _Config {
|
|
|
1441
1832
|
}
|
|
1442
1833
|
const result = parseBytes(v.raw, unit);
|
|
1443
1834
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1835
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1444
1836
|
return result;
|
|
1445
1837
|
}
|
|
1446
1838
|
getConfig(path2) {
|
|
@@ -1452,6 +1844,10 @@ var Config = class _Config {
|
|
|
1452
1844
|
getList(path2) {
|
|
1453
1845
|
const v = this.lookupNode(path2);
|
|
1454
1846
|
if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
|
|
1847
|
+
if (v.kind === "object") {
|
|
1848
|
+
const converted = numericObjectToArray(v);
|
|
1849
|
+
if (converted !== null) return converted.map(hoconToJs);
|
|
1850
|
+
}
|
|
1455
1851
|
if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
|
|
1456
1852
|
return v.items.map(hoconToJs);
|
|
1457
1853
|
}
|
|
@@ -1576,6 +1972,7 @@ function defaultReadFileSync(filePath) {
|
|
|
1576
1972
|
}
|
|
1577
1973
|
function buildResolveContext(input, opts) {
|
|
1578
1974
|
const tokens = tokenize(input);
|
|
1975
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1579
1976
|
const ast = parseTokens(tokens);
|
|
1580
1977
|
const resolveOpts = {
|
|
1581
1978
|
env: getEnv(opts),
|