@o3co/ts.hocon 0.1.5 → 1.0.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
@@ -11,6 +11,130 @@ function coerceNumber(value) {
11
11
  if (!/^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) return void 0;
12
12
  return Number(value);
13
13
  }
14
+ var DURATION_UNITS = {
15
+ ns: 1e-6,
16
+ nanosecond: 1e-6,
17
+ nanoseconds: 1e-6,
18
+ us: 1e-3,
19
+ microsecond: 1e-3,
20
+ microseconds: 1e-3,
21
+ ms: 1,
22
+ millisecond: 1,
23
+ milliseconds: 1,
24
+ s: 1e3,
25
+ second: 1e3,
26
+ seconds: 1e3,
27
+ m: 6e4,
28
+ minute: 6e4,
29
+ minutes: 6e4,
30
+ h: 36e5,
31
+ hour: 36e5,
32
+ hours: 36e5,
33
+ d: 864e5,
34
+ day: 864e5,
35
+ days: 864e5
36
+ };
37
+ var OUTPUT_DURATION_UNITS = {
38
+ ns: 1e-6,
39
+ us: 1e-3,
40
+ ms: 1,
41
+ s: 1e3,
42
+ m: 6e4,
43
+ h: 36e5,
44
+ d: 864e5
45
+ };
46
+ function parseDuration(value, outputUnit = "ms") {
47
+ const trimmed = value.trim();
48
+ let i = 0;
49
+ while (i < trimmed.length) {
50
+ const ch = trimmed[i];
51
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
52
+ i++;
53
+ }
54
+ if (i === 0) return NaN;
55
+ const num = Number(trimmed.slice(0, i));
56
+ if (Number.isNaN(num)) return NaN;
57
+ const unit = trimmed.slice(i).trim().toLowerCase();
58
+ const mult = DURATION_UNITS[unit];
59
+ if (mult === void 0) return NaN;
60
+ const ms = num * mult;
61
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
62
+ if (divisor === void 0) return NaN;
63
+ return ms / divisor;
64
+ }
65
+ var BYTE_UNITS = {
66
+ B: 1,
67
+ byte: 1,
68
+ bytes: 1,
69
+ KB: 1e3,
70
+ kilobyte: 1e3,
71
+ kilobytes: 1e3,
72
+ KiB: 1024,
73
+ kibibyte: 1024,
74
+ kibibytes: 1024,
75
+ MB: 1e6,
76
+ megabyte: 1e6,
77
+ megabytes: 1e6,
78
+ MiB: 1048576,
79
+ mebibyte: 1048576,
80
+ mebibytes: 1048576,
81
+ GB: 1e9,
82
+ gigabyte: 1e9,
83
+ gigabytes: 1e9,
84
+ GiB: 1073741824,
85
+ gibibyte: 1073741824,
86
+ gibibytes: 1073741824,
87
+ TB: 1e12,
88
+ terabyte: 1e12,
89
+ terabytes: 1e12,
90
+ TiB: 1099511627776,
91
+ tebibyte: 1099511627776,
92
+ tebibytes: 1099511627776,
93
+ // lowercase short-form aliases
94
+ b: 1,
95
+ kb: 1e3,
96
+ kib: 1024,
97
+ mb: 1e6,
98
+ mib: 1048576,
99
+ gb: 1e9,
100
+ gib: 1073741824,
101
+ tb: 1e12,
102
+ tib: 1099511627776
103
+ };
104
+ var OUTPUT_BYTE_UNITS = {
105
+ B: 1,
106
+ KB: 1e3,
107
+ KiB: 1024,
108
+ MB: 1e6,
109
+ MiB: 1048576,
110
+ GB: 1e9,
111
+ GiB: 1073741824,
112
+ TB: 1e12,
113
+ TiB: 1099511627776
114
+ };
115
+ function parseBytes(value, outputUnit = "B") {
116
+ const trimmed = value.trim();
117
+ let i = 0;
118
+ while (i < trimmed.length) {
119
+ const ch = trimmed[i];
120
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
121
+ i++;
122
+ }
123
+ if (i === 0) return NaN;
124
+ const num = Number(trimmed.slice(0, i));
125
+ if (Number.isNaN(num)) return NaN;
126
+ const unit = trimmed.slice(i).trim();
127
+ let mult = BYTE_UNITS[unit];
128
+ if (mult === void 0) {
129
+ mult = BYTE_UNITS[unit.toLowerCase()];
130
+ }
131
+ if (mult === void 0) return NaN;
132
+ const bytes = num * mult;
133
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
134
+ if (divisor === void 0) return NaN;
135
+ const result = bytes / divisor;
136
+ return outputUnit === "B" ? Math.round(result) : result;
137
+ }
14
138
 
