@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.js CHANGED
@@ -165,6 +165,23 @@ var ConfigError = class extends Error {
165
165
  }
166
166
  };
167
167
 
168
+ // src/value/numeric-array.ts
169
+ function numericObjectToArray(value) {
170
+ if (value.kind !== "object") return null;
171
+ if (value.fields.size === 0) return null;
172
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
173
+ const eligible = [];
174
+ for (const [key, val] of value.fields) {
175
+ if (!canonicalInt.test(key)) continue;
176
+ const n = Number(key);
177
+ if (!Number.isInteger(n) || n > 2147483647) continue;
178
+ eligible.push({ n, v: val });
179
+ }
180
+ if (eligible.length === 0) return null;
181
+ eligible.sort((a, b) => a.n - b.n);
182
+ return eligible.map((e) => e.v);
183
+ }
184
+
168
185
  // src/config.ts
169
186
  var Config = class _Config {
170
187
  constructor(root) {
@@ -218,6 +235,10 @@ var Config = class _Config {
218
235
  getList(path2) {
219
236
  const v = this.lookupNode(path2);
220
237
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
238
+ if (v.kind === "object") {
239
+ const converted = numericObjectToArray(v);
240
+ if (converted !== null) return converted.map(hoconToJs);
241
+ }
221
242
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
222
243
  return v.items.map(hoconToJs);
223
244
  }
@@ -330,6 +351,20 @@ function deepMergeHocon(receiver, fallback) {
330
351
  }
331
352
 
332
353
  // src/internal/lexer/lexer.ts
354
+ function isHoconWhitespace(ch) {
355
+ const cp = ch.codePointAt(0) ?? -1;
356
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
357
+ if (cp >= 28 && cp <= 31) return true;
358
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
359
+ if (cp === 5760) return true;
360
+ if (cp >= 8192 && cp <= 8202) return true;
361
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
362
+ if (cp === 12288) return true;
363
+ return false;
364
+ }
365
+ function isHoconNewline(ch) {
366
+ return ch === "\n";
367
+ }
333
368
  var SINGLE_CHAR_TOKENS = {
334
369
  "{": "lbrace",
335
370
  "}": "rbrace",
@@ -352,18 +387,18 @@ var Lexer = class {
352
387
  while (this.pos < this.input.length) {
353
388
  const sl = this.line, sc = this.col;
354
389
  const ch = this.peek();
355
- if (ch === " " || ch === " " || ch === "\r") {
356
- this.advance();
357
- this.hadSpace = true;
358
- continue;
359
- }
360
- if (ch === "\n") {
390
+ if (isHoconNewline(ch)) {
361
391
  this.advance();
362
392
  if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
363
393
  this.push("newline", "\n", sl, sc);
364
394
  }
365
395
  continue;
366
396
  }
397
+ if (isHoconWhitespace(ch)) {
398
+ this.advance();
399
+ this.hadSpace = true;
400
+ continue;
401
+ }
367
402
  if (ch === "/" && this.peek(1) === "/") {
368
403
  while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
369
404
  this.hadSpace = true;
@@ -393,16 +428,16 @@ var Lexer = class {
393
428
  if (ch === "$" && this.peek(1) === "{") {
394
429
  this.advance();
395
430
  this.advance();
396
- const optional = this.peek() === "?";
397
- if (optional) this.advance();
398
- let path2 = "";
399
- while (this.pos < this.input.length && this.peek() !== "}") {
400
- if (this.peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
401
- path2 += this.advance();
402
- }
403
- if (this.peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
404
- this.advance();
405
- this.push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
431
+ const payload = this.parseSubstBody(sl, sc);
432
+ const value = payload.segments.map((s) => {
433
+ const t = s.text;
434
+ if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
435
+ const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
436
+ return `"${escaped}"`;
437
+ }
438
+ return t;
439
+ }).join(".");
440
+ this.pushSubst(payload, value, sl, sc);
406
441
  continue;
407
442
  }
408
443
  if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
@@ -437,62 +472,19 @@ var Lexer = class {
437
472
  }
438
473
  if (ch === '"') {
439
474
  this.advance();
440
- let value = "";
441
- while (this.pos < this.input.length && this.peek() !== '"') {
442
- if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
443
- if (this.peek() === "\\") {
444
- this.advance();
445
- const esc = this.advance();
446
- switch (esc) {
447
- case "n":
448
- value += "\n";
449
- break;
450
- case "t":
451
- value += " ";
452
- break;
453
- case "r":
454
- value += "\r";
455
- break;
456
- case '"':
457
- value += '"';
458
- break;
459
- case "\\":
460
- value += "\\";
461
- break;
462
- case "/":
463
- value += "/";
464
- break;
465
- case "b":
466
- value += "\b";
467
- break;
468
- case "f":
469
- value += "\f";
470
- break;
471
- case "u": {
472
- if (this.pos + 4 > this.input.length) {
473
- throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
474
- }
475
- const hex = this.input.slice(this.pos, this.pos + 4);
476
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
477
- throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
478
- }
479
- value += String.fromCharCode(parseInt(hex, 16));
480
- for (let i = 0; i < 4; i++) this.advance();
481
- break;
482
- }
483
- default:
484
- throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
485
- }
486
- } else {
487
- value += this.advance();
488
- }
489
- }
490
- if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
491
- this.advance();
475
+ const value = this.readQuotedStringBody(sl, sc);
492
476
  this.push("string", value, sl, sc, true);
493
477
  continue;
494
478
  }
495
479
  if (isUnquotedStart(ch)) {
480
+ if (ch === "-" && !isDecimalDigit(this.peek(1))) {
481
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
482
+ throw new ParseError(
483
+ `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
484
+ sl,
485
+ sc
486
+ );
487
+ }
496
488
  let value = "";
497
489
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
498
490
  value += this.advance();
@@ -522,22 +514,201 @@ var Lexer = class {
522
514
  this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
523
515
  this.hadSpace = false;
524
516
  }
517
+ pushSubst(payload, value, l, c) {
518
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
519
+ this.hadSpace = false;
520
+ }
521
+ /**
522
+ * Read body of a quoted string. Opening `"` must already be consumed.
523
+ * Returns decoded string. Throws ParseError on unterminated/invalid-escape.
524
+ * openLine/openCol are the position of the opening `"` for error reporting.
525
+ */
526
+ readQuotedStringBody(openLine, openCol) {
527
+ let value = "";
528
+ while (this.pos < this.input.length && this.peek() !== '"') {
529
+ if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
530
+ if (this.peek() === "\\") {
531
+ const escCol = this.col;
532
+ this.advance();
533
+ if (this.pos >= this.input.length) {
534
+ throw new ParseError("unterminated string", openLine, openCol);
535
+ }
536
+ const esc = this.advance();
537
+ switch (esc) {
538
+ case "n":
539
+ value += "\n";
540
+ break;
541
+ case "t":
542
+ value += " ";
543
+ break;
544
+ case "r":
545
+ value += "\r";
546
+ break;
547
+ case '"':
548
+ value += '"';
549
+ break;
550
+ case "\\":
551
+ value += "\\";
552
+ break;
553
+ case "/":
554
+ value += "/";
555
+ break;
556
+ case "b":
557
+ value += "\b";
558
+ break;
559
+ case "f":
560
+ value += "\f";
561
+ break;
562
+ case "u": {
563
+ if (this.pos + 4 > this.input.length) {
564
+ throw new ParseError("invalid unicode escape", openLine, escCol);
565
+ }
566
+ const hex = this.input.slice(this.pos, this.pos + 4);
567
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
568
+ throw new ParseError("invalid unicode escape", openLine, escCol);
569
+ }
570
+ const code = parseInt(hex, 16);
571
+ value += String.fromCharCode(code);
572
+ for (let i = 0; i < 4; i++) this.advance();
573
+ break;
574
+ }
575
+ default:
576
+ throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
577
+ }
578
+ } else {
579
+ value += this.advance();
580
+ }
581
+ }
582
+ if (this.pos >= this.input.length || this.peek() !== '"') {
583
+ throw new ParseError("unterminated string", openLine, openCol);
584
+ }
585
+ this.advance();
586
+ return value;
587
+ }
588
+ /**
589
+ * Parse the body of a `${...}` substitution (called after `${` has been consumed).
590
+ * Implements Appendix A state machine from the cross-language spec.
591
+ */
592
+ parseSubstBody(startLine, startCol) {
593
+ let optional = false;
594
+ if (this.peek() === "?") {
595
+ this.advance();
596
+ optional = true;
597
+ }
598
+ let curText = "";
599
+ let curStarted = false;
600
+ let curLine = 0;
601
+ let curCol = 0;
602
+ let pendingWs = "";
603
+ const segments = [];
604
+ let lastDot = null;
605
+ while (true) {
606
+ if (this.pos >= this.input.length) {
607
+ throw new ParseError("unterminated substitution", startLine, startCol);
608
+ }
609
+ const ch = this.peek();
610
+ if (ch === "}") {
611
+ this.advance();
612
+ pendingWs = "";
613
+ break;
614
+ } else if (ch === '"') {
615
+ const qLine = startLine;
616
+ const qCol = this.col;
617
+ if (curStarted) {
618
+ curText += pendingWs;
619
+ }
620
+ pendingWs = "";
621
+ this.advance();
622
+ const decoded = this.readQuotedStringBody(qLine, qCol);
623
+ curText += decoded;
624
+ if (!curStarted) {
625
+ curLine = qLine;
626
+ curCol = qCol;
627
+ curStarted = true;
628
+ }
629
+ } else if (isUnquotedSubstChar(ch)) {
630
+ if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
631
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
632
+ throw new ParseError(
633
+ `unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
634
+ startLine,
635
+ this.col
636
+ );
637
+ }
638
+ const uCol = this.col;
639
+ if (curStarted) {
640
+ curText += pendingWs;
641
+ }
642
+ pendingWs = "";
643
+ if (!curStarted) {
644
+ curLine = startLine;
645
+ curCol = uCol;
646
+ curStarted = true;
647
+ }
648
+ while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
649
+ curText += this.advance();
650
+ }
651
+ } else if (ch === ".") {
652
+ const dotCol = this.col;
653
+ pendingWs = "";
654
+ if (!curStarted) {
655
+ throw new ParseError("empty segment in path", startLine, dotCol);
656
+ }
657
+ segments.push({ text: curText, line: curLine, col: curCol });
658
+ curText = "";
659
+ curStarted = false;
660
+ curLine = 0;
661
+ curCol = 0;
662
+ lastDot = [startLine, dotCol];
663
+ this.advance();
664
+ } else if (isHoconNewline(ch)) {
665
+ throw new ParseError("unterminated substitution", startLine, startCol);
666
+ } else if (isHoconWhitespace(ch)) {
667
+ pendingWs += ch;
668
+ this.advance();
669
+ } else {
670
+ throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
671
+ }
672
+ }
673
+ if (curStarted) {
674
+ segments.push({ text: curText, line: curLine, col: curCol });
675
+ } else if (segments.length === 0) {
676
+ throw new ParseError("empty substitution path", startLine, startCol);
677
+ } else {
678
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
679
+ throw new ParseError("empty segment in path", errLine, errCol);
680
+ }
681
+ return { segments, optional };
682
+ }
525
683
  };
526
684
  function tokenize(input) {
527
685
  return new Lexer(input).tokenize();
528
686
  }
687
+ function isUnquotedSubstChar(ch) {
688
+ if (ch === "" || isHoconWhitespace(ch)) return false;
689
+ if ('"\\'.includes(ch)) return false;
690
+ if ("{}[]".includes(ch)) return false;
691
+ if (":=,+#`^?!@*&$.".includes(ch)) return false;
692
+ return true;
693
+ }
694
+ function isDecimalDigit(ch) {
695
+ return ch >= "0" && ch <= "9";
696
+ }
529
697
  function isUnquotedStart(ch) {
530
- return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
698
+ if (ch === "" || isHoconWhitespace(ch)) return false;
699
+ if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
700
+ return true;
531
701
  }
532
702
  function isUnquotedContinue(ch, nextFn) {
533
- if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
703
+ if (ch === "" || isHoconWhitespace(ch)) return false;
704
+ if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
534
705
  if (ch === "+" && nextFn() === "=") return false;
535
706
  if (ch === "/" && nextFn() === "/") return false;
536
707
  return true;
537
708
  }
538
709
 
539
710
  // src/internal/parser/parser.ts
540
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
711
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
541
712
  var Parser = class {
542
713
  constructor(tokens) {
543
714
  this.tokens = tokens;
@@ -641,10 +812,21 @@ var Parser = class {
641
812
  trailingDot = false;
642
813
  } else if (t.kind === "unquoted") {
643
814
  this.advance();
644
- const parts = t.value.split(".");
815
+ const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
816
+ const parts = raw.split(".");
645
817
  const filtered = parts.filter((s) => s.length > 0);
818
+ for (const seg of filtered) {
819
+ if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
820
+ const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
821
+ throw new ParseError(
822
+ `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
823
+ t.line,
824
+ t.col
825
+ );
826
+ }
827
+ }
646
828
  segments.push(...filtered);
647
- trailingDot = t.value.endsWith(".");
829
+ trailingDot = raw.endsWith(".");
648
830
  } else {
649
831
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
650
832
  break;
@@ -656,6 +838,10 @@ var Parser = class {
656
838
  trailingDot = true;
657
839
  continue;
658
840
  }
841
+ if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
842
+ trailingDot = true;
843
+ continue;
844
+ }
659
845
  break;
660
846
  }
661
847
  return segments;
@@ -731,9 +917,13 @@ var Parser = class {
731
917
  } else if (t.kind === "lbracket") {
732
918
  this.advance();
733
919
  node = this.parseArray();
734
- } else if (t.kind === "subst" || t.kind === "opt_subst") {
920
+ } else if (t.kind === "subst") {
735
921
  this.advance();
736
- node = { kind: "subst", path: t.value, optional: t.kind === "opt_subst", pos: { line: t.line, col: t.col } };
922
+ if (!t.subst) {
923
+ throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
924
+ }
925
+ const payload = t.subst;
926
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
737
927
  } else if (t.kind === "string" || t.kind === "triple_string") {
738
928
  this.advance();
739
929
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -803,64 +993,20 @@ function makeResObj() {
803
993
  }
804
994
 
805
995
  // src/internal/resolver/utils.ts
806
- function parseSubstPath(raw) {
807
- const segments = [];
808
- let i = 0;
809
- while (i < raw.length) {
810
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
811
- if (i >= raw.length) break;
812
- if (raw[i] === '"') {
813
- i++;
814
- let seg = "";
815
- while (i < raw.length && raw[i] !== '"') {
816
- if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
817
- seg += raw[i + 1];
818
- i += 2;
819
- } else {
820
- seg += raw[i];
821
- i++;
822
- }
823
- }
824
- if (i < raw.length) i++;
825
- segments.push(seg);
826
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
827
- if (i < raw.length && raw[i] === ".") i++;
828
- } else if (raw[i] === ".") {
829
- segments.push("");
830
- i++;
831
- } else {
832
- let seg = "";
833
- while (i < raw.length && raw[i] !== ".") {
834
- seg += raw[i];
835
- i++;
836
- }
837
- segments.push(seg.trim());
838
- if (i < raw.length && raw[i] === ".") i++;
839
- }
840
- }
841
- return segments;
842
- }
843
996
  function segmentsToKey(segments) {
844
997
  return segments.map((s) => {
845
- if (s === "" || /[^a-zA-Z0-9\-_]/.test(s)) {
846
- return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
998
+ const t = s.text;
999
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
1000
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
847
1001
  }
848
- return s;
1002
+ return t;
849
1003
  }).join(".");
850
1004
  }
851
1005
  function lookupPath(root, segments) {
852
1006
  const [head, ...tail] = segments;
853
- if (head === void 0 || head === "") {
854
- if (segments.length > 0) {
855
- const val2 = root.fields.get("");
856
- if (val2 === void 0) return void 0;
857
- if (tail.length === 0) return val2;
858
- if (isResObj(val2)) return lookupPath(val2, tail);
859
- return void 0;
860
- }
861
- return void 0;
862
- }
863
- const val = root.fields.get(head);
1007
+ if (head === void 0) return void 0;
1008
+ const key = head.text;
1009
+ const val = root.fields.get(key);
864
1010
  if (val === void 0) return void 0;
865
1011
  if (tail.length === 0) return val;
866
1012
  if (isResObj(val)) return lookupPath(val, tail);
@@ -869,7 +1015,7 @@ function lookupPath(root, segments) {
869
1015
  function lookupResObj(root, segments) {
870
1016
  let cur = root;
871
1017
  for (const seg of segments) {
872
- const val = cur.fields.get(seg);
1018
+ const val = cur.fields.get(seg.text);
873
1019
  if (val === void 0 || !isResObj(val)) return void 0;
874
1020
  cur = val;
875
1021
  }
@@ -1146,7 +1292,7 @@ var StructureBuilder = class _StructureBuilder {
1146
1292
  return;
1147
1293
  }
1148
1294
  const [head, ...tail] = field.key;
1149
- if (!head) return;
1295
+ if (head === void 0) return;
1150
1296
  if (tail.length > 0) {
1151
1297
  const syntheticAst = {
1152
1298
  kind: "object",
@@ -1188,7 +1334,7 @@ var StructureBuilder = class _StructureBuilder {
1188
1334
  return;
1189
1335
  }
1190
1336
  const [head, ...tail] = field.key;
1191
- if (!head) return;
1337
+ if (head === void 0) return;
1192
1338
  if (tail.length > 0) {
1193
1339
  const syntheticAst = {
1194
1340
  kind: "object",
@@ -1234,7 +1380,7 @@ var StructureBuilder = class _StructureBuilder {
1234
1380
  return inner;
1235
1381
  }
1236
1382
  case "subst":
1237
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1383
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1238
1384
  case "concat":
1239
1385
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1240
1386
  case "include":
@@ -1258,7 +1404,7 @@ var StructureBuilder = class _StructureBuilder {
1258
1404
  case "object":
1259
1405
  return await this.buildAsync(ast, pathPrefix);
1260
1406
  case "subst":
1261
- return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1407
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1262
1408
  case "concat": {
1263
1409
  const nodes = [];
1264
1410
  for (const n of ast.nodes) {
@@ -1273,7 +1419,8 @@ var StructureBuilder = class _StructureBuilder {
1273
1419
  // ---- Relativize substitution paths for nested includes ----
1274
1420
  relativizeSubstPaths(val, prefixSegments) {
1275
1421
  if (isSubst(val)) {
1276
- val.segments = [...prefixSegments, ...val.segments];
1422
+ const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
1423
+ val.segments = [...prefixAsSegments, ...val.segments];
1277
1424
  val.prefixLen += prefixSegments.length;
1278
1425
  return;
1279
1426
  }
@@ -1369,7 +1516,7 @@ var SubstitutionResolver = class {
1369
1516
  return hv;
1370
1517
  }
1371
1518
  segmentsEqual(a, b) {
1372
- return a.length === b.length && a.every((seg, i) => seg === b[i]);
1519
+ return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1373
1520
  }
1374
1521
  resolveSubst(s, scope) {
1375
1522
  const key = segmentsToKey(s.segments);
@@ -1377,14 +1524,14 @@ var SubstitutionResolver = class {
1377
1524
  if (this.resolving.has(key)) {
1378
1525
  let prior;
1379
1526
  if (s.prefixLen > 0) {
1380
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1527
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1381
1528
  const parentScope = lookupResObj(
1382
1529
  this.root,
1383
1530
  s.segments.slice(0, s.segments.length - 1)
1384
1531
  );
1385
1532
  prior = parentScope?.priorValues.get(leafSeg);
1386
1533
  } else {
1387
- const rootSeg = s.segments[0] ?? "";
1534
+ const rootSeg = s.segments[0]?.text ?? "";
1388
1535
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1389
1536
  }
1390
1537
  if (prior !== void 0) {
@@ -1415,14 +1562,14 @@ var SubstitutionResolver = class {
1415
1562
  if (isSelfRef) {
1416
1563
  let prior;
1417
1564
  if (s.prefixLen > 0) {
1418
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1565
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1419
1566
  const parentScope = lookupResObj(
1420
1567
  this.root,
1421
1568
  s.segments.slice(0, s.segments.length - 1)
1422
1569
  );
1423
1570
  prior = parentScope?.priorValues.get(leafSeg);
1424
1571
  } else {
1425
- const rootSeg = s.segments[0] ?? "";
1572
+ const rootSeg = s.segments[0]?.text ?? "";
1426
1573
  prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1427
1574
  }
1428
1575
  if (prior !== void 0) {
@@ -1435,7 +1582,7 @@ var SubstitutionResolver = class {
1435
1582
  let result = this.resolveVal(found, scope);
1436
1583
  const effectiveLen = s.segments.length - s.prefixLen;
1437
1584
  if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1438
- const leafSeg = s.segments[s.segments.length - 1] ?? "";
1585
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1439
1586
  let prior;
1440
1587
  if (s.prefixLen > 0) {
1441
1588
  const parentScope = lookupResObj(
@@ -1459,8 +1606,8 @@ var SubstitutionResolver = class {
1459
1606
  if (result !== void 0) this.cache.set(key, result);
1460
1607
  return result;
1461
1608
  }
1462
- const envKey = s.segments.join(".");
1463
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).join(".")] : void 0);
1609
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1610
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1464
1611
  if (envVal !== void 0) {
1465
1612
  const result = { kind: "scalar", raw: envVal, valueType: "string" };
1466
1613
  this.cache.set(key, result);
@@ -1482,32 +1629,48 @@ var SubstitutionResolver = class {
1482
1629
  if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1483
1630
  if (resolved.length === 1) return resolved[0];
1484
1631
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1485
- if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1486
- const merged = /* @__PURE__ */ new Map();
1487
- for (const v of nonSep) {
1488
- if (v.kind === "object") {
1489
- for (const [k, val] of v.fields) {
1490
- const existing = merged.get(k);
1491
- if (existing?.kind === "object" && val.kind === "object") {
1492
- merged.set(k, deepMergeHoconValues(existing, val));
1493
- } else {
1494
- merged.set(k, val);
1495
- }
1496
- }
1632
+ if (nonSep.length === 0) {
1633
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1634
+ return { kind: "scalar", raw: str, valueType: "string" };
1635
+ }
1636
+ const joinPair = (left, right) => {
1637
+ if (left.kind === "object" && right.kind === "object") {
1638
+ return deepMergeHoconValues(left, right);
1639
+ }
1640
+ if (left.kind === "array" && right.kind === "object") {
1641
+ const converted = numericObjectToArray(right);
1642
+ if (converted !== null) {
1643
+ return { kind: "array", items: [...left.items, ...converted] };
1497
1644
  }
1645
+ return { kind: "array", items: [...left.items, right] };
1498
1646
  }
1499
- return { kind: "object", fields: merged };
1500
- }
1501
- if (resolved.some((v) => v.kind === "array")) {
1502
- const items = [];
1503
- for (const v of resolved) {
1504
- if (v.kind === "array") items.push(...v.items);
1505
- else items.push(v);
1647
+ if (left.kind === "object" && right.kind === "array") {
1648
+ const converted = numericObjectToArray(left);
1649
+ if (converted !== null) {
1650
+ return { kind: "array", items: [...converted, ...right.items] };
1651
+ }
1652
+ return { kind: "array", items: [left, ...right.items] };
1653
+ }
1654
+ if (left.kind === "array" && right.kind === "array") {
1655
+ return { kind: "array", items: [...left.items, ...right.items] };
1506
1656
  }
1507
- return { kind: "array", items };
1657
+ if (left.kind === "array") {
1658
+ return { kind: "array", items: [...left.items, right] };
1659
+ }
1660
+ if (right.kind === "array") {
1661
+ return { kind: "array", items: [left, ...right.items] };
1662
+ }
1663
+ const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1664
+ const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1665
+ return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
1666
+ };
1667
+ const [head, ...tail] = nonSep;
1668
+ const folded = tail.reduce(joinPair, head);
1669
+ if (folded.kind === "scalar") {
1670
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1671
+ return { kind: "scalar", raw: str, valueType: "string" };
1508
1672
  }
1509
- const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1510
- return { kind: "scalar", raw: str, valueType: "string" };
1673
+ return folded;
1511
1674
  }
1512
1675
  resolveAppend(a, scope) {
1513
1676
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };