@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.cjs CHANGED
@@ -205,6 +205,20 @@ var ConfigError = class extends Error {
205
205
  };
206
206
 
207
207
  // src/internal/lexer/lexer.ts
208
+ function isHoconWhitespace(ch) {
209
+ const cp = ch.codePointAt(0) ?? -1;
210
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
211
+ if (cp >= 28 && cp <= 31) return true;
212
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
213
+ if (cp === 5760) return true;
214
+ if (cp >= 8192 && cp <= 8202) return true;
215
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
216
+ if (cp === 12288) return true;
217
+ return false;
218
+ }
219
+ function isHoconNewline(ch) {
220
+ return ch === "\n";
221
+ }
208
222
  var SINGLE_CHAR_TOKENS = {
209
223
  "{": "lbrace",
210
224
  "}": "rbrace",
@@ -227,18 +241,18 @@ var Lexer = class {
227
241
  while (this.pos < this.input.length) {
228
242
  const sl = this.line, sc = this.col;
229
243
  const ch = this.peek();
230
- if (ch === " " || ch === " " || ch === "\r") {
231
- this.advance();
232
- this.hadSpace = true;
233
- continue;
234
- }
235
- if (ch === "\n") {
244
+ if (isHoconNewline(ch)) {
236
245
  this.advance();
237
246
  if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
238
247
  this.push("newline", "\n", sl, sc);
239
248
  }
240
249
  continue;
241
250
  }
251
+ if (isHoconWhitespace(ch)) {
252
+ this.advance();
253
+ this.hadSpace = true;
254
+ continue;
255
+ }
242
256
  if (ch === "/" && this.peek(1) === "/") {
243
257
  while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
244
258
  this.hadSpace = true;
@@ -268,16 +282,16 @@ var Lexer = class {
268
282
  if (ch === "$" && this.peek(1) === "{") {
269
283
  this.advance();
270
284
  this.advance();
271
- const optional = this.peek() === "?";
272
- if (optional) this.advance();
273
- let path2 = "";
274
- while (this.pos < this.input.length && this.peek() !== "}") {
275
- if (this.peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
276
- path2 += this.advance();
277
- }
278
- if (this.peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
279
- this.advance();
280
- this.push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
285
+ const payload = this.parseSubstBody(sl, sc);
286
+ const value = payload.segments.map((s) => {
287
+ const t = s.text;
288
+ if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
289
+ const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
290
+ return `"${escaped}"`;
291
+ }
292
+ return t;
293
+ }).join(".");
294
+ this.pushSubst(payload, value, sl, sc);
281
295
  continue;
282
296
  }
283
297
  if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
@@ -312,62 +326,19 @@ var Lexer = class {
312
326
  }
313
327
  if (ch === '"') {
314
328
  this.advance();
315
- let value = "";
316
- while (this.pos < this.input.length && this.peek() !== '"') {
317
- if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
318
- if (this.peek() === "\\") {
319
- this.advance();
320
- const esc = this.advance();
321
- switch (esc) {
322
- case "n":
323
- value += "\n";
324
- break;
325
- case "t":
326
- value += " ";
327
- break;
328
- case "r":
329
- value += "\r";
330
- break;
331
- case '"':
332
- value += '"';
333
- break;
334
- case "\\":
335
- value += "\\";
336
- break;
337
- case "/":
338
- value += "/";
339
- break;
340
- case "b":
341
- value += "\b";
342
- break;
343
- case "f":
344
- value += "\f";
345
- break;
346
- case "u": {
347
- if (this.pos + 4 > this.input.length) {
348
- throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
349
- }
350
- const hex = this.input.slice(this.pos, this.pos + 4);
351
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
352
- throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
353
- }
354
- value += String.fromCharCode(parseInt(hex, 16));
355
- for (let i = 0; i < 4; i++) this.advance();
356
- break;
357
- }
358
- default:
359
- throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
360
- }
361
- } else {
362
- value += this.advance();
363
- }
364
- }
365
- if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
366
- this.advance();
329
+ const value = this.readQuotedStringBody(sl, sc);
367
330
  this.push("string", value, sl, sc, true);
368
331
  continue;
369
332
  }
370
333
  if (isUnquotedStart(ch)) {
334
+ if (ch === "-" && !isDecimalDigit(this.peek(1))) {
335
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
336
+ throw new ParseError(
337
+ `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
338
+ sl,
339
+ sc
340
+ );
341
+ }
371
342
  let value = "";
372
343
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
373
344
  value += this.advance();
@@ -397,22 +368,201 @@ var Lexer = class {
397
368
  this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
398
369
  this.hadSpace = false;
399
370
  }
371
+ pushSubst(payload, value, l, c) {
372
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
373
+ this.hadSpace = false;
374
+ }
375
+ /**
376
+ * Read body of a quoted string. Opening `"` must already be consumed.
377
+ * Returns decoded string. Throws ParseError on unterminated/invalid-escape.
378
+ * openLine/openCol are the position of the opening `"` for error reporting.
379
+ */
380
+ readQuotedStringBody(openLine, openCol) {
381
+ let value = "";
382
+ while (this.pos < this.input.length && this.peek() !== '"') {
383
+ if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
384
+ if (this.peek() === "\\") {
385
+ const escCol = this.col;
386
+ this.advance();
387
+ if (this.pos >= this.input.length) {
388
+ throw new ParseError("unterminated string", openLine, openCol);
389
+ }
390
+ const esc = this.advance();
391
+ switch (esc) {
392
+ case "n":
393
+ value += "\n";
394
+ break;
395
+ case "t":
396
+ value += " ";
397
+ break;
398
+ case "r":
399
+ value += "\r";
400
+ break;
401
+ case '"':
402
+ value += '"';
403
+ break;
404
+ case "\\":
405
+ value += "\\";
406
+ break;
407
+ case "/":
408
+ value += "/";
409
+ break;
410
+ case "b":
411
+ value += "\b";
412
+ break;
413
+ case "f":
414
+ value += "\f";
415
+ break;
416
+ case "u": {
417
+ if (this.pos + 4 > this.input.length) {
418
+ throw new ParseError("invalid unicode escape", openLine, escCol);
419
+ }
420
+ const hex = this.input.slice(this.pos, this.pos + 4);
421
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
422
+ throw new ParseError("invalid unicode escape", openLine, escCol);
423
+ }
424
+ const code = parseInt(hex, 16);
425
+ value += String.fromCharCode(code);
426
+ for (let i = 0; i < 4; i++) this.advance();
427
+ break;
428
+ }
429
+ default:
430
+ throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
431
+ }
432
+ } else {
433
+ value += this.advance();
434
+ }
435
+ }
436
+ if (this.pos >= this.input.length || this.peek() !== '"') {
437
+ throw new ParseError("unterminated string", openLine, openCol);
438
+ }
439
+ this.advance();
440
+ return value;
441
+ }
442
+ /**
443
+ * Parse the body of a `${...}` substitution (called after `${` has been consumed).
444
+ * Implements Appendix A state machine from the cross-language spec.
445
+ */
446
+ parseSubstBody(startLine, startCol) {
447
+ let optional = false;
448
+ if (this.peek() === "?") {
449
+ this.advance();
450
+ optional = true;
451
+ }
452
+ let curText = "";
453
+ let curStarted = false;
454
+ let curLine = 0;
455
+ let curCol = 0;
456
+ let pendingWs = "";
457
+ const segments = [];
458
+ let lastDot = null;
459
+ while (true) {
460
+ if (this.pos >= this.input.length) {
461
+ throw new ParseError("unterminated substitution", startLine, startCol);
462
+ }
463
+ const ch = this.peek();
464
+ if (ch === "}") {
465
+ this.advance();
466
+ pendingWs = "";
467
+ break;
468
+ } else if (ch === '"') {
469
+ const qLine = startLine;
470
+ const qCol = this.col;
471
+ if (curStarted) {
472
+ curText += pendingWs;
473
+ }
474
+ pendingWs = "";
475
+ this.advance();
476
+ const decoded = this.readQuotedStringBody(qLine, qCol);
477
+ curText += decoded;
478
+ if (!curStarted) {
479
+ curLine = qLine;
480
+ curCol = qCol;
481
+ curStarted = true;
482
+ }
483
+ } else if (isUnquotedSubstChar(ch)) {
484
+ if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
485
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
486
+ throw new ParseError(
487
+ `unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
488
+ startLine,
489
+ this.col
490
+ );
491
+ }
492
+ const uCol = this.col;
493
+ if (curStarted) {
494
+ curText += pendingWs;
495
+ }
496
+ pendingWs = "";
497
+ if (!curStarted) {
498
+ curLine = startLine;
499
+ curCol = uCol;
500
+ curStarted = true;
501
+ }
502
+ while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
503
+ curText += this.advance();
504
+ }
505
+ } else if (ch === ".") {
506
+ const dotCol = this.col;
507
+ pendingWs = "";
508
+ if (!curStarted) {
509
+ throw new ParseError("empty segment in path", startLine, dotCol);
510
+ }
511
+ segments.push({ text: curText, line: curLine, col: curCol });
512
+ curText = "";
513
+ curStarted = false;
514
+ curLine = 0;
515
+ curCol = 0;
516
+ lastDot = [startLine, dotCol];
517
+ this.advance();
518
+ } else if (isHoconNewline(ch)) {
519
+ throw new ParseError("unterminated substitution", startLine, startCol);
520
+ } else if (isHoconWhitespace(ch)) {
521
+ pendingWs += ch;
522
+ this.advance();
523
+ } else {
524
+ throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
525
+ }
526
+ }
527
+ if (curStarted) {
528
+ segments.push({ text: curText, line: curLine, col: curCol });
529
+ } else if (segments.length === 0) {
530
+ throw new ParseError("empty substitution path", startLine, startCol);
531
+ } else {
532
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
533
+ throw new ParseError("empty segment in path", errLine, errCol);
534
+ }
535
+ return { segments, optional };
536
+ }
400
537
  };
401
538
  function tokenize(input) {
402
539
  return new Lexer(input).tokenize();
403
540
  }
541
+ function isUnquotedSubstChar(ch) {
542
+ if (ch === "" || isHoconWhitespace(ch)) return false;
543
+ if ('"\\'.includes(ch)) return false;
544
+ if ("{}[]".includes(ch)) return false;
545
+ if (":=,+#`^?!@*&$.".includes(ch)) return false;
546
+ return true;
547
+ }
548
+ function isDecimalDigit(ch) {
549
+ return ch >= "0" && ch <= "9";
550
+ }
404
551
  function isUnquotedStart(ch) {
405
- return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
552
+ if (ch === "" || isHoconWhitespace(ch)) return false;
553
+ if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
554
+ return true;
406
555
  }
407
556
  function isUnquotedContinue(ch, nextFn) {
408
- if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
557
+ if (ch === "" || isHoconWhitespace(ch)) return false;
558
+ if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
409
559
  if (ch === "+" && nextFn() === "=") return false;
410
560
  if (ch === "/" && nextFn() === "/") return false;
411
561
  return true;
412
562
  }
413
563
 
414
564
  // src/internal/parser/parser.ts
415
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
565
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
416
566
  var Parser = class {
417
567
  constructor(tokens) {
418
568
  this.tokens = tokens;
@@ -516,10 +666,21 @@ var Parser = class {
516
666
  trailingDot = false;
517
667
  } else if (t.kind === "unquoted") {
518
668
  this.advance();
519
- const parts = t.value.split(".");
669
+ const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
670
+ const parts = raw.split(".");
520
671
  const filtered = parts.filter((s) => s.length > 0);
672
+ for (const seg of filtered) {
673
+ if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
674
+ const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
675
+ throw new ParseError(
676
+ `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
677
+ t.line,
678
+ t.col
679
+ );
680
+ }
681
+ }
521
682
  segments.push(...filtered);
522
- trailingDot = t.value.endsWith(".");
683
+ trailingDot = raw.endsWith(".");
523
684
  } else {
524
685
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
525
686
  break;
@@ -531,6 +692,10 @@ var Parser = class {
531
692
  trailingDot = true;
532
693
  continue;
533
694
  }
695
+ if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
696
+ trailingDot = true;
697
+ continue;
698
+ }
534
699
  break;
535
700
  }
536
701
  return segments;
@@ -606,9 +771,13 @@ var Parser = class {
606
771
  } else if (t.kind === "lbracket") {
607
772
  this.advance();
608
773
  node = this.parseArray();
609
- } else if (t.kind === "subst" || t.kind === "opt_subst") {
774
+ } else if (t.kind === "subst") {
610
775
  this.advance();
611
- node = { kind: "subst", path: t.value, optional: t.kind === "opt_subst", pos: { line: t.line, col: t.col } };
776
+ if (!t.subst) {
777
+ throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
778
+ }
779
+ const payload = t.subst;
780
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
612
781
  } else if (t.kind === "string" || t.kind === "triple_string") {
613
782
  this.advance();
614
783
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -678,64 +847,20 @@ function makeResObj() {
678
847
  }
679
848
 
680
849
  // src/internal/resolver/utils.ts
681
- function parseSubstPath(raw) {
682
- const segments = [];
683
- let i = 0;
684
- while (i < raw.length) {
685
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
686
- if (i >= raw.length) break;
687
- if (raw[i] === '"') {
688
- i++;
689
- let seg = "";
690
- while (i < raw.length && raw[i] !== '"') {
691
- if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
692
- seg += raw[i + 1];
693
- i += 2;
694
- } else {
695
- seg += raw[i];
696
- i++;
697
- }
698
- }
699
- if (i < raw.length) i++;
700
- segments.push(seg);
701
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
702
- if (i < raw.length && raw[i] === ".") i++;
703
- } else if (raw[i] === ".") {
704
- segments.push("");
705
- i++;
706
- } else {
707
- let seg = "";
708
- while (i < raw.length && raw[i] !== ".") {
709
- seg += raw[i];
710
- i++;
711
- }
712
- segments.push(seg.trim());
713
- if (i < raw.length && raw[i] === ".") i++;
714
- }
715
- }
716
- return segments;
717
- }
718
850
  function segmentsToKey(segments) {
719
851
  return segments.map((s) => {
720
- if (s === "" || /[^a-zA-Z0-9\-_]/.test(s)) {
721
- return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
852
+ const t = s.text;
853
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
854
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
722
855
  }
723
- return s;
856
+ return t;
724
857
  }).join(".");
725
858
  }
726
859
  function lookupPath(root, segments) {
727
860
  const [head, ...tail] = segments;
728
- if (head === void 0 || head === "") {
729
- if (segments.length > 0) {
730
- const val2 = root.fields.get("");
731
- if (val2 === void 0) return void 0;
732
- if (tail.length === 0) return val2;
733
- if (isResObj(val2)) return lookupPath(val2, tail);
734
- return void 0;
735
- }
736
- return void 0;
737
- }
738
- const val = root.fields.get(head);
861
+ if (head === void 0) return void 0;
862
+ const key = head.text;
863
+ const val = root.fields.get(key);
739
864
  if (val === void 0) return void 0;
740
865
  if (tail.length === 0) return val;
741
866
  if (isResObj(val)) return lookupPath(val, tail);
@@ -744,7 +869,7 @@ function lookupPath(root, segments) {
744
869
  function lookupResObj(root, segments) {
745
870
  let cur = root;
746
871
  for (const seg of segments) {
747
- const val = cur.fields.get(seg);
872
+ const val = cur.fields.get(seg.text);
748
873
  if (val === void 0 || !isResObj(val)) return void 0;
749
874
  cur = val;
750
875
  }
@@ -1021,7 +1146,7 @@ var StructureBuilder = class _StructureBuilder {
1021
1146
  return;
1022
1147
  }
1023
1148
  const [head, ...tail] = field.key;
1024
- if (!head) return;
1149
+ if (head === void 0) return;
1025
1150
  if (tail.length > 0) {
1026
1151
  const syntheticAst = {
1027
1152
  kind: "object",
@@ -1063,7 +1188,7 @@ var StructureBuilder = class _StructureBuilder {
1063
1188
  return;
1064
1189
  }
1065
1190
  const [head, ...tail] = field.key;
1066
- if (!head) return;
1191
+ if (head === void 0) return;
1067
1192
  if (tail.length > 0) {
1068
1193
  const syntheticAst = {
1069
1194
  kind: "object",
@@ -1109,7 +1234,7 @@ var StructureBuilder = class _StructureBuilder {
1109
1234
  return inner;
1110
1235
  }
1111
1236
  case "subst":
1112
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1237
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1113
1238
  case "concat":
1114
1239
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1115
1240
  case "include":
@@ -1133,7 +1258,7 @@ var StructureBuilder = class _StructureBuilder {
1133
1258
  case "object":
1134
1259
  return await this.buildAsync(ast, pathPrefix);
1135
1260
  case "subst":
1136
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1261
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1137
1262
  case "concat": {
1138
1263
  const nodes = [];
1139
1264
  for (const n of ast.nodes) {
@@ -1148,7 +1273,8 @@ var StructureBuilder = class _StructureBuilder {
1148
1273
  // ---- Relativize substitution paths for nested includes ----
1149
1274
  relativizeSubstPaths(val, prefixSegments) {
1150
1275
  if (isSubst(val)) {
1151
- val.segments = [...prefixSegments, ...val.segments];
1276
+ const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
1277
+ val.segments = [...prefixAsSegments, ...val.segments];
1152
1278
  val.prefixLen += prefixSegments.length;
1153
1279
  return;
1154
1280
  }
@@ -1184,6 +1310,23 @@ var StructureBuilder = class _StructureBuilder {
1184
1310
  }
1185
1311
  };
1186
1312
 
1313
+ // src/value/numeric-array.ts
1314
+ function numericObjectToArray(value) {
1315
+ if (value.kind !== "object") return null;
1316
+ if (value.fields.size === 0) return null;
1317
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
1318
+ const eligible = [];
1319
+ for (const [key, val] of value.fields) {
1320
+ if (!canonicalInt.test(key)) continue;
1321
+ const n = Number(key);
1322
+ if (!Number.isInteger(n) || n > 2147483647) continue;
1323
+ eligible.push({ n, v: val });
1324
+ }
1325
+ if (eligible.length === 0) return null;
1326
+ eligible.sort((a, b) => a.n - b.n);
1327
+ return eligible.map((e) => e.v);
1328
+ }
1329
+
1187
1330
  // src/internal/resolver/substitution-resolver.ts
1188
1331
  var SubstitutionResolver = class {
1189
1332
  constructor(root, opts) {
@@ -1244,7 +1387,7 @@ var SubstitutionResolver = class {
1244
1387
  return hv;
1245
1388
  }
1246
1389
  segmentsEqual(a, b) {
1247
- return a.length === b.length && a.every((seg, i) => seg === b[i]);
1390
+ return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1248
1391
  }
1249
1392
  resolveSubst(s, scope) {
1250
1393
  const key = segmentsToKey(s.segments);
@@ -1252,14 +1395,14 @@ var SubstitutionResolver = class {
1252
1395
  if (this.resolving.has(key)) {
1253
1396
  let prior;
1254
1397
  if (s.prefixLen > 0) {
1255
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1398
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1256
1399
  const parentScope = lookupResObj(
1257
1400
  this.root,
1258
1401
  s.segments.slice(0, s.segments.length - 1)
1259
1402
  );
1260
1403
  prior = parentScope?.priorValues.get(leafSeg);
1261
1404
  } else {
1262
- const rootSeg = s.segments[0] ?? "";
1405
+ const rootSeg = s.segments[0]?.text ?? "";
1263
1406
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1264
1407
  }
1265
1408
  if (prior !== void 0) {
@@ -1290,14 +1433,14 @@ var SubstitutionResolver = class {
1290
1433
  if (isSelfRef) {
1291
1434
  let prior;
1292
1435
  if (s.prefixLen > 0) {
1293
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1436
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1294
1437
  const parentScope = lookupResObj(
1295
1438
  this.root,
1296
1439
  s.segments.slice(0, s.segments.length - 1)
1297
1440
  );
1298
1441
  prior = parentScope?.priorValues.get(leafSeg);
1299
1442
  } else {
1300
- const rootSeg = s.segments[0] ?? "";
1443
+ const rootSeg = s.segments[0]?.text ?? "";
1301
1444
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1302
1445
  }
1303
1446
  if (prior !== void 0) {
@@ -1310,7 +1453,7 @@ var SubstitutionResolver = class {
1310
1453
  let result = this.resolveVal(found, scope);
1311
1454
  const effectiveLen = s.segments.length - s.prefixLen;
1312
1455
  if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1313
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1456
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1314
1457
  let prior;
1315
1458
  if (s.prefixLen > 0) {
1316
1459
  const parentScope = lookupResObj(
@@ -1334,8 +1477,8 @@ var SubstitutionResolver = class {
1334
1477
  if (result !== void 0) this.cache.set(key, result);
1335
1478
  return result;
1336
1479
  }
1337
- const envKey = s.segments.join(".");
1338
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).join(".")] : void 0);
1480
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1481
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1339
1482
  if (envVal !== void 0) {
1340
1483
  const result = { kind: "scalar", raw: envVal, valueType: "string" };
1341
1484
  this.cache.set(key, result);
@@ -1357,32 +1500,48 @@ var SubstitutionResolver = class {
1357
1500
  if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1358
1501
  if (resolved.length === 1) return resolved[0];
1359
1502
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1360
- if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1361
- const merged = /* @__PURE__ */ new Map();
1362
- for (const v of nonSep) {
1363
- if (v.kind === "object") {
1364
- for (const [k, val] of v.fields) {
1365
- const existing = merged.get(k);
1366
- if (existing?.kind === "object" && val.kind === "object") {
1367
- merged.set(k, deepMergeHoconValues(existing, val));
1368
- } else {
1369
- merged.set(k, val);
1370
- }
1371
- }
1503
+ if (nonSep.length === 0) {
1504
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1505
+ return { kind: "scalar", raw: str, valueType: "string" };
1506
+ }
1507
+ const joinPair = (left, right) => {
1508
+ if (left.kind === "object" && right.kind === "object") {
1509
+ return deepMergeHoconValues(left, right);
1510
+ }
1511
+ if (left.kind === "array" && right.kind === "object") {
1512
+ const converted = numericObjectToArray(right);
1513
+ if (converted !== null) {
1514
+ return { kind: "array", items: [...left.items, ...converted] };
1372
1515
  }
1516
+ return { kind: "array", items: [...left.items, right] };
1373
1517
  }
1374
- return { kind: "object", fields: merged };
1375
- }
1376
- if (resolved.some((v) => v.kind === "array")) {
1377
- const items = [];
1378
- for (const v of resolved) {
1379
- if (v.kind === "array") items.push(...v.items);
1380
- else items.push(v);
1518
+ if (left.kind === "object" && right.kind === "array") {
1519
+ const converted = numericObjectToArray(left);
1520
+ if (converted !== null) {
1521
+ return { kind: "array", items: [...converted, ...right.items] };
1522
+ }
1523
+ return { kind: "array", items: [left, ...right.items] };
1524
+ }
1525
+ if (left.kind === "array" && right.kind === "array") {
1526
+ return { kind: "array", items: [...left.items, ...right.items] };
1527
+ }
1528
+ if (left.kind === "array") {
1529
+ return { kind: "array", items: [...left.items, right] };
1381
1530
  }
1382
- return { kind: "array", items };
1531
+ if (right.kind === "array") {
1532
+ return { kind: "array", items: [left, ...right.items] };
1533
+ }
1534
+ const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1535
+ const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1536
+ return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
1537
+ };
1538
+ const [head, ...tail] = nonSep;
1539
+ const folded = tail.reduce(joinPair, head);
1540
+ if (folded.kind === "scalar") {
1541
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1542
+ return { kind: "scalar", raw: str, valueType: "string" };
1383
1543
  }
1384
- const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1385
- return { kind: "scalar", raw: str, valueType: "string" };
1544
+ return folded;
1386
1545
  }
1387
1546
  resolveAppend(a, scope) {
1388
1547
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
@@ -1452,6 +1611,10 @@ var Config = class _Config {
1452
1611
  getList(path2) {
1453
1612
  const v = this.lookupNode(path2);
1454
1613
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
1614
+ if (v.kind === "object") {
1615
+ const converted = numericObjectToArray(v);
1616
+ if (converted !== null) return converted.map(hoconToJs);
1617
+ }
1455
1618
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
1456
1619
  return v.items.map(hoconToJs);
1457
1620
  }