15
139
  // src/errors.ts
16
140
  var ParseError = class extends Error {
@@ -127,6 +251,7 @@ function tokenize(input) {
127
251
  advance();
128
252
  advance();
129
253
  let value = "";
254
+ let closed = false;
130
255
  while (pos < input.length) {
131
256
  if (peek() === '"') {
132
257
  let quoteCount = 0;
@@ -136,6 +261,7 @@ function tokenize(input) {
136
261
  }
137
262
  if (quoteCount >= 3) {
138
263
  for (let i = 0; i < quoteCount - 3; i++) value += '"';
264
+ closed = true;
139
265
  break;
140
266
  }
141
267
  for (let i = 0; i < quoteCount; i++) value += '"';
@@ -143,6 +269,9 @@ function tokenize(input) {
143
269
  }
144
270
  value += advance();
145
271
  }
272
+ if (!closed) {
273
+ throw new ParseError("unterminated triple-quoted string", sl, sc);
274
+ }
146
275
  if (value.startsWith("\n")) value = value.slice(1);
147
276
  push("triple_string", value, sl, sc, true);
148
277
  continue;
@@ -181,13 +310,19 @@ function tokenize(input) {
181
310
  value += "\f";
182
311
  break;
183
312
  case "u": {
313
+ if (pos + 4 > input.length) {
314
+ throw new ParseError("Invalid unicode escape: not enough characters", line, col);
315
+ }
184
316
  const hex = input.slice(pos, pos + 4);
317
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
318
+ throw new ParseError(`Invalid unicode escape: \\u${hex}`, line, col);
319
+ }
185
320
  value += String.fromCharCode(parseInt(hex, 16));
186
321
  for (let i = 0; i < 4; i++) advance();
187
322
  break;
188
323
  }
189
324
  default:
190
- value += esc;
325
+ throw new ParseError(`Unknown escape sequence: \\${esc}`, line, col);
191
326
  }
192
327
  } else {
193
328
  value += advance();
@@ -212,10 +347,10 @@ function tokenize(input) {
212
347
  return tokens;
213
348
  }
214
349
  function isUnquotedStart(ch) {
215
- return ch !== "" && !'{}[],:=+#\n\r "$'.includes(ch);
350
+ return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
216
351
  }
217
352
  function isUnquotedContinue(ch, nextFn) {
218
- if (ch === "" || '{}[],:=\n\r #"$'.includes(ch) || ch === " ") return false;
353
+ if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
219
354
  if (ch === "+" && nextFn() === "=") return false;
220
355
  if (ch === "/" && nextFn() === "/") return false;
221
356
  return true;
@@ -317,20 +452,46 @@ function parseTokens(tokens) {
317
452
  skip("newline");
318
453
  const t2 = peek();
319
454
  let path2;
320
- if (t2.kind === "string") {
455
+ let required = false;
456
+ if (t2.kind === "unquoted" && (t2.value === "required(" || t2.value === "required" || t2.value.startsWith("required("))) {
457
+ if (t2.value === "required") {
458
+ const next2 = tokens[pos + 1] ?? { kind: "eof", value: "" };
459
+ if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
460
+ throw new ParseError("include required must be followed by (", t2.line, t2.col);
461
+ }
462
+ }
463
+ required = true;
464
+ advance();
465
+ const innerPrefix = t2.value.startsWith("required(") ? t2.value.slice("required(".length) : "";
466
+ if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
467
+ throw new ParseError("include url(...) and classpath(...) are not supported", t2.line, t2.col);
468
+ }
469
+ const next = peek();
470
+ if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
471
+ throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
472
+ }
473
+ while (peek().kind !== "string" && peek().kind !== "eof") advance();
474
+ if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
475
+ path2 = advance().value;
476
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
477
+ } else if (t2.kind === "string") {
321
478
  path2 = advance().value;
322
479
  } else if (t2.kind === "unquoted" && (t2.value === "file(" || t2.value === "file")) {
323
480
  advance();
324
481
  while (peek().kind !== "string" && peek().kind !== "eof") advance();
325
482
  if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
326
483
  path2 = advance().value;
327
- while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof") advance();
484
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
485
+ } else if (t2.kind === "unquoted" && (t2.value === "url" || t2.value.startsWith("url("))) {
486
+ throw new ParseError("include url(...) is not supported", t2.line, t2.col);
487
+ } else if (t2.kind === "unquoted" && (t2.value === "classpath" || t2.value.startsWith("classpath("))) {
488
+ throw new ParseError("include classpath(...) is not supported", t2.line, t2.col);
328
489
  } else {
329
490
  throw new ParseError(`expected include path, got ${t2.kind}`, t2.line, t2.col);
330
491
  }
331
492
  return {
332
493
  key: [],
333
- value: { kind: "include", path: path2, pos: p },
494
+ value: { kind: "include", path: path2, required, pos: p },
334
495
  append: false,
335
496
  pos: p
336
497
  };
@@ -365,7 +526,7 @@ function parseTokens(tokens) {
365
526
  break;
366
527
  }
367
528
  if (hadSpace) {
368
- parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col } });
529
+ parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col }, _separator: true });
369
530
  }
