@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.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
// src/coerce.ts
|
|
2
2
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
3
|
+
function isHoconWs(cp) {
|
|
4
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
5
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
6
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
7
|
+
if (cp === 5760) return true;
|
|
8
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
9
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
10
|
+
if (cp === 12288) return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
function trimHoconWs(s) {
|
|
14
|
+
let start = 0;
|
|
15
|
+
let end = s.length;
|
|
16
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
17
|
+
start++;
|
|
18
|
+
}
|
|
19
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
20
|
+
end--;
|
|
21
|
+
}
|
|
22
|
+
return s.slice(start, end);
|
|
23
|
+
}
|
|
3
24
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
4
25
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
5
26
|
function coerceBoolean(value) {
|
|
@@ -45,22 +66,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
45
66
|
d: 864e5
|
|
46
67
|
};
|
|
47
68
|
function parseDuration(value, outputUnit = "ms") {
|
|
48
|
-
const trimmed = value
|
|
69
|
+
const trimmed = trimHoconWs(value);
|
|
49
70
|
let i = 0;
|
|
50
71
|
while (i < trimmed.length) {
|
|
51
72
|
const ch = trimmed[i];
|
|
52
|
-
if (
|
|
73
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
53
78
|
i++;
|
|
54
79
|
}
|
|
55
80
|
if (i === 0) return NaN;
|
|
56
81
|
const num = Number(trimmed.slice(0, i));
|
|
57
82
|
if (Number.isNaN(num)) return NaN;
|
|
58
|
-
const unit = trimmed.slice(i)
|
|
83
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
84
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
85
|
+
if (divisor === void 0) return NaN;
|
|
86
|
+
if (unit === "") {
|
|
87
|
+
return num / divisor;
|
|
88
|
+
}
|
|
59
89
|
const mult = DURATION_UNITS[unit];
|
|
60
90
|
if (mult === void 0) return NaN;
|
|
61
91
|
const ms = num * mult;
|
|
62
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
63
|
-
if (divisor === void 0) return NaN;
|
|
64
92
|
return ms / divisor;
|
|
65
93
|
}
|
|
66
94
|
var BYTE_UNITS = {
|
|
@@ -91,6 +119,22 @@ var BYTE_UNITS = {
|
|
|
91
119
|
TiB: 1099511627776,
|
|
92
120
|
tebibyte: 1099511627776,
|
|
93
121
|
tebibytes: 1099511627776,
|
|
122
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
123
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
124
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
125
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
126
|
+
K: 1024,
|
|
127
|
+
k: 1024,
|
|
128
|
+
M: 1024 ** 2,
|
|
129
|
+
m: 1024 ** 2,
|
|
130
|
+
G: 1024 ** 3,
|
|
131
|
+
g: 1024 ** 3,
|
|
132
|
+
T: 1024 ** 4,
|
|
133
|
+
t: 1024 ** 4,
|
|
134
|
+
P: 1024 ** 5,
|
|
135
|
+
p: 1024 ** 5,
|
|
136
|
+
E: 1024 ** 6,
|
|
137
|
+
e: 1024 ** 6,
|
|
94
138
|
// lowercase short-form aliases
|
|
95
139
|
b: 1,
|
|
96
140
|
kb: 1e3,
|
|
@@ -114,25 +158,39 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
114
158
|
TiB: 1099511627776
|
|
115
159
|
};
|
|
116
160
|
function parseBytes(value, outputUnit = "B") {
|
|
117
|
-
const trimmed = value
|
|
161
|
+
const trimmed = trimHoconWs(value);
|
|
118
162
|
let i = 0;
|
|
119
163
|
while (i < trimmed.length) {
|
|
120
164
|
const ch = trimmed[i];
|
|
121
|
-
if (
|
|
165
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
122
170
|
i++;
|
|
123
171
|
}
|
|
124
172
|
if (i === 0) return NaN;
|
|
125
173
|
const num = Number(trimmed.slice(0, i));
|
|
126
174
|
if (Number.isNaN(num)) return NaN;
|
|
127
|
-
const unit = trimmed.slice(i)
|
|
175
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
176
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
177
|
+
if (divisor === void 0) return NaN;
|
|
178
|
+
if (unit === "") {
|
|
179
|
+
const bytes2 = Math.trunc(num);
|
|
180
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
181
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
182
|
+
}
|
|
183
|
+
return bytes2 / divisor;
|
|
184
|
+
}
|
|
128
185
|
let mult = BYTE_UNITS[unit];
|
|
129
186
|
if (mult === void 0) {
|
|
130
187
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
131
188
|
}
|
|
132
189
|
if (mult === void 0) return NaN;
|
|
133
190
|
const bytes = num * mult;
|
|
134
|
-
|
|
135
|
-
|
|
191
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
192
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
193
|
+
}
|
|
136
194
|
const result = bytes / divisor;
|
|
137
195
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
138
196
|
}
|
|
@@ -166,6 +224,20 @@ var ConfigError = class extends Error {
|
|
|
166
224
|
};
|
|
167
225
|
|
|
168
226
|
// src/internal/lexer/lexer.ts
|
|
227
|
+
function isHoconWhitespace(ch) {
|
|
228
|
+
const cp = ch.codePointAt(0) ?? -1;
|
|
229
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
230
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
231
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
232
|
+
if (cp === 5760) return true;
|
|
233
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
234
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
235
|
+
if (cp === 12288) return true;
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function isHoconNewline(ch) {
|
|
239
|
+
return ch === "\n";
|
|
240
|
+
}
|
|
169
241
|
var SINGLE_CHAR_TOKENS = {
|
|
170
242
|
"{": "lbrace",
|
|
171
243
|
"}": "rbrace",
|
|
@@ -188,18 +260,18 @@ var Lexer = class {
|
|
|
188
260
|
while (this.pos < this.input.length) {
|
|
189
261
|
const sl = this.line, sc = this.col;
|
|
190
262
|
const ch = this.peek();
|
|
191
|
-
if (ch
|
|
192
|
-
this.advance();
|
|
193
|
-
this.hadSpace = true;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (ch === "\n") {
|
|
263
|
+
if (isHoconNewline(ch)) {
|
|
197
264
|
this.advance();
|
|
198
265
|
if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
|
|
199
266
|
this.push("newline", "\n", sl, sc);
|
|
200
267
|
}
|
|
201
268
|
continue;
|
|
202
269
|
}
|
|
270
|
+
if (isHoconWhitespace(ch)) {
|
|
271
|
+
this.advance();
|
|
272
|
+
this.hadSpace = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
203
275
|
if (ch === "/" && this.peek(1) === "/") {
|
|
204
276
|
while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
|
|
205
277
|
this.hadSpace = true;
|
|
@@ -229,16 +301,16 @@ var Lexer = class {
|
|
|
229
301
|
if (ch === "$" && this.peek(1) === "{") {
|
|
230
302
|
this.advance();
|
|
231
303
|
this.advance();
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.
|
|
304
|
+
const payload = this.parseSubstBody(sl, sc);
|
|
305
|
+
const value = payload.segments.map((s) => {
|
|
306
|
+
const t = s.text;
|
|
307
|
+
if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
|
|
308
|
+
const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
309
|
+
return `"${escaped}"`;
|
|
310
|
+
}
|
|
311
|
+
return t;
|
|
312
|
+
}).join(".");
|
|
313
|
+
this.pushSubst(payload, value, sl, sc);
|
|
242
314
|
continue;
|
|
243
315
|
}
|
|
244
316
|
if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
|
|
@@ -273,58 +345,7 @@ var Lexer = class {
|
|
|
273
345
|
}
|
|
274
346
|
if (ch === '"') {
|
|
275
347
|
this.advance();
|
|
276
|
-
|
|
277
|
-
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
278
|
-
if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
|
|
279
|
-
if (this.peek() === "\\") {
|
|
280
|
-
this.advance();
|
|
281
|
-
const esc = this.advance();
|
|
282
|
-
switch (esc) {
|
|
283
|
-
case "n":
|
|
284
|
-
value += "\n";
|
|
285
|
-
break;
|
|
286
|
-
case "t":
|
|
287
|
-
value += " ";
|
|
288
|
-
break;
|
|
289
|
-
case "r":
|
|
290
|
-
value += "\r";
|
|
291
|
-
break;
|
|
292
|
-
case '"':
|
|
293
|
-
value += '"';
|
|
294
|
-
break;
|
|
295
|
-
case "\\":
|
|
296
|
-
value += "\\";
|
|
297
|
-
break;
|
|
298
|
-
case "/":
|
|
299
|
-
value += "/";
|
|
300
|
-
break;
|
|
301
|
-
case "b":
|
|
302
|
-
value += "\b";
|
|
303
|
-
break;
|
|
304
|
-
case "f":
|
|
305
|
-
value += "\f";
|
|
306
|
-
break;
|
|
307
|
-
case "u": {
|
|
308
|
-
if (this.pos + 4 > this.input.length) {
|
|
309
|
-
throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
|
|
310
|
-
}
|
|
311
|
-
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
312
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
313
|
-
throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
|
|
314
|
-
}
|
|
315
|
-
value += String.fromCharCode(parseInt(hex, 16));
|
|
316
|
-
for (let i = 0; i < 4; i++) this.advance();
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
default:
|
|
320
|
-
throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
323
|
-
value += this.advance();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
|
|
327
|
-
this.advance();
|
|
348
|
+
const value = this.readQuotedStringBody(sl, sc);
|
|
328
349
|
this.push("string", value, sl, sc, true);
|
|
329
350
|
continue;
|
|
330
351
|
}
|
|
@@ -358,22 +379,255 @@ var Lexer = class {
|
|
|
358
379
|
this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
|
|
359
380
|
this.hadSpace = false;
|
|
360
381
|
}
|
|
382
|
+
pushSubst(payload, value, l, c) {
|
|
383
|
+
this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
|
|
384
|
+
this.hadSpace = false;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Read body of a quoted string. Opening `"` must already be consumed.
|
|
388
|
+
* Returns decoded string. Throws ParseError on unterminated/invalid-escape.
|
|
389
|
+
* openLine/openCol are the position of the opening `"` for error reporting.
|
|
390
|
+
*/
|
|
391
|
+
readQuotedStringBody(openLine, openCol) {
|
|
392
|
+
let value = "";
|
|
393
|
+
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
394
|
+
if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
|
|
395
|
+
if (this.peek() === "\\") {
|
|
396
|
+
const escCol = this.col;
|
|
397
|
+
this.advance();
|
|
398
|
+
if (this.pos >= this.input.length) {
|
|
399
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
400
|
+
}
|
|
401
|
+
const esc = this.advance();
|
|
402
|
+
switch (esc) {
|
|
403
|
+
case "n":
|
|
404
|
+
value += "\n";
|
|
405
|
+
break;
|
|
406
|
+
case "t":
|
|
407
|
+
value += " ";
|
|
408
|
+
break;
|
|
409
|
+
case "r":
|
|
410
|
+
value += "\r";
|
|
411
|
+
break;
|
|
412
|
+
case '"':
|
|
413
|
+
value += '"';
|
|
414
|
+
break;
|
|
415
|
+
case "\\":
|
|
416
|
+
value += "\\";
|
|
417
|
+
break;
|
|
418
|
+
case "/":
|
|
419
|
+
value += "/";
|
|
420
|
+
break;
|
|
421
|
+
case "b":
|
|
422
|
+
value += "\b";
|
|
423
|
+
break;
|
|
424
|
+
case "f":
|
|
425
|
+
value += "\f";
|
|
426
|
+
break;
|
|
427
|
+
case "u": {
|
|
428
|
+
if (this.pos + 4 > this.input.length) {
|
|
429
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
430
|
+
}
|
|
431
|
+
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
432
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
433
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
434
|
+
}
|
|
435
|
+
const code = parseInt(hex, 16);
|
|
436
|
+
value += String.fromCharCode(code);
|
|
437
|
+
for (let i = 0; i < 4; i++) this.advance();
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
default:
|
|
441
|
+
throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
value += this.advance();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (this.pos >= this.input.length || this.peek() !== '"') {
|
|
448
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
449
|
+
}
|
|
450
|
+
this.advance();
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
455
|
+
* Implements Appendix A state machine from the cross-language spec.
|
|
456
|
+
*
|
|
457
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
458
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
459
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
460
|
+
*/
|
|
461
|
+
parseSubstBody(startLine, startCol) {
|
|
462
|
+
let optional = false;
|
|
463
|
+
if (this.peek() === "?") {
|
|
464
|
+
this.advance();
|
|
465
|
+
optional = true;
|
|
466
|
+
}
|
|
467
|
+
let curText = "";
|
|
468
|
+
let curStarted = false;
|
|
469
|
+
let curLine = 0;
|
|
470
|
+
let curCol = 0;
|
|
471
|
+
let pendingWs = "";
|
|
472
|
+
const segments = [];
|
|
473
|
+
let lastDot = null;
|
|
474
|
+
let listSuffix = false;
|
|
475
|
+
while (true) {
|
|
476
|
+
if (this.pos >= this.input.length) {
|
|
477
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
478
|
+
}
|
|
479
|
+
const ch = this.peek();
|
|
480
|
+
if (ch === "}") {
|
|
481
|
+
this.advance();
|
|
482
|
+
pendingWs = "";
|
|
483
|
+
break;
|
|
484
|
+
} else if (ch === "[") {
|
|
485
|
+
if (!curStarted) {
|
|
486
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
487
|
+
}
|
|
488
|
+
for (const w of pendingWs) {
|
|
489
|
+
if (w !== " " && w !== " ") {
|
|
490
|
+
throw new ParseError(
|
|
491
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
492
|
+
startLine,
|
|
493
|
+
this.col
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
498
|
+
pendingWs = "";
|
|
499
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
500
|
+
break;
|
|
501
|
+
} else if (ch === '"') {
|
|
502
|
+
const qLine = startLine;
|
|
503
|
+
const qCol = this.col;
|
|
504
|
+
if (curStarted) {
|
|
505
|
+
curText += pendingWs;
|
|
506
|
+
}
|
|
507
|
+
pendingWs = "";
|
|
508
|
+
this.advance();
|
|
509
|
+
const decoded = this.readQuotedStringBody(qLine, qCol);
|
|
510
|
+
curText += decoded;
|
|
511
|
+
if (!curStarted) {
|
|
512
|
+
curLine = qLine;
|
|
513
|
+
curCol = qCol;
|
|
514
|
+
curStarted = true;
|
|
515
|
+
}
|
|
516
|
+
} else if (isUnquotedSubstChar(ch)) {
|
|
517
|
+
if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
|
|
518
|
+
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
519
|
+
throw new ParseError(
|
|
520
|
+
`unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
521
|
+
startLine,
|
|
522
|
+
this.col
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const uCol = this.col;
|
|
526
|
+
if (curStarted) {
|
|
527
|
+
curText += pendingWs;
|
|
528
|
+
}
|
|
529
|
+
pendingWs = "";
|
|
530
|
+
if (!curStarted) {
|
|
531
|
+
curLine = startLine;
|
|
532
|
+
curCol = uCol;
|
|
533
|
+
curStarted = true;
|
|
534
|
+
}
|
|
535
|
+
while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
|
|
536
|
+
curText += this.advance();
|
|
537
|
+
}
|
|
538
|
+
} else if (ch === ".") {
|
|
539
|
+
const dotCol = this.col;
|
|
540
|
+
pendingWs = "";
|
|
541
|
+
if (!curStarted) {
|
|
542
|
+
throw new ParseError("empty segment in path", startLine, dotCol);
|
|
543
|
+
}
|
|
544
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
545
|
+
curText = "";
|
|
546
|
+
curStarted = false;
|
|
547
|
+
curLine = 0;
|
|
548
|
+
curCol = 0;
|
|
549
|
+
lastDot = [startLine, dotCol];
|
|
550
|
+
this.advance();
|
|
551
|
+
} else if (isHoconNewline(ch)) {
|
|
552
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
553
|
+
} else if (isHoconWhitespace(ch)) {
|
|
554
|
+
pendingWs += ch;
|
|
555
|
+
this.advance();
|
|
556
|
+
} else {
|
|
557
|
+
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (!listSuffix) {
|
|
561
|
+
if (curStarted) {
|
|
562
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
563
|
+
} else if (segments.length === 0) {
|
|
564
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
565
|
+
} else {
|
|
566
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
567
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { segments, optional, listSuffix };
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
574
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
575
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
576
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
577
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
578
|
+
*/
|
|
579
|
+
parseListSuffix(startLine) {
|
|
580
|
+
this.advance();
|
|
581
|
+
const afterBracket = this.peek();
|
|
582
|
+
if (afterBracket !== "]") {
|
|
583
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
584
|
+
throw new ParseError(
|
|
585
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
586
|
+
startLine,
|
|
587
|
+
this.col
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
this.advance();
|
|
591
|
+
if (this.peek() !== "}") {
|
|
592
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
593
|
+
throw new ParseError(
|
|
594
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
595
|
+
startLine,
|
|
596
|
+
this.col
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
this.advance();
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
361
602
|
};
|
|
362
603
|
function tokenize(input) {
|
|
363
604
|
return new Lexer(input).tokenize();
|
|
364
605
|
}
|
|
606
|
+
function isUnquotedSubstChar(ch) {
|
|
607
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
608
|
+
if ('"\\'.includes(ch)) return false;
|
|
609
|
+
if ("{}[]".includes(ch)) return false;
|
|
610
|
+
if (":=,+#`^?!@*&$.".includes(ch)) return false;
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
function isDecimalDigit(ch) {
|
|
614
|
+
return ch >= "0" && ch <= "9";
|
|
615
|
+
}
|
|
365
616
|
function isUnquotedStart(ch) {
|
|
366
|
-
|
|
617
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
618
|
+
if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
|
|
619
|
+
return true;
|
|
367
620
|
}
|
|
368
621
|
function isUnquotedContinue(ch, nextFn) {
|
|
369
|
-
if (ch === "" ||
|
|
622
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
623
|
+
if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
|
|
370
624
|
if (ch === "+" && nextFn() === "=") return false;
|
|
371
625
|
if (ch === "/" && nextFn() === "/") return false;
|
|
372
626
|
return true;
|
|
373
627
|
}
|
|
374
628
|
|
|
375
629
|
// src/internal/parser/parser.ts
|
|
376
|
-
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
|
|
630
|
+
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
|
|
377
631
|
var Parser = class {
|
|
378
632
|
constructor(tokens) {
|
|
379
633
|
this.tokens = tokens;
|
|
@@ -431,6 +685,14 @@ var Parser = class {
|
|
|
431
685
|
const t = this.peek();
|
|
432
686
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
433
687
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
688
|
+
const next = this.peek(1);
|
|
689
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
690
|
+
throw new ParseError(
|
|
691
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
692
|
+
t.line,
|
|
693
|
+
t.col
|
|
694
|
+
);
|
|
695
|
+
}
|
|
434
696
|
this.advance();
|
|
435
697
|
fields.push(this.parseInclude());
|
|
436
698
|
this.skip("newline");
|
|
@@ -439,7 +701,14 @@ var Parser = class {
|
|
|
439
701
|
continue;
|
|
440
702
|
}
|
|
441
703
|
const keyPos = this.currentPos();
|
|
442
|
-
const key = this.parseKey();
|
|
704
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
705
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
706
|
+
throw new ParseError(
|
|
707
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
708
|
+
keyPos.line,
|
|
709
|
+
keyPos.col
|
|
710
|
+
);
|
|
711
|
+
}
|
|
443
712
|
this.skip("newline");
|
|
444
713
|
let append = false;
|
|
445
714
|
const sep = this.peek();
|
|
@@ -469,18 +738,31 @@ var Parser = class {
|
|
|
469
738
|
parseKey() {
|
|
470
739
|
const segments = [];
|
|
471
740
|
let trailingDot = false;
|
|
741
|
+
let firstWasQuoted = false;
|
|
472
742
|
while (true) {
|
|
473
743
|
const t = this.peek();
|
|
474
744
|
if (t.kind === "string") {
|
|
475
745
|
this.advance();
|
|
746
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
476
747
|
segments.push(t.value);
|
|
477
748
|
trailingDot = false;
|
|
478
749
|
} else if (t.kind === "unquoted") {
|
|
479
750
|
this.advance();
|
|
480
|
-
const
|
|
751
|
+
const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
|
|
752
|
+
const parts = raw.split(".");
|
|
481
753
|
const filtered = parts.filter((s) => s.length > 0);
|
|
754
|
+
for (const seg of filtered) {
|
|
755
|
+
if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
|
|
756
|
+
const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
|
|
757
|
+
throw new ParseError(
|
|
758
|
+
`unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
|
|
759
|
+
t.line,
|
|
760
|
+
t.col
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
482
764
|
segments.push(...filtered);
|
|
483
|
-
trailingDot =
|
|
765
|
+
trailingDot = raw.endsWith(".");
|
|
484
766
|
} else {
|
|
485
767
|
if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
|
|
486
768
|
break;
|
|
@@ -492,9 +774,13 @@ var Parser = class {
|
|
|
492
774
|
trailingDot = true;
|
|
493
775
|
continue;
|
|
494
776
|
}
|
|
777
|
+
if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
|
|
778
|
+
trailingDot = true;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
495
781
|
break;
|
|
496
782
|
}
|
|
497
|
-
return segments;
|
|
783
|
+
return { segments, firstWasQuoted };
|
|
498
784
|
}
|
|
499
785
|
parseInclude() {
|
|
500
786
|
const p = this.currentPos();
|
|
@@ -567,9 +853,13 @@ var Parser = class {
|
|
|
567
853
|
} else if (t.kind === "lbracket") {
|
|
568
854
|
this.advance();
|
|
569
855
|
node = this.parseArray();
|
|
570
|
-
} else if (t.kind === "subst"
|
|
856
|
+
} else if (t.kind === "subst") {
|
|
571
857
|
this.advance();
|
|
572
|
-
|
|
858
|
+
if (!t.subst) {
|
|
859
|
+
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
860
|
+
}
|
|
861
|
+
const payload = t.subst;
|
|
862
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
573
863
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
574
864
|
this.advance();
|
|
575
865
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -620,6 +910,20 @@ function parseTokens(tokens) {
|
|
|
620
910
|
return new Parser(tokens).parse();
|
|
621
911
|
}
|
|
622
912
|
|
|
913
|
+
// src/internal/parser/empty-check.ts
|
|
914
|
+
function hasContentTokens(tokens) {
|
|
915
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
916
|
+
}
|
|
917
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
918
|
+
if (!hasContentTokens(tokens)) {
|
|
919
|
+
throw new ParseError(
|
|
920
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
921
|
+
1,
|
|
922
|
+
1
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
623
927
|
// src/internal/resolver/types.ts
|
|
624
928
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
625
929
|
function isSubst(v) {
|
|
@@ -639,64 +943,20 @@ function makeResObj() {
|
|
|
639
943
|
}
|
|
640
944
|
|
|
641
945
|
// src/internal/resolver/utils.ts
|
|
642
|
-
function parseSubstPath(raw) {
|
|
643
|
-
const segments = [];
|
|
644
|
-
let i = 0;
|
|
645
|
-
while (i < raw.length) {
|
|
646
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
647
|
-
if (i >= raw.length) break;
|
|
648
|
-
if (raw[i] === '"') {
|
|
649
|
-
i++;
|
|
650
|
-
let seg = "";
|
|
651
|
-
while (i < raw.length && raw[i] !== '"') {
|
|
652
|
-
if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
|
|
653
|
-
seg += raw[i + 1];
|
|
654
|
-
i += 2;
|
|
655
|
-
} else {
|
|
656
|
-
seg += raw[i];
|
|
657
|
-
i++;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
if (i < raw.length) i++;
|
|
661
|
-
segments.push(seg);
|
|
662
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
663
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
664
|
-
} else if (raw[i] === ".") {
|
|
665
|
-
segments.push("");
|
|
666
|
-
i++;
|
|
667
|
-
} else {
|
|
668
|
-
let seg = "";
|
|
669
|
-
while (i < raw.length && raw[i] !== ".") {
|
|
670
|
-
seg += raw[i];
|
|
671
|
-
i++;
|
|
672
|
-
}
|
|
673
|
-
segments.push(seg.trim());
|
|
674
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return segments;
|
|
678
|
-
}
|
|
679
946
|
function segmentsToKey(segments) {
|
|
680
947
|
return segments.map((s) => {
|
|
681
|
-
|
|
682
|
-
|
|
948
|
+
const t = s.text;
|
|
949
|
+
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
950
|
+
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
683
951
|
}
|
|
684
|
-
return
|
|
952
|
+
return t;
|
|
685
953
|
}).join(".");
|
|
686
954
|
}
|
|
687
955
|
function lookupPath(root, segments) {
|
|
688
956
|
const [head, ...tail] = segments;
|
|
689
|
-
if (head === void 0
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (val2 === void 0) return void 0;
|
|
693
|
-
if (tail.length === 0) return val2;
|
|
694
|
-
if (isResObj(val2)) return lookupPath(val2, tail);
|
|
695
|
-
return void 0;
|
|
696
|
-
}
|
|
697
|
-
return void 0;
|
|
698
|
-
}
|
|
699
|
-
const val = root.fields.get(head);
|
|
957
|
+
if (head === void 0) return void 0;
|
|
958
|
+
const key = head.text;
|
|
959
|
+
const val = root.fields.get(key);
|
|
700
960
|
if (val === void 0) return void 0;
|
|
701
961
|
if (tail.length === 0) return val;
|
|
702
962
|
if (isResObj(val)) return lookupPath(val, tail);
|
|
@@ -705,7 +965,7 @@ function lookupPath(root, segments) {
|
|
|
705
965
|
function lookupResObj(root, segments) {
|
|
706
966
|
let cur = root;
|
|
707
967
|
for (const seg of segments) {
|
|
708
|
-
const val = cur.fields.get(seg);
|
|
968
|
+
const val = cur.fields.get(seg.text);
|
|
709
969
|
if (val === void 0 || !isResObj(val)) return void 0;
|
|
710
970
|
cur = val;
|
|
711
971
|
}
|
|
@@ -765,6 +1025,7 @@ import * as nodePath from "path";
|
|
|
765
1025
|
// src/internal/properties/properties.ts
|
|
766
1026
|
function parseProperties(input) {
|
|
767
1027
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1028
|
+
const pairs = [];
|
|
768
1029
|
for (const line of input.split("\n")) {
|
|
769
1030
|
const trimmed = line.trim();
|
|
770
1031
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -773,6 +1034,10 @@ function parseProperties(input) {
|
|
|
773
1034
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
774
1035
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
775
1036
|
if (key === "") continue;
|
|
1037
|
+
pairs.push([key, value]);
|
|
1038
|
+
}
|
|
1039
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1040
|
+
for (const [key, value] of pairs) {
|
|
776
1041
|
setNested(root, key.split("."), value);
|
|
777
1042
|
}
|
|
778
1043
|
return root;
|
|
@@ -796,6 +1061,7 @@ function setNested(obj, segments, value) {
|
|
|
796
1061
|
}
|
|
797
1062
|
const last = segments[segments.length - 1];
|
|
798
1063
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1064
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
799
1065
|
current[last] = value;
|
|
800
1066
|
}
|
|
801
1067
|
function propertiesToHoconValue(input) {
|
|
@@ -909,7 +1175,9 @@ var IncludeLoader = class {
|
|
|
909
1175
|
if (candidate.endsWith(".properties")) {
|
|
910
1176
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
911
1177
|
}
|
|
912
|
-
const
|
|
1178
|
+
const tokens = tokenize(content);
|
|
1179
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1180
|
+
const ast = parseTokens(tokens);
|
|
913
1181
|
return this.onBuildResObj(ast, {
|
|
914
1182
|
env,
|
|
915
1183
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -933,7 +1201,9 @@ var IncludeLoader = class {
|
|
|
933
1201
|
if (candidate.endsWith(".properties")) {
|
|
934
1202
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
935
1203
|
}
|
|
936
|
-
const
|
|
1204
|
+
const tokens = tokenize(content);
|
|
1205
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1206
|
+
const ast = parseTokens(tokens);
|
|
937
1207
|
return this.onBuildResObjAsync(ast, {
|
|
938
1208
|
env,
|
|
939
1209
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -982,7 +1252,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
982
1252
|
return;
|
|
983
1253
|
}
|
|
984
1254
|
const [head, ...tail] = field.key;
|
|
985
|
-
if (
|
|
1255
|
+
if (head === void 0) return;
|
|
986
1256
|
if (tail.length > 0) {
|
|
987
1257
|
const syntheticAst = {
|
|
988
1258
|
kind: "object",
|
|
@@ -1024,7 +1294,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1024
1294
|
return;
|
|
1025
1295
|
}
|
|
1026
1296
|
const [head, ...tail] = field.key;
|
|
1027
|
-
if (
|
|
1297
|
+
if (head === void 0) return;
|
|
1028
1298
|
if (tail.length > 0) {
|
|
1029
1299
|
const syntheticAst = {
|
|
1030
1300
|
kind: "object",
|
|
@@ -1070,9 +1340,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1070
1340
|
return inner;
|
|
1071
1341
|
}
|
|
1072
1342
|
case "subst":
|
|
1073
|
-
return { _kind: "subst-placeholder", segments:
|
|
1343
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1074
1344
|
case "concat":
|
|
1075
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1345
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1076
1346
|
case "include":
|
|
1077
1347
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1078
1348
|
}
|
|
@@ -1094,13 +1364,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1094
1364
|
case "object":
|
|
1095
1365
|
return await this.buildAsync(ast, pathPrefix);
|
|
1096
1366
|
case "subst":
|
|
1097
|
-
return { _kind: "subst-placeholder", segments:
|
|
1367
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1098
1368
|
case "concat": {
|
|
1099
1369
|
const nodes = [];
|
|
1100
1370
|
for (const n of ast.nodes) {
|
|
1101
1371
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1102
1372
|
}
|
|
1103
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1373
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1104
1374
|
}
|
|
1105
1375
|
case "include":
|
|
1106
1376
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1109,7 +1379,8 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1109
1379
|
// ---- Relativize substitution paths for nested includes ----
|
|
1110
1380
|
relativizeSubstPaths(val, prefixSegments) {
|
|
1111
1381
|
if (isSubst(val)) {
|
|
1112
|
-
|
|
1382
|
+
const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
|
|
1383
|
+
val.segments = [...prefixAsSegments, ...val.segments];
|
|
1113
1384
|
val.prefixLen += prefixSegments.length;
|
|
1114
1385
|
return;
|
|
1115
1386
|
}
|
|
@@ -1145,6 +1416,23 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1145
1416
|
}
|
|
1146
1417
|
};
|
|
1147
1418
|
|
|
1419
|
+
// src/value/numeric-array.ts
|
|
1420
|
+
function numericObjectToArray(value) {
|
|
1421
|
+
if (value.kind !== "object") return null;
|
|
1422
|
+
if (value.fields.size === 0) return null;
|
|
1423
|
+
const canonicalInt = /^(0|[1-9][0-9]*)$/;
|
|
1424
|
+
const eligible = [];
|
|
1425
|
+
for (const [key, val] of value.fields) {
|
|
1426
|
+
if (!canonicalInt.test(key)) continue;
|
|
1427
|
+
const n = Number(key);
|
|
1428
|
+
if (!Number.isInteger(n) || n > 2147483647) continue;
|
|
1429
|
+
eligible.push({ n, v: val });
|
|
1430
|
+
}
|
|
1431
|
+
if (eligible.length === 0) return null;
|
|
1432
|
+
eligible.sort((a, b) => a.n - b.n);
|
|
1433
|
+
return eligible.map((e) => e.v);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1148
1436
|
// src/internal/resolver/substitution-resolver.ts
|
|
1149
1437
|
var SubstitutionResolver = class {
|
|
1150
1438
|
constructor(root, opts) {
|
|
@@ -1152,6 +1440,19 @@ var SubstitutionResolver = class {
|
|
|
1152
1440
|
this.opts = opts;
|
|
1153
1441
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1154
1442
|
this.cache = /* @__PURE__ */ new Map();
|
|
1443
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1444
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1445
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1446
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1447
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1448
|
+
//
|
|
1449
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1450
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1451
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1452
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1453
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1454
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1455
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1155
1456
|
}
|
|
1156
1457
|
resolve() {
|
|
1157
1458
|
return this.resolveResObj(this.root);
|
|
@@ -1190,7 +1491,14 @@ var SubstitutionResolver = class {
|
|
|
1190
1491
|
}
|
|
1191
1492
|
resolveVal(v, scope) {
|
|
1192
1493
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1193
|
-
if (isConcat(v))
|
|
1494
|
+
if (isConcat(v)) {
|
|
1495
|
+
this.resolvingConcats.add(v);
|
|
1496
|
+
try {
|
|
1497
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1498
|
+
} finally {
|
|
1499
|
+
this.resolvingConcats.delete(v);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1194
1502
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1195
1503
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1196
1504
|
const hv = v;
|
|
@@ -1204,23 +1512,20 @@ var SubstitutionResolver = class {
|
|
|
1204
1512
|
}
|
|
1205
1513
|
return hv;
|
|
1206
1514
|
}
|
|
1207
|
-
segmentsEqual(a, b) {
|
|
1208
|
-
return a.length === b.length && a.every((seg, i) => seg === b[i]);
|
|
1209
|
-
}
|
|
1210
1515
|
resolveSubst(s, scope) {
|
|
1211
|
-
const key = segmentsToKey(s.segments);
|
|
1516
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1212
1517
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1213
1518
|
if (this.resolving.has(key)) {
|
|
1214
1519
|
let prior;
|
|
1215
|
-
if (s.
|
|
1216
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1520
|
+
if (s.segments.length > 1) {
|
|
1521
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1217
1522
|
const parentScope = lookupResObj(
|
|
1218
1523
|
this.root,
|
|
1219
1524
|
s.segments.slice(0, s.segments.length - 1)
|
|
1220
1525
|
);
|
|
1221
1526
|
prior = parentScope?.priorValues.get(leafSeg);
|
|
1222
1527
|
} else {
|
|
1223
|
-
const rootSeg = s.segments[0] ?? "";
|
|
1528
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1224
1529
|
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1225
1530
|
}
|
|
1226
1531
|
if (prior !== void 0) {
|
|
@@ -1244,34 +1549,36 @@ var SubstitutionResolver = class {
|
|
|
1244
1549
|
try {
|
|
1245
1550
|
const found = lookupPath(this.root, s.segments);
|
|
1246
1551
|
if (found !== void 0) {
|
|
1247
|
-
if (
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
prior = parentScope?.priorValues.get(leafSeg);
|
|
1260
|
-
} else {
|
|
1261
|
-
const rootSeg = s.segments[0] ?? "";
|
|
1262
|
-
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1263
|
-
}
|
|
1264
|
-
if (prior !== void 0) {
|
|
1265
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1266
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1267
|
-
return result2;
|
|
1268
|
-
}
|
|
1552
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1553
|
+
let prior;
|
|
1554
|
+
if (s.segments.length > 1) {
|
|
1555
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1556
|
+
const parentScope = lookupResObj(
|
|
1557
|
+
this.root,
|
|
1558
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1559
|
+
);
|
|
1560
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1561
|
+
} else {
|
|
1562
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1563
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1269
1564
|
}
|
|
1565
|
+
if (prior !== void 0) {
|
|
1566
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1567
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1568
|
+
return result2;
|
|
1569
|
+
}
|
|
1570
|
+
if (s.optional) return void 0;
|
|
1571
|
+
throw new ResolveError(
|
|
1572
|
+
`could not resolve substitution: \${${key}}`,
|
|
1573
|
+
key,
|
|
1574
|
+
s.line,
|
|
1575
|
+
s.col
|
|
1576
|
+
);
|
|
1270
1577
|
}
|
|
1271
1578
|
let result = this.resolveVal(found, scope);
|
|
1272
1579
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
1273
1580
|
if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
|
|
1274
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1581
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1275
1582
|
let prior;
|
|
1276
1583
|
if (s.prefixLen > 0) {
|
|
1277
1584
|
const parentScope = lookupResObj(
|
|
@@ -1295,8 +1602,23 @@ var SubstitutionResolver = class {
|
|
|
1295
1602
|
if (result !== void 0) this.cache.set(key, result);
|
|
1296
1603
|
return result;
|
|
1297
1604
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1605
|
+
if (s.listSuffix) {
|
|
1606
|
+
const result = this.resolveEnvList(s);
|
|
1607
|
+
if (result !== void 0) {
|
|
1608
|
+
this.cache.set(key, result);
|
|
1609
|
+
return result;
|
|
1610
|
+
}
|
|
1611
|
+
if (s.optional) return void 0;
|
|
1612
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1613
|
+
throw new ResolveError(
|
|
1614
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1615
|
+
key,
|
|
1616
|
+
s.line,
|
|
1617
|
+
s.col
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1621
|
+
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1300
1622
|
if (envVal !== void 0) {
|
|
1301
1623
|
const result = { kind: "scalar", raw: envVal, valueType: "string" };
|
|
1302
1624
|
this.cache.set(key, result);
|
|
@@ -1313,37 +1635,106 @@ var SubstitutionResolver = class {
|
|
|
1313
1635
|
this.resolving.delete(key);
|
|
1314
1636
|
}
|
|
1315
1637
|
}
|
|
1316
|
-
|
|
1638
|
+
/**
|
|
1639
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1640
|
+
*
|
|
1641
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1642
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1643
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1644
|
+
*
|
|
1645
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1646
|
+
* none do (caller applies optional/required handling).
|
|
1647
|
+
*/
|
|
1648
|
+
resolveEnvList(s) {
|
|
1649
|
+
const bases = this.candidateBases(s);
|
|
1650
|
+
for (const base of bases) {
|
|
1651
|
+
const key0 = base + "_0";
|
|
1652
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1653
|
+
const items = [];
|
|
1654
|
+
for (let i = 0; ; i++) {
|
|
1655
|
+
const k = base + "_" + i;
|
|
1656
|
+
if (!(k in this.opts.env)) break;
|
|
1657
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1658
|
+
}
|
|
1659
|
+
return { kind: "array", items };
|
|
1660
|
+
}
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1665
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1666
|
+
*/
|
|
1667
|
+
candidateBases(s) {
|
|
1668
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1669
|
+
if (s.prefixLen === 0) return [full];
|
|
1670
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1671
|
+
return [full, bare];
|
|
1672
|
+
}
|
|
1673
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1317
1674
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1318
1675
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1319
1676
|
if (resolved.length === 1) return resolved[0];
|
|
1320
1677
|
const nonSep = resolved.filter((v) => !separatorValues.has(v));
|
|
1321
|
-
if (nonSep.length
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
}
|
|
1678
|
+
if (nonSep.length === 0) {
|
|
1679
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1680
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1681
|
+
}
|
|
1682
|
+
const joinPair = (left, right) => {
|
|
1683
|
+
if (left.kind === "object" && right.kind === "object") {
|
|
1684
|
+
return deepMergeHoconValues(left, right);
|
|
1685
|
+
}
|
|
1686
|
+
if (left.kind === "array" && right.kind === "object") {
|
|
1687
|
+
const converted = numericObjectToArray(right);
|
|
1688
|
+
if (converted !== null) {
|
|
1689
|
+
return { kind: "array", items: [...left.items, ...converted] };
|
|
1333
1690
|
}
|
|
1691
|
+
throw new ResolveError(
|
|
1692
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1693
|
+
"",
|
|
1694
|
+
line,
|
|
1695
|
+
col
|
|
1696
|
+
);
|
|
1334
1697
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1698
|
+
if (left.kind === "object" && right.kind === "array") {
|
|
1699
|
+
const converted = numericObjectToArray(left);
|
|
1700
|
+
if (converted !== null) {
|
|
1701
|
+
return { kind: "array", items: [...converted, ...right.items] };
|
|
1702
|
+
}
|
|
1703
|
+
throw new ResolveError(
|
|
1704
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1705
|
+
"",
|
|
1706
|
+
line,
|
|
1707
|
+
col
|
|
1708
|
+
);
|
|
1342
1709
|
}
|
|
1343
|
-
|
|
1710
|
+
if (left.kind === "array" && right.kind === "array") {
|
|
1711
|
+
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1712
|
+
}
|
|
1713
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1714
|
+
throw new ResolveError(
|
|
1715
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1716
|
+
"",
|
|
1717
|
+
line,
|
|
1718
|
+
col
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1722
|
+
throw new ResolveError(
|
|
1723
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1724
|
+
"",
|
|
1725
|
+
line,
|
|
1726
|
+
col
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1730
|
+
};
|
|
1731
|
+
const [head, ...tail] = nonSep;
|
|
1732
|
+
const folded = tail.reduce(joinPair, head);
|
|
1733
|
+
if (folded.kind === "scalar") {
|
|
1734
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1735
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1344
1736
|
}
|
|
1345
|
-
|
|
1346
|
-
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1737
|
+
return folded;
|
|
1347
1738
|
}
|
|
1348
1739
|
resolveAppend(a, scope) {
|
|
1349
1740
|
const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
|
|
@@ -1402,6 +1793,7 @@ var Config = class _Config {
|
|
|
1402
1793
|
}
|
|
1403
1794
|
const result = parseBytes(v.raw, unit);
|
|
1404
1795
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1796
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1405
1797
|
return result;
|
|
1406
1798
|
}
|
|
1407
1799
|
getConfig(path2) {
|
|
@@ -1413,6 +1805,10 @@ var Config = class _Config {
|
|
|
1413
1805
|
getList(path2) {
|
|
1414
1806
|
const v = this.lookupNode(path2);
|
|
1415
1807
|
if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
|
|
1808
|
+
if (v.kind === "object") {
|
|
1809
|
+
const converted = numericObjectToArray(v);
|
|
1810
|
+
if (converted !== null) return converted.map(hoconToJs);
|
|
1811
|
+
}
|
|
1416
1812
|
if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
|
|
1417
1813
|
return v.items.map(hoconToJs);
|
|
1418
1814
|
}
|
|
@@ -1537,6 +1933,7 @@ function defaultReadFileSync(filePath) {
|
|
|
1537
1933
|
}
|
|
1538
1934
|
function buildResolveContext(input, opts) {
|
|
1539
1935
|
const tokens = tokenize(input);
|
|
1936
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1540
1937
|
const ast = parseTokens(tokens);
|
|
1541
1938
|
const resolveOpts = {
|
|
1542
1939
|
env: getEnv(opts),
|