@o3co/ts.hocon 1.5.2 → 1.6.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
@@ -299,7 +299,7 @@ function stringSegmentsToKey(segments) {
299
299
  }).join(".");
300
300
  }
301
301
  function containsSelfRef(v, fullKey) {
302
- if (isSubst(v)) return substFullKey(v) === fullKey;
302
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
303
303
  if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
304
304
  if (isAppend(v)) {
305
305
  return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
@@ -366,15 +366,75 @@ function foldSelfRef(v, fullKey, replacement) {
366
366
  }
367
367
  function foldOrSkipPrior(prior, fullKey, old) {
368
368
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
369
- if (old === void 0) return void 0;
369
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
370
370
  return foldSelfRef(prior, fullKey, old);
371
371
  }
372
+ function foldOptionalSelfRefAbsent(v, fullKey) {
373
+ if (isSubst(v) && substFullKey(v) === fullKey) {
374
+ if (!v.optional) return void 0;
375
+ return {
376
+ _kind: "subst-placeholder",
377
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
378
+ optional: v.optional,
379
+ knownAbsent: true,
380
+ listSuffix: v.listSuffix,
381
+ line: v.line,
382
+ col: v.col,
383
+ prefixLen: v.prefixLen
384
+ };
385
+ }
386
+ if (isConcat(v)) {
387
+ const nodes = [];
388
+ for (const node of v.nodes) {
389
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
390
+ if (folded === void 0) return void 0;
391
+ nodes.push(folded);
392
+ }
393
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
394
+ }
395
+ if (isAppend(v)) {
396
+ const existing = foldOptionalSelfRefAbsent(v.existing, fullKey);
397
+ const elem = foldOptionalSelfRefAbsent(v.elem, fullKey);
398
+ if (existing === void 0 || elem === void 0) return void 0;
399
+ return { _kind: "append-placeholder", existing, elem };
400
+ }
401
+ if (isResObj(v)) {
402
+ const fields = /* @__PURE__ */ new Map();
403
+ for (const [key, value] of v.fields) {
404
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
405
+ if (folded === void 0) return void 0;
406
+ fields.set(key, folded);
407
+ }
408
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues) };
409
+ }
410
+ const hv = v;
411
+ if (hv.kind === "array") {
412
+ const items = [];
413
+ for (const item of hv.items) {
414
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
415
+ if (folded === void 0) return void 0;
416
+ items.push(folded);
417
+ }
418
+ return { kind: "array", items };
419
+ }
420
+ if (hv.kind === "object") {
421
+ const fields = /* @__PURE__ */ new Map();
422
+ for (const [key, value] of hv.fields) {
423
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
424
+ if (folded === void 0) return void 0;
425
+ fields.set(key, folded);
426
+ }
427
+ return { kind: "object", fields };
428
+ }
429
+ return cloneResolverValue(v);
430
+ }
372
431
  function cloneResolverValue(v) {
373
432
  if (isSubst(v)) {
374
433
  return {
375
434
  _kind: "subst-placeholder",
376
435
  segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
377
436
  optional: v.optional,
437
+ knownAbsent: v.knownAbsent,
378
438
  listSuffix: v.listSuffix,
379
439
  line: v.line,
380
440
  col: v.col,
@@ -439,7 +499,7 @@ function foldNestedSelfRefs(v, pathPrefix) {
439
499
  return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
440
500
  }
441
501
  function containsSubstByPath(v, target) {
442
- if (isSubst(v)) return segmentsTextEqual(v.segments, target);
502
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
443
503
  if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
444
504
  if (isAppend(v)) {
445
505
  return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
@@ -606,6 +666,11 @@ var Lexer = class {
606
666
  this.line = 1;
607
667
  this.col = 1;
608
668
  this.hadSpace = false;
669
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
670
+ // token push. Used so the parser can preserve path-expression whitespace
671
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
672
+ // Comment text is NOT accumulated; only the actual WS chars.
673
+ this.whitespaceBuffer = "";
609
674
  this.tokens = [];
610
675
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
611
676
  }
@@ -621,6 +686,7 @@ var Lexer = class {
621
686
  continue;
622
687
  }
623
688
  if (isHoconWhitespace(ch)) {
689
+ this.whitespaceBuffer += ch;
624
690
  this.advance();
625
691
  this.hadSpace = true;
626
692
  continue;
@@ -712,7 +778,7 @@ var Lexer = class {
712
778
  }
713
779
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
714
780
  }
715
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
781
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
716
782
  return this.tokens;
717
783
  }
718
784
  peek(offset = 0) {
@@ -729,12 +795,14 @@ var Lexer = class {
729
795
  return ch;
730
796
  }
731
797
  push(kind, value, l, c, isQuoted = false) {
732
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
798
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
733
799
  this.hadSpace = false;
800
+ this.whitespaceBuffer = "";
734
801
  }
735
802
  pushSubst(payload, value, l, c) {
736
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
803
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
737
804
  this.hadSpace = false;
805
+ this.whitespaceBuffer = "";
738
806
  }
739
807
  /**
740
808
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -980,7 +1048,7 @@ function isUnquotedContinue(ch, nextFn) {
980
1048
  }
981
1049
 
982
1050
  // src/internal/parser/parser.ts
983
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1051
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
984
1052
  var Parser = class {
985
1053
  constructor(tokens) {
986
1054
  this.tokens = tokens;
@@ -1093,13 +1161,17 @@ var Parser = class {
1093
1161
  let trailingDot = false;
1094
1162
  let firstWasQuoted = false;
1095
1163
  let spaceConcat = false;
1164
+ let postDotPrefix = "";
1096
1165
  while (true) {
1097
1166
  const t = this.peek();
1098
1167
  if (t.kind === "string") {
1099
1168
  this.advance();
1100
1169
  if (segments.length === 0) firstWasQuoted = true;
1101
1170
  if (spaceConcat) {
1102
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1171
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1172
+ } else if (postDotPrefix !== "") {
1173
+ segments.push(`${postDotPrefix}${t.value}`);
1174
+ postDotPrefix = "";
1103
1175
  } else {
1104
1176
  segments.push(t.value);
1105
1177
  }
@@ -1109,24 +1181,23 @@ var Parser = class {
1109
1181
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
1110
1182
  const parts = raw.split(".");
1111
1183
  const filtered = parts.filter((s) => s.length > 0);
1112
- for (const seg of filtered) {
1113
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
1114
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
1115
- throw new ParseError(
1116
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
1117
- t.line,
1118
- t.col
1119
- );
1120
- }
1121
- }
1122
- if (spaceConcat && filtered.length > 0) {
1184
+ if (spaceConcat) {
1185
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
1123
1186
  if (raw.startsWith(".")) {
1124
1187
  segments.push(...filtered);
1125
- } else {
1188
+ } else if (filtered.length > 0) {
1126
1189
  const [head, ...tail] = filtered;
1127
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1190
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
1128
1191
  segments.push(...tail);
1129
1192
  }
1193
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1194
+ segments.push(postDotPrefix);
1195
+ postDotPrefix = "";
1196
+ segments.push(...filtered);
1197
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1198
+ const [head, ...tail] = filtered;
1199
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1200
+ postDotPrefix = "";
1130
1201
  } else {
1131
1202
  segments.push(...filtered);
1132
1203
  }
@@ -1136,11 +1207,23 @@ var Parser = class {
1136
1207
  break;
1137
1208
  }
1138
1209
  spaceConcat = false;
1139
- if (trailingDot) continue;
1210
+ if (trailingDot) {
1211
+ const next2 = this.peek();
1212
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1213
+ postDotPrefix = next2.precedingWhitespace;
1214
+ } else {
1215
+ postDotPrefix = "";
1216
+ }
1217
+ continue;
1218
+ }
1140
1219
  const next = this.peek();
1141
1220
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
1142
1221
  this.advance();
1143
1222
  trailingDot = true;
1223
+ const afterDot = this.peek();
1224
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1225
+ postDotPrefix = afterDot.precedingWhitespace;
1226
+ }
1144
1227
  continue;
1145
1228
  }
1146
1229
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1153,6 +1236,14 @@ var Parser = class {
1153
1236
  }
1154
1237
  break;
1155
1238
  }
1239
+ if (trailingDot) {
1240
+ const here = this.peek();
1241
+ throw new ParseError(
1242
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1243
+ here.line,
1244
+ here.col
1245
+ );
1246
+ }
1156
1247
  return { segments, firstWasQuoted };
1157
1248
  }
1158
1249
  parseInclude() {
@@ -1325,7 +1416,8 @@ var Parser = class {
1325
1416
  break;
1326
1417
  }
1327
1418
  if (hadSpace) {
1328
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1419
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1420
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1329
1421
  }
1330
1422
  parts.push(node);
1331
1423
  }
@@ -1786,9 +1878,9 @@ var StructureBuilder = class _StructureBuilder {
1786
1878
  const existing = obj.fields.get(head);
1787
1879
  const newVal = this.astToResolverValue(field.value, childPrefix);
1788
1880
  if (existing !== void 0) {
1789
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1790
1881
  const oldPrior = obj.priorValues.get(head);
1791
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1882
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1883
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1792
1884
  if (prior !== void 0) obj.priorValues.set(head, prior);
1793
1885
  }
1794
1886
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1840,9 +1932,9 @@ var StructureBuilder = class _StructureBuilder {
1840
1932
  const existing = obj.fields.get(head);
1841
1933
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1842
1934
  if (existing !== void 0) {
1843
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1844
1935
  const oldPrior = obj.priorValues.get(head);
1845
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1936
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1937
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1846
1938
  if (prior !== void 0) obj.priorValues.set(head, prior);
1847
1939
  }
1848
1940
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1865,7 +1957,7 @@ var StructureBuilder = class _StructureBuilder {
1865
1957
  return inner;
1866
1958
  }
1867
1959
  case "subst":
1868
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1960
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, knownAbsent: false, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1869
1961
  case "concat":
1870
1962
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1871
1963
  case "include":
@@ -1889,7 +1981,7 @@ var StructureBuilder = class _StructureBuilder {
1889
1981
  case "object":
1890
1982
  return await this.buildAsync(ast, pathPrefix);
1891
1983
  case "subst":
1892
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1984
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, knownAbsent: false, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1893
1985
  case "concat": {
1894
1986
  const nodes = [];
1895
1987
  for (const n of ast.nodes) {
@@ -2007,6 +2099,8 @@ var SubstitutionResolver = class {
2007
2099
  const result = /* @__PURE__ */ new Map();
2008
2100
  for (const [key, val] of obj.fields) {
2009
2101
  this.resolvingFieldPath.push(key);
2102
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2103
+ this.cache.delete(fullCacheKey);
2010
2104
  let resolved;
2011
2105
  try {
2012
2106
  resolved = this.resolveVal(val, obj);
@@ -2019,28 +2113,42 @@ var SubstitutionResolver = class {
2019
2113
  if (prior !== void 0) {
2020
2114
  const priorResolved = this.resolveVal(prior, obj);
2021
2115
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
2022
- result.set(
2023
- key,
2024
- deepMergeHoconValues(
2025
- priorResolved,
2026
- resolved
2027
- )
2116
+ const finalValue = deepMergeHoconValues(
2117
+ priorResolved,
2118
+ resolved
2028
2119
  );
2120
+ result.set(key, finalValue);
2121
+ this.cache.set(fullCacheKey, finalValue);
2122
+ this.cacheDescendants(fullCacheKey, finalValue);
2029
2123
  continue;
2030
2124
  }
2031
2125
  }
2032
2126
  }
2033
2127
  result.set(key, resolved);
2128
+ this.cache.set(fullCacheKey, resolved);
2129
+ this.cacheDescendants(fullCacheKey, resolved);
2034
2130
  } else {
2035
2131
  const prior = obj.priorValues.get(key);
2036
2132
  if (prior !== void 0) {
2037
2133
  const priorResolved = this.resolveVal(prior, obj);
2038
- if (priorResolved !== void 0) result.set(key, priorResolved);
2134
+ if (priorResolved !== void 0) {
2135
+ result.set(key, priorResolved);
2136
+ this.cache.set(fullCacheKey, priorResolved);
2137
+ this.cacheDescendants(fullCacheKey, priorResolved);
2138
+ }
2039
2139
  }
2040
2140
  }
2041
2141
  }
2042
2142
  return { kind: "object", fields: result };
2043
2143
  }
2144
+ cacheDescendants(prefix, value) {
2145
+ if (value.kind !== "object") return;
2146
+ for (const [key, child] of value.fields) {
2147
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2148
+ this.cache.set(childKey, child);
2149
+ this.cacheDescendants(childKey, child);
2150
+ }
2151
+ }
2044
2152
  resolveVal(v, scope) {
2045
2153
  if (isSubst(v)) return this.resolveSubst(v, scope);
2046
2154
  if (isConcat(v)) {
@@ -2066,6 +2174,7 @@ var SubstitutionResolver = class {
2066
2174
  return hv;
2067
2175
  }
2068
2176
  resolveSubst(s, scope) {
2177
+ if (s.knownAbsent) return void 0;
2069
2178
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2070
2179
  if (this.cache.has(key)) return this.cache.get(key);
2071
2180
  if (this.resolving.has(key)) {
@@ -2158,6 +2267,28 @@ var SubstitutionResolver = class {
2158
2267
  if (result !== void 0) this.cache.set(key, result);
2159
2268
  return result;
2160
2269
  }
2270
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2271
+ const originalSegments = s.segments.slice(s.prefixLen);
2272
+ const fallbackFound = lookupPath(this.root, originalSegments);
2273
+ if (fallbackFound !== void 0) {
2274
+ let result = this.resolveVal(fallbackFound, scope);
2275
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2276
+ const rootSeg = originalSegments[0]?.text ?? "";
2277
+ const prior = this.root.priorValues.get(rootSeg);
2278
+ if (prior !== void 0) {
2279
+ const priorResolved = this.resolveVal(prior, scope);
2280
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2281
+ result = deepMergeHoconValues(
2282
+ priorResolved,
2283
+ result
2284
+ );
2285
+ }
2286
+ }
2287
+ }
2288
+ if (result !== void 0) this.cache.set(key, result);
2289
+ return result;
2290
+ }
2291
+ }
2161
2292
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
2162
2293
  const result = this.resolveEnvList(s);
2163
2294
  if (result !== void 0) {