@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/index.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
|
}
|
|
@@ -165,6 +223,23 @@ var ConfigError = class extends Error {
|
|
|
165
223
|
}
|
|
166
224
|
};
|
|
167
225
|
|
|
226
|
+
// src/value/numeric-array.ts
|
|
227
|
+
function numericObjectToArray(value) {
|
|
228
|
+
if (value.kind !== "object") return null;
|
|
229
|
+
if (value.fields.size === 0) return null;
|
|
230
|
+
const canonicalInt = /^(0|[1-9][0-9]*)$/;
|
|
231
|
+
const eligible = [];
|
|
232
|
+
for (const [key, val] of value.fields) {
|
|
233
|
+
if (!canonicalInt.test(key)) continue;
|
|
234
|
+
const n = Number(key);
|
|
235
|
+
if (!Number.isInteger(n) || n > 2147483647) continue;
|
|
236
|
+
eligible.push({ n, v: val });
|
|
237
|
+
}
|
|
238
|
+
if (eligible.length === 0) return null;
|
|
239
|
+
eligible.sort((a, b) => a.n - b.n);
|
|
240
|
+
return eligible.map((e) => e.v);
|
|
241
|
+
}
|
|
242
|
+
|
|
168
243
|
// src/config.ts
|
|
169
244
|
var Config = class _Config {
|
|
170
245
|
constructor(root) {
|
|
@@ -207,6 +282,7 @@ var Config = class _Config {
|
|
|
207
282
|
}
|
|
208
283
|
const result = parseBytes(v.raw, unit);
|
|
209
284
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
285
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
210
286
|
return result;
|
|
211
287
|
}
|
|
212
288
|
getConfig(path2) {
|
|
@@ -218,6 +294,10 @@ var Config = class _Config {
|
|
|
218
294
|
getList(path2) {
|
|
219
295
|
const v = this.lookupNode(path2);
|
|
220
296
|
if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
|
|
297
|
+
if (v.kind === "object") {
|
|
298
|
+
const converted = numericObjectToArray(v);
|
|
299
|
+
if (converted !== null) return converted.map(hoconToJs);
|
|
300
|
+
}
|
|
221
301
|
if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
|
|
222
302
|
return v.items.map(hoconToJs);
|
|
223
303
|
}
|
|
@@ -330,6 +410,20 @@ function deepMergeHocon(receiver, fallback) {
|
|
|
330
410
|
}
|
|
331
411
|
|
|
332
412
|
// src/internal/lexer/lexer.ts
|
|
413
|
+
function isHoconWhitespace(ch) {
|
|
414
|
+
const cp = ch.codePointAt(0) ?? -1;
|
|
415
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
416
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
417
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
418
|
+
if (cp === 5760) return true;
|
|
419
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
420
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
421
|
+
if (cp === 12288) return true;
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
function isHoconNewline(ch) {
|
|
425
|
+
return ch === "\n";
|
|
426
|
+
}
|
|
333
427
|
var SINGLE_CHAR_TOKENS = {
|
|
334
428
|
"{": "lbrace",
|
|
335
429
|
"}": "rbrace",
|
|
@@ -352,18 +446,18 @@ var Lexer = class {
|
|
|
352
446
|
while (this.pos < this.input.length) {
|
|
353
447
|
const sl = this.line, sc = this.col;
|
|
354
448
|
const ch = this.peek();
|
|
355
|
-
if (ch
|
|
356
|
-
this.advance();
|
|
357
|
-
this.hadSpace = true;
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
if (ch === "\n") {
|
|
449
|
+
if (isHoconNewline(ch)) {
|
|
361
450
|
this.advance();
|
|
362
451
|
if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
|
|
363
452
|
this.push("newline", "\n", sl, sc);
|
|
364
453
|
}
|
|
365
454
|
continue;
|
|
366
455
|
}
|
|
456
|
+
if (isHoconWhitespace(ch)) {
|
|
457
|
+
this.advance();
|
|
458
|
+
this.hadSpace = true;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
367
461
|
if (ch === "/" && this.peek(1) === "/") {
|
|
368
462
|
while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
|
|
369
463
|
this.hadSpace = true;
|
|
@@ -393,16 +487,16 @@ var Lexer = class {
|
|
|
393
487
|
if (ch === "$" && this.peek(1) === "{") {
|
|
394
488
|
this.advance();
|
|
395
489
|
this.advance();
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
this.
|
|
490
|
+
const payload = this.parseSubstBody(sl, sc);
|
|
491
|
+
const value = payload.segments.map((s) => {
|
|
492
|
+
const t = s.text;
|
|
493
|
+
if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
|
|
494
|
+
const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
495
|
+
return `"${escaped}"`;
|
|
496
|
+
}
|
|
497
|
+
return t;
|
|
498
|
+
}).join(".");
|
|
499
|
+
this.pushSubst(payload, value, sl, sc);
|
|
406
500
|
continue;
|
|
407
501
|
}
|
|
408
502
|
if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
|
|
@@ -437,58 +531,7 @@ var Lexer = class {
|
|
|
437
531
|
}
|
|
438
532
|
if (ch === '"') {
|
|
439
533
|
this.advance();
|
|
440
|
-
|
|
441
|
-
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
442
|
-
if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
|
|
443
|
-
if (this.peek() === "\\") {
|
|
444
|
-
this.advance();
|
|
445
|
-
const esc = this.advance();
|
|
446
|
-
switch (esc) {
|
|
447
|
-
case "n":
|
|
448
|
-
value += "\n";
|
|
449
|
-
break;
|
|
450
|
-
case "t":
|
|
451
|
-
value += " ";
|
|
452
|
-
break;
|
|
453
|
-
case "r":
|
|
454
|
-
value += "\r";
|
|
455
|
-
break;
|
|
456
|
-
case '"':
|
|
457
|
-
value += '"';
|
|
458
|
-
break;
|
|
459
|
-
case "\\":
|
|
460
|
-
value += "\\";
|
|
461
|
-
break;
|
|
462
|
-
case "/":
|
|
463
|
-
value += "/";
|
|
464
|
-
break;
|
|
465
|
-
case "b":
|
|
466
|
-
value += "\b";
|
|
467
|
-
break;
|
|
468
|
-
case "f":
|
|
469
|
-
value += "\f";
|
|
470
|
-
break;
|
|
471
|
-
case "u": {
|
|
472
|
-
if (this.pos + 4 > this.input.length) {
|
|
473
|
-
throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
|
|
474
|
-
}
|
|
475
|
-
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
476
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
477
|
-
throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
|
|
478
|
-
}
|
|
479
|
-
value += String.fromCharCode(parseInt(hex, 16));
|
|
480
|
-
for (let i = 0; i < 4; i++) this.advance();
|
|
481
|
-
break;
|
|
482
|
-
}
|
|
483
|
-
default:
|
|
484
|
-
throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
|
|
485
|
-
}
|
|
486
|
-
} else {
|
|
487
|
-
value += this.advance();
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
|
|
491
|
-
this.advance();
|
|
534
|
+
const value = this.readQuotedStringBody(sl, sc);
|
|
492
535
|
this.push("string", value, sl, sc, true);
|
|
493
536
|
continue;
|
|
494
537
|
}
|
|
@@ -522,22 +565,255 @@ var Lexer = class {
|
|
|
522
565
|
this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
|
|
523
566
|
this.hadSpace = false;
|
|
524
567
|
}
|
|
568
|
+
pushSubst(payload, value, l, c) {
|
|
569
|
+
this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
|
|
570
|
+
this.hadSpace = false;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Read body of a quoted string. Opening `"` must already be consumed.
|
|
574
|
+
* Returns decoded string. Throws ParseError on unterminated/invalid-escape.
|
|
575
|
+
* openLine/openCol are the position of the opening `"` for error reporting.
|
|
576
|
+
*/
|
|
577
|
+
readQuotedStringBody(openLine, openCol) {
|
|
578
|
+
let value = "";
|
|
579
|
+
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
580
|
+
if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
|
|
581
|
+
if (this.peek() === "\\") {
|
|
582
|
+
const escCol = this.col;
|
|
583
|
+
this.advance();
|
|
584
|
+
if (this.pos >= this.input.length) {
|
|
585
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
586
|
+
}
|
|
587
|
+
const esc = this.advance();
|
|
588
|
+
switch (esc) {
|
|
589
|
+
case "n":
|
|
590
|
+
value += "\n";
|
|
591
|
+
break;
|
|
592
|
+
case "t":
|
|
593
|
+
value += " ";
|
|
594
|
+
break;
|
|
595
|
+
case "r":
|
|
596
|
+
value += "\r";
|
|
597
|
+
break;
|
|
598
|
+
case '"':
|
|
599
|
+
value += '"';
|
|
600
|
+
break;
|
|
601
|
+
case "\\":
|
|
602
|
+
value += "\\";
|
|
603
|
+
break;
|
|
604
|
+
case "/":
|
|
605
|
+
value += "/";
|
|
606
|
+
break;
|
|
607
|
+
case "b":
|
|
608
|
+
value += "\b";
|
|
609
|
+
break;
|
|
610
|
+
case "f":
|
|
611
|
+
value += "\f";
|
|
612
|
+
break;
|
|
613
|
+
case "u": {
|
|
614
|
+
if (this.pos + 4 > this.input.length) {
|
|
615
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
616
|
+
}
|
|
617
|
+
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
618
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
619
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
620
|
+
}
|
|
621
|
+
const code = parseInt(hex, 16);
|
|
622
|
+
value += String.fromCharCode(code);
|
|
623
|
+
for (let i = 0; i < 4; i++) this.advance();
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
default:
|
|
627
|
+
throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
value += this.advance();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (this.pos >= this.input.length || this.peek() !== '"') {
|
|
634
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
635
|
+
}
|
|
636
|
+
this.advance();
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
641
|
+
* Implements Appendix A state machine from the cross-language spec.
|
|
642
|
+
*
|
|
643
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
644
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
645
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
646
|
+
*/
|
|
647
|
+
parseSubstBody(startLine, startCol) {
|
|
648
|
+
let optional = false;
|
|
649
|
+
if (this.peek() === "?") {
|
|
650
|
+
this.advance();
|
|
651
|
+
optional = true;
|
|
652
|
+
}
|
|
653
|
+
let curText = "";
|
|
654
|
+
let curStarted = false;
|
|
655
|
+
let curLine = 0;
|
|
656
|
+
let curCol = 0;
|
|
657
|
+
let pendingWs = "";
|
|
658
|
+
const segments = [];
|
|
659
|
+
let lastDot = null;
|
|
660
|
+
let listSuffix = false;
|
|
661
|
+
while (true) {
|
|
662
|
+
if (this.pos >= this.input.length) {
|
|
663
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
664
|
+
}
|
|
665
|
+
const ch = this.peek();
|
|
666
|
+
if (ch === "}") {
|
|
667
|
+
this.advance();
|
|
668
|
+
pendingWs = "";
|
|
669
|
+
break;
|
|
670
|
+
} else if (ch === "[") {
|
|
671
|
+
if (!curStarted) {
|
|
672
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
673
|
+
}
|
|
674
|
+
for (const w of pendingWs) {
|
|
675
|
+
if (w !== " " && w !== " ") {
|
|
676
|
+
throw new ParseError(
|
|
677
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
678
|
+
startLine,
|
|
679
|
+
this.col
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
684
|
+
pendingWs = "";
|
|
685
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
686
|
+
break;
|
|
687
|
+
} else if (ch === '"') {
|
|
688
|
+
const qLine = startLine;
|
|
689
|
+
const qCol = this.col;
|
|
690
|
+
if (curStarted) {
|
|
691
|
+
curText += pendingWs;
|
|
692
|
+
}
|
|
693
|
+
pendingWs = "";
|
|
694
|
+
this.advance();
|
|
695
|
+
const decoded = this.readQuotedStringBody(qLine, qCol);
|
|
696
|
+
curText += decoded;
|
|
697
|
+
if (!curStarted) {
|
|
698
|
+
curLine = qLine;
|
|
699
|
+
curCol = qCol;
|
|
700
|
+
curStarted = true;
|
|
701
|
+
}
|
|
702
|
+
} else if (isUnquotedSubstChar(ch)) {
|
|
703
|
+
if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
|
|
704
|
+
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
705
|
+
throw new ParseError(
|
|
706
|
+
`unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
707
|
+
startLine,
|
|
708
|
+
this.col
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
const uCol = this.col;
|
|
712
|
+
if (curStarted) {
|
|
713
|
+
curText += pendingWs;
|
|
714
|
+
}
|
|
715
|
+
pendingWs = "";
|
|
716
|
+
if (!curStarted) {
|
|
717
|
+
curLine = startLine;
|
|
718
|
+
curCol = uCol;
|
|
719
|
+
curStarted = true;
|
|
720
|
+
}
|
|
721
|
+
while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
|
|
722
|
+
curText += this.advance();
|
|
723
|
+
}
|
|
724
|
+
} else if (ch === ".") {
|
|
725
|
+
const dotCol = this.col;
|
|
726
|
+
pendingWs = "";
|
|
727
|
+
if (!curStarted) {
|
|
728
|
+
throw new ParseError("empty segment in path", startLine, dotCol);
|
|
729
|
+
}
|
|
730
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
731
|
+
curText = "";
|
|
732
|
+
curStarted = false;
|
|
733
|
+
curLine = 0;
|
|
734
|
+
curCol = 0;
|
|
735
|
+
lastDot = [startLine, dotCol];
|
|
736
|
+
this.advance();
|
|
737
|
+
} else if (isHoconNewline(ch)) {
|
|
738
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
739
|
+
} else if (isHoconWhitespace(ch)) {
|
|
740
|
+
pendingWs += ch;
|
|
741
|
+
this.advance();
|
|
742
|
+
} else {
|
|
743
|
+
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!listSuffix) {
|
|
747
|
+
if (curStarted) {
|
|
748
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
749
|
+
} else if (segments.length === 0) {
|
|
750
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
751
|
+
} else {
|
|
752
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
753
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { segments, optional, listSuffix };
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
760
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
761
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
762
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
763
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
764
|
+
*/
|
|
765
|
+
parseListSuffix(startLine) {
|
|
766
|
+
this.advance();
|
|
767
|
+
const afterBracket = this.peek();
|
|
768
|
+
if (afterBracket !== "]") {
|
|
769
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
770
|
+
throw new ParseError(
|
|
771
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
772
|
+
startLine,
|
|
773
|
+
this.col
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
this.advance();
|
|
777
|
+
if (this.peek() !== "}") {
|
|
778
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
779
|
+
throw new ParseError(
|
|
780
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
781
|
+
startLine,
|
|
782
|
+
this.col
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
this.advance();
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
525
788
|
};
|
|
526
789
|
function tokenize(input) {
|
|
527
790
|
return new Lexer(input).tokenize();
|
|
528
791
|
}
|
|
792
|
+
function isUnquotedSubstChar(ch) {
|
|
793
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
794
|
+
if ('"\\'.includes(ch)) return false;
|
|
795
|
+
if ("{}[]".includes(ch)) return false;
|
|
796
|
+
if (":=,+#`^?!@*&$.".includes(ch)) return false;
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
function isDecimalDigit(ch) {
|
|
800
|
+
return ch >= "0" && ch <= "9";
|
|
801
|
+
}
|
|
529
802
|
function isUnquotedStart(ch) {
|
|
530
|
-
|
|
803
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
804
|
+
if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
|
|
805
|
+
return true;
|
|
531
806
|
}
|
|
532
807
|
function isUnquotedContinue(ch, nextFn) {
|
|
533
|
-
if (ch === "" ||
|
|
808
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
809
|
+
if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
|
|
534
810
|
if (ch === "+" && nextFn() === "=") return false;
|
|
535
811
|
if (ch === "/" && nextFn() === "/") return false;
|
|
536
812
|
return true;
|
|
537
813
|
}
|
|
538
814
|
|
|
539
815
|
// src/internal/parser/parser.ts
|
|
540
|
-
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
|
|
816
|
+
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
|
|
541
817
|
var Parser = class {
|
|
542
818
|
constructor(tokens) {
|
|
543
819
|
this.tokens = tokens;
|
|
@@ -595,6 +871,14 @@ var Parser = class {
|
|
|
595
871
|
const t = this.peek();
|
|
596
872
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
597
873
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
874
|
+
const next = this.peek(1);
|
|
875
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
876
|
+
throw new ParseError(
|
|
877
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
878
|
+
t.line,
|
|
879
|
+
t.col
|
|
880
|
+
);
|
|
881
|
+
}
|
|
598
882
|
this.advance();
|
|
599
883
|
fields.push(this.parseInclude());
|
|
600
884
|
this.skip("newline");
|
|
@@ -603,7 +887,14 @@ var Parser = class {
|
|
|
603
887
|
continue;
|
|
604
888
|
}
|
|
605
889
|
const keyPos = this.currentPos();
|
|
606
|
-
const key = this.parseKey();
|
|
890
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
891
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
892
|
+
throw new ParseError(
|
|
893
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
894
|
+
keyPos.line,
|
|
895
|
+
keyPos.col
|
|
896
|
+
);
|
|
897
|
+
}
|
|
607
898
|
this.skip("newline");
|
|
608
899
|
let append = false;
|
|
609
900
|
const sep = this.peek();
|
|
@@ -633,18 +924,31 @@ var Parser = class {
|
|
|
633
924
|
parseKey() {
|
|
634
925
|
const segments = [];
|
|
635
926
|
let trailingDot = false;
|
|
927
|
+
let firstWasQuoted = false;
|
|
636
928
|
while (true) {
|
|
637
929
|
const t = this.peek();
|
|
638
930
|
if (t.kind === "string") {
|
|
639
931
|
this.advance();
|
|
932
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
640
933
|
segments.push(t.value);
|
|
641
934
|
trailingDot = false;
|
|
642
935
|
} else if (t.kind === "unquoted") {
|
|
643
936
|
this.advance();
|
|
644
|
-
const
|
|
937
|
+
const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
|
|
938
|
+
const parts = raw.split(".");
|
|
645
939
|
const filtered = parts.filter((s) => s.length > 0);
|
|
940
|
+
for (const seg of filtered) {
|
|
941
|
+
if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
|
|
942
|
+
const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
|
|
943
|
+
throw new ParseError(
|
|
944
|
+
`unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
|
|
945
|
+
t.line,
|
|
946
|
+
t.col
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
646
950
|
segments.push(...filtered);
|
|
647
|
-
trailingDot =
|
|
951
|
+
trailingDot = raw.endsWith(".");
|
|
648
952
|
} else {
|
|
649
953
|
if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
|
|
650
954
|
break;
|
|
@@ -656,9 +960,13 @@ var Parser = class {
|
|
|
656
960
|
trailingDot = true;
|
|
657
961
|
continue;
|
|
658
962
|
}
|
|
963
|
+
if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
|
|
964
|
+
trailingDot = true;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
659
967
|
break;
|
|
660
968
|
}
|
|
661
|
-
return segments;
|
|
969
|
+
return { segments, firstWasQuoted };
|
|
662
970
|
}
|
|
663
971
|
parseInclude() {
|
|
664
972
|
const p = this.currentPos();
|
|
@@ -731,9 +1039,13 @@ var Parser = class {
|
|
|
731
1039
|
} else if (t.kind === "lbracket") {
|
|
732
1040
|
this.advance();
|
|
733
1041
|
node = this.parseArray();
|
|
734
|
-
} else if (t.kind === "subst"
|
|
1042
|
+
} else if (t.kind === "subst") {
|
|
735
1043
|
this.advance();
|
|
736
|
-
|
|
1044
|
+
if (!t.subst) {
|
|
1045
|
+
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
1046
|
+
}
|
|
1047
|
+
const payload = t.subst;
|
|
1048
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
737
1049
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
738
1050
|
this.advance();
|
|
739
1051
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -784,6 +1096,20 @@ function parseTokens(tokens) {
|
|
|
784
1096
|
return new Parser(tokens).parse();
|
|
785
1097
|
}
|
|
786
1098
|
|
|
1099
|
+
// src/internal/parser/empty-check.ts
|
|
1100
|
+
function hasContentTokens(tokens) {
|
|
1101
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
1102
|
+
}
|
|
1103
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
1104
|
+
if (!hasContentTokens(tokens)) {
|
|
1105
|
+
throw new ParseError(
|
|
1106
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
1107
|
+
1,
|
|
1108
|
+
1
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
787
1113
|
// src/internal/resolver/types.ts
|
|
788
1114
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
789
1115
|
function isSubst(v) {
|
|
@@ -803,64 +1129,20 @@ function makeResObj() {
|
|
|
803
1129
|
}
|
|
804
1130
|
|
|
805
1131
|
// src/internal/resolver/utils.ts
|
|
806
|
-
function parseSubstPath(raw) {
|
|
807
|
-
const segments = [];
|
|
808
|
-
let i = 0;
|
|
809
|
-
while (i < raw.length) {
|
|
810
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
811
|
-
if (i >= raw.length) break;
|
|
812
|
-
if (raw[i] === '"') {
|
|
813
|
-
i++;
|
|
814
|
-
let seg = "";
|
|
815
|
-
while (i < raw.length && raw[i] !== '"') {
|
|
816
|
-
if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
|
|
817
|
-
seg += raw[i + 1];
|
|
818
|
-
i += 2;
|
|
819
|
-
} else {
|
|
820
|
-
seg += raw[i];
|
|
821
|
-
i++;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
if (i < raw.length) i++;
|
|
825
|
-
segments.push(seg);
|
|
826
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
827
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
828
|
-
} else if (raw[i] === ".") {
|
|
829
|
-
segments.push("");
|
|
830
|
-
i++;
|
|
831
|
-
} else {
|
|
832
|
-
let seg = "";
|
|
833
|
-
while (i < raw.length && raw[i] !== ".") {
|
|
834
|
-
seg += raw[i];
|
|
835
|
-
i++;
|
|
836
|
-
}
|
|
837
|
-
segments.push(seg.trim());
|
|
838
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
return segments;
|
|
842
|
-
}
|
|
843
1132
|
function segmentsToKey(segments) {
|
|
844
1133
|
return segments.map((s) => {
|
|
845
|
-
|
|
846
|
-
|
|
1134
|
+
const t = s.text;
|
|
1135
|
+
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
1136
|
+
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
847
1137
|
}
|
|
848
|
-
return
|
|
1138
|
+
return t;
|
|
849
1139
|
}).join(".");
|
|
850
1140
|
}
|
|
851
1141
|
function lookupPath(root, segments) {
|
|
852
1142
|
const [head, ...tail] = segments;
|
|
853
|
-
if (head === void 0
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (val2 === void 0) return void 0;
|
|
857
|
-
if (tail.length === 0) return val2;
|
|
858
|
-
if (isResObj(val2)) return lookupPath(val2, tail);
|
|
859
|
-
return void 0;
|
|
860
|
-
}
|
|
861
|
-
return void 0;
|
|
862
|
-
}
|
|
863
|
-
const val = root.fields.get(head);
|
|
1143
|
+
if (head === void 0) return void 0;
|
|
1144
|
+
const key = head.text;
|
|
1145
|
+
const val = root.fields.get(key);
|
|
864
1146
|
if (val === void 0) return void 0;
|
|
865
1147
|
if (tail.length === 0) return val;
|
|
866
1148
|
if (isResObj(val)) return lookupPath(val, tail);
|
|
@@ -869,7 +1151,7 @@ function lookupPath(root, segments) {
|
|
|
869
1151
|
function lookupResObj(root, segments) {
|
|
870
1152
|
let cur = root;
|
|
871
1153
|
for (const seg of segments) {
|
|
872
|
-
const val = cur.fields.get(seg);
|
|
1154
|
+
const val = cur.fields.get(seg.text);
|
|
873
1155
|
if (val === void 0 || !isResObj(val)) return void 0;
|
|
874
1156
|
cur = val;
|
|
875
1157
|
}
|
|
@@ -929,6 +1211,7 @@ import * as nodePath from "path";
|
|
|
929
1211
|
// src/internal/properties/properties.ts
|
|
930
1212
|
function parseProperties(input) {
|
|
931
1213
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1214
|
+
const pairs = [];
|
|
932
1215
|
for (const line of input.split("\n")) {
|
|
933
1216
|
const trimmed = line.trim();
|
|
934
1217
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -937,6 +1220,10 @@ function parseProperties(input) {
|
|
|
937
1220
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
938
1221
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
939
1222
|
if (key === "") continue;
|
|
1223
|
+
pairs.push([key, value]);
|
|
1224
|
+
}
|
|
1225
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1226
|
+
for (const [key, value] of pairs) {
|
|
940
1227
|
setNested(root, key.split("."), value);
|
|
941
1228
|
}
|
|
942
1229
|
return root;
|
|
@@ -960,6 +1247,7 @@ function setNested(obj, segments, value) {
|
|
|
960
1247
|
}
|
|
961
1248
|
const last = segments[segments.length - 1];
|
|
962
1249
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1250
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
963
1251
|
current[last] = value;
|
|
964
1252
|
}
|
|
965
1253
|
function propertiesToHoconValue(input) {
|
|
@@ -1073,7 +1361,9 @@ var IncludeLoader = class {
|
|
|
1073
1361
|
if (candidate.endsWith(".properties")) {
|
|
1074
1362
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1075
1363
|
}
|
|
1076
|
-
const
|
|
1364
|
+
const tokens = tokenize(content);
|
|
1365
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1366
|
+
const ast = parseTokens(tokens);
|
|
1077
1367
|
return this.onBuildResObj(ast, {
|
|
1078
1368
|
env,
|
|
1079
1369
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1097,7 +1387,9 @@ var IncludeLoader = class {
|
|
|
1097
1387
|
if (candidate.endsWith(".properties")) {
|
|
1098
1388
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1099
1389
|
}
|
|
1100
|
-
const
|
|
1390
|
+
const tokens = tokenize(content);
|
|
1391
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1392
|
+
const ast = parseTokens(tokens);
|
|
1101
1393
|
return this.onBuildResObjAsync(ast, {
|
|
1102
1394
|
env,
|
|
1103
1395
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1146,7 +1438,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1146
1438
|
return;
|
|
1147
1439
|
}
|
|
1148
1440
|
const [head, ...tail] = field.key;
|
|
1149
|
-
if (
|
|
1441
|
+
if (head === void 0) return;
|
|
1150
1442
|
if (tail.length > 0) {
|
|
1151
1443
|
const syntheticAst = {
|
|
1152
1444
|
kind: "object",
|
|
@@ -1188,7 +1480,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1188
1480
|
return;
|
|
1189
1481
|
}
|
|
1190
1482
|
const [head, ...tail] = field.key;
|
|
1191
|
-
if (
|
|
1483
|
+
if (head === void 0) return;
|
|
1192
1484
|
if (tail.length > 0) {
|
|
1193
1485
|
const syntheticAst = {
|
|
1194
1486
|
kind: "object",
|
|
@@ -1234,9 +1526,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1234
1526
|
return inner;
|
|
1235
1527
|
}
|
|
1236
1528
|
case "subst":
|
|
1237
|
-
return { _kind: "subst-placeholder", segments:
|
|
1529
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1238
1530
|
case "concat":
|
|
1239
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1531
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1240
1532
|
case "include":
|
|
1241
1533
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1242
1534
|
}
|
|
@@ -1258,13 +1550,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1258
1550
|
case "object":
|
|
1259
1551
|
return await this.buildAsync(ast, pathPrefix);
|
|
1260
1552
|
case "subst":
|
|
1261
|
-
return { _kind: "subst-placeholder", segments:
|
|
1553
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1262
1554
|
case "concat": {
|
|
1263
1555
|
const nodes = [];
|
|
1264
1556
|
for (const n of ast.nodes) {
|
|
1265
1557
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1266
1558
|
}
|
|
1267
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1559
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1268
1560
|
}
|
|
1269
1561
|
case "include":
|
|
1270
1562
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1273,7 +1565,8 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1273
1565
|
// ---- Relativize substitution paths for nested includes ----
|
|
1274
1566
|
relativizeSubstPaths(val, prefixSegments) {
|
|
1275
1567
|
if (isSubst(val)) {
|
|
1276
|
-
|
|
1568
|
+
const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
|
|
1569
|
+
val.segments = [...prefixAsSegments, ...val.segments];
|
|
1277
1570
|
val.prefixLen += prefixSegments.length;
|
|
1278
1571
|
return;
|
|
1279
1572
|
}
|
|
@@ -1316,6 +1609,19 @@ var SubstitutionResolver = class {
|
|
|
1316
1609
|
this.opts = opts;
|
|
1317
1610
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1318
1611
|
this.cache = /* @__PURE__ */ new Map();
|
|
1612
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1613
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1614
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1615
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1616
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1617
|
+
//
|
|
1618
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1619
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1620
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1621
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1622
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1623
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1624
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1319
1625
|
}
|
|
1320
1626
|
resolve() {
|
|
1321
1627
|
return this.resolveResObj(this.root);
|
|
@@ -1354,7 +1660,14 @@ var SubstitutionResolver = class {
|
|
|
1354
1660
|
}
|
|
1355
1661
|
resolveVal(v, scope) {
|
|
1356
1662
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1357
|
-
if (isConcat(v))
|
|
1663
|
+
if (isConcat(v)) {
|
|
1664
|
+
this.resolvingConcats.add(v);
|
|
1665
|
+
try {
|
|
1666
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1667
|
+
} finally {
|
|
1668
|
+
this.resolvingConcats.delete(v);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1358
1671
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1359
1672
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1360
1673
|
const hv = v;
|
|
@@ -1368,23 +1681,20 @@ var SubstitutionResolver = class {
|
|
|
1368
1681
|
}
|
|
1369
1682
|
return hv;
|
|
1370
1683
|
}
|
|
1371
|
-
segmentsEqual(a, b) {
|
|
1372
|
-
return a.length === b.length && a.every((seg, i) => seg === b[i]);
|
|
1373
|
-
}
|
|
1374
1684
|
resolveSubst(s, scope) {
|
|
1375
|
-
const key = segmentsToKey(s.segments);
|
|
1685
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1376
1686
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1377
1687
|
if (this.resolving.has(key)) {
|
|
1378
1688
|
let prior;
|
|
1379
|
-
if (s.
|
|
1380
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1689
|
+
if (s.segments.length > 1) {
|
|
1690
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1381
1691
|
const parentScope = lookupResObj(
|
|
1382
1692
|
this.root,
|
|
1383
1693
|
s.segments.slice(0, s.segments.length - 1)
|
|
1384
1694
|
);
|
|
1385
1695
|
prior = parentScope?.priorValues.get(leafSeg);
|
|
1386
1696
|
} else {
|
|
1387
|
-
const rootSeg = s.segments[0] ?? "";
|
|
1697
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1388
1698
|
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1389
1699
|
}
|
|
1390
1700
|
if (prior !== void 0) {
|
|
@@ -1408,34 +1718,36 @@ var SubstitutionResolver = class {
|
|
|
1408
1718
|
try {
|
|
1409
1719
|
const found = lookupPath(this.root, s.segments);
|
|
1410
1720
|
if (found !== void 0) {
|
|
1411
|
-
if (
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
if (prior !== void 0) {
|
|
1429
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1430
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1431
|
-
return result2;
|
|
1432
|
-
}
|
|
1721
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1722
|
+
let prior;
|
|
1723
|
+
if (s.segments.length > 1) {
|
|
1724
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1725
|
+
const parentScope = lookupResObj(
|
|
1726
|
+
this.root,
|
|
1727
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1728
|
+
);
|
|
1729
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1730
|
+
} else {
|
|
1731
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1732
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1733
|
+
}
|
|
1734
|
+
if (prior !== void 0) {
|
|
1735
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1736
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1737
|
+
return result2;
|
|
1433
1738
|
}
|
|
1739
|
+
if (s.optional) return void 0;
|
|
1740
|
+
throw new ResolveError(
|
|
1741
|
+
`could not resolve substitution: \${${key}}`,
|
|
1742
|
+
key,
|
|
1743
|
+
s.line,
|
|
1744
|
+
s.col
|
|
1745
|
+
);
|
|
1434
1746
|
}
|
|
1435
1747
|
let result = this.resolveVal(found, scope);
|
|
1436
1748
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
1437
1749
|
if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
|
|
1438
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1750
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1439
1751
|
let prior;
|
|
1440
1752
|
if (s.prefixLen > 0) {
|
|
1441
1753
|
const parentScope = lookupResObj(
|
|
@@ -1459,8 +1771,23 @@ var SubstitutionResolver = class {
|
|
|
1459
1771
|
if (result !== void 0) this.cache.set(key, result);
|
|
1460
1772
|
return result;
|
|
1461
1773
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1774
|
+
if (s.listSuffix) {
|
|
1775
|
+
const result = this.resolveEnvList(s);
|
|
1776
|
+
if (result !== void 0) {
|
|
1777
|
+
this.cache.set(key, result);
|
|
1778
|
+
return result;
|
|
1779
|
+
}
|
|
1780
|
+
if (s.optional) return void 0;
|
|
1781
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1782
|
+
throw new ResolveError(
|
|
1783
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1784
|
+
key,
|
|
1785
|
+
s.line,
|
|
1786
|
+
s.col
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1790
|
+
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1464
1791
|
if (envVal !== void 0) {
|
|
1465
1792
|
const result = { kind: "scalar", raw: envVal, valueType: "string" };
|
|
1466
1793
|
this.cache.set(key, result);
|
|
@@ -1477,37 +1804,106 @@ var SubstitutionResolver = class {
|
|
|
1477
1804
|
this.resolving.delete(key);
|
|
1478
1805
|
}
|
|
1479
1806
|
}
|
|
1480
|
-
|
|
1807
|
+
/**
|
|
1808
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1809
|
+
*
|
|
1810
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1811
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1812
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1813
|
+
*
|
|
1814
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1815
|
+
* none do (caller applies optional/required handling).
|
|
1816
|
+
*/
|
|
1817
|
+
resolveEnvList(s) {
|
|
1818
|
+
const bases = this.candidateBases(s);
|
|
1819
|
+
for (const base of bases) {
|
|
1820
|
+
const key0 = base + "_0";
|
|
1821
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1822
|
+
const items = [];
|
|
1823
|
+
for (let i = 0; ; i++) {
|
|
1824
|
+
const k = base + "_" + i;
|
|
1825
|
+
if (!(k in this.opts.env)) break;
|
|
1826
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1827
|
+
}
|
|
1828
|
+
return { kind: "array", items };
|
|
1829
|
+
}
|
|
1830
|
+
return void 0;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1834
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1835
|
+
*/
|
|
1836
|
+
candidateBases(s) {
|
|
1837
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1838
|
+
if (s.prefixLen === 0) return [full];
|
|
1839
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1840
|
+
return [full, bare];
|
|
1841
|
+
}
|
|
1842
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1481
1843
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1482
1844
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1483
1845
|
if (resolved.length === 1) return resolved[0];
|
|
1484
1846
|
const nonSep = resolved.filter((v) => !separatorValues.has(v));
|
|
1485
|
-
if (nonSep.length
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1847
|
+
if (nonSep.length === 0) {
|
|
1848
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1849
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1850
|
+
}
|
|
1851
|
+
const joinPair = (left, right) => {
|
|
1852
|
+
if (left.kind === "object" && right.kind === "object") {
|
|
1853
|
+
return deepMergeHoconValues(left, right);
|
|
1854
|
+
}
|
|
1855
|
+
if (left.kind === "array" && right.kind === "object") {
|
|
1856
|
+
const converted = numericObjectToArray(right);
|
|
1857
|
+
if (converted !== null) {
|
|
1858
|
+
return { kind: "array", items: [...left.items, ...converted] };
|
|
1497
1859
|
}
|
|
1860
|
+
throw new ResolveError(
|
|
1861
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1862
|
+
"",
|
|
1863
|
+
line,
|
|
1864
|
+
col
|
|
1865
|
+
);
|
|
1498
1866
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1867
|
+
if (left.kind === "object" && right.kind === "array") {
|
|
1868
|
+
const converted = numericObjectToArray(left);
|
|
1869
|
+
if (converted !== null) {
|
|
1870
|
+
return { kind: "array", items: [...converted, ...right.items] };
|
|
1871
|
+
}
|
|
1872
|
+
throw new ResolveError(
|
|
1873
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1874
|
+
"",
|
|
1875
|
+
line,
|
|
1876
|
+
col
|
|
1877
|
+
);
|
|
1506
1878
|
}
|
|
1507
|
-
|
|
1879
|
+
if (left.kind === "array" && right.kind === "array") {
|
|
1880
|
+
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1881
|
+
}
|
|
1882
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1883
|
+
throw new ResolveError(
|
|
1884
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1885
|
+
"",
|
|
1886
|
+
line,
|
|
1887
|
+
col
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1891
|
+
throw new ResolveError(
|
|
1892
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1893
|
+
"",
|
|
1894
|
+
line,
|
|
1895
|
+
col
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1899
|
+
};
|
|
1900
|
+
const [head, ...tail] = nonSep;
|
|
1901
|
+
const folded = tail.reduce(joinPair, head);
|
|
1902
|
+
if (folded.kind === "scalar") {
|
|
1903
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1904
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1508
1905
|
}
|
|
1509
|
-
|
|
1510
|
-
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1906
|
+
return folded;
|
|
1511
1907
|
}
|
|
1512
1908
|
resolveAppend(a, scope) {
|
|
1513
1909
|
const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
|
|
@@ -1544,6 +1940,7 @@ async function defaultReadFile(filePath) {
|
|
|
1544
1940
|
}
|
|
1545
1941
|
function buildResolveContext(input, opts) {
|
|
1546
1942
|
const tokens = tokenize(input);
|
|
1943
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1547
1944
|
const ast = parseTokens(tokens);
|
|
1548
1945
|
const resolveOpts = {
|
|
1549
1946
|
env: getEnv(opts),
|