@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/index.cjs CHANGED
@@ -208,6 +208,23 @@ var ConfigError = class extends Error {
208
208
  }
209
209
  };
210
210
 
211
+ // src/value/numeric-array.ts
212
+ function numericObjectToArray(value) {
213
+ if (value.kind !== "object") return null;
214
+ if (value.fields.size === 0) return null;
215
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
216
+ const eligible = [];
217
+ for (const [key, val] of value.fields) {
218
+ if (!canonicalInt.test(key)) continue;
219
+ const n = Number(key);
220
+ if (!Number.isInteger(n) || n > 2147483647) continue;
221
+ eligible.push({ n, v: val });
222
+ }
223
+ if (eligible.length === 0) return null;
224
+ eligible.sort((a, b) => a.n - b.n);
225
+ return eligible.map((e) => e.v);
226
+ }
227
+
211
228
  // src/config.ts
212
229
  var Config = class _Config {
213
230
  constructor(root) {
@@ -261,6 +278,10 @@ var Config = class _Config {
261
278
  getList(path2) {
262
279
  const v = this.lookupNode(path2);
263
280
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
281
+ if (v.kind === "object") {
282
+ const converted = numericObjectToArray(v);
283
+ if (converted !== null) return converted.map(hoconToJs);
284
+ }
264
285
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
265
286
  return v.items.map(hoconToJs);
266
287
  }
@@ -373,6 +394,20 @@ function deepMergeHocon(receiver, fallback) {
373
394
  }
374
395
 
375
396
  // src/internal/lexer/lexer.ts
397
+ function isHoconWhitespace(ch) {
398
+ const cp = ch.codePointAt(0) ?? -1;
399
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
400
+ if (cp >= 28 && cp <= 31) return true;
401
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
402
+ if (cp === 5760) return true;
403
+ if (cp >= 8192 && cp <= 8202) return true;
404
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
405
+ if (cp === 12288) return true;
406
+ return false;
407
+ }
408
+ function isHoconNewline(ch) {
409
+ return ch === "\n";
410
+ }
376
411
  var SINGLE_CHAR_TOKENS = {
377
412
  "{": "lbrace",
378
413
  "}": "rbrace",
@@ -395,18 +430,18 @@ var Lexer = class {
395
430
  while (this.pos < this.input.length) {
396
431
  const sl = this.line, sc = this.col;
397
432
  const ch = this.peek();
398
- if (ch === " " || ch === " " || ch === "\r") {
399
- this.advance();
400
- this.hadSpace = true;
401
- continue;
402
- }
403
- if (ch === "\n") {
433
+ if (isHoconNewline(ch)) {
404
434
  this.advance();
405
435
  if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
406
436
  this.push("newline", "\n", sl, sc);
407
437
  }
408
438
  continue;
409
439
  }
440
+ if (isHoconWhitespace(ch)) {
441
+ this.advance();
442
+ this.hadSpace = true;
443
+ continue;
444
+ }
410
445
  if (ch === "/" && this.peek(1) === "/") {
411
446
  while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
412
447
  this.hadSpace = true;
@@ -436,16 +471,16 @@ var Lexer = class {
436
471
  if (ch === "$" && this.peek(1) === "{") {
437
472
  this.advance();
438
473
  this.advance();
439
- const optional = this.peek() === "?";
440
- if (optional) this.advance();
441
- let path2 = "";
442
- while (this.pos < this.input.length && this.peek() !== "}") {
443
- if (this.peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
444
- path2 += this.advance();
445
- }
446
- if (this.peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
447
- this.advance();
448
- this.push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
474
+ const payload = this.parseSubstBody(sl, sc);
475
+ const value = payload.segments.map((s) => {
476
+ const t = s.text;
477
+ if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
478
+ const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
479
+ return `"${escaped}"`;
480
+ }
481
+ return t;
482
+ }).join(".");
483
+ this.pushSubst(payload, value, sl, sc);
449
484
  continue;
450
485
  }
451
486
  if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
@@ -480,62 +515,19 @@ var Lexer = class {
480
515
  }
481
516
  if (ch === '"') {
482
517
  this.advance();
483
- let value = "";
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();
518
+ const value = this.readQuotedStringBody(sl, sc);
535
519
  this.push("string", value, sl, sc, true);
536
520
  continue;
537
521
  }
538
522
  if (isUnquotedStart(ch)) {
523
+ if (ch === "-" && !isDecimalDigit(this.peek(1))) {
524
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
525
+ throw new ParseError(
526
+ `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
527
+ sl,
528
+ sc
529
+ );
530
+ }
539
531
  let value = "";
540
532
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
541
533
  value += this.advance();
@@ -565,22 +557,201 @@ var Lexer = class {
565
557
  this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
566
558
  this.hadSpace = false;
567
559
  }
560
+ pushSubst(payload, value, l, c) {
561
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
562
+ this.hadSpace = false;
563
+ }
564
+ /**
565
+ * Read body of a quoted string. Opening `"` must already be consumed.
566
+ * Returns decoded string. Throws ParseError on unterminated/invalid-escape.
567
+ * openLine/openCol are the position of the opening `"` for error reporting.
568
+ */
569
+ readQuotedStringBody(openLine, openCol) {
570
+ let value = "";
571
+ while (this.pos < this.input.length && this.peek() !== '"') {
572
+ if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
573
+ if (this.peek() === "\\") {
574
+ const escCol = this.col;
575
+ this.advance();
576
+ if (this.pos >= this.input.length) {
577
+ throw new ParseError("unterminated string", openLine, openCol);
578
+ }
579
+ const esc = this.advance();
580
+ switch (esc) {
581
+ case "n":
582
+ value += "\n";
583
+ break;
584
+ case "t":
585
+ value += " ";
586
+ break;
587
+ case "r":
588
+ value += "\r";
589
+ break;
590
+ case '"':
591
+ value += '"';
592
+ break;
593
+ case "\\":
594
+ value += "\\";
595
+ break;
596
+ case "/":
597
+ value += "/";
598
+ break;
599
+ case "b":
600
+ value += "\b";
601
+ break;
602
+ case "f":
603
+ value += "\f";
604
+ break;
605
+ case "u": {
606
+ if (this.pos + 4 > this.input.length) {
607
+ throw new ParseError("invalid unicode escape", openLine, escCol);
608
+ }
609
+ const hex = this.input.slice(this.pos, this.pos + 4);
610
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
611
+ throw new ParseError("invalid unicode escape", openLine, escCol);
612
+ }
613
+ const code = parseInt(hex, 16);
614
+ value += String.fromCharCode(code);
615
+ for (let i = 0; i < 4; i++) this.advance();
616
+ break;
617
+ }
618
+ default:
619
+ throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
620
+ }
621
+ } else {
622
+ value += this.advance();
623
+ }
624
+ }
625
+ if (this.pos >= this.input.length || this.peek() !== '"') {
626
+ throw new ParseError("unterminated string", openLine, openCol);
627
+ }
628
+ this.advance();
629
+ return value;
630
+ }
631
+ /**
632
+ * Parse the body of a `${...}` substitution (called after `${` has been consumed).
633
+ * Implements Appendix A state machine from the cross-language spec.
634
+ */
635
+ parseSubstBody(startLine, startCol) {
636
+ let optional = false;
637
+ if (this.peek() === "?") {
638
+ this.advance();
639
+ optional = true;
640
+ }
641
+ let curText = "";
642
+ let curStarted = false;
643
+ let curLine = 0;
644
+ let curCol = 0;
645
+ let pendingWs = "";
646
+ const segments = [];
647
+ let lastDot = null;
648
+ while (true) {
649
+ if (this.pos >= this.input.length) {
650
+ throw new ParseError("unterminated substitution", startLine, startCol);
651
+ }
652
+ const ch = this.peek();
653
+ if (ch === "}") {
654
+ this.advance();
655
+ pendingWs = "";
656
+ break;
657
+ } else if (ch === '"') {
658
+ const qLine = startLine;
659
+ const qCol = this.col;
660
+ if (curStarted) {
661
+ curText += pendingWs;
662
+ }
663
+ pendingWs = "";
664
+ this.advance();
665
+ const decoded = this.readQuotedStringBody(qLine, qCol);
666
+ curText += decoded;
667
+ if (!curStarted) {
668
+ curLine = qLine;
669
+ curCol = qCol;
670
+ curStarted = true;
671
+ }
672
+ } else if (isUnquotedSubstChar(ch)) {
673
+ if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
674
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
675
+ throw new ParseError(
676
+ `unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
677
+ startLine,
678
+ this.col
679
+ );
680
+ }
681
+ const uCol = this.col;
682
+ if (curStarted) {
683
+ curText += pendingWs;
684
+ }
685
+ pendingWs = "";
686
+ if (!curStarted) {
687
+ curLine = startLine;
688
+ curCol = uCol;
689
+ curStarted = true;
690
+ }
691
+ while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
692
+ curText += this.advance();
693
+ }
694
+ } else if (ch === ".") {
695
+ const dotCol = this.col;
696
+ pendingWs = "";
697
+ if (!curStarted) {
698
+ throw new ParseError("empty segment in path", startLine, dotCol);
699
+ }
700
+ segments.push({ text: curText, line: curLine, col: curCol });
701
+ curText = "";
702
+ curStarted = false;
703
+ curLine = 0;
704
+ curCol = 0;
705
+ lastDot = [startLine, dotCol];
706
+ this.advance();
707
+ } else if (isHoconNewline(ch)) {
708
+ throw new ParseError("unterminated substitution", startLine, startCol);
709
+ } else if (isHoconWhitespace(ch)) {
710
+ pendingWs += ch;
711
+ this.advance();
712
+ } else {
713
+ throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
714
+ }
715
+ }
716
+ if (curStarted) {
717
+ segments.push({ text: curText, line: curLine, col: curCol });
718
+ } else if (segments.length === 0) {
719
+ throw new ParseError("empty substitution path", startLine, startCol);
720
+ } else {
721
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
722
+ throw new ParseError("empty segment in path", errLine, errCol);
723
+ }
724
+ return { segments, optional };
725
+ }
568
726
  };
569
727
  function tokenize(input) {
570
728
  return new Lexer(input).tokenize();
571
729
  }
730
+ function isUnquotedSubstChar(ch) {
731
+ if (ch === "" || isHoconWhitespace(ch)) return false;
732
+ if ('"\\'.includes(ch)) return false;
733
+ if ("{}[]".includes(ch)) return false;
734
+ if (":=,+#`^?!@*&$.".includes(ch)) return false;
735
+ return true;
736
+ }
737
+ function isDecimalDigit(ch) {
738
+ return ch >= "0" && ch <= "9";
739
+ }
572
740
  function isUnquotedStart(ch) {
573
- return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
741
+ if (ch === "" || isHoconWhitespace(ch)) return false;
742
+ if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
743
+ return true;
574
744
  }
575
745
  function isUnquotedContinue(ch, nextFn) {
576
- if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
746
+ if (ch === "" || isHoconWhitespace(ch)) return false;
747
+ if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
577
748
  if (ch === "+" && nextFn() === "=") return false;
578
749
  if (ch === "/" && nextFn() === "/") return false;
579
750
  return true;
580
751
  }
581
752
 
582
753
  // src/internal/parser/parser.ts
583
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
754
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
584
755
  var Parser = class {
585
756
  constructor(tokens) {
586
757
  this.tokens = tokens;
@@ -684,10 +855,21 @@ var Parser = class {
684
855
  trailingDot = false;
685
856
  } else if (t.kind === "unquoted") {
686
857
  this.advance();
687
- const parts = t.value.split(".");
858
+ const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
859
+ const parts = raw.split(".");
688
860
  const filtered = parts.filter((s) => s.length > 0);
861
+ for (const seg of filtered) {
862
+ if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
863
+ const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
864
+ throw new ParseError(
865
+ `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
866
+ t.line,
867
+ t.col
868
+ );
869
+ }
870
+ }
689
871
  segments.push(...filtered);
690
- trailingDot = t.value.endsWith(".");
872
+ trailingDot = raw.endsWith(".");
691
873
  } else {
692
874
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
693
875
  break;
@@ -699,6 +881,10 @@ var Parser = class {
699
881
  trailingDot = true;
700
882
  continue;
701
883
  }
884
+ if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
885
+ trailingDot = true;
886
+ continue;
887
+ }
702
888
  break;
703
889
  }
704
890
  return segments;
@@ -774,9 +960,13 @@ var Parser = class {
774
960
  } else if (t.kind === "lbracket") {
775
961
  this.advance();
776
962
  node = this.parseArray();
777
- } else if (t.kind === "subst" || t.kind === "opt_subst") {
963
+ } else if (t.kind === "subst") {
778
964
  this.advance();
779
- node = { kind: "subst", path: t.value, optional: t.kind === "opt_subst", pos: { line: t.line, col: t.col } };
965
+ if (!t.subst) {
966
+ throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
967
+ }
968
+ const payload = t.subst;
969
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
780
970
  } else if (t.kind === "string" || t.kind === "triple_string") {
781
971
  this.advance();
782
972
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -846,64 +1036,20 @@ function makeResObj() {
846
1036
  }
847
1037
 
848
1038
  // 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
1039
  function segmentsToKey(segments) {
887
1040
  return segments.map((s) => {
888
- if (s === "" || /[^a-zA-Z0-9\-_]/.test(s)) {
889
- return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1041
+ const t = s.text;
1042
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
1043
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
890
1044
  }
891
- return s;
1045
+ return t;
892
1046
  }).join(".");
893
1047
  }
894
1048
  function lookupPath(root, segments) {
895
1049
  const [head, ...tail] = segments;
896
- if (head === void 0 || head === "") {
897
- if (segments.length > 0) {
898
- const val2 = root.fields.get("");
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);
1050
+ if (head === void 0) return void 0;
1051
+ const key = head.text;
1052
+ const val = root.fields.get(key);
907
1053
  if (val === void 0) return void 0;
908
1054
  if (tail.length === 0) return val;
909
1055
  if (isResObj(val)) return lookupPath(val, tail);
@@ -912,7 +1058,7 @@ function lookupPath(root, segments) {
912
1058
  function lookupResObj(root, segments) {
913
1059
  let cur = root;
914
1060
  for (const seg of segments) {
915
- const val = cur.fields.get(seg);
1061
+ const val = cur.fields.get(seg.text);
916
1062
  if (val === void 0 || !isResObj(val)) return void 0;
917
1063
  cur = val;
918
1064
  }
@@ -1189,7 +1335,7 @@ var StructureBuilder = class _StructureBuilder {
1189
1335
  return;
1190
1336
  }
1191
1337
  const [head, ...tail] = field.key;
1192
- if (!head) return;
1338
+ if (head === void 0) return;
1193
1339
  if (tail.length > 0) {
1194
1340
  const syntheticAst = {
1195
1341
  kind: "object",
@@ -1231,7 +1377,7 @@ var StructureBuilder = class _StructureBuilder {
1231
1377
  return;
1232
1378
  }
1233
1379
  const [head, ...tail] = field.key;
1234
- if (!head) return;
1380
+ if (head === void 0) return;
1235
1381
  if (tail.length > 0) {
1236
1382
  const syntheticAst = {
1237
1383
  kind: "object",
@@ -1277,7 +1423,7 @@ var StructureBuilder = class _StructureBuilder {
1277
1423
  return inner;
1278
1424
  }
1279
1425
  case "subst":
1280
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1426
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1281
1427
  case "concat":
1282
1428
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1283
1429
  case "include":
@@ -1301,7 +1447,7 @@ var StructureBuilder = class _StructureBuilder {
1301
1447
  case "object":
1302
1448
  return await this.buildAsync(ast, pathPrefix);
1303
1449
  case "subst":
1304
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1450
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1305
1451
  case "concat": {
1306
1452
  const nodes = [];
1307
1453
  for (const n of ast.nodes) {
@@ -1316,7 +1462,8 @@ var StructureBuilder = class _StructureBuilder {
1316
1462
  // ---- Relativize substitution paths for nested includes ----
1317
1463
  relativizeSubstPaths(val, prefixSegments) {
1318
1464
  if (isSubst(val)) {
1319
- val.segments = [...prefixSegments, ...val.segments];
1465
+ const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
1466
+ val.segments = [...prefixAsSegments, ...val.segments];
1320
1467
  val.prefixLen += prefixSegments.length;
1321
1468
  return;
1322
1469
  }
@@ -1412,7 +1559,7 @@ var SubstitutionResolver = class {
1412
1559
  return hv;
1413
1560
  }
1414
1561
  segmentsEqual(a, b) {
1415
- return a.length === b.length && a.every((seg, i) => seg === b[i]);
1562
+ return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1416
1563
  }
1417
1564
  resolveSubst(s, scope) {
1418
1565
  const key = segmentsToKey(s.segments);
@@ -1420,14 +1567,14 @@ var SubstitutionResolver = class {
1420
1567
  if (this.resolving.has(key)) {
1421
1568
  let prior;
1422
1569
  if (s.prefixLen > 0) {
1423
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1570
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1424
1571
  const parentScope = lookupResObj(
1425
1572
  this.root,
1426
1573
  s.segments.slice(0, s.segments.length - 1)
1427
1574
  );
1428
1575
  prior = parentScope?.priorValues.get(leafSeg);
1429
1576
  } else {
1430
- const rootSeg = s.segments[0] ?? "";
1577
+ const rootSeg = s.segments[0]?.text ?? "";
1431
1578
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1432
1579
  }
1433
1580
  if (prior !== void 0) {
@@ -1458,14 +1605,14 @@ var SubstitutionResolver = class {
1458
1605
  if (isSelfRef) {
1459
1606
  let prior;
1460
1607
  if (s.prefixLen > 0) {
1461
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1608
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1462
1609
  const parentScope = lookupResObj(
1463
1610
  this.root,
1464
1611
  s.segments.slice(0, s.segments.length - 1)
1465
1612
  );
1466
1613
  prior = parentScope?.priorValues.get(leafSeg);
1467
1614
  } else {
1468
- const rootSeg = s.segments[0] ?? "";
1615
+ const rootSeg = s.segments[0]?.text ?? "";
1469
1616
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1470
1617
  }
1471
1618
  if (prior !== void 0) {
@@ -1478,7 +1625,7 @@ var SubstitutionResolver = class {
1478
1625
  let result = this.resolveVal(found, scope);
1479
1626
  const effectiveLen = s.segments.length - s.prefixLen;
1480
1627
  if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1481
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1628
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1482
1629
  let prior;
1483
1630
  if (s.prefixLen > 0) {
1484
1631
  const parentScope = lookupResObj(
@@ -1502,8 +1649,8 @@ var SubstitutionResolver = class {
1502
1649
  if (result !== void 0) this.cache.set(key, result);
1503
1650
  return result;
1504
1651
  }
1505
- const envKey = s.segments.join(".");
1506
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).join(".")] : void 0);
1652
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1653
+ 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
1654
  if (envVal !== void 0) {
1508
1655
  const result = { kind: "scalar", raw: envVal, valueType: "string" };
1509
1656
  this.cache.set(key, result);
@@ -1525,32 +1672,48 @@ var SubstitutionResolver = class {
1525
1672
  if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1526
1673
  if (resolved.length === 1) return resolved[0];
1527
1674
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1528
- if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1529
- const merged = /* @__PURE__ */ new Map();
1530
- for (const v of nonSep) {
1531
- if (v.kind === "object") {
1532
- for (const [k, val] of v.fields) {
1533
- const existing = merged.get(k);
1534
- if (existing?.kind === "object" && val.kind === "object") {
1535
- merged.set(k, deepMergeHoconValues(existing, val));
1536
- } else {
1537
- merged.set(k, val);
1538
- }
1539
- }
1675
+ if (nonSep.length === 0) {
1676
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1677
+ return { kind: "scalar", raw: str, valueType: "string" };
1678
+ }
1679
+ const joinPair = (left, right) => {
1680
+ if (left.kind === "object" && right.kind === "object") {
1681
+ return deepMergeHoconValues(left, right);
1682
+ }
1683
+ if (left.kind === "array" && right.kind === "object") {
1684
+ const converted = numericObjectToArray(right);
1685
+ if (converted !== null) {
1686
+ return { kind: "array", items: [...left.items, ...converted] };
1540
1687
  }
1688
+ return { kind: "array", items: [...left.items, right] };
1541
1689
  }
1542
- return { kind: "object", fields: merged };
1543
- }
1544
- if (resolved.some((v) => v.kind === "array")) {
1545
- const items = [];
1546
- for (const v of resolved) {
1547
- if (v.kind === "array") items.push(...v.items);
1548
- else items.push(v);
1690
+ if (left.kind === "object" && right.kind === "array") {
1691
+ const converted = numericObjectToArray(left);
1692
+ if (converted !== null) {
1693
+ return { kind: "array", items: [...converted, ...right.items] };
1694
+ }
1695
+ return { kind: "array", items: [left, ...right.items] };
1696
+ }
1697
+ if (left.kind === "array" && right.kind === "array") {
1698
+ return { kind: "array", items: [...left.items, ...right.items] };
1549
1699
  }
1550
- return { kind: "array", items };
1700
+ if (left.kind === "array") {
1701
+ return { kind: "array", items: [...left.items, right] };
1702
+ }
1703
+ if (right.kind === "array") {
1704
+ return { kind: "array", items: [left, ...right.items] };
1705
+ }
1706
+ const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1707
+ const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1708
+ return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
1709
+ };
1710
+ const [head, ...tail] = nonSep;
1711
+ const folded = tail.reduce(joinPair, head);
1712
+ if (folded.kind === "scalar") {
1713
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1714
+ return { kind: "scalar", raw: str, valueType: "string" };
1551
1715
  }
1552
- const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1553
- return { kind: "scalar", raw: str, valueType: "string" };
1716
+ return folded;
1554
1717
  }
1555
1718
  resolveAppend(a, scope) {
1556
1719
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };