@o3co/ts.hocon 1.1.0 → 1.2.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/dist/zod.js CHANGED
@@ -166,6 +166,20 @@ var ConfigError = class extends Error {
166
166
  };
167
167
 
168
168
  // src/internal/lexer/lexer.ts
169
+ function isHoconWhitespace(ch) {
170
+ const cp = ch.codePointAt(0) ?? -1;
171
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
172
+ if (cp >= 28 && cp <= 31) return true;
173
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
174
+ if (cp === 5760) return true;
175
+ if (cp >= 8192 && cp <= 8202) return true;
176
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
177
+ if (cp === 12288) return true;
178
+ return false;
179
+ }
180
+ function isHoconNewline(ch) {
181
+ return ch === "\n";
182
+ }
169
183
  var SINGLE_CHAR_TOKENS = {
170
184
  "{": "lbrace",
171
185
  "}": "rbrace",
@@ -188,18 +202,18 @@ var Lexer = class {
188
202
  while (this.pos < this.input.length) {
189
203
  const sl = this.line, sc = this.col;
190
204
  const ch = this.peek();
191
- if (ch === " " || ch === " " || ch === "\r") {
192
- this.advance();
193
- this.hadSpace = true;
194
- continue;
195
- }
196
- if (ch === "\n") {
205
+ if (isHoconNewline(ch)) {
197
206
  this.advance();
198
207
  if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
199
208
  this.push("newline", "\n", sl, sc);
200
209
  }
201
210
  continue;
202
211
  }
212
+ if (isHoconWhitespace(ch)) {
213
+ this.advance();
214
+ this.hadSpace = true;
215
+ continue;
216
+ }
203
217
  if (ch === "/" && this.peek(1) === "/") {
204
218
  while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
205
219
  this.hadSpace = true;
@@ -229,16 +243,16 @@ var Lexer = class {
229
243
  if (ch === "$" && this.peek(1) === "{") {
230
244
  this.advance();
231
245
  this.advance();
232
- const optional = this.peek() === "?";
233
- if (optional) this.advance();
234
- let path2 = "";
235
- while (this.pos < this.input.length && this.peek() !== "}") {
236
- if (this.peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
237
- path2 += this.advance();
238
- }
239
- if (this.peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
240
- this.advance();
241
- this.push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
246
+ const payload = this.parseSubstBody(sl, sc);
247
+ const value = payload.segments.map((s) => {
248
+ const t = s.text;
249
+ if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
250
+ const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
251
+ return `"${escaped}"`;
252
+ }
253
+ return t;
254
+ }).join(".");
255
+ this.pushSubst(payload, value, sl, sc);
242
256
  continue;
243
257
  }
244
258
  if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
@@ -273,62 +287,19 @@ var Lexer = class {
273
287
  }
274
288
  if (ch === '"') {
275
289
  this.advance();
276
- let value = "";
277
- while (this.pos < this.input.length && this.peek() !== '"') {
278
- if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
279
- if (this.peek() === "\\") {
280
- this.advance();
281
- const esc = this.advance();
282
- switch (esc) {
283
- case "n":
284
- value += "\n";
285
- break;
286
- case "t":
287
- value += " ";
288
- break;
289
- case "r":
290
- value += "\r";
291
- break;
292
- case '"':
293
- value += '"';
294
- break;
295
- case "\\":
296
- value += "\\";
297
- break;
298
- case "/":
299
- value += "/";
300
- break;
301
- case "b":
302
- value += "\b";
303
- break;
304
- case "f":
305
- value += "\f";
306
- break;
307
- case "u": {
308
- if (this.pos + 4 > this.input.length) {
309
- throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
310
- }
311
- const hex = this.input.slice(this.pos, this.pos + 4);
312
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
313
- throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
314
- }
315
- value += String.fromCharCode(parseInt(hex, 16));
316
- for (let i = 0; i < 4; i++) this.advance();
317
- break;
318
- }
319
- default:
320
- throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
321
- }
322
- } else {
323
- value += this.advance();
324
- }
325
- }
326
- if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
327
- this.advance();
290
+ const value = this.readQuotedStringBody(sl, sc);
328
291
  this.push("string", value, sl, sc, true);
329
292
  continue;
330
293
  }
331
294
  if (isUnquotedStart(ch)) {
295
+ if (ch === "-" && !isDecimalDigit(this.peek(1))) {
296
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
297
+ throw new ParseError(
298
+ `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
299
+ sl,
300
+ sc
301
+ );
302
+ }
332
303
  let value = "";
333
304
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
334
305
  value += this.advance();
@@ -358,22 +329,201 @@ var Lexer = class {
358
329
  this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
359
330
  this.hadSpace = false;
360
331
  }
332
+ pushSubst(payload, value, l, c) {
333
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
334
+ this.hadSpace = false;
335
+ }
336
+ /**
337
+ * Read body of a quoted string. Opening `"` must already be consumed.
338
+ * Returns decoded string. Throws ParseError on unterminated/invalid-escape.
339
+ * openLine/openCol are the position of the opening `"` for error reporting.
340
+ */
341
+ readQuotedStringBody(openLine, openCol) {
342
+ let value = "";
343
+ while (this.pos < this.input.length && this.peek() !== '"') {
344
+ if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
345
+ if (this.peek() === "\\") {
346
+ const escCol = this.col;
347
+ this.advance();
348
+ if (this.pos >= this.input.length) {
349
+ throw new ParseError("unterminated string", openLine, openCol);
350
+ }
351
+ const esc = this.advance();
352
+ switch (esc) {
353
+ case "n":
354
+ value += "\n";
355
+ break;
356
+ case "t":
357
+ value += " ";
358
+ break;
359
+ case "r":
360
+ value += "\r";
361
+ break;
362
+ case '"':
363
+ value += '"';
364
+ break;
365
+ case "\\":
366
+ value += "\\";
367
+ break;
368
+ case "/":
369
+ value += "/";
370
+ break;
371
+ case "b":
372
+ value += "\b";
373
+ break;
374
+ case "f":
375
+ value += "\f";
376
+ break;
377
+ case "u": {
378
+ if (this.pos + 4 > this.input.length) {
379
+ throw new ParseError("invalid unicode escape", openLine, escCol);
380
+ }
381
+ const hex = this.input.slice(this.pos, this.pos + 4);
382
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
383
+ throw new ParseError("invalid unicode escape", openLine, escCol);
384
+ }
385
+ const code = parseInt(hex, 16);
386
+ value += String.fromCharCode(code);
387
+ for (let i = 0; i < 4; i++) this.advance();
388
+ break;
389
+ }
390
+ default:
391
+ throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
392
+ }
393
+ } else {
394
+ value += this.advance();
395
+ }
396
+ }
397
+ if (this.pos >= this.input.length || this.peek() !== '"') {
398
+ throw new ParseError("unterminated string", openLine, openCol);
399
+ }
400
+ this.advance();
401
+ return value;
402
+ }
403
+ /**
404
+ * Parse the body of a `${...}` substitution (called after `${` has been consumed).
405
+ * Implements Appendix A state machine from the cross-language spec.
406
+ */
407
+ parseSubstBody(startLine, startCol) {
408
+ let optional = false;
409
+ if (this.peek() === "?") {
410
+ this.advance();
411
+ optional = true;
412
+ }
413
+ let curText = "";
414
+ let curStarted = false;
415
+ let curLine = 0;
416
+ let curCol = 0;
417
+ let pendingWs = "";
418
+ const segments = [];
419
+ let lastDot = null;
420
+ while (true) {
421
+ if (this.pos >= this.input.length) {
422
+ throw new ParseError("unterminated substitution", startLine, startCol);
423
+ }
424
+ const ch = this.peek();
425
+ if (ch === "}") {
426
+ this.advance();
427
+ pendingWs = "";
428
+ break;
429
+ } else if (ch === '"') {
430
+ const qLine = startLine;
431
+ const qCol = this.col;
432
+ if (curStarted) {
433
+ curText += pendingWs;
434
+ }
435
+ pendingWs = "";
436
+ this.advance();
437
+ const decoded = this.readQuotedStringBody(qLine, qCol);
438
+ curText += decoded;
439
+ if (!curStarted) {
440
+ curLine = qLine;
441
+ curCol = qCol;
442
+ curStarted = true;
443
+ }
444
+ } else if (isUnquotedSubstChar(ch)) {
445
+ if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
446
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
447
+ throw new ParseError(
448
+ `unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
449
+ startLine,
450
+ this.col
451
+ );
452
+ }
453
+ const uCol = this.col;
454
+ if (curStarted) {
455
+ curText += pendingWs;
456
+ }
457
+ pendingWs = "";
458
+ if (!curStarted) {
459
+ curLine = startLine;
460
+ curCol = uCol;
461
+ curStarted = true;
462
+ }
463
+ while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
464
+ curText += this.advance();
465
+ }
466
+ } else if (ch === ".") {
467
+ const dotCol = this.col;
468
+ pendingWs = "";
469
+ if (!curStarted) {
470
+ throw new ParseError("empty segment in path", startLine, dotCol);
471
+ }
472
+ segments.push({ text: curText, line: curLine, col: curCol });
473
+ curText = "";
474
+ curStarted = false;
475
+ curLine = 0;
476
+ curCol = 0;
477
+ lastDot = [startLine, dotCol];
478
+ this.advance();
479
+ } else if (isHoconNewline(ch)) {
480
+ throw new ParseError("unterminated substitution", startLine, startCol);
481
+ } else if (isHoconWhitespace(ch)) {
482
+ pendingWs += ch;
483
+ this.advance();
484
+ } else {
485
+ throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
486
+ }
487
+ }
488
+ if (curStarted) {
489
+ segments.push({ text: curText, line: curLine, col: curCol });
490
+ } else if (segments.length === 0) {
491
+ throw new ParseError("empty substitution path", startLine, startCol);
492
+ } else {
493
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
494
+ throw new ParseError("empty segment in path", errLine, errCol);
495
+ }
496
+ return { segments, optional };
497
+ }
361
498
  };
362
499
  function tokenize(input) {
363
500
  return new Lexer(input).tokenize();
364
501
  }
502
+ function isUnquotedSubstChar(ch) {
503
+ if (ch === "" || isHoconWhitespace(ch)) return false;
504
+ if ('"\\'.includes(ch)) return false;
505
+ if ("{}[]".includes(ch)) return false;
506
+ if (":=,+#`^?!@*&$.".includes(ch)) return false;
507
+ return true;
508
+ }
509
+ function isDecimalDigit(ch) {
510
+ return ch >= "0" && ch <= "9";
511
+ }
365
512
  function isUnquotedStart(ch) {
366
- return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
513
+ if (ch === "" || isHoconWhitespace(ch)) return false;
514
+ if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
515
+ return true;
367
516
  }
368
517
  function isUnquotedContinue(ch, nextFn) {
369
- if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
518
+ if (ch === "" || isHoconWhitespace(ch)) return false;
519
+ if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
370
520
  if (ch === "+" && nextFn() === "=") return false;
371
521
  if (ch === "/" && nextFn() === "/") return false;
372
522
  return true;
373
523
  }
374
524
 
375
525
  // src/internal/parser/parser.ts
376
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
526
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
377
527
  var Parser = class {
378
528
  constructor(tokens) {
379
529
  this.tokens = tokens;
@@ -477,10 +627,21 @@ var Parser = class {
477
627
  trailingDot = false;
478
628
  } else if (t.kind === "unquoted") {
479
629
  this.advance();
480
- const parts = t.value.split(".");
630
+ const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
631
+ const parts = raw.split(".");
481
632
  const filtered = parts.filter((s) => s.length > 0);
633
+ for (const seg of filtered) {
634
+ if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
635
+ const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
636
+ throw new ParseError(
637
+ `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
638
+ t.line,
639
+ t.col
640
+ );
641
+ }
642
+ }
482
643
  segments.push(...filtered);
483
- trailingDot = t.value.endsWith(".");
644
+ trailingDot = raw.endsWith(".");
484
645
  } else {
485
646
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
486
647
  break;
@@ -492,6 +653,10 @@ var Parser = class {
492
653
  trailingDot = true;
493
654
  continue;
494
655
  }
656
+ if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
657
+ trailingDot = true;
658
+ continue;
659
+ }
495
660
  break;
496
661
  }
497
662
  return segments;
@@ -567,9 +732,13 @@ var Parser = class {
567
732
  } else if (t.kind === "lbracket") {
568
733
  this.advance();
569
734
  node = this.parseArray();
570
- } else if (t.kind === "subst" || t.kind === "opt_subst") {
735
+ } else if (t.kind === "subst") {
571
736
  this.advance();
572
- node = { kind: "subst", path: t.value, optional: t.kind === "opt_subst", pos: { line: t.line, col: t.col } };
737
+ if (!t.subst) {
738
+ throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
739
+ }
740
+ const payload = t.subst;
741
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
573
742
  } else if (t.kind === "string" || t.kind === "triple_string") {
574
743
  this.advance();
575
744
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -639,64 +808,20 @@ function makeResObj() {
639
808
  }
640
809
 
641
810
  // src/internal/resolver/utils.ts
642
- function parseSubstPath(raw) {
643
- const segments = [];
644
- let i = 0;
645
- while (i < raw.length) {
646
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
647
- if (i >= raw.length) break;
648
- if (raw[i] === '"') {
649
- i++;
650
- let seg = "";
651
- while (i < raw.length && raw[i] !== '"') {
652
- if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
653
- seg += raw[i + 1];
654
- i += 2;
655
- } else {
656
- seg += raw[i];
657
- i++;
658
- }
659
- }
660
- if (i < raw.length) i++;
661
- segments.push(seg);
662
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
663
- if (i < raw.length && raw[i] === ".") i++;
664
- } else if (raw[i] === ".") {
665
- segments.push("");
666
- i++;
667
- } else {
668
- let seg = "";
669
- while (i < raw.length && raw[i] !== ".") {
670
- seg += raw[i];
671
- i++;
672
- }
673
- segments.push(seg.trim());
674
- if (i < raw.length && raw[i] === ".") i++;
675
- }
676
- }
677
- return segments;
678
- }
679
811
  function segmentsToKey(segments) {
680
812
  return segments.map((s) => {
681
- if (s === "" || /[^a-zA-Z0-9\-_]/.test(s)) {
682
- return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
813
+ const t = s.text;
814
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
815
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
683
816
  }
684
- return s;
817
+ return t;
685
818
  }).join(".");
686
819
  }
687
820
  function lookupPath(root, segments) {
688
821
  const [head, ...tail] = segments;
689
- if (head === void 0 || head === "") {
690
- if (segments.length > 0) {
691
- const val2 = root.fields.get("");
692
- if (val2 === void 0) return void 0;
693
- if (tail.length === 0) return val2;
694
- if (isResObj(val2)) return lookupPath(val2, tail);
695
- return void 0;
696
- }
697
- return void 0;
698
- }
699
- const val = root.fields.get(head);
822
+ if (head === void 0) return void 0;
823
+ const key = head.text;
824
+ const val = root.fields.get(key);
700
825
  if (val === void 0) return void 0;
701
826
  if (tail.length === 0) return val;
702
827
  if (isResObj(val)) return lookupPath(val, tail);
@@ -705,7 +830,7 @@ function lookupPath(root, segments) {
705
830
  function lookupResObj(root, segments) {
706
831
  let cur = root;
707
832
  for (const seg of segments) {
708
- const val = cur.fields.get(seg);
833
+ const val = cur.fields.get(seg.text);
709
834
  if (val === void 0 || !isResObj(val)) return void 0;
710
835
  cur = val;
711
836
  }
@@ -982,7 +1107,7 @@ var StructureBuilder = class _StructureBuilder {
982
1107
  return;
983
1108
  }
984
1109
  const [head, ...tail] = field.key;
985
- if (!head) return;
1110
+ if (head === void 0) return;
986
1111
  if (tail.length > 0) {
987
1112
  const syntheticAst = {
988
1113
  kind: "object",
@@ -1024,7 +1149,7 @@ var StructureBuilder = class _StructureBuilder {
1024
1149
  return;
1025
1150
  }
1026
1151
  const [head, ...tail] = field.key;
1027
- if (!head) return;
1152
+ if (head === void 0) return;
1028
1153
  if (tail.length > 0) {
1029
1154
  const syntheticAst = {
1030
1155
  kind: "object",
@@ -1070,7 +1195,7 @@ var StructureBuilder = class _StructureBuilder {
1070
1195
  return inner;
1071
1196
  }
1072
1197
  case "subst":
1073
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1198
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1074
1199
  case "concat":
1075
1200
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1076
1201
  case "include":
@@ -1094,7 +1219,7 @@ var StructureBuilder = class _StructureBuilder {
1094
1219
  case "object":
1095
1220
  return await this.buildAsync(ast, pathPrefix);
1096
1221
  case "subst":
1097
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1222
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1098
1223
  case "concat": {
1099
1224
  const nodes = [];
1100
1225
  for (const n of ast.nodes) {
@@ -1109,7 +1234,8 @@ var StructureBuilder = class _StructureBuilder {
1109
1234
  // ---- Relativize substitution paths for nested includes ----
1110
1235
  relativizeSubstPaths(val, prefixSegments) {
1111
1236
  if (isSubst(val)) {
1112
- val.segments = [...prefixSegments, ...val.segments];
1237
+ const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
1238
+ val.segments = [...prefixAsSegments, ...val.segments];
1113
1239
  val.prefixLen += prefixSegments.length;
1114
1240
  return;
1115
1241
  }
@@ -1145,6 +1271,23 @@ var StructureBuilder = class _StructureBuilder {
1145
1271
  }
1146
1272
  };
1147
1273
 
1274
+ // src/value/numeric-array.ts
1275
+ function numericObjectToArray(value) {
1276
+ if (value.kind !== "object") return null;
1277
+ if (value.fields.size === 0) return null;
1278
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
1279
+ const eligible = [];
1280
+ for (const [key, val] of value.fields) {
1281
+ if (!canonicalInt.test(key)) continue;
1282
+ const n = Number(key);
1283
+ if (!Number.isInteger(n) || n > 2147483647) continue;
1284
+ eligible.push({ n, v: val });
1285
+ }
1286
+ if (eligible.length === 0) return null;
1287
+ eligible.sort((a, b) => a.n - b.n);
1288
+ return eligible.map((e) => e.v);
1289
+ }
1290
+
1148
1291
  // src/internal/resolver/substitution-resolver.ts
1149
1292
  var SubstitutionResolver = class {
1150
1293
  constructor(root, opts) {
@@ -1205,7 +1348,7 @@ var SubstitutionResolver = class {
1205
1348
  return hv;
1206
1349
  }
1207
1350
  segmentsEqual(a, b) {
1208
- return a.length === b.length && a.every((seg, i) => seg === b[i]);
1351
+ return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1209
1352
  }
1210
1353
  resolveSubst(s, scope) {
1211
1354
  const key = segmentsToKey(s.segments);
@@ -1213,14 +1356,14 @@ var SubstitutionResolver = class {
1213
1356
  if (this.resolving.has(key)) {
1214
1357
  let prior;
1215
1358
  if (s.prefixLen > 0) {
1216
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1359
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1217
1360
  const parentScope = lookupResObj(
1218
1361
  this.root,
1219
1362
  s.segments.slice(0, s.segments.length - 1)
1220
1363
  );
1221
1364
  prior = parentScope?.priorValues.get(leafSeg);
1222
1365
  } else {
1223
- const rootSeg = s.segments[0] ?? "";
1366
+ const rootSeg = s.segments[0]?.text ?? "";
1224
1367
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1225
1368
  }
1226
1369
  if (prior !== void 0) {
@@ -1251,14 +1394,14 @@ var SubstitutionResolver = class {
1251
1394
  if (isSelfRef) {
1252
1395
  let prior;
1253
1396
  if (s.prefixLen > 0) {
1254
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1397
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1255
1398
  const parentScope = lookupResObj(
1256
1399
  this.root,
1257
1400
  s.segments.slice(0, s.segments.length - 1)
1258
1401
  );
1259
1402
  prior = parentScope?.priorValues.get(leafSeg);
1260
1403
  } else {
1261
- const rootSeg = s.segments[0] ?? "";
1404
+ const rootSeg = s.segments[0]?.text ?? "";
1262
1405
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1263
1406
  }
1264
1407
  if (prior !== void 0) {
@@ -1271,7 +1414,7 @@ var SubstitutionResolver = class {
1271
1414
  let result = this.resolveVal(found, scope);
1272
1415
  const effectiveLen = s.segments.length - s.prefixLen;
1273
1416
  if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1274
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1417
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1275
1418
  let prior;
1276
1419
  if (s.prefixLen > 0) {
1277
1420
  const parentScope = lookupResObj(
@@ -1295,8 +1438,8 @@ var SubstitutionResolver = class {
1295
1438
  if (result !== void 0) this.cache.set(key, result);
1296
1439
  return result;
1297
1440
  }
1298
- const envKey = s.segments.join(".");
1299
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).join(".")] : void 0);
1441
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1442
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1300
1443
  if (envVal !== void 0) {
1301
1444
  const result = { kind: "scalar", raw: envVal, valueType: "string" };
1302
1445
  this.cache.set(key, result);
@@ -1318,32 +1461,48 @@ var SubstitutionResolver = class {
1318
1461
  if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1319
1462
  if (resolved.length === 1) return resolved[0];
1320
1463
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1321
- if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1322
- const merged = /* @__PURE__ */ new Map();
1323
- for (const v of nonSep) {
1324
- if (v.kind === "object") {
1325
- for (const [k, val] of v.fields) {
1326
- const existing = merged.get(k);
1327
- if (existing?.kind === "object" && val.kind === "object") {
1328
- merged.set(k, deepMergeHoconValues(existing, val));
1329
- } else {
1330
- merged.set(k, val);
1331
- }
1332
- }
1464
+ if (nonSep.length === 0) {
1465
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1466
+ return { kind: "scalar", raw: str, valueType: "string" };
1467
+ }
1468
+ const joinPair = (left, right) => {
1469
+ if (left.kind === "object" && right.kind === "object") {
1470
+ return deepMergeHoconValues(left, right);
1471
+ }
1472
+ if (left.kind === "array" && right.kind === "object") {
1473
+ const converted = numericObjectToArray(right);
1474
+ if (converted !== null) {
1475
+ return { kind: "array", items: [...left.items, ...converted] };
1333
1476
  }
1477
+ return { kind: "array", items: [...left.items, right] };
1334
1478
  }
1335
- return { kind: "object", fields: merged };
1336
- }
1337
- if (resolved.some((v) => v.kind === "array")) {
1338
- const items = [];
1339
- for (const v of resolved) {
1340
- if (v.kind === "array") items.push(...v.items);
1341
- else items.push(v);
1479
+ if (left.kind === "object" && right.kind === "array") {
1480
+ const converted = numericObjectToArray(left);
1481
+ if (converted !== null) {
1482
+ return { kind: "array", items: [...converted, ...right.items] };
1483
+ }
1484
+ return { kind: "array", items: [left, ...right.items] };
1485
+ }
1486
+ if (left.kind === "array" && right.kind === "array") {
1487
+ return { kind: "array", items: [...left.items, ...right.items] };
1488
+ }
1489
+ if (left.kind === "array") {
1490
+ return { kind: "array", items: [...left.items, right] };
1342
1491
  }
1343
- return { kind: "array", items };
1492
+ if (right.kind === "array") {
1493
+ return { kind: "array", items: [left, ...right.items] };
1494
+ }
1495
+ const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1496
+ const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1497
+ return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
1498
+ };
1499
+ const [head, ...tail] = nonSep;
1500
+ const folded = tail.reduce(joinPair, head);
1501
+ if (folded.kind === "scalar") {
1502
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1503
+ return { kind: "scalar", raw: str, valueType: "string" };
1344
1504
  }
1345
- const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1346
- return { kind: "scalar", raw: str, valueType: "string" };
1505
+ return folded;
1347
1506
  }
1348
1507
  resolveAppend(a, scope) {
1349
1508
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
@@ -1413,6 +1572,10 @@ var Config = class _Config {
1413
1572
  getList(path2) {
1414
1573
  const v = this.lookupNode(path2);
1415
1574
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
1575
+ if (v.kind === "object") {
1576
+ const converted = numericObjectToArray(v);
1577
+ if (converted !== null) return converted.map(hoconToJs);
1578
+ }
1416
1579
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
1417
1580
  return v.items.map(hoconToJs);
1418
1581
  }