370
531
  parts.push(node);
371
532
  }
@@ -401,13 +562,88 @@ function parseTokens(tokens) {
401
562
  const t = peek();
402
563
  if (t.kind === "lbrace") {
403
564
  advance();
404
- return parseObject(true);
565
+ const first = parseObject(true);
566
+ const allFields = first.kind === "object" ? [...first.fields] : [];
567
+ while (true) {
568
+ skip("newline");
569
+ if (peek().kind === "eof") break;
570
+ if (peek().kind === "lbrace") {
571
+ advance();
572
+ const extra = parseObject(true);
573
+ if (extra.kind === "object") allFields.push(...extra.fields);
574
+ } else {
575
+ const rest = parseObject(false);
576
+ if (rest.kind === "object") allFields.push(...rest.fields);
577
+ break;
578
+ }
579
+ }
580
+ skip("newline");
581
+ const remaining = peek();
582
+ if (remaining.kind !== "eof") {
583
+ throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
584
+ }
585
+ return { kind: "object", fields: allFields, pos: first.pos };
405
586
  }
406
587
  return parseObject(false);
407
588
  }
408
589
 
409
590
  // src/internal/resolver/resolver.ts
410
591
  import * as nodePath from "path";
592
+
593
+ // src/internal/properties/properties.ts
594
+ function parseProperties(input) {
595
+ const root = /* @__PURE__ */ Object.create(null);
596
+ for (const line of input.split("\n")) {
597
+ const trimmed = line.trim();
598
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
599
+ const sepIdx = findSeparator(trimmed);
600
+ if (sepIdx === -1) continue;
601
+ const key = trimmed.slice(0, sepIdx).trim();
602
+ const value = trimmed.slice(sepIdx + 1).trim();
603
+ if (key === "") continue;
604
+ setNested(root, key.split("."), value);
605
+ }
606
+ return root;
607
+ }
608
+ function findSeparator(line) {
609
+ for (let i = 0; i < line.length; i++) {
610
+ if (line[i] === "=" || line[i] === ":") return i;
611
+ }
612
+ return -1;
613
+ }
614
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
615
+ function setNested(obj, segments, value) {
616
+ let current = obj;
617
+ for (let i = 0; i < segments.length - 1; i++) {
618
+ const seg = segments[i];
619
+ if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
620
+ if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
621
+ current[seg] = /* @__PURE__ */ Object.create(null);
622
+ }
623
+ current = current[seg];
624
+ }
625
+ const last = segments[segments.length - 1];
626
+ if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
627
+ current[last] = value;
628
+ }
629
+ function propertiesToHoconValue(input) {
630
+ const parsed = parseProperties(input);
631
+ return recordToHoconValue(parsed);
632
+ }
633
+ function recordToHoconValue(obj) {
634
+ const fields = /* @__PURE__ */ new Map();
635
+ for (const [key, val] of Object.entries(obj)) {
636
+ if (typeof val === "string") {
637
+ fields.set(key, { kind: "scalar", value: val });
638
+ } else if (val !== null && typeof val === "object") {
639
+ fields.set(key, recordToHoconValue(val));
640
+ }
641
+ }
642
+ return { kind: "object", fields };
643
+ }
644
+
645
+ // src/internal/resolver/resolver.ts
646
+ var separatorValues = /* @__PURE__ */ new WeakSet();
411
647
  function isSubst(v) {
412
648
  return v._kind === "subst-placeholder";
413
649
  }
@@ -441,7 +677,7 @@ function buildResObj(ast, opts) {
441
677
  }
442
678
  function applyField(obj, field, opts) {
443
679
  if (field.key.length === 0 && field.value.kind === "include") {
444
- const included = loadInclude(field.value.path, opts);
680
+ const included = loadInclude(field.value.path, field.value.required, opts);
445
681
  deepMergeResObjInto(obj, included);
446
682
  return;
447
683
  }
@@ -479,8 +715,11 @@ function applyField(obj, field, opts) {
479
715
  }
480
716
  function astToResolverValue(ast, opts) {
481
717
  switch (ast.kind) {
482
- case "scalar":
483
- return { kind: "scalar", value: ast.value };
718
+ case "scalar": {
719
+ const sv = { kind: "scalar", value: ast.value };
720
+ if (ast._separator) separatorValues.add(sv);
721
+ return sv;
722
+ }
484
723
  case "array":
485
724
  return { kind: "array", items: ast.items.map((i) => astToResolverValue(i, opts)) };
486
725
  case "object": {
@@ -495,6 +734,18 @@ function astToResolverValue(ast, opts) {
495
734
  return { kind: "scalar", value: null };
496
735
  }
497
736
  }
737
+ function hoconValueToResObj(hv) {
738
+ const obj = makeResObj();
739
+ if (hv.kind !== "object") return obj;
740
+ for (const [key, val] of hv.fields) {
741
+ if (val.kind === "object") {
742
+ obj.fields.set(key, hoconValueToResObj(val));
743
+ } else {
744
+ obj.fields.set(key, val);
745
+ }
746
+ }
747
+ return obj;
748
+ }
498
749
  function deepMergeResObjInto(dst, src) {
499
750
  for (const [k, srcVal] of src.fields) {
500
751
  const dstVal = dst.fields.get(k);
@@ -582,12 +833,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
582
833
  const resolved = nodes.map((n) => resolveVal(n, scope, root, resolving, resolvedCache, opts)).filter((v) => v !== void 0);
583
834
  if (resolved.length === 0) return { kind: "scalar", value: null };
584
835
  if (resolved.length === 1) return resolved[0];
585
- if (resolved.every((v) => v.kind === "object")) {
836
+ const nonSep = resolved.filter((v) => !separatorValues.has(v));
837
+ if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
586
838
  const merged = /* @__PURE__ */ new Map();
587
- for (const v of resolved) {
839
+ for (const v of nonSep) {
588
840
  if (v.kind === "object") {
589
841
  for (const [k, val] of v.fields) {
590
- merged.set(k, val);
842
+ const existing = merged.get(k);
843
+ if (existing?.kind === "object" && val.kind === "object") {
844
+ merged.set(k, deepMergeHoconValues(existing, val));
845
+ } else {
846
+ merged.set(k, val);
847
+ }
591
848
  }
592
849
  }
593
850
  }
@@ -604,6 +861,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
604
861
  const str = resolved.map((v) => v.kind === "scalar" ? String(v.value) : JSON.stringify(v)).join("");
605
862
  return { kind: "scalar", value: str };
606
863
  }
864
+ function deepMergeHoconValues(base, overlay) {
865
+ const merged = new Map(base.fields);
866
+ for (const [k, v] of overlay.fields) {
867
+ const existing = merged.get(k);
868
+ if (existing?.kind === "object" && v.kind === "object") {
869
+ merged.set(k, deepMergeHoconValues(existing, v));
870
+ } else {
871
+ merged.set(k, v);
872
+ }
873
+ }
874
+ return { kind: "object", fields: merged };
875
+ }
607
876
  function resolveAppend(a, scope, root, resolving, resolvedCache, opts) {
608
877
  const existing = resolveVal(a.existing, scope, root, resolving, resolvedCache, opts) ?? { kind: "array", items: [] };
609
878
  const elem = resolveVal(a.elem, scope, root, resolving, resolvedCache, opts);
@@ -661,35 +930,68 @@ function parseSubstPath(raw) {
661
930
  }
662
931
  return segments;
663
932
  }
664
- function loadInclude(includePath, opts) {
665
- const { baseDir, readFileSync: readFileSync2, includeStack = [], env } = opts;
933
+ function isFileNotFoundError(e) {
934
+ if (!(e instanceof Error)) return false;
935
+ const code = e.code;
936
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
937
+ const msg = e.message.toLowerCase();
938
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
939
+ }
940
+ function loadSingleInclude(candidate, opts) {
941
+ const { readFileSync: readFileSync2, includeStack = [], env } = opts;
942
+ if (includeStack.includes(candidate)) {
943
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
944
+ }
945
+ let content;
946
+ try {
947
+ content = readFileSync2(candidate);
948
+ } catch (e) {
949
+ if (isFileNotFoundError(e)) return void 0;
950
+ throw e;
951
+ }
952
+ if (candidate.endsWith(".properties")) {
953
+ return hoconValueToResObj(propertiesToHoconValue(content));
954
+ }
955
+ const ast = parseTokens(tokenize(content));
956
+ return buildResObj(ast, {
957
+ env,
958
+ baseDir: nodePath.dirname(candidate),
959
+ readFileSync: readFileSync2,
960
+ includeStack: [...includeStack, candidate]
961
+ });
962
+ }
963
+ function loadInclude(includePath, required, opts) {
964
+ const { baseDir, includeStack = [] } = opts;
666
965
  const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
667
966
  if (includeStack.includes(absPath)) {
668
967
  throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
669
968
  }
670
- const candidates = [absPath];
671
- if (!absPath.endsWith(".conf") && !absPath.endsWith(".json") && !absPath.endsWith(".properties")) {
672
- candidates.push(absPath + ".conf", absPath + ".json");
969
+ if (includeStack.length >= 50) {
970
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
673
971
  }
674
- for (const candidate of candidates) {
675
- let content;
676
- try {
677
- content = readFileSync2(candidate);
678
- } catch {
679
- continue;
972
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
973
+ if (hasExplicitExt) {
974
+ const result = loadSingleInclude(absPath, opts);
975
+ if (result !== void 0) return result;
976
+ if (required) {
977
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
680
978
  }
681
- if (includeStack.includes(candidate)) {
682
- throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
979
+ return makeResObj();
980
+ }
981
+ const merged = makeResObj();
982
+ let foundAny = false;
983
+ const probeExts = [".properties", ".json", ".conf"];
984
+ for (const ext of probeExts) {
985
+ const obj = loadSingleInclude(`${absPath}${ext}`, opts);
986
+ if (obj !== void 0) {
987
+ deepMergeResObjInto(merged, obj);
988
+ foundAny = true;
683
989
  }
684
- const ast = parseTokens(tokenize(content));
685
- return buildResObj(ast, {
686
- env,
687
- baseDir: nodePath.dirname(candidate),
688
- readFileSync: readFileSync2,
689
- includeStack: [...includeStack, candidate]
690
- });
691
990
  }
692
- return makeResObj();
991
+ if (!foundAny && required) {
992
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
993
+ }
994
+ return merged;
693
995
  }
694
996
 
695
997
  // src/config.ts
@@ -725,6 +1027,20 @@ var Config = class _Config {
725
1027
  }
726
1028
  throw new ConfigError(`expected boolean at ${path2}, got ${typeof v}`, path2);
727
1029
  }
1030
+ getDuration(path2, unit) {
1031
+ const v = this.requireScalar(path2);
1032
+ if (typeof v !== "string") throw new ConfigError(`expected duration string at ${path2}, got ${typeof v}`, path2);
1033
+ const result = parseDuration(v, unit);
1034
+ if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v)}`, path2);
1035
+ return result;
1036
+ }
1037
+ getBytes(path2, unit) {
1038
+ const v = this.requireScalar(path2);
1039
+ if (typeof v !== "string") throw new ConfigError(`expected byte size string at ${path2}, got ${typeof v}`, path2);
1040
+ const result = parseBytes(v, unit);
1041
+ if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v)}`, path2);
1042
+ return result;
1043
+ }
728
1044
  getConfig(path2) {
729
1045
  const v = this.lookupNode(path2);
730
1046
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
@@ -751,7 +1067,7 @@ var Config = class _Config {
751
1067
  return hoconToJs(this.root);
752
1068
  }
753
1069
  lookupNode(path2) {
754
- const segments = path2.split(".");
1070
+ const segments = splitConfigPath(path2);
755
1071
  let current = this.root;
756
1072
  for (const seg of segments) {
757
1073
  if (current.kind !== "object") return void 0;
@@ -768,6 +1084,45 @@ var Config = class _Config {
768
1084
  return v.value;
769
1085
  }
770
1086
  };
1087
+ function splitConfigPath(path2) {
1088
+ const segments = [];
1089
+ let i = 0;
1090
+ while (i < path2.length) {
1091
+ if (path2[i] === '"') {
1092
+ i++;
1093
+ let segment = "";
1094
+ let closed = false;
1095
+ while (i < path2.length) {
1096
+ const ch = path2[i];
1097
+ if (ch === "\\" && i + 1 < path2.length) {
1098
+ const next = path2[i + 1];
1099
+ segment += next;
1100
+ i += 2;
1101
+ continue;
1102
+ }
1103
+ if (ch === '"') {
1104
+ closed = true;
1105
+ i++;
1106
+ break;
1107
+ }
1108
+ segment += ch;
1109
+ i++;
1110
+ }
1111
+ if (!closed) throw new ConfigError(`unterminated quoted path segment: ${path2}`, path2);
1112
+ segments.push(segment);
1113
+ if (i < path2.length && path2[i] === ".") i++;
1114
+ } else {
1115
+ const dot = path2.indexOf(".", i);
1116
+ if (dot === -1) {
1117
+ segments.push(path2.slice(i));
1118
+ break;
1119
+ }
1120
+ segments.push(path2.slice(i, dot));
1121
+ i = dot + 1;
1122
+ }
1123
+ }
1124
+ return segments;
1125
+ }
771
1126
  function hoconToJs(v) {
772
1127
  switch (v.kind) {
773
1128
  case "scalar":
@@ -805,14 +1160,19 @@ function getEnv(opts) {
805
1160
  function defaultReadFileSync(filePath) {
806
1161
  return fs.readFileSync(filePath, "utf-8");
807
1162
  }
808
- function parse(input, opts = {}) {
1163
+ function buildResolveContext(input, opts) {
809
1164
  const tokens = tokenize(input);
810
1165
  const ast = parseTokens(tokens);
811
1166
  const resolveOpts = {
812
1167
  env: getEnv(opts),
813
1168
  baseDir: opts.baseDir,
814
- readFileSync: opts.readFileSync ?? defaultReadFileSync
1169
+ readFileSync: opts.readFileSync ?? defaultReadFileSync,
1170
+ readFile: opts.readFile
815
1171
  };
1172
+ return { ast, resolveOpts };
1173
+ }
1174
+ function parse(input, opts = {}) {
1175
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
816
1176
  const value = resolve2(ast, resolveOpts);
817
1177
  if (value.kind !== "object") throw new Error("resolved value is not an object");
818
1178
  return new Config(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/ts.hocon",
3
- "version": "0.1.5",
3
+ "version": "1.0.0",
4
4
  "description": "Full Lightbend HOCON spec-compliant parser and config library for TypeScript",
5
5
  "keywords": [
6
6
  "hocon",
@@ -41,6 +41,7 @@
41
41
  "@types/node": "^25.5.0",
42
42
  "@vitest/coverage-v8": "^4.1.2",
43
43
  "tsup": "^8.5.1",
44
+ "typedoc": "^0.28.18",
44
45
  "typescript": "^5.9.3",
45
46
  "vitest": "^4.1.2",
46
47
  "zod": "^4.3.6"
@@ -49,12 +50,19 @@
49
50
  "type": "git",
50
51
  "url": "https://github.com/o3co/ts.hocon"
51
52
  },
53
+ "license": "Apache-2.0",
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "homepage": "https://o3co.github.io/ts.hocon/",
58
+ "bugs": "https://github.com/o3co/ts.hocon/issues",
52
59
  "scripts": {
53
60
  "build": "tsup",
54
61
  "test": "vitest run",
55
62
  "test:watch": "vitest",
56
63
  "typecheck": "tsc --noEmit",
57
64
  "coverage": "vitest run --coverage",
58
- "bench": "vitest bench"
65
+ "bench": "vitest bench",
66
+ "docs": "typedoc"
59
67
  }
60
68
  }