@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/zod.js CHANGED
@@ -303,7 +303,7 @@ function stringSegmentsToKey(segments) {
303
303
  }).join(".");
304
304
  }
305
305
  function containsSelfRef(v, fullKey) {
306
- if (isSubst(v)) return substFullKey(v) === fullKey;
306
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
307
307
  if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
308
308
  if (isAppend(v)) {
309
309
  return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
@@ -370,15 +370,75 @@ function foldSelfRef(v, fullKey, replacement) {
370
370
  }
371
371
  function foldOrSkipPrior(prior, fullKey, old) {
372
372
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
373
- if (old === void 0) return void 0;
373
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
374
374
  return foldSelfRef(prior, fullKey, old);
375
375
  }
376
+ function foldOptionalSelfRefAbsent(v, fullKey) {
377
+ if (isSubst(v) && substFullKey(v) === fullKey) {
378
+ if (!v.optional) return void 0;
379
+ return {
380
+ _kind: "subst-placeholder",
381
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
382
+ optional: v.optional,
383
+ knownAbsent: true,
384
+ listSuffix: v.listSuffix,
385
+ line: v.line,
386
+ col: v.col,
387
+ prefixLen: v.prefixLen
388
+ };
389
+ }
390
+ if (isConcat(v)) {
391
+ const nodes = [];
392
+ for (const node of v.nodes) {
393
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
394
+ if (folded === void 0) return void 0;
395
+ nodes.push(folded);
396
+ }
397
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
398
+ }
399
+ if (isAppend(v)) {
400
+ const existing = foldOptionalSelfRefAbsent(v.existing, fullKey);
401
+ const elem = foldOptionalSelfRefAbsent(v.elem, fullKey);
402
+ if (existing === void 0 || elem === void 0) return void 0;
403
+ return { _kind: "append-placeholder", existing, elem };
404
+ }
405
+ if (isResObj(v)) {
406
+ const fields = /* @__PURE__ */ new Map();
407
+ for (const [key, value] of v.fields) {
408
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
409
+ if (folded === void 0) return void 0;
410
+ fields.set(key, folded);
411
+ }
412
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues) };
413
+ }
414
+ const hv = v;
415
+ if (hv.kind === "array") {
416
+ const items = [];
417
+ for (const item of hv.items) {
418
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
419
+ if (folded === void 0) return void 0;
420
+ items.push(folded);
421
+ }
422
+ return { kind: "array", items };
423
+ }
424
+ if (hv.kind === "object") {
425
+ const fields = /* @__PURE__ */ new Map();
426
+ for (const [key, value] of hv.fields) {
427
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
428
+ if (folded === void 0) return void 0;
429
+ fields.set(key, folded);
430
+ }
431
+ return { kind: "object", fields };
432
+ }
433
+ return cloneResolverValue(v);
434
+ }
376
435
  function cloneResolverValue(v) {
377
436
  if (isSubst(v)) {
378
437
  return {
379
438
  _kind: "subst-placeholder",
380
439
  segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
381
440
  optional: v.optional,
441
+ knownAbsent: v.knownAbsent,
382
442
  listSuffix: v.listSuffix,
383
443
  line: v.line,
384
444
  col: v.col,
@@ -443,7 +503,7 @@ function foldNestedSelfRefs(v, pathPrefix) {
443
503
  return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
444
504
  }
445
505
  function containsSubstByPath(v, target) {
446
- if (isSubst(v)) return segmentsTextEqual(v.segments, target);
506
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
447
507
  if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
448
508
  if (isAppend(v)) {
449
509
  return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
@@ -610,6 +670,11 @@ var Lexer = class {
610
670
  this.line = 1;
611
671
  this.col = 1;
612
672
  this.hadSpace = false;
673
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
674
+ // token push. Used so the parser can preserve path-expression whitespace
675
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
676
+ // Comment text is NOT accumulated; only the actual WS chars.
677
+ this.whitespaceBuffer = "";
613
678
  this.tokens = [];
614
679
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
615
680
  }
@@ -625,6 +690,7 @@ var Lexer = class {
625
690
  continue;
626
691
  }
627
692
  if (isHoconWhitespace(ch)) {
693
+ this.whitespaceBuffer += ch;
628
694
  this.advance();
629
695
  this.hadSpace = true;
630
696
  continue;
@@ -716,7 +782,7 @@ var Lexer = class {
716
782
  }
717
783
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
718
784
  }
719
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
785
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
720
786
  return this.tokens;
721
787
  }
722
788
  peek(offset = 0) {
@@ -733,12 +799,14 @@ var Lexer = class {
733
799
  return ch;
734
800
  }
735
801
  push(kind, value, l, c, isQuoted = false) {
736
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
802
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
737
803
  this.hadSpace = false;
804
+ this.whitespaceBuffer = "";
738
805
  }
739
806
  pushSubst(payload, value, l, c) {
740
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
807
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
741
808
  this.hadSpace = false;
809
+ this.whitespaceBuffer = "";
742
810
  }
743
811
  /**
744
812
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -984,7 +1052,7 @@ function isUnquotedContinue(ch, nextFn) {
984
1052
  }
985
1053
 
986
1054
  // src/internal/parser/parser.ts
987
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1055
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
988
1056
  var Parser = class {
989
1057
  constructor(tokens) {
990
1058
  this.tokens = tokens;
@@ -1097,13 +1165,17 @@ var Parser = class {
1097
1165
  let trailingDot = false;
1098
1166
  let firstWasQuoted = false;
1099
1167
  let spaceConcat = false;
1168
+ let postDotPrefix = "";
1100
1169
  while (true) {
1101
1170
  const t = this.peek();
1102
1171
  if (t.kind === "string") {
1103
1172
  this.advance();
1104
1173
  if (segments.length === 0) firstWasQuoted = true;
1105
1174
  if (spaceConcat) {
1106
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1175
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1176
+ } else if (postDotPrefix !== "") {
1177
+ segments.push(`${postDotPrefix}${t.value}`);
1178
+ postDotPrefix = "";
1107
1179
  } else {
1108
1180
  segments.push(t.value);
1109
1181
  }
@@ -1113,24 +1185,23 @@ var Parser = class {
1113
1185
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
1114
1186
  const parts = raw.split(".");
1115
1187
  const filtered = parts.filter((s) => s.length > 0);
1116
- for (const seg of filtered) {
1117
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
1118
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
1119
- throw new ParseError(
1120
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
1121
- t.line,
1122
- t.col
1123
- );
1124
- }
1125
- }
1126
- if (spaceConcat && filtered.length > 0) {
1188
+ if (spaceConcat) {
1189
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
1127
1190
  if (raw.startsWith(".")) {
1128
1191
  segments.push(...filtered);
1129
- } else {
1192
+ } else if (filtered.length > 0) {
1130
1193
  const [head, ...tail] = filtered;
1131
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1194
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
1132
1195
  segments.push(...tail);
1133
1196
  }
1197
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1198
+ segments.push(postDotPrefix);
1199
+ postDotPrefix = "";
1200
+ segments.push(...filtered);
1201
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1202
+ const [head, ...tail] = filtered;
1203
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1204
+ postDotPrefix = "";
1134
1205
  } else {
1135
1206
  segments.push(...filtered);
1136
1207
  }
@@ -1140,11 +1211,23 @@ var Parser = class {
1140
1211
  break;
1141
1212
  }
1142
1213
  spaceConcat = false;
1143
- if (trailingDot) continue;
1214
+ if (trailingDot) {
1215
+ const next2 = this.peek();
1216
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1217
+ postDotPrefix = next2.precedingWhitespace;
1218
+ } else {
1219
+ postDotPrefix = "";
1220
+ }
1221
+ continue;
1222
+ }
1144
1223
  const next = this.peek();
1145
1224
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
1146
1225
  this.advance();
1147
1226
  trailingDot = true;
1227
+ const afterDot = this.peek();
1228
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1229
+ postDotPrefix = afterDot.precedingWhitespace;
1230
+ }
1148
1231
  continue;
1149
1232
  }
1150
1233
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1157,6 +1240,14 @@ var Parser = class {
1157
1240
  }
1158
1241
  break;
1159
1242
  }
1243
+ if (trailingDot) {
1244
+ const here = this.peek();
1245
+ throw new ParseError(
1246
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1247
+ here.line,
1248
+ here.col
1249
+ );
1250
+ }
1160
1251
  return { segments, firstWasQuoted };
1161
1252
  }
1162
1253
  parseInclude() {
@@ -1329,7 +1420,8 @@ var Parser = class {
1329
1420
  break;
1330
1421
  }
1331
1422
  if (hadSpace) {
1332
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1423
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1424
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1333
1425
  }
1334
1426
  parts.push(node);
1335
1427
  }
@@ -1790,9 +1882,9 @@ var StructureBuilder = class _StructureBuilder {
1790
1882
  const existing = obj.fields.get(head);
1791
1883
  const newVal = this.astToResolverValue(field.value, childPrefix);
1792
1884
  if (existing !== void 0) {
1793
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1794
1885
  const oldPrior = obj.priorValues.get(head);
1795
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1886
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1887
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1796
1888
  if (prior !== void 0) obj.priorValues.set(head, prior);
1797
1889
  }
1798
1890
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1844,9 +1936,9 @@ var StructureBuilder = class _StructureBuilder {
1844
1936
  const existing = obj.fields.get(head);
1845
1937
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1846
1938
  if (existing !== void 0) {
1847
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1848
1939
  const oldPrior = obj.priorValues.get(head);
1849
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1940
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1941
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1850
1942
  if (prior !== void 0) obj.priorValues.set(head, prior);
1851
1943
  }
1852
1944
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1869,7 +1961,7 @@ var StructureBuilder = class _StructureBuilder {
1869
1961
  return inner;
1870
1962
  }
1871
1963
  case "subst":
1872
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1964
+ 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 };
1873
1965
  case "concat":
1874
1966
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1875
1967
  case "include":
@@ -1893,7 +1985,7 @@ var StructureBuilder = class _StructureBuilder {
1893
1985
  case "object":
1894
1986
  return await this.buildAsync(ast, pathPrefix);
1895
1987
  case "subst":
1896
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1988
+ 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 };
1897
1989
  case "concat": {
1898
1990
  const nodes = [];
1899
1991
  for (const n of ast.nodes) {
@@ -2011,6 +2103,8 @@ var SubstitutionResolver = class {
2011
2103
  const result = /* @__PURE__ */ new Map();
2012
2104
  for (const [key, val] of obj.fields) {
2013
2105
  this.resolvingFieldPath.push(key);
2106
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2107
+ this.cache.delete(fullCacheKey);
2014
2108
  let resolved;
2015
2109
  try {
2016
2110
  resolved = this.resolveVal(val, obj);
@@ -2023,28 +2117,42 @@ var SubstitutionResolver = class {
2023
2117
  if (prior !== void 0) {
2024
2118
  const priorResolved = this.resolveVal(prior, obj);
2025
2119
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
2026
- result.set(
2027
- key,
2028
- deepMergeHoconValues(
2029
- priorResolved,
2030
- resolved
2031
- )
2120
+ const finalValue = deepMergeHoconValues(
2121
+ priorResolved,
2122
+ resolved
2032
2123
  );
2124
+ result.set(key, finalValue);
2125
+ this.cache.set(fullCacheKey, finalValue);
2126
+ this.cacheDescendants(fullCacheKey, finalValue);
2033
2127
  continue;
2034
2128
  }
2035
2129
  }
2036
2130
  }
2037
2131
  result.set(key, resolved);
2132
+ this.cache.set(fullCacheKey, resolved);
2133
+ this.cacheDescendants(fullCacheKey, resolved);
2038
2134
  } else {
2039
2135
  const prior = obj.priorValues.get(key);
2040
2136
  if (prior !== void 0) {
2041
2137
  const priorResolved = this.resolveVal(prior, obj);
2042
- if (priorResolved !== void 0) result.set(key, priorResolved);
2138
+ if (priorResolved !== void 0) {
2139
+ result.set(key, priorResolved);
2140
+ this.cache.set(fullCacheKey, priorResolved);
2141
+ this.cacheDescendants(fullCacheKey, priorResolved);
2142
+ }
2043
2143
  }
2044
2144
  }
2045
2145
  }
2046
2146
  return { kind: "object", fields: result };
2047
2147
  }
2148
+ cacheDescendants(prefix, value) {
2149
+ if (value.kind !== "object") return;
2150
+ for (const [key, child] of value.fields) {
2151
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2152
+ this.cache.set(childKey, child);
2153
+ this.cacheDescendants(childKey, child);
2154
+ }
2155
+ }
2048
2156
  resolveVal(v, scope) {
2049
2157
  if (isSubst(v)) return this.resolveSubst(v, scope);
2050
2158
  if (isConcat(v)) {
@@ -2070,6 +2178,7 @@ var SubstitutionResolver = class {
2070
2178
  return hv;
2071
2179
  }
2072
2180
  resolveSubst(s, scope) {
2181
+ if (s.knownAbsent) return void 0;
2073
2182
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2074
2183
  if (this.cache.has(key)) return this.cache.get(key);
2075
2184
  if (this.resolving.has(key)) {
@@ -2162,6 +2271,28 @@ var SubstitutionResolver = class {
2162
2271
  if (result !== void 0) this.cache.set(key, result);
2163
2272
  return result;
2164
2273
  }
2274
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2275
+ const originalSegments = s.segments.slice(s.prefixLen);
2276
+ const fallbackFound = lookupPath(this.root, originalSegments);
2277
+ if (fallbackFound !== void 0) {
2278
+ let result = this.resolveVal(fallbackFound, scope);
2279
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2280
+ const rootSeg = originalSegments[0]?.text ?? "";
2281
+ const prior = this.root.priorValues.get(rootSeg);
2282
+ if (prior !== void 0) {
2283
+ const priorResolved = this.resolveVal(prior, scope);
2284
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2285
+ result = deepMergeHoconValues(
2286
+ priorResolved,
2287
+ result
2288
+ );
2289
+ }
2290
+ }
2291
+ }
2292
+ if (result !== void 0) this.cache.set(key, result);
2293
+ return result;
2294
+ }
2295
+ }
2165
2296
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
2166
2297
  const result = this.resolveEnvList(s);
2167
2298
  if (result !== void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/ts.hocon",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Full Lightbend HOCON spec-compliant parser and config library for TypeScript",
5
5
  "keywords": [
6
6
  "hocon",