@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.cjs
CHANGED
|
@@ -43,6 +43,27 @@ module.exports = __toCommonJS(src_exports);
|
|
|
43
43
|
|
|
44
44
|
// src/coerce.ts
|
|
45
45
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
46
|
+
function isHoconWs(cp) {
|
|
47
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
48
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
49
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
50
|
+
if (cp === 5760) return true;
|
|
51
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
52
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
53
|
+
if (cp === 12288) return true;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function trimHoconWs(s) {
|
|
57
|
+
let start = 0;
|
|
58
|
+
let end = s.length;
|
|
59
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
60
|
+
start++;
|
|
61
|
+
}
|
|
62
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
63
|
+
end--;
|
|
64
|
+
}
|
|
65
|
+
return s.slice(start, end);
|
|
66
|
+
}
|
|
46
67
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
47
68
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
48
69
|
function coerceBoolean(value) {
|
|
@@ -88,22 +109,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
88
109
|
d: 864e5
|
|
89
110
|
};
|
|
90
111
|
function parseDuration(value, outputUnit = "ms") {
|
|
91
|
-
const trimmed = value
|
|
112
|
+
const trimmed = trimHoconWs(value);
|
|
92
113
|
let i = 0;
|
|
93
114
|
while (i < trimmed.length) {
|
|
94
115
|
const ch = trimmed[i];
|
|
95
|
-
if (
|
|
116
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
117
|
+
i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
96
121
|
i++;
|
|
97
122
|
}
|
|
98
123
|
if (i === 0) return NaN;
|
|
99
124
|
const num = Number(trimmed.slice(0, i));
|
|
100
125
|
if (Number.isNaN(num)) return NaN;
|
|
101
|
-
const unit = trimmed.slice(i)
|
|
126
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
127
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
128
|
+
if (divisor === void 0) return NaN;
|
|
129
|
+
if (unit === "") {
|
|
130
|
+
return num / divisor;
|
|
131
|
+
}
|
|
102
132
|
const mult = DURATION_UNITS[unit];
|
|
103
133
|
if (mult === void 0) return NaN;
|
|
104
134
|
const ms = num * mult;
|
|
105
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
106
|
-
if (divisor === void 0) return NaN;
|
|
107
135
|
return ms / divisor;
|
|
108
136
|
}
|
|
109
137
|
var BYTE_UNITS = {
|
|
@@ -134,6 +162,22 @@ var BYTE_UNITS = {
|
|
|
134
162
|
TiB: 1099511627776,
|
|
135
163
|
tebibyte: 1099511627776,
|
|
136
164
|
tebibytes: 1099511627776,
|
|
165
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
166
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
167
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
168
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
169
|
+
K: 1024,
|
|
170
|
+
k: 1024,
|
|
171
|
+
M: 1024 ** 2,
|
|
172
|
+
m: 1024 ** 2,
|
|
173
|
+
G: 1024 ** 3,
|
|
174
|
+
g: 1024 ** 3,
|
|
175
|
+
T: 1024 ** 4,
|
|
176
|
+
t: 1024 ** 4,
|
|
177
|
+
P: 1024 ** 5,
|
|
178
|
+
p: 1024 ** 5,
|
|
179
|
+
E: 1024 ** 6,
|
|
180
|
+
e: 1024 ** 6,
|
|
137
181
|
// lowercase short-form aliases
|
|
138
182
|
b: 1,
|
|
139
183
|
kb: 1e3,
|
|
@@ -157,25 +201,39 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
157
201
|
TiB: 1099511627776
|
|
158
202
|
};
|
|
159
203
|
function parseBytes(value, outputUnit = "B") {
|
|
160
|
-
const trimmed = value
|
|
204
|
+
const trimmed = trimHoconWs(value);
|
|
161
205
|
let i = 0;
|
|
162
206
|
while (i < trimmed.length) {
|
|
163
207
|
const ch = trimmed[i];
|
|
164
|
-
if (
|
|
208
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
209
|
+
i++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
165
213
|
i++;
|
|
166
214
|
}
|
|
167
215
|
if (i === 0) return NaN;
|
|
168
216
|
const num = Number(trimmed.slice(0, i));
|
|
169
217
|
if (Number.isNaN(num)) return NaN;
|
|
170
|
-
const unit = trimmed.slice(i)
|
|
218
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
219
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
220
|
+
if (divisor === void 0) return NaN;
|
|
221
|
+
if (unit === "") {
|
|
222
|
+
const bytes2 = Math.trunc(num);
|
|
223
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
224
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
225
|
+
}
|
|
226
|
+
return bytes2 / divisor;
|
|
227
|
+
}
|
|
171
228
|
let mult = BYTE_UNITS[unit];
|
|
172
229
|
if (mult === void 0) {
|
|
173
230
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
174
231
|
}
|
|
175
232
|
if (mult === void 0) return NaN;
|
|
176
233
|
const bytes = num * mult;
|
|
177
|
-
|
|
178
|
-
|
|
234
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
235
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
236
|
+
}
|
|
179
237
|
const result = bytes / divisor;
|
|
180
238
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
181
239
|
}
|
|
@@ -208,6 +266,23 @@ var ConfigError = class extends Error {
|
|
|
208
266
|
}
|
|
209
267
|
};
|
|
210
268
|
|
|
269
|
+
// src/value/numeric-array.ts
|
|
270
|
+
function numericObjectToArray(value) {
|
|
271
|
+
if (value.kind !== "object") return null;
|
|
272
|
+
if (value.fields.size === 0) return null;
|
|
273
|
+
const canonicalInt = /^(0|[1-9][0-9]*)$/;
|
|
274
|
+
const eligible = [];
|
|
275
|
+
for (const [key, val] of value.fields) {
|
|
276
|
+
if (!canonicalInt.test(key)) continue;
|
|
277
|
+
const n = Number(key);
|
|
278
|
+
if (!Number.isInteger(n) || n > 2147483647) continue;
|
|
279
|
+
eligible.push({ n, v: val });
|
|
280
|
+
}
|
|
281
|
+
if (eligible.length === 0) return null;
|
|
282
|
+
eligible.sort((a, b) => a.n - b.n);
|
|
283
|
+
return eligible.map((e) => e.v);
|
|
284
|
+
}
|
|
285
|
+
|
|
211
286
|
// src/config.ts
|
|
212
287
|
var Config = class _Config {
|
|
213
288
|
constructor(root) {
|
|
@@ -250,6 +325,7 @@ var Config = class _Config {
|
|
|
250
325
|
}
|
|
251
326
|
const result = parseBytes(v.raw, unit);
|
|
252
327
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
328
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
253
329
|
return result;
|
|
254
330
|
}
|
|
255
331
|
getConfig(path2) {
|
|
@@ -261,6 +337,10 @@ var Config = class _Config {
|
|
|
261
337
|
getList(path2) {
|
|
262
338
|
const v = this.lookupNode(path2);
|
|
263
339
|
if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
|
|
340
|
+
if (v.kind === "object") {
|
|
341
|
+
const converted = numericObjectToArray(v);
|
|
342
|
+
if (converted !== null) return converted.map(hoconToJs);
|
|
343
|
+
}
|
|
264
344
|
if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
|
|
265
345
|
return v.items.map(hoconToJs);
|
|
266
346
|
}
|
|
@@ -373,6 +453,20 @@ function deepMergeHocon(receiver, fallback) {
|
|
|
373
453
|
}
|
|
374
454
|
|
|
375
455
|
// src/internal/lexer/lexer.ts
|
|
456
|
+
function isHoconWhitespace(ch) {
|
|
457
|
+
const cp = ch.codePointAt(0) ?? -1;
|
|
458
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
459
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
460
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
461
|
+
if (cp === 5760) return true;
|
|
462
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
463
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
464
|
+
if (cp === 12288) return true;
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
function isHoconNewline(ch) {
|
|
468
|
+
return ch === "\n";
|
|
469
|
+
}
|
|
376
470
|
var SINGLE_CHAR_TOKENS = {
|
|
377
471
|
"{": "lbrace",
|
|
378
472
|
"}": "rbrace",
|
|
@@ -395,18 +489,18 @@ var Lexer = class {
|
|
|
395
489
|
while (this.pos < this.input.length) {
|
|
396
490
|
const sl = this.line, sc = this.col;
|
|
397
491
|
const ch = this.peek();
|
|
398
|
-
if (ch
|
|
399
|
-
this.advance();
|
|
400
|
-
this.hadSpace = true;
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
if (ch === "\n") {
|
|
492
|
+
if (isHoconNewline(ch)) {
|
|
404
493
|
this.advance();
|
|
405
494
|
if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
|
|
406
495
|
this.push("newline", "\n", sl, sc);
|
|
407
496
|
}
|
|
408
497
|
continue;
|
|
409
498
|
}
|
|
499
|
+
if (isHoconWhitespace(ch)) {
|
|
500
|
+
this.advance();
|
|
501
|
+
this.hadSpace = true;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
410
504
|
if (ch === "/" && this.peek(1) === "/") {
|
|
411
505
|
while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
|
|
412
506
|
this.hadSpace = true;
|
|
@@ -436,16 +530,16 @@ var Lexer = class {
|
|
|
436
530
|
if (ch === "$" && this.peek(1) === "{") {
|
|
437
531
|
this.advance();
|
|
438
532
|
this.advance();
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
this.
|
|
533
|
+
const payload = this.parseSubstBody(sl, sc);
|
|
534
|
+
const value = payload.segments.map((s) => {
|
|
535
|
+
const t = s.text;
|
|
536
|
+
if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
|
|
537
|
+
const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
538
|
+
return `"${escaped}"`;
|
|
539
|
+
}
|
|
540
|
+
return t;
|
|
541
|
+
}).join(".");
|
|
542
|
+
this.pushSubst(payload, value, sl, sc);
|
|
449
543
|
continue;
|
|
450
544
|
}
|
|
451
545
|
if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
|
|
@@ -480,58 +574,7 @@ var Lexer = class {
|
|
|
480
574
|
}
|
|
481
575
|
if (ch === '"') {
|
|
482
576
|
this.advance();
|
|
483
|
-
|
|
484
|
-
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
485
|
-
if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
|
|
486
|
-
if (this.peek() === "\\") {
|
|
487
|
-
this.advance();
|
|
488
|
-
const esc = this.advance();
|
|
489
|
-
switch (esc) {
|
|
490
|
-
case "n":
|
|
491
|
-
value += "\n";
|
|
492
|
-
break;
|
|
493
|
-
case "t":
|
|
494
|
-
value += " ";
|
|
495
|
-
break;
|
|
496
|
-
case "r":
|
|
497
|
-
value += "\r";
|
|
498
|
-
break;
|
|
499
|
-
case '"':
|
|
500
|
-
value += '"';
|
|
501
|
-
break;
|
|
502
|
-
case "\\":
|
|
503
|
-
value += "\\";
|
|
504
|
-
break;
|
|
505
|
-
case "/":
|
|
506
|
-
value += "/";
|
|
507
|
-
break;
|
|
508
|
-
case "b":
|
|
509
|
-
value += "\b";
|
|
510
|
-
break;
|
|
511
|
-
case "f":
|
|
512
|
-
value += "\f";
|
|
513
|
-
break;
|
|
514
|
-
case "u": {
|
|
515
|
-
if (this.pos + 4 > this.input.length) {
|
|
516
|
-
throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
|
|
517
|
-
}
|
|
518
|
-
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
519
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
520
|
-
throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
|
|
521
|
-
}
|
|
522
|
-
value += String.fromCharCode(parseInt(hex, 16));
|
|
523
|
-
for (let i = 0; i < 4; i++) this.advance();
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
default:
|
|
527
|
-
throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
|
|
528
|
-
}
|
|
529
|
-
} else {
|
|
530
|
-
value += this.advance();
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
|
|
534
|
-
this.advance();
|
|
577
|
+
const value = this.readQuotedStringBody(sl, sc);
|
|
535
578
|
this.push("string", value, sl, sc, true);
|
|
536
579
|
continue;
|
|
537
580
|
}
|
|
@@ -565,22 +608,255 @@ var Lexer = class {
|
|
|
565
608
|
this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
|
|
566
609
|
this.hadSpace = false;
|
|
567
610
|
}
|
|
611
|
+
pushSubst(payload, value, l, c) {
|
|
612
|
+
this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
|
|
613
|
+
this.hadSpace = false;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Read body of a quoted string. Opening `"` must already be consumed.
|
|
617
|
+
* Returns decoded string. Throws ParseError on unterminated/invalid-escape.
|
|
618
|
+
* openLine/openCol are the position of the opening `"` for error reporting.
|
|
619
|
+
*/
|
|
620
|
+
readQuotedStringBody(openLine, openCol) {
|
|
621
|
+
let value = "";
|
|
622
|
+
while (this.pos < this.input.length && this.peek() !== '"') {
|
|
623
|
+
if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
|
|
624
|
+
if (this.peek() === "\\") {
|
|
625
|
+
const escCol = this.col;
|
|
626
|
+
this.advance();
|
|
627
|
+
if (this.pos >= this.input.length) {
|
|
628
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
629
|
+
}
|
|
630
|
+
const esc = this.advance();
|
|
631
|
+
switch (esc) {
|
|
632
|
+
case "n":
|
|
633
|
+
value += "\n";
|
|
634
|
+
break;
|
|
635
|
+
case "t":
|
|
636
|
+
value += " ";
|
|
637
|
+
break;
|
|
638
|
+
case "r":
|
|
639
|
+
value += "\r";
|
|
640
|
+
break;
|
|
641
|
+
case '"':
|
|
642
|
+
value += '"';
|
|
643
|
+
break;
|
|
644
|
+
case "\\":
|
|
645
|
+
value += "\\";
|
|
646
|
+
break;
|
|
647
|
+
case "/":
|
|
648
|
+
value += "/";
|
|
649
|
+
break;
|
|
650
|
+
case "b":
|
|
651
|
+
value += "\b";
|
|
652
|
+
break;
|
|
653
|
+
case "f":
|
|
654
|
+
value += "\f";
|
|
655
|
+
break;
|
|
656
|
+
case "u": {
|
|
657
|
+
if (this.pos + 4 > this.input.length) {
|
|
658
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
659
|
+
}
|
|
660
|
+
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
661
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
662
|
+
throw new ParseError("invalid unicode escape", openLine, escCol);
|
|
663
|
+
}
|
|
664
|
+
const code = parseInt(hex, 16);
|
|
665
|
+
value += String.fromCharCode(code);
|
|
666
|
+
for (let i = 0; i < 4; i++) this.advance();
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
default:
|
|
670
|
+
throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
value += this.advance();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (this.pos >= this.input.length || this.peek() !== '"') {
|
|
677
|
+
throw new ParseError("unterminated string", openLine, openCol);
|
|
678
|
+
}
|
|
679
|
+
this.advance();
|
|
680
|
+
return value;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
684
|
+
* Implements Appendix A state machine from the cross-language spec.
|
|
685
|
+
*
|
|
686
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
687
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
688
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
689
|
+
*/
|
|
690
|
+
parseSubstBody(startLine, startCol) {
|
|
691
|
+
let optional = false;
|
|
692
|
+
if (this.peek() === "?") {
|
|
693
|
+
this.advance();
|
|
694
|
+
optional = true;
|
|
695
|
+
}
|
|
696
|
+
let curText = "";
|
|
697
|
+
let curStarted = false;
|
|
698
|
+
let curLine = 0;
|
|
699
|
+
let curCol = 0;
|
|
700
|
+
let pendingWs = "";
|
|
701
|
+
const segments = [];
|
|
702
|
+
let lastDot = null;
|
|
703
|
+
let listSuffix = false;
|
|
704
|
+
while (true) {
|
|
705
|
+
if (this.pos >= this.input.length) {
|
|
706
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
707
|
+
}
|
|
708
|
+
const ch = this.peek();
|
|
709
|
+
if (ch === "}") {
|
|
710
|
+
this.advance();
|
|
711
|
+
pendingWs = "";
|
|
712
|
+
break;
|
|
713
|
+
} else if (ch === "[") {
|
|
714
|
+
if (!curStarted) {
|
|
715
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
716
|
+
}
|
|
717
|
+
for (const w of pendingWs) {
|
|
718
|
+
if (w !== " " && w !== " ") {
|
|
719
|
+
throw new ParseError(
|
|
720
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
721
|
+
startLine,
|
|
722
|
+
this.col
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
727
|
+
pendingWs = "";
|
|
728
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
729
|
+
break;
|
|
730
|
+
} else if (ch === '"') {
|
|
731
|
+
const qLine = startLine;
|
|
732
|
+
const qCol = this.col;
|
|
733
|
+
if (curStarted) {
|
|
734
|
+
curText += pendingWs;
|
|
735
|
+
}
|
|
736
|
+
pendingWs = "";
|
|
737
|
+
this.advance();
|
|
738
|
+
const decoded = this.readQuotedStringBody(qLine, qCol);
|
|
739
|
+
curText += decoded;
|
|
740
|
+
if (!curStarted) {
|
|
741
|
+
curLine = qLine;
|
|
742
|
+
curCol = qCol;
|
|
743
|
+
curStarted = true;
|
|
744
|
+
}
|
|
745
|
+
} else if (isUnquotedSubstChar(ch)) {
|
|
746
|
+
if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
|
|
747
|
+
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
748
|
+
throw new ParseError(
|
|
749
|
+
`unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
750
|
+
startLine,
|
|
751
|
+
this.col
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
const uCol = this.col;
|
|
755
|
+
if (curStarted) {
|
|
756
|
+
curText += pendingWs;
|
|
757
|
+
}
|
|
758
|
+
pendingWs = "";
|
|
759
|
+
if (!curStarted) {
|
|
760
|
+
curLine = startLine;
|
|
761
|
+
curCol = uCol;
|
|
762
|
+
curStarted = true;
|
|
763
|
+
}
|
|
764
|
+
while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
|
|
765
|
+
curText += this.advance();
|
|
766
|
+
}
|
|
767
|
+
} else if (ch === ".") {
|
|
768
|
+
const dotCol = this.col;
|
|
769
|
+
pendingWs = "";
|
|
770
|
+
if (!curStarted) {
|
|
771
|
+
throw new ParseError("empty segment in path", startLine, dotCol);
|
|
772
|
+
}
|
|
773
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
774
|
+
curText = "";
|
|
775
|
+
curStarted = false;
|
|
776
|
+
curLine = 0;
|
|
777
|
+
curCol = 0;
|
|
778
|
+
lastDot = [startLine, dotCol];
|
|
779
|
+
this.advance();
|
|
780
|
+
} else if (isHoconNewline(ch)) {
|
|
781
|
+
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
782
|
+
} else if (isHoconWhitespace(ch)) {
|
|
783
|
+
pendingWs += ch;
|
|
784
|
+
this.advance();
|
|
785
|
+
} else {
|
|
786
|
+
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (!listSuffix) {
|
|
790
|
+
if (curStarted) {
|
|
791
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
792
|
+
} else if (segments.length === 0) {
|
|
793
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
794
|
+
} else {
|
|
795
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
796
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return { segments, optional, listSuffix };
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
803
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
804
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
805
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
806
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
807
|
+
*/
|
|
808
|
+
parseListSuffix(startLine) {
|
|
809
|
+
this.advance();
|
|
810
|
+
const afterBracket = this.peek();
|
|
811
|
+
if (afterBracket !== "]") {
|
|
812
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
813
|
+
throw new ParseError(
|
|
814
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
815
|
+
startLine,
|
|
816
|
+
this.col
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
this.advance();
|
|
820
|
+
if (this.peek() !== "}") {
|
|
821
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
822
|
+
throw new ParseError(
|
|
823
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
824
|
+
startLine,
|
|
825
|
+
this.col
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
this.advance();
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
568
831
|
};
|
|
569
832
|
function tokenize(input) {
|
|
570
833
|
return new Lexer(input).tokenize();
|
|
571
834
|
}
|
|
835
|
+
function isUnquotedSubstChar(ch) {
|
|
836
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
837
|
+
if ('"\\'.includes(ch)) return false;
|
|
838
|
+
if ("{}[]".includes(ch)) return false;
|
|
839
|
+
if (":=,+#`^?!@*&$.".includes(ch)) return false;
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
function isDecimalDigit(ch) {
|
|
843
|
+
return ch >= "0" && ch <= "9";
|
|
844
|
+
}
|
|
572
845
|
function isUnquotedStart(ch) {
|
|
573
|
-
|
|
846
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
847
|
+
if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
|
|
848
|
+
return true;
|
|
574
849
|
}
|
|
575
850
|
function isUnquotedContinue(ch, nextFn) {
|
|
576
|
-
if (ch === "" ||
|
|
851
|
+
if (ch === "" || isHoconWhitespace(ch)) return false;
|
|
852
|
+
if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
|
|
577
853
|
if (ch === "+" && nextFn() === "=") return false;
|
|
578
854
|
if (ch === "/" && nextFn() === "/") return false;
|
|
579
855
|
return true;
|
|
580
856
|
}
|
|
581
857
|
|
|
582
858
|
// src/internal/parser/parser.ts
|
|
583
|
-
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
|
|
859
|
+
var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
|
|
584
860
|
var Parser = class {
|
|
585
861
|
constructor(tokens) {
|
|
586
862
|
this.tokens = tokens;
|
|
@@ -638,6 +914,14 @@ var Parser = class {
|
|
|
638
914
|
const t = this.peek();
|
|
639
915
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
640
916
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
917
|
+
const next = this.peek(1);
|
|
918
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
919
|
+
throw new ParseError(
|
|
920
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
921
|
+
t.line,
|
|
922
|
+
t.col
|
|
923
|
+
);
|
|
924
|
+
}
|
|
641
925
|
this.advance();
|
|
642
926
|
fields.push(this.parseInclude());
|
|
643
927
|
this.skip("newline");
|
|
@@ -646,7 +930,14 @@ var Parser = class {
|
|
|
646
930
|
continue;
|
|
647
931
|
}
|
|
648
932
|
const keyPos = this.currentPos();
|
|
649
|
-
const key = this.parseKey();
|
|
933
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
934
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
935
|
+
throw new ParseError(
|
|
936
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
937
|
+
keyPos.line,
|
|
938
|
+
keyPos.col
|
|
939
|
+
);
|
|
940
|
+
}
|
|
650
941
|
this.skip("newline");
|
|
651
942
|
let append = false;
|
|
652
943
|
const sep = this.peek();
|
|
@@ -676,18 +967,31 @@ var Parser = class {
|
|
|
676
967
|
parseKey() {
|
|
677
968
|
const segments = [];
|
|
678
969
|
let trailingDot = false;
|
|
970
|
+
let firstWasQuoted = false;
|
|
679
971
|
while (true) {
|
|
680
972
|
const t = this.peek();
|
|
681
973
|
if (t.kind === "string") {
|
|
682
974
|
this.advance();
|
|
975
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
683
976
|
segments.push(t.value);
|
|
684
977
|
trailingDot = false;
|
|
685
978
|
} else if (t.kind === "unquoted") {
|
|
686
979
|
this.advance();
|
|
687
|
-
const
|
|
980
|
+
const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
|
|
981
|
+
const parts = raw.split(".");
|
|
688
982
|
const filtered = parts.filter((s) => s.length > 0);
|
|
983
|
+
for (const seg of filtered) {
|
|
984
|
+
if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
|
|
985
|
+
const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
|
|
986
|
+
throw new ParseError(
|
|
987
|
+
`unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
|
|
988
|
+
t.line,
|
|
989
|
+
t.col
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
689
993
|
segments.push(...filtered);
|
|
690
|
-
trailingDot =
|
|
994
|
+
trailingDot = raw.endsWith(".");
|
|
691
995
|
} else {
|
|
692
996
|
if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
|
|
693
997
|
break;
|
|
@@ -699,9 +1003,13 @@ var Parser = class {
|
|
|
699
1003
|
trailingDot = true;
|
|
700
1004
|
continue;
|
|
701
1005
|
}
|
|
1006
|
+
if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
|
|
1007
|
+
trailingDot = true;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
702
1010
|
break;
|
|
703
1011
|
}
|
|
704
|
-
return segments;
|
|
1012
|
+
return { segments, firstWasQuoted };
|
|
705
1013
|
}
|
|
706
1014
|
parseInclude() {
|
|
707
1015
|
const p = this.currentPos();
|
|
@@ -774,9 +1082,13 @@ var Parser = class {
|
|
|
774
1082
|
} else if (t.kind === "lbracket") {
|
|
775
1083
|
this.advance();
|
|
776
1084
|
node = this.parseArray();
|
|
777
|
-
} else if (t.kind === "subst"
|
|
1085
|
+
} else if (t.kind === "subst") {
|
|
778
1086
|
this.advance();
|
|
779
|
-
|
|
1087
|
+
if (!t.subst) {
|
|
1088
|
+
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
1089
|
+
}
|
|
1090
|
+
const payload = t.subst;
|
|
1091
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
780
1092
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
781
1093
|
this.advance();
|
|
782
1094
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -827,6 +1139,20 @@ function parseTokens(tokens) {
|
|
|
827
1139
|
return new Parser(tokens).parse();
|
|
828
1140
|
}
|
|
829
1141
|
|
|
1142
|
+
// src/internal/parser/empty-check.ts
|
|
1143
|
+
function hasContentTokens(tokens) {
|
|
1144
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
1145
|
+
}
|
|
1146
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
1147
|
+
if (!hasContentTokens(tokens)) {
|
|
1148
|
+
throw new ParseError(
|
|
1149
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
1150
|
+
1,
|
|
1151
|
+
1
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
830
1156
|
// src/internal/resolver/types.ts
|
|
831
1157
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
832
1158
|
function isSubst(v) {
|
|
@@ -846,64 +1172,20 @@ function makeResObj() {
|
|
|
846
1172
|
}
|
|
847
1173
|
|
|
848
1174
|
// src/internal/resolver/utils.ts
|
|
849
|
-
function parseSubstPath(raw) {
|
|
850
|
-
const segments = [];
|
|
851
|
-
let i = 0;
|
|
852
|
-
while (i < raw.length) {
|
|
853
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
854
|
-
if (i >= raw.length) break;
|
|
855
|
-
if (raw[i] === '"') {
|
|
856
|
-
i++;
|
|
857
|
-
let seg = "";
|
|
858
|
-
while (i < raw.length && raw[i] !== '"') {
|
|
859
|
-
if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
|
|
860
|
-
seg += raw[i + 1];
|
|
861
|
-
i += 2;
|
|
862
|
-
} else {
|
|
863
|
-
seg += raw[i];
|
|
864
|
-
i++;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
if (i < raw.length) i++;
|
|
868
|
-
segments.push(seg);
|
|
869
|
-
while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
|
|
870
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
871
|
-
} else if (raw[i] === ".") {
|
|
872
|
-
segments.push("");
|
|
873
|
-
i++;
|
|
874
|
-
} else {
|
|
875
|
-
let seg = "";
|
|
876
|
-
while (i < raw.length && raw[i] !== ".") {
|
|
877
|
-
seg += raw[i];
|
|
878
|
-
i++;
|
|
879
|
-
}
|
|
880
|
-
segments.push(seg.trim());
|
|
881
|
-
if (i < raw.length && raw[i] === ".") i++;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
return segments;
|
|
885
|
-
}
|
|
886
1175
|
function segmentsToKey(segments) {
|
|
887
1176
|
return segments.map((s) => {
|
|
888
|
-
|
|
889
|
-
|
|
1177
|
+
const t = s.text;
|
|
1178
|
+
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
1179
|
+
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
890
1180
|
}
|
|
891
|
-
return
|
|
1181
|
+
return t;
|
|
892
1182
|
}).join(".");
|
|
893
1183
|
}
|
|
894
1184
|
function lookupPath(root, segments) {
|
|
895
1185
|
const [head, ...tail] = segments;
|
|
896
|
-
if (head === void 0
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (val2 === void 0) return void 0;
|
|
900
|
-
if (tail.length === 0) return val2;
|
|
901
|
-
if (isResObj(val2)) return lookupPath(val2, tail);
|
|
902
|
-
return void 0;
|
|
903
|
-
}
|
|
904
|
-
return void 0;
|
|
905
|
-
}
|
|
906
|
-
const val = root.fields.get(head);
|
|
1186
|
+
if (head === void 0) return void 0;
|
|
1187
|
+
const key = head.text;
|
|
1188
|
+
const val = root.fields.get(key);
|
|
907
1189
|
if (val === void 0) return void 0;
|
|
908
1190
|
if (tail.length === 0) return val;
|
|
909
1191
|
if (isResObj(val)) return lookupPath(val, tail);
|
|
@@ -912,7 +1194,7 @@ function lookupPath(root, segments) {
|
|
|
912
1194
|
function lookupResObj(root, segments) {
|
|
913
1195
|
let cur = root;
|
|
914
1196
|
for (const seg of segments) {
|
|
915
|
-
const val = cur.fields.get(seg);
|
|
1197
|
+
const val = cur.fields.get(seg.text);
|
|
916
1198
|
if (val === void 0 || !isResObj(val)) return void 0;
|
|
917
1199
|
cur = val;
|
|
918
1200
|
}
|
|
@@ -972,6 +1254,7 @@ var nodePath = __toESM(require("path"), 1);
|
|
|
972
1254
|
// src/internal/properties/properties.ts
|
|
973
1255
|
function parseProperties(input) {
|
|
974
1256
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1257
|
+
const pairs = [];
|
|
975
1258
|
for (const line of input.split("\n")) {
|
|
976
1259
|
const trimmed = line.trim();
|
|
977
1260
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -980,6 +1263,10 @@ function parseProperties(input) {
|
|
|
980
1263
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
981
1264
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
982
1265
|
if (key === "") continue;
|
|
1266
|
+
pairs.push([key, value]);
|
|
1267
|
+
}
|
|
1268
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1269
|
+
for (const [key, value] of pairs) {
|
|
983
1270
|
setNested(root, key.split("."), value);
|
|
984
1271
|
}
|
|
985
1272
|
return root;
|
|
@@ -1003,6 +1290,7 @@ function setNested(obj, segments, value) {
|
|
|
1003
1290
|
}
|
|
1004
1291
|
const last = segments[segments.length - 1];
|
|
1005
1292
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1293
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
1006
1294
|
current[last] = value;
|
|
1007
1295
|
}
|
|
1008
1296
|
function propertiesToHoconValue(input) {
|
|
@@ -1116,7 +1404,9 @@ var IncludeLoader = class {
|
|
|
1116
1404
|
if (candidate.endsWith(".properties")) {
|
|
1117
1405
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1118
1406
|
}
|
|
1119
|
-
const
|
|
1407
|
+
const tokens = tokenize(content);
|
|
1408
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1409
|
+
const ast = parseTokens(tokens);
|
|
1120
1410
|
return this.onBuildResObj(ast, {
|
|
1121
1411
|
env,
|
|
1122
1412
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1140,7 +1430,9 @@ var IncludeLoader = class {
|
|
|
1140
1430
|
if (candidate.endsWith(".properties")) {
|
|
1141
1431
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1142
1432
|
}
|
|
1143
|
-
const
|
|
1433
|
+
const tokens = tokenize(content);
|
|
1434
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1435
|
+
const ast = parseTokens(tokens);
|
|
1144
1436
|
return this.onBuildResObjAsync(ast, {
|
|
1145
1437
|
env,
|
|
1146
1438
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1189,7 +1481,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1189
1481
|
return;
|
|
1190
1482
|
}
|
|
1191
1483
|
const [head, ...tail] = field.key;
|
|
1192
|
-
if (
|
|
1484
|
+
if (head === void 0) return;
|
|
1193
1485
|
if (tail.length > 0) {
|
|
1194
1486
|
const syntheticAst = {
|
|
1195
1487
|
kind: "object",
|
|
@@ -1231,7 +1523,7 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1231
1523
|
return;
|
|
1232
1524
|
}
|
|
1233
1525
|
const [head, ...tail] = field.key;
|
|
1234
|
-
if (
|
|
1526
|
+
if (head === void 0) return;
|
|
1235
1527
|
if (tail.length > 0) {
|
|
1236
1528
|
const syntheticAst = {
|
|
1237
1529
|
kind: "object",
|
|
@@ -1277,9 +1569,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1277
1569
|
return inner;
|
|
1278
1570
|
}
|
|
1279
1571
|
case "subst":
|
|
1280
|
-
return { _kind: "subst-placeholder", segments:
|
|
1572
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1281
1573
|
case "concat":
|
|
1282
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1574
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1283
1575
|
case "include":
|
|
1284
1576
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1285
1577
|
}
|
|
@@ -1301,13 +1593,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1301
1593
|
case "object":
|
|
1302
1594
|
return await this.buildAsync(ast, pathPrefix);
|
|
1303
1595
|
case "subst":
|
|
1304
|
-
return { _kind: "subst-placeholder", segments:
|
|
1596
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1305
1597
|
case "concat": {
|
|
1306
1598
|
const nodes = [];
|
|
1307
1599
|
for (const n of ast.nodes) {
|
|
1308
1600
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1309
1601
|
}
|
|
1310
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1602
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1311
1603
|
}
|
|
1312
1604
|
case "include":
|
|
1313
1605
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1316,7 +1608,8 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1316
1608
|
// ---- Relativize substitution paths for nested includes ----
|
|
1317
1609
|
relativizeSubstPaths(val, prefixSegments) {
|
|
1318
1610
|
if (isSubst(val)) {
|
|
1319
|
-
|
|
1611
|
+
const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
|
|
1612
|
+
val.segments = [...prefixAsSegments, ...val.segments];
|
|
1320
1613
|
val.prefixLen += prefixSegments.length;
|
|
1321
1614
|
return;
|
|
1322
1615
|
}
|
|
@@ -1359,6 +1652,19 @@ var SubstitutionResolver = class {
|
|
|
1359
1652
|
this.opts = opts;
|
|
1360
1653
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1361
1654
|
this.cache = /* @__PURE__ */ new Map();
|
|
1655
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1656
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1657
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1658
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1659
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1660
|
+
//
|
|
1661
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1662
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1663
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1664
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1665
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1666
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1667
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1362
1668
|
}
|
|
1363
1669
|
resolve() {
|
|
1364
1670
|
return this.resolveResObj(this.root);
|
|
@@ -1397,7 +1703,14 @@ var SubstitutionResolver = class {
|
|
|
1397
1703
|
}
|
|
1398
1704
|
resolveVal(v, scope) {
|
|
1399
1705
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1400
|
-
if (isConcat(v))
|
|
1706
|
+
if (isConcat(v)) {
|
|
1707
|
+
this.resolvingConcats.add(v);
|
|
1708
|
+
try {
|
|
1709
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1710
|
+
} finally {
|
|
1711
|
+
this.resolvingConcats.delete(v);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1401
1714
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1402
1715
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1403
1716
|
const hv = v;
|
|
@@ -1411,23 +1724,20 @@ var SubstitutionResolver = class {
|
|
|
1411
1724
|
}
|
|
1412
1725
|
return hv;
|
|
1413
1726
|
}
|
|
1414
|
-
segmentsEqual(a, b) {
|
|
1415
|
-
return a.length === b.length && a.every((seg, i) => seg === b[i]);
|
|
1416
|
-
}
|
|
1417
1727
|
resolveSubst(s, scope) {
|
|
1418
|
-
const key = segmentsToKey(s.segments);
|
|
1728
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1419
1729
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1420
1730
|
if (this.resolving.has(key)) {
|
|
1421
1731
|
let prior;
|
|
1422
|
-
if (s.
|
|
1423
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1732
|
+
if (s.segments.length > 1) {
|
|
1733
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1424
1734
|
const parentScope = lookupResObj(
|
|
1425
1735
|
this.root,
|
|
1426
1736
|
s.segments.slice(0, s.segments.length - 1)
|
|
1427
1737
|
);
|
|
1428
1738
|
prior = parentScope?.priorValues.get(leafSeg);
|
|
1429
1739
|
} else {
|
|
1430
|
-
const rootSeg = s.segments[0] ?? "";
|
|
1740
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1431
1741
|
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1432
1742
|
}
|
|
1433
1743
|
if (prior !== void 0) {
|
|
@@ -1451,34 +1761,36 @@ var SubstitutionResolver = class {
|
|
|
1451
1761
|
try {
|
|
1452
1762
|
const found = lookupPath(this.root, s.segments);
|
|
1453
1763
|
if (found !== void 0) {
|
|
1454
|
-
if (
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
if (prior !== void 0) {
|
|
1472
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1473
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1474
|
-
return result2;
|
|
1475
|
-
}
|
|
1764
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1765
|
+
let prior;
|
|
1766
|
+
if (s.segments.length > 1) {
|
|
1767
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1768
|
+
const parentScope = lookupResObj(
|
|
1769
|
+
this.root,
|
|
1770
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1771
|
+
);
|
|
1772
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1773
|
+
} else {
|
|
1774
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1775
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1776
|
+
}
|
|
1777
|
+
if (prior !== void 0) {
|
|
1778
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1779
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1780
|
+
return result2;
|
|
1476
1781
|
}
|
|
1782
|
+
if (s.optional) return void 0;
|
|
1783
|
+
throw new ResolveError(
|
|
1784
|
+
`could not resolve substitution: \${${key}}`,
|
|
1785
|
+
key,
|
|
1786
|
+
s.line,
|
|
1787
|
+
s.col
|
|
1788
|
+
);
|
|
1477
1789
|
}
|
|
1478
1790
|
let result = this.resolveVal(found, scope);
|
|
1479
1791
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
1480
1792
|
if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
|
|
1481
|
-
const leafSeg = s.segments[s.segments.length - 1] ?? "";
|
|
1793
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1482
1794
|
let prior;
|
|
1483
1795
|
if (s.prefixLen > 0) {
|
|
1484
1796
|
const parentScope = lookupResObj(
|
|
@@ -1502,8 +1814,23 @@ var SubstitutionResolver = class {
|
|
|
1502
1814
|
if (result !== void 0) this.cache.set(key, result);
|
|
1503
1815
|
return result;
|
|
1504
1816
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1817
|
+
if (s.listSuffix) {
|
|
1818
|
+
const result = this.resolveEnvList(s);
|
|
1819
|
+
if (result !== void 0) {
|
|
1820
|
+
this.cache.set(key, result);
|
|
1821
|
+
return result;
|
|
1822
|
+
}
|
|
1823
|
+
if (s.optional) return void 0;
|
|
1824
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1825
|
+
throw new ResolveError(
|
|
1826
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1827
|
+
key,
|
|
1828
|
+
s.line,
|
|
1829
|
+
s.col
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1833
|
+
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1507
1834
|
if (envVal !== void 0) {
|
|
1508
1835
|
const result = { kind: "scalar", raw: envVal, valueType: "string" };
|
|
1509
1836
|
this.cache.set(key, result);
|
|
@@ -1520,37 +1847,106 @@ var SubstitutionResolver = class {
|
|
|
1520
1847
|
this.resolving.delete(key);
|
|
1521
1848
|
}
|
|
1522
1849
|
}
|
|
1523
|
-
|
|
1850
|
+
/**
|
|
1851
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1852
|
+
*
|
|
1853
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1854
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1855
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1856
|
+
*
|
|
1857
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1858
|
+
* none do (caller applies optional/required handling).
|
|
1859
|
+
*/
|
|
1860
|
+
resolveEnvList(s) {
|
|
1861
|
+
const bases = this.candidateBases(s);
|
|
1862
|
+
for (const base of bases) {
|
|
1863
|
+
const key0 = base + "_0";
|
|
1864
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1865
|
+
const items = [];
|
|
1866
|
+
for (let i = 0; ; i++) {
|
|
1867
|
+
const k = base + "_" + i;
|
|
1868
|
+
if (!(k in this.opts.env)) break;
|
|
1869
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1870
|
+
}
|
|
1871
|
+
return { kind: "array", items };
|
|
1872
|
+
}
|
|
1873
|
+
return void 0;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1877
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1878
|
+
*/
|
|
1879
|
+
candidateBases(s) {
|
|
1880
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1881
|
+
if (s.prefixLen === 0) return [full];
|
|
1882
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1883
|
+
return [full, bare];
|
|
1884
|
+
}
|
|
1885
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1524
1886
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1525
1887
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1526
1888
|
if (resolved.length === 1) return resolved[0];
|
|
1527
1889
|
const nonSep = resolved.filter((v) => !separatorValues.has(v));
|
|
1528
|
-
if (nonSep.length
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1890
|
+
if (nonSep.length === 0) {
|
|
1891
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1892
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1893
|
+
}
|
|
1894
|
+
const joinPair = (left, right) => {
|
|
1895
|
+
if (left.kind === "object" && right.kind === "object") {
|
|
1896
|
+
return deepMergeHoconValues(left, right);
|
|
1897
|
+
}
|
|
1898
|
+
if (left.kind === "array" && right.kind === "object") {
|
|
1899
|
+
const converted = numericObjectToArray(right);
|
|
1900
|
+
if (converted !== null) {
|
|
1901
|
+
return { kind: "array", items: [...left.items, ...converted] };
|
|
1540
1902
|
}
|
|
1903
|
+
throw new ResolveError(
|
|
1904
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1905
|
+
"",
|
|
1906
|
+
line,
|
|
1907
|
+
col
|
|
1908
|
+
);
|
|
1541
1909
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1910
|
+
if (left.kind === "object" && right.kind === "array") {
|
|
1911
|
+
const converted = numericObjectToArray(left);
|
|
1912
|
+
if (converted !== null) {
|
|
1913
|
+
return { kind: "array", items: [...converted, ...right.items] };
|
|
1914
|
+
}
|
|
1915
|
+
throw new ResolveError(
|
|
1916
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1917
|
+
"",
|
|
1918
|
+
line,
|
|
1919
|
+
col
|
|
1920
|
+
);
|
|
1549
1921
|
}
|
|
1550
|
-
|
|
1922
|
+
if (left.kind === "array" && right.kind === "array") {
|
|
1923
|
+
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1924
|
+
}
|
|
1925
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1926
|
+
throw new ResolveError(
|
|
1927
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1928
|
+
"",
|
|
1929
|
+
line,
|
|
1930
|
+
col
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1934
|
+
throw new ResolveError(
|
|
1935
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1936
|
+
"",
|
|
1937
|
+
line,
|
|
1938
|
+
col
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1942
|
+
};
|
|
1943
|
+
const [head, ...tail] = nonSep;
|
|
1944
|
+
const folded = tail.reduce(joinPair, head);
|
|
1945
|
+
if (folded.kind === "scalar") {
|
|
1946
|
+
const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
|
|
1947
|
+
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1551
1948
|
}
|
|
1552
|
-
|
|
1553
|
-
return { kind: "scalar", raw: str, valueType: "string" };
|
|
1949
|
+
return folded;
|
|
1554
1950
|
}
|
|
1555
1951
|
resolveAppend(a, scope) {
|
|
1556
1952
|
const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
|
|
@@ -1587,6 +1983,7 @@ async function defaultReadFile(filePath) {
|
|
|
1587
1983
|
}
|
|
1588
1984
|
function buildResolveContext(input, opts) {
|
|
1589
1985
|
const tokens = tokenize(input);
|
|
1986
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1590
1987
|
const ast = parseTokens(tokens);
|
|
1591
1988
|
const resolveOpts = {
|
|
1592
1989
|
env: getEnv(opts),
|