@voxgig/sdkgen 4.5.2 → 4.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.
Files changed (176) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/action/check.js +15 -0
  3. package/dist/action/check.js.map +1 -1
  4. package/dist/cmp/AgentGuide.js +1 -1
  5. package/dist/cmp/AgentGuide.js.map +1 -1
  6. package/dist/cmp/AgentGuideContent.d.ts +1 -1
  7. package/dist/cmp/AgentGuideContent.js +8 -2
  8. package/dist/cmp/AgentGuideContent.js.map +1 -1
  9. package/dist/cmp/Feature.js +17 -0
  10. package/dist/cmp/Feature.js.map +1 -1
  11. package/dist/cmp/FeatureDocs.d.ts +1 -1
  12. package/dist/cmp/FeatureDocs.js +6 -1
  13. package/dist/cmp/FeatureDocs.js.map +1 -1
  14. package/dist/cmp/ReadmeExplanation.js +3 -1
  15. package/dist/cmp/ReadmeExplanation.js.map +1 -1
  16. package/dist/cmp/ReadmeFeatures.js +1 -1
  17. package/dist/cmp/ReadmeFeatures.js.map +1 -1
  18. package/dist/cmp/ReadmeRefFeatures.js +1 -1
  19. package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
  20. package/dist/cmp/ReadmeTop.js +1 -1
  21. package/dist/helpers/applicability.d.ts +6 -0
  22. package/dist/helpers/applicability.js +103 -0
  23. package/dist/helpers/applicability.js.map +1 -0
  24. package/dist/helpers/collectDeps.js +4 -2
  25. package/dist/helpers/collectDeps.js.map +1 -1
  26. package/dist/helpers/serverVars.d.ts +2 -1
  27. package/dist/helpers/serverVars.js +17 -0
  28. package/dist/helpers/serverVars.js.map +1 -1
  29. package/dist/sdkgen.d.ts +3 -2
  30. package/dist/sdkgen.js +9 -2
  31. package/dist/sdkgen.js.map +1 -1
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/dist/utility.js +7 -1
  34. package/dist/utility.js.map +1 -1
  35. package/model/sdkgen.aon +39 -0
  36. package/package.json +1 -1
  37. package/project/.sdk/model/feature/feature-index.aon +1 -0
  38. package/project/.sdk/model/feature/secrets.aon +118 -0
  39. package/project/.sdk/model/target/ts.aon +8 -1
  40. package/project/.sdk/src/cmp/c/Config_c.ts +5 -1
  41. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +5 -1
  42. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +5 -1
  43. package/project/.sdk/src/cmp/dart/Config_dart.ts +5 -1
  44. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +5 -1
  45. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +5 -1
  46. package/project/.sdk/src/cmp/go/Config_go.ts +5 -1
  47. package/project/.sdk/src/cmp/go/Main_go.ts +6 -2
  48. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +16 -3
  49. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +16 -3
  50. package/project/.sdk/src/cmp/go/TestDirect_go.ts +28 -4
  51. package/project/.sdk/src/cmp/go/TestEntity_go.ts +43 -10
  52. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +5 -5
  53. package/project/.sdk/src/cmp/java/Config_java.ts +22 -1
  54. package/project/.sdk/src/cmp/java/Main_java.ts +5 -1
  55. package/project/.sdk/src/cmp/java/ReadmeQuick_java.ts +17 -3
  56. package/project/.sdk/src/cmp/java/ReadmeTopQuick_java.ts +17 -3
  57. package/project/.sdk/src/cmp/java/TestDirect_java.ts +23 -4
  58. package/project/.sdk/src/cmp/java/TestEntity_java.ts +29 -4
  59. package/project/.sdk/src/cmp/js/Config_js.ts +5 -1
  60. package/project/.sdk/src/cmp/js/Main_js.ts +5 -1
  61. package/project/.sdk/src/cmp/js/Package_js.ts +4 -1
  62. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +5 -1
  63. package/project/.sdk/src/cmp/lean/Config_lean.ts +5 -1
  64. package/project/.sdk/src/cmp/lua/Config_lua.ts +5 -1
  65. package/project/.sdk/src/cmp/lua/Main_lua.ts +5 -1
  66. package/project/.sdk/src/cmp/lua/Package_lua.ts +4 -1
  67. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +5 -1
  68. package/project/.sdk/src/cmp/perl/Main_perl.ts +5 -1
  69. package/project/.sdk/src/cmp/php/Config_php.ts +5 -1
  70. package/project/.sdk/src/cmp/php/Main_php.ts +5 -1
  71. package/project/.sdk/src/cmp/py/Config_py.ts +5 -1
  72. package/project/.sdk/src/cmp/py/Main_py.ts +5 -1
  73. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +16 -3
  74. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +16 -3
  75. package/project/.sdk/src/cmp/py/TestDirect_py.ts +24 -5
  76. package/project/.sdk/src/cmp/py/TestEntity_py.ts +23 -3
  77. package/project/.sdk/src/cmp/rb/Config_rb.ts +5 -1
  78. package/project/.sdk/src/cmp/rb/Main_rb.ts +5 -1
  79. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -1
  80. package/project/.sdk/src/cmp/rust/Main_rust.ts +5 -1
  81. package/project/.sdk/src/cmp/scala/Config_scala.ts +5 -1
  82. package/project/.sdk/src/cmp/scala/Main_scala.ts +5 -1
  83. package/project/.sdk/src/cmp/swift/Config_swift.ts +5 -1
  84. package/project/.sdk/src/cmp/ts/Config_ts.ts +5 -1
  85. package/project/.sdk/src/cmp/ts/Main_ts.ts +57 -1
  86. package/project/.sdk/src/cmp/ts/Package_ts.ts +4 -1
  87. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +15 -3
  88. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +18 -5
  89. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +19 -5
  90. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +67 -7
  91. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +27 -4
  92. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +9 -2
  93. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -2
  94. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +7 -0
  95. package/project/.sdk/src/cmp/zig/Config_zig.ts +5 -1
  96. package/project/.sdk/tm/c/utility/make_options.c +22 -0
  97. package/project/.sdk/tm/clojure/src/sdk/core.clj +18 -1
  98. package/project/.sdk/tm/cpp/utility/pipeline.hpp +23 -0
  99. package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +15 -0
  100. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +23 -0
  101. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +22 -0
  102. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -0
  103. package/project/.sdk/tm/go/core/context.go +1 -1
  104. package/project/.sdk/tm/go/feature/cost_feature.go +1 -1
  105. package/project/.sdk/tm/go/feature/paging_feature.go +5 -5
  106. package/project/.sdk/tm/go/feature/test_feature.go +12 -5
  107. package/project/.sdk/tm/go/test/custom_utility_test.go +15 -0
  108. package/project/.sdk/tm/go/test/feature_corpus_test.go +7 -0
  109. package/project/.sdk/tm/go/test/runner_test.go +46 -0
  110. package/project/.sdk/tm/go/test/struct_runner_test.go +7 -7
  111. package/project/.sdk/tm/go/test/struct_utility_test.go +63 -27
  112. package/project/.sdk/tm/go/utility/fetcher.go +2 -2
  113. package/project/.sdk/tm/go/utility/make_options.go +24 -5
  114. package/project/.sdk/tm/go/utility/make_point.go +1 -1
  115. package/project/.sdk/tm/go/utility/make_spec.go +17 -1
  116. package/project/.sdk/tm/go/utility/prepare_auth.go +1 -1
  117. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +370 -307
  118. package/project/.sdk/tm/go/utility/transform_request.go +29 -1
  119. package/project/.sdk/tm/go/utility/transform_response.go +20 -1
  120. package/project/.sdk/tm/java/test/CustomUtilityTest.java +10 -0
  121. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +7 -0
  122. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +6 -0
  123. package/project/.sdk/tm/java/test/RunnerSupport.java +43 -0
  124. package/project/.sdk/tm/java/utility/MakeOptions.java +84 -1
  125. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +24 -0
  126. package/project/.sdk/tm/js/src/utility/StructUtility.js +2076 -1837
  127. package/project/.sdk/tm/js/test/feature/Corpus.test.js +9 -0
  128. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +21 -0
  129. package/project/.sdk/tm/lean/src/SdkUtility.lean +12 -1
  130. package/project/.sdk/tm/ocaml/sdk_runtime.ml +11 -0
  131. package/project/.sdk/tm/perl/utility/make_options.pm +18 -0
  132. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +10 -1
  133. package/project/.sdk/tm/php/utility/MakeOptions.php +24 -0
  134. package/project/.sdk/tm/py/pkg/utility/make_options.py +20 -0
  135. package/project/.sdk/tm/py/test/runner.py +32 -0
  136. package/project/.sdk/tm/py/test/test_feature_corpus.py +10 -1
  137. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +9 -1
  138. package/project/.sdk/tm/rb/utility/make_options.rb +17 -0
  139. package/project/.sdk/tm/rust/utility/make_options.rs +25 -0
  140. package/project/.sdk/tm/scala/utility/Make.scala +17 -0
  141. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -0
  142. package/project/.sdk/tm/ts/src/feature/secrets/SecretsFeature.ts +419 -0
  143. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +1221 -0
  144. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +381 -0
  145. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +151 -0
  146. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +42 -0
  147. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +18 -0
  148. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1321 -710
  149. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +9 -0
  150. package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +764 -0
  151. package/project/.sdk/tm/ts/test/omni.test.ts +84 -0
  152. package/project/.sdk/tm/ts/test/omni.ts +86 -0
  153. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +6 -2
  154. package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +2 -2
  155. package/project/.sdk/tm/ts/test/utility.ts +122 -0
  156. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +692 -0
  157. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +238 -0
  158. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +188 -0
  159. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +38 -0
  160. package/project/.sdk/tm/zig/core/utility.zig +23 -0
  161. package/project/sdkgen-package.json +2 -1
  162. package/src/action/check.ts +18 -0
  163. package/src/cmp/AgentGuide.ts +1 -1
  164. package/src/cmp/AgentGuideContent.ts +9 -2
  165. package/src/cmp/Feature.ts +19 -0
  166. package/src/cmp/FeatureDocs.ts +7 -1
  167. package/src/cmp/ReadmeExplanation.ts +4 -1
  168. package/src/cmp/ReadmeFeatures.ts +1 -1
  169. package/src/cmp/ReadmeRefFeatures.ts +1 -1
  170. package/src/cmp/ReadmeTop.ts +1 -1
  171. package/src/helpers/applicability.ts +123 -0
  172. package/src/helpers/collectDeps.ts +5 -1
  173. package/src/helpers/serverVars.ts +19 -0
  174. package/src/sdkgen.ts +9 -1
  175. package/src/utility.ts +10 -1
  176. package/project/.sdk/tm/ts/test/runner.ts +0 -425
@@ -1,6 +1,9 @@
1
+ // VENDORED: @voxgig/struct 0.3.2 (typescript/src/StructUtility.ts)
2
+ // Source: https://github.com/voxgig/struct @ 94409354d0a60fda8098c83e290583b7dee76c84
3
+ // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
1
4
  /* Copyright (c) 2025-2026 Voxgig Ltd. MIT LICENSE. */
2
5
 
3
- // VERSION: @voxgig/struct 0.0.10
6
+ // VERSION: @voxgig/struct 0.3.2
4
7
 
5
8
  /* Voxgig Struct
6
9
  * =============
@@ -53,7 +56,6 @@
53
56
  *
54
57
  */
55
58
 
56
-
57
59
  // String constants are explicitly defined.
58
60
 
59
61
  // Mode value for inject step (bitfield).
@@ -137,12 +139,20 @@ const TYPENAME = [
137
139
  S_function,
138
140
  S_symbol,
139
141
  S_null,
140
- '', '', '',
141
- '', '', '', '',
142
+ '',
143
+ '',
144
+ '',
145
+ '',
146
+ '',
147
+ '',
148
+ '',
142
149
  S_list,
143
150
  S_map,
144
151
  S_instance,
145
- '', '', '', '',
152
+ '',
153
+ '',
154
+ '',
155
+ '',
146
156
  S_scalar,
147
157
  S_node,
148
158
  ]
@@ -154,23 +164,19 @@ const NONE = undefined
154
164
  const SKIP = { '`$SKIP`': true }
155
165
  const DELETE = { '`$DELETE`': true }
156
166
 
157
-
158
167
  // Regular expression constants
159
- const R_INTEGER_KEY = /^[-0-9]+$/ // Match integer keys (including <0).
160
- const R_ESCAPE_REGEXP = /[.*+?^${}()|[\]\\]/g // Chars that need escaping in regexp.
161
- const R_TRAILING_SLASH = /\/+$/ // Trailing slashes in URLs.
162
- const R_LEADING_TRAILING_SLASH = /([^\/])\/+/ // Multiple slashes in URL middle.
163
- const R_LEADING_SLASH = /^\/+/ // Leading slashes in URLs.
164
- const R_QUOTES = /"/g // Double quotes for removal.
165
- const R_DOT = /\./g // Dots in path strings.
166
- const R_CLONE_REF = /^`\$REF:([0-9]+)`$/ // Copy reference in cloning.
167
- const R_META_PATH = /^([^$]+)\$([=~])(.+)$/ // Meta path syntax.
168
- const R_DOUBLE_DOLLAR = /\$\$/g // Double dollar escape sequence.
169
- const R_TRANSFORM_NAME = /`\$([A-Z]+)`/g // Transform command names.
170
- const R_INJECTION_FULL = /^`(\$[A-Z]+|[^`]*)[0-9]*`$/ // Full string injection pattern.
171
- const R_BT_ESCAPE = /\$BT/g // Backtick escape sequence.
172
- const R_DS_ESCAPE = /\$DS/g // Dollar sign escape sequence.
173
- const R_INJECTION_PARTIAL = /`([^`]+)`/g // Partial string injection pattern.
168
+ const R_INTEGER_KEY = /^[-0-9]+$/ // Match integer keys (including <0).
169
+ const R_ESCAPE_REGEXP = /[.*+?^${}()|[\]\\]/g // Chars that need escaping in regexp.
170
+ const R_QUOTES = /"/g // Double quotes for removal.
171
+ const R_DOT = /\./g // Dots in path strings.
172
+ const R_CLONE_REF = /^`\$REF:([0-9]+)`$/ // Copy reference in cloning.
173
+ const R_META_PATH = /^([^$]+)\$([=~])(.+)$/ // Meta path syntax.
174
+ const R_DOUBLE_DOLLAR = /\$\$/g // Double dollar escape sequence.
175
+ const R_TRANSFORM_NAME = /`\$([A-Z]+)`/g // Transform command names.
176
+ const R_INJECTION_FULL = /^`(\$[A-Z]+|[^`]*)[0-9]*`$/ // Full string injection pattern.
177
+ const R_BT_ESCAPE = /\$BT/g // Backtick escape sequence.
178
+ const R_DS_ESCAPE = /\$DS/g // Dollar sign escape sequence.
179
+ const R_INJECTION_PARTIAL = /`([^`]+)`/g // Partial string injection pattern.
174
180
 
175
181
  // Default max depth (for walk etc).
176
182
  const MAXDEPTH = 32
@@ -181,7 +187,6 @@ type PropKey = string | number
181
187
  // Type that can be indexed by both string and number keys.
182
188
  type Indexable = { [key: string]: any } & { [key: number]: any }
183
189
 
184
-
185
190
  // For each key in a node (map or list), perform value injections in
186
191
  // three phases: on key value, before child, and then on key value again.
187
192
  // This mode is passed via the Injection structure.
@@ -191,38 +196,40 @@ type InjectMode = number
191
196
  // - `a.b.c`: insert value at {a:{b:{c:1}}}
192
197
  // - `$FOO`: apply transform FOO
193
198
  type Injector = (
194
- inj: Injection, // Injection state.
195
- val: any, // Injection value specification.
196
- ref: string, // Original injection reference string.
197
- store: any, // Current source root value.
199
+ inj: Injection, // Injection state.
200
+ val: any, // Injection value specification.
201
+ ref: string, // Original injection reference string.
202
+ store: any, // Current source root value.
198
203
  ) => any
199
204
 
200
205
  // Apply a custom modification to injections.
201
206
  type Modify = (
202
- val: any, // Value.
203
- key?: PropKey, // Value key, if any,
204
- parent?: any, // Parent node, if any.
205
- inj?: Injection, // Injection state, if any.
206
- store?: any, // Store, if any
207
+ val: any, // Value.
208
+ key?: PropKey, // Value key, if any,
209
+ parent?: any, // Parent node, if any.
210
+ inj?: Injection, // Injection state, if any.
211
+ store?: any, // Store, if any
207
212
  ) => void
208
213
 
209
214
  // Function applied to each node and leaf when walking a node structure depth first.
210
215
  // For {a:{b:1}} the call sequence args will be: b, 1, {b:1}, [a,b].
216
+ // NOTE: the `path` array passed to the callback is reused across calls during
217
+ // a single walk (one shared array per depth). It is valid only for the
218
+ // duration of the callback invocation. If a callback needs to retain the
219
+ // path, it MUST clone it (e.g. `path.slice()`).
211
220
  type WalkApply = (
212
- // Map keys are strings, list keys are numbers, top key is NONE
221
+ // Map keys are strings, list keys are numbers, top key is NONE
213
222
  key: string | number | undefined,
214
223
  val: any,
215
224
  parent: any,
216
- path: string[]
225
+ path: string[],
217
226
  ) => any
218
227
 
219
-
220
228
  // Return type string for narrowest type.
221
229
  function typename(t: number) {
222
230
  return getelem(TYPENAME, Math.clz32(t), TYPENAME[0])
223
231
  }
224
232
 
225
-
226
233
  // Get a defined value. Returns alt if val is undefined.
227
234
  function getdef(val: any, alt: any) {
228
235
  if (NONE === val) {
@@ -231,7 +238,6 @@ function getdef(val: any, alt: any) {
231
238
  return val
232
239
  }
233
240
 
234
-
235
241
  // Value is a node - defined, and a map (hash) or list (array).
236
242
  // NOTE: typescript
237
243
  // things
@@ -239,47 +245,43 @@ function isnode(val: any): val is Indexable {
239
245
  return null != val && S_object == typeof val
240
246
  }
241
247
 
242
-
243
248
  // Value is a defined map (hash) with string keys.
244
249
  function ismap(val: any): val is { [key: string]: any } {
245
250
  return null != val && S_object == typeof val && !Array.isArray(val)
246
251
  }
247
252
 
248
-
249
253
  // Value is a defined list (array) with integer keys (indexes).
250
254
  function islist(val: any): val is any[] {
251
255
  return Array.isArray(val)
252
256
  }
253
257
 
254
-
255
258
  // Value is a defined string (non-empty) or integer key.
256
259
  function iskey(key: any): key is PropKey {
257
260
  const keytype = typeof key
258
261
  return (S_string === keytype && S_MT !== key) || S_number === keytype
259
262
  }
260
263
 
261
-
262
264
  // Check for an "empty" value - undefined, empty string, array, object.
263
265
  function isempty(val: any) {
264
- return null == val || S_MT === val ||
266
+ return (
267
+ null == val ||
268
+ S_MT === val ||
265
269
  (Array.isArray(val) && 0 === val.length) ||
266
270
  (S_object === typeof val && 0 === Object.keys(val).length)
271
+ )
267
272
  }
268
273
 
269
-
270
274
  // Value is a function.
271
- function isfunc(val: any): val is Function {
275
+ function isfunc(val: any): val is (...args: any[]) => any {
272
276
  return S_function === typeof val
273
277
  }
274
278
 
275
-
276
279
  // The integer size of the value. For arrays and strings, the length,
277
280
  // for numbers, the integer part, for boolean, true is 1 and falso 0, for all other values, 0.
278
281
  function size(val: any): number {
279
282
  if (islist(val)) {
280
283
  return val.length
281
- }
282
- else if (ismap(val)) {
284
+ } else if (ismap(val)) {
283
285
  return Object.keys(val).length
284
286
  }
285
287
 
@@ -287,19 +289,15 @@ function size(val: any): number {
287
289
 
288
290
  if (S_string == valtype) {
289
291
  return val.length
290
- }
291
- else if (S_number == typeof val) {
292
+ } else if (S_number == typeof val) {
292
293
  return Math.floor(val)
293
- }
294
- else if (S_boolean == typeof val) {
294
+ } else if (S_boolean == typeof val) {
295
295
  return true === val ? 1 : 0
296
- }
297
- else {
296
+ } else {
298
297
  return 0
299
298
  }
300
299
  }
301
300
 
302
-
303
301
  // Extract part of an array or string into a new value, from the start
304
302
  // point to the end point. If no end is specified, extract to the
305
303
  // full length of the value. Negative arguments count from the end of
@@ -307,7 +305,7 @@ function size(val: any): number {
307
305
  // is inclusive, and end is *exclusive*.
308
306
  // NOTE: input lists are not mutated by default. Use the mutate
309
307
  // argument to mutate lists in place.
310
- function slice<V extends any>(val: V, start?: number, end?: number, mutate?: boolean): V {
308
+ function slice<V>(val: V, start?: number, end?: number, mutate?: boolean): V {
311
309
  if (S_number === typeof val) {
312
310
  start = null == start || S_number !== typeof start ? Number.MIN_SAFE_INTEGER : start
313
311
  end = (null == end || S_number !== typeof end ? Number.MAX_SAFE_INTEGER : end) - 1
@@ -327,21 +325,16 @@ function slice<V extends any>(val: V, start?: number, end?: number, mutate?: boo
327
325
  end = 0
328
326
  }
329
327
  start = 0
330
- }
331
-
332
- else if (null != end) {
328
+ } else if (null != end) {
333
329
  if (end < 0) {
334
330
  end = vlen + end
335
331
  if (end < 0) {
336
332
  end = 0
337
333
  }
338
- }
339
- else if (vlen < end) {
334
+ } else if (vlen < end) {
340
335
  end = vlen
341
336
  }
342
- }
343
-
344
- else {
337
+ } else {
345
338
  end = vlen
346
339
  }
347
340
 
@@ -355,21 +348,17 @@ function slice<V extends any>(val: V, start?: number, end?: number, mutate?: boo
355
348
  for (let i = 0, j = start; j < end; i++, j++) {
356
349
  val[i] = val[j]
357
350
  }
358
- val.length = (end - start)
359
- }
360
- else {
351
+ val.length = end - start
352
+ } else {
361
353
  val = val.slice(start, end) as V
362
354
  }
363
- }
364
- else if (S_string === typeof val) {
355
+ } else if (S_string === typeof val) {
365
356
  val = (val as string).substring(start, end) as V
366
357
  }
367
- }
368
- else {
358
+ } else {
369
359
  if (islist(val)) {
370
360
  val = [] as V
371
- }
372
- else if (S_string === typeof val) {
361
+ } else if (S_string === typeof val) {
373
362
  val = S_MT as V
374
363
  }
375
364
  }
@@ -378,19 +367,16 @@ function slice<V extends any>(val: V, start?: number, end?: number, mutate?: boo
378
367
  return val
379
368
  }
380
369
 
381
-
382
370
  // String padding.
383
371
  function pad(str: any, padding?: number, padchar?: string): string {
384
372
  str = S_string === typeof str ? str : stringify(str)
385
373
  padding = null == padding ? 44 : padding
386
- padchar = null == padchar ? S_SP : ((padchar + S_SP)[0])
374
+ padchar = null == padchar ? S_SP : (padchar + S_SP)[0]
387
375
  return -1 < padding ? str.padEnd(padding, padchar) : str.padStart(0 - padding, padchar)
388
376
  }
389
377
 
390
-
391
378
  // Determine the type of a value as a bit code.
392
379
  function typify(value: any): number {
393
-
394
380
  if (undefined === value) {
395
381
  return T_noval
396
382
  }
@@ -399,41 +385,30 @@ function typify(value: any): number {
399
385
 
400
386
  if (null === value) {
401
387
  return T_scalar | T_null
402
- }
403
- else if (S_number === typestr) {
388
+ } else if (S_number === typestr) {
404
389
  if (Number.isInteger(value)) {
405
390
  return T_scalar | T_number | T_integer
406
- }
407
- else if (isNaN(value)) {
391
+ } else if (isNaN(value)) {
408
392
  return T_noval
409
- }
410
- else {
393
+ } else {
411
394
  return T_scalar | T_number | T_decimal
412
395
  }
413
- }
414
- else if (S_string === typestr) {
396
+ } else if (S_string === typestr) {
415
397
  return T_scalar | T_string
416
- }
417
- else if (S_boolean === typestr) {
398
+ } else if (S_boolean === typestr) {
418
399
  return T_scalar | T_boolean
419
- }
420
- else if (S_function === typestr) {
400
+ } else if (S_function === typestr) {
421
401
  return T_scalar | T_function
422
402
  }
423
403
 
424
404
  // For languages that have symbolic atoms.
425
405
  else if (S_symbol === typestr) {
426
406
  return T_scalar | T_symbol
427
- }
428
-
429
- else if (Array.isArray(value)) {
407
+ } else if (Array.isArray(value)) {
430
408
  return T_node | T_list
431
- }
432
-
433
- else if (S_object === typestr) {
434
-
409
+ } else if (S_object === typestr) {
435
410
  if (value.constructor instanceof Function) {
436
- let cname = value.constructor.name
411
+ const cname = value.constructor.name
437
412
  if ('Object' !== cname && 'Array' !== cname) {
438
413
  return T_node | T_instance
439
414
  }
@@ -446,7 +421,6 @@ function typify(value: any): number {
446
421
  return T_any
447
422
  }
448
423
 
449
-
450
424
  // Get a list element. The key should be an integer, or a string
451
425
  // that can parse to an integer only. Negative integers count from the end of the list.
452
426
  function getelem(val: any, key: any, alt?: any) {
@@ -457,8 +431,8 @@ function getelem(val: any, key: any, alt?: any) {
457
431
  }
458
432
 
459
433
  if (islist(val)) {
460
- let nkey = parseInt(key)
461
- if (Number.isInteger(nkey) && ('' + key).match(R_INTEGER_KEY)) {
434
+ const nkey = parseInt(key)
435
+ if (Number.isInteger(nkey) && re_test(R_INTEGER_KEY, '' + key)) {
462
436
  if (nkey < 0) {
463
437
  key = val.length + nkey
464
438
  }
@@ -466,14 +440,14 @@ function getelem(val: any, key: any, alt?: any) {
466
440
  }
467
441
  }
468
442
 
469
- if (NONE === out) {
443
+ // null at a slot counts as "no value" — same Group A rule as getprop.
444
+ if (null == out) {
470
445
  return 0 < (T_function & typify(alt)) ? alt() : alt
471
446
  }
472
447
 
473
448
  return out
474
449
  }
475
450
 
476
-
477
451
  // Safely get a property of a node. Undefined arguments return undefined.
478
452
  // If the key is not found, return the alternative value, if any.
479
453
  function getprop(val: any, key: any, alt?: any) {
@@ -487,13 +461,27 @@ function getprop(val: any, key: any, alt?: any) {
487
461
  out = val[key]
488
462
  }
489
463
 
490
- if (NONE === out) {
464
+ // JSON null at a key is treated as "no value" for the default-substitution
465
+ // rule. This unifies cross-port behaviour: every host language conflates
466
+ // null and absent at the value level either always or via positional
467
+ // checks; the library follows that constraint on observation.
468
+ if (null == out) {
491
469
  return alt
492
470
  }
493
471
 
494
472
  return out
495
473
  }
496
474
 
475
+ // Internal: literal value lookup that preserves stored JSON null. Group B
476
+ // callers (validate / transform commands / builders / inject internals) use
477
+ // this when they need to inspect the raw stored value at a slot regardless
478
+ // of whether it is null. The public getprop / getelem / haskey APIs treat
479
+ // null as absent (Group A) per UNDEF_SPEC.md.
480
+ function _lookup(val: any, key: any): any {
481
+ if (NONE === val || NONE === key) return NONE
482
+ if (isnode(val)) return val[key]
483
+ return NONE
484
+ }
497
485
 
498
486
  // Convert different types of keys to string representation.
499
487
  // String keys are returned as is.
@@ -509,42 +497,38 @@ function strkey(key: any = NONE): string {
509
497
 
510
498
  if (0 < (T_string & t)) {
511
499
  return key
512
- }
513
- else if (0 < (T_boolean & t)) {
500
+ } else if (0 < (T_boolean & t)) {
514
501
  return S_MT
515
- }
516
- else if (0 < (T_number & t)) {
502
+ } else if (0 < (T_number & t)) {
517
503
  return key % 1 === 0 ? String(key) : String(Math.floor(key))
518
504
  }
519
505
 
520
506
  return S_MT
521
507
  }
522
508
 
523
-
524
509
  // Sorted keys of a map, or indexes (as strings) of a list.
525
510
  // Root utility - only uses language facilities.
526
511
  function keysof(val: any): string[] {
527
- return !isnode(val) ? [] :
528
- ismap(val) ? Object.keys(val).sort() : (val as any).map((_n: any, i: number) => S_MT + i)
512
+ return !isnode(val)
513
+ ? []
514
+ : ismap(val)
515
+ ? Object.keys(val).sort()
516
+ : (val as any).map((_n: any, i: number) => S_MT + i)
529
517
  }
530
518
 
531
-
532
519
  // Value of property with name key in node val is defined.
533
520
  // Root utility - only uses language facilities.
534
521
  function haskey(val: any, key: any) {
535
- return NONE !== getprop(val, key)
522
+ // null at a key counts as "no value" — same rule as getprop.
523
+ return null != getprop(val, key)
536
524
  }
537
525
 
538
-
539
526
  // List the sorted keys of a map or list as an array of tuples of the form [key, value].
540
527
  // As with keysof, list indexes are converted to strings.
541
528
  // Root utility - only uses language facilities.
542
- function items(val: any): [string, any][];
543
- function items<T>(val: any, apply: (item: [string, any]) => T): T[];
544
- function items(
545
- val: any,
546
- apply?: (item: [string, any]) => any
547
- ): any[] {
529
+ function items(val: any): [string, any][]
530
+ function items<T>(val: any, apply: (item: [string, any]) => T): T[]
531
+ function items(val: any, apply?: (item: [string, any]) => any): any[] {
548
532
  let out: [string, any][] = keysof(val).map((k: any) => [k, val[k]])
549
533
  if (null != apply) {
550
534
  out = out.map(apply)
@@ -552,7 +536,6 @@ function items(
552
536
  return out
553
537
  }
554
538
 
555
-
556
539
  // To replicate the array spread operator:
557
540
  // a=1, b=[2,3], c=[4,5]
558
541
  // [a,...b,c] -> [1,2,3,[4,5]]
@@ -565,12 +548,11 @@ function flatten(list: any[], depth?: number) {
565
548
  return list.flat(getdef(depth, 1))
566
549
  }
567
550
 
568
-
569
551
  // Filter item values using check function.
570
552
  function filter(val: any, check: (item: [string, any]) => boolean): any[] {
571
- let all = items(val)
572
- let numall = size(all)
573
- let out = []
553
+ const all = items(val)
554
+ const numall = size(all)
555
+ const out = []
574
556
  for (let i = 0; i < numall; i++) {
575
557
  if (check(all[i])) {
576
558
  out.push(all[i][1])
@@ -579,13 +561,15 @@ function filter(val: any, check: (item: [string, any]) => boolean): any[] {
579
561
  return out
580
562
  }
581
563
 
582
-
583
564
  // Escape regular expression.
584
565
  function escre(s: string) {
585
- // s = null == s ? S_MT : s
586
- return replace(s, R_ESCAPE_REGEXP, '\\$&')
566
+ return re_replace(R_ESCAPE_REGEXP, s, '\\$&')
587
567
  }
588
568
 
569
+ // Alias so call sites read naturally next to the other re_* helpers.
570
+ function re_escape(s: string) {
571
+ return escre(s)
572
+ }
589
573
 
590
574
  // Escape URLs.
591
575
  function escurl(s: string) {
@@ -593,24 +577,89 @@ function escurl(s: string) {
593
577
  return encodeURIComponent(s)
594
578
  }
595
579
 
580
+ // ===========================================================================
581
+ // Regex utility — uniform API. Every port exposes these same names. The
582
+ // dialect is the RE2 subset documented in /REGEX.md. Internal library code
583
+ // uses these helpers instead of host-language regex methods directly.
584
+ // ===========================================================================
585
+
586
+ // Compile a regex (or return as-is if already compiled). Cached behaviour is
587
+ // up to the port; in TS we just construct a RegExp.
588
+ function re_compile(pattern: string | RegExp, flags?: string): RegExp {
589
+ if (pattern instanceof RegExp) {
590
+ return pattern
591
+ }
592
+ return new RegExp(pattern, flags)
593
+ }
594
+
595
+ // First match. Returns [whole, capture1, ...] or null.
596
+ function re_find(pattern: string | RegExp, input: string): RegExpMatchArray | null {
597
+ const re = pattern instanceof RegExp ? pattern : new RegExp(pattern)
598
+ return input.match(re)
599
+ }
600
+
601
+ // All non-overlapping matches, left to right. Each element is the same shape
602
+ // as re_find's array.
603
+ function re_find_all(pattern: string | RegExp, input: string): RegExpMatchArray[] {
604
+ let re: RegExp
605
+ if (pattern instanceof RegExp) {
606
+ re = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags + 'g')
607
+ } else {
608
+ re = new RegExp(pattern, 'g')
609
+ }
610
+ const out: RegExpMatchArray[] = []
611
+ let m: RegExpExecArray | null
612
+ while ((m = re.exec(input)) !== null) {
613
+ out.push(m as unknown as RegExpMatchArray)
614
+ if (m[0] === '') re.lastIndex++ // avoid infinite loop on empty match
615
+ }
616
+ return out
617
+ }
618
+
619
+ // Replace every match. `replacement` is either a string (with $& and $1..$9)
620
+ // or a callback receiving the same array as re_find returns.
621
+ function re_replace(
622
+ pattern: string | RegExp,
623
+ input: string,
624
+ replacement: string | ((m: RegExpMatchArray) => string),
625
+ ): string {
626
+ let re: RegExp
627
+ if (pattern instanceof RegExp) {
628
+ re = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags + 'g')
629
+ } else {
630
+ re = new RegExp(pattern, 'g')
631
+ }
632
+ if (typeof replacement === 'function') {
633
+ return input.replace(re, (...args: any[]) => {
634
+ // Strip the trailing offset + full string args String.prototype.replace passes.
635
+ // The callback below receives just the capture groups (whole at [0]).
636
+ const groups = args.slice(0, -2) as unknown as RegExpMatchArray
637
+ return replacement(groups)
638
+ })
639
+ }
640
+ return input.replace(re, replacement)
641
+ }
642
+
643
+ // Boolean test for any match.
644
+ function re_test(pattern: string | RegExp, input: string): boolean {
645
+ const re = pattern instanceof RegExp ? pattern : new RegExp(pattern)
646
+ return re.test(input)
647
+ }
596
648
 
597
649
  // Replace a search string (all), or a regexp, in a source string.
598
650
  function replace(s: string, from: string | RegExp, to: any) {
599
651
  let rs = s
600
- let ts = typify(s)
652
+ const ts = typify(s)
601
653
  if (0 === (T_string & ts)) {
602
654
  rs = stringify(s)
603
- }
604
- else if (0 < ((T_noval | T_null) & ts)) {
655
+ } else if (0 < ((T_noval | T_null) & ts)) {
605
656
  rs = S_MT
606
- }
607
- else {
657
+ } else {
608
658
  rs = stringify(s)
609
659
  }
610
660
  return rs.replace(from, to)
611
661
  }
612
662
 
613
-
614
663
  // Concatenate url part strings, merging sep char as needed.
615
664
  function join(arr: any[], sep?: string, url?: boolean) {
616
665
  const sarr = size(arr)
@@ -619,41 +668,45 @@ function join(arr: any[], sep?: string, url?: boolean) {
619
668
  const out = filter(
620
669
  items(
621
670
  // filter(arr, (n) => null != n[1] && S_MT !== n[1]),
622
- filter(arr, (n) => (0 < (T_string & typify(n[1]))) && S_MT !== n[1]),
671
+ filter(arr, (n) => 0 < (T_string & typify(n[1])) && S_MT !== n[1]),
623
672
  (n) => {
624
- let i = +n[0]
673
+ const i = +n[0]
625
674
  let s = n[1]
626
675
 
627
676
  if (NONE !== sepre && S_MT !== sepre) {
628
677
  if (url && 0 === i) {
629
- s = replace(s, RegExp(sepre + '+$'), S_MT)
678
+ s = re_replace(re_compile(sepre + '+$'), s, S_MT)
630
679
  return s
631
680
  }
632
681
 
633
682
  if (0 < i) {
634
- s = replace(s, RegExp('^' + sepre + '+'), S_MT)
683
+ s = re_replace(re_compile('^' + sepre + '+'), s, S_MT)
635
684
  }
636
685
 
637
686
  if (i < sarr - 1 || !url) {
638
- s = replace(s, RegExp(sepre + '+$'), S_MT)
687
+ s = re_replace(re_compile(sepre + '+$'), s, S_MT)
639
688
  }
640
689
 
641
- s = replace(s, RegExp('([^' + sepre + '])' + sepre + '+([^' + sepre + '])'),
642
- '$1' + sepdef + '$2')
690
+ s = re_replace(
691
+ re_compile('([^' + sepre + '])' + sepre + '+([^' + sepre + '])'),
692
+ s,
693
+ '$1' + sepdef + '$2',
694
+ )
643
695
  }
644
696
 
645
697
  return s
646
- }), (n) => S_MT !== n[1])
647
- .join(sepdef)
698
+ },
699
+ ),
700
+ (n) => S_MT !== n[1],
701
+ ).join(sepdef)
648
702
 
649
703
  return out
650
704
  }
651
705
 
652
-
653
706
  // Output JSON in a "standard" format, with 2 space indents, each property on a new line,
654
707
  // and spaces after {[: and before ]}. Any "wierd" values (NaN, etc) are output as null.
655
708
  // In general, the behaivor of of JavaScript's JSON.stringify(val,null,2) is followed.
656
- function jsonify(val: any, flags?: { indent?: number, offset?: number }) {
709
+ function jsonify(val: any, flags?: { indent?: number; offset?: number }) {
657
710
  let str = S_null
658
711
 
659
712
  if (null != val) {
@@ -667,14 +720,14 @@ function jsonify(val: any, flags?: { indent?: number, offset?: number }) {
667
720
  if (0 < offset) {
668
721
  // Left offset entire indented JSON so that it aligns with surrounding code
669
722
  // indented by offset. Assume first brace is on line with asignment, so not offset.
670
- str = '{\n' +
723
+ str =
724
+ '{\n' +
671
725
  join(
672
- items(
673
- slice(str.split('\n'), 1),
674
- (n: any) => pad(n[1], 0 - offset - size(n[1]))), '\n')
726
+ items(slice(str.split('\n'), 1), (n: any) => pad(n[1], 0 - offset - size(n[1]))),
727
+ '\n',
728
+ )
675
729
  }
676
- }
677
- catch (e: any) {
730
+ } catch {
678
731
  str = '__JSONIFY_FAILED__'
679
732
  }
680
733
  }
@@ -682,7 +735,6 @@ function jsonify(val: any, flags?: { indent?: number, offset?: number }) {
682
735
  return str
683
736
  }
684
737
 
685
-
686
738
  // Safely stringify a value for humans (NOT JSON!).
687
739
  function stringify(val: any, maxlen?: number, pretty?: any): string {
688
740
  let valstr = S_MT
@@ -694,66 +746,73 @@ function stringify(val: any, maxlen?: number, pretty?: any): string {
694
746
 
695
747
  if (S_string === typeof val) {
696
748
  valstr = val
697
- }
698
- else {
749
+ } else {
699
750
  try {
700
- valstr = JSON.stringify(val, function(_key: string, val: any) {
701
- if (
702
- val !== null &&
703
- typeof val === "object" &&
704
- !Array.isArray(val)
705
- ) {
751
+ valstr = JSON.stringify(val, function (_key: string, val: any) {
752
+ if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
753
+ // Emit map keys in sorted order (deterministic output). Building the
754
+ // sorted copy directly avoids the intermediate key/tuple arrays that
755
+ // items() would allocate for every node.
756
+ const keys = Object.keys(val).sort()
706
757
  const sortedObj: any = {}
707
- items(val, (n) => {
708
- sortedObj[n[0]] = val[n[0]]
709
- })
758
+ for (let i = 0; i < keys.length; i++) {
759
+ sortedObj[keys[i]] = val[keys[i]]
760
+ }
710
761
  return sortedObj
711
762
  }
712
763
  return val
713
764
  })
714
- valstr = valstr.replace(R_QUOTES, S_MT)
715
- }
716
- catch (err: any) {
765
+ valstr = re_replace(R_QUOTES, valstr, S_MT)
766
+ } catch {
717
767
  valstr = '__STRINGIFY_FAILED__'
718
768
  }
719
769
  }
720
770
 
721
771
  if (null != maxlen && -1 < maxlen) {
722
- let js = valstr.substring(0, maxlen)
723
- valstr = maxlen < valstr.length ? (js.substring(0, maxlen - 3) + '...') : valstr
772
+ const js = valstr.substring(0, maxlen)
773
+ valstr = maxlen < valstr.length ? js.substring(0, maxlen - 3) + '...' : valstr
724
774
  }
725
775
 
726
776
  if (pretty) {
727
777
  // Indicate deeper JSON levels with different terminal colors (simplistic wrt strings).
728
- let c = items(
778
+ const c = items(
729
779
  [81, 118, 213, 39, 208, 201, 45, 190, 129, 51, 160, 121, 226, 33, 207, 69],
730
- (n) => '\x1b[38;5;' + n[1] + 'm'),
731
- r = '\x1b[0m', d = 0, o = c[0], t = o
780
+ (n) => '\x1b[38;5;' + n[1] + 'm',
781
+ )
782
+ const r = '\x1b[0m'
783
+ let d = 0,
784
+ o = c[0],
785
+ t = o
732
786
  for (const ch of valstr) {
733
787
  if (ch === '{' || ch === '[') {
734
- d++; o = c[d % c.length]; t += o + ch
788
+ d++
789
+ o = c[d % c.length]
790
+ t += o + ch
735
791
  } else if (ch === '}' || ch === ']') {
736
- t += o + ch; d--; o = c[d % c.length]
792
+ t += o + ch
793
+ d--
794
+ o = c[d % c.length]
737
795
  } else {
738
796
  t += o + ch
739
797
  }
740
798
  }
741
799
  return t + r
742
-
743
800
  }
744
801
 
745
802
  return valstr
746
803
  }
747
804
 
748
-
749
805
  // Build a human friendly path string.
750
806
  function pathify(val: any, startin?: number, endin?: number) {
751
807
  let pathstr: string | undefined = NONE
752
808
 
753
- let path: any[] | undefined = islist(val) ? val :
754
- S_string == typeof val ? [val] :
755
- S_number == typeof val ? [val] :
756
- NONE
809
+ let path: any[] | undefined = islist(val)
810
+ ? val
811
+ : S_string == typeof val
812
+ ? [val]
813
+ : S_number == typeof val
814
+ ? [val]
815
+ : NONE
757
816
 
758
817
  const start = null == startin ? 0 : -1 < startin ? startin : 0
759
818
  const end = null == endin ? 0 : -1 < endin ? endin : 0
@@ -762,15 +821,17 @@ function pathify(val: any, startin?: number, endin?: number) {
762
821
  path = slice(path, start, path.length - end)
763
822
  if (0 === path.length) {
764
823
  pathstr = '<root>'
765
- }
766
- else {
824
+ } else {
767
825
  pathstr = join(
768
826
  items(
769
- filter(path, (n) => iskey(n[1])), (n) => {
770
- let p = n[1]
771
- return S_number === typeof p ? S_MT + Math.floor(p) :
772
- p.replace(R_DOT, S_MT)
773
- }), S_DT)
827
+ filter(path, (n) => iskey(n[1])),
828
+ (n) => {
829
+ const p = n[1]
830
+ return S_number === typeof p ? S_MT + Math.floor(p) : re_replace(R_DOT, p, S_MT)
831
+ },
832
+ ),
833
+ S_DT,
834
+ )
774
835
  }
775
836
  }
776
837
 
@@ -781,34 +842,58 @@ function pathify(val: any, startin?: number, endin?: number) {
781
842
  return pathstr
782
843
  }
783
844
 
784
-
785
845
  // Clone a JSON-like data structure.
786
846
  // NOTE: function and instance values are copied, *not* cloned.
787
847
  function clone(val: any): any {
788
848
  const refs: any[] = []
789
- const reftype = T_function | T_instance
790
- const replacer: any = (_k: any, v: any) => 0 < (reftype & typify(v)) ?
791
- (refs.push(v), '`$REF:' + (refs.length - 1) + '`') : v
792
- const reviver: any = (_k: any, v: any, m: any) => S_string === typeof v ?
793
- (m = v.match(R_CLONE_REF), m ? refs[m[1]] : v) : v
794
- const out = NONE === val ? NONE : JSON.parse(JSON.stringify(val, replacer), reviver)
795
- return out
849
+ // Copy (don't clone) functions and non-plain-object instances by reference.
850
+ // This inlines `0 < ((T_function | T_instance) & typify(v))` — the same set
851
+ // of values (functions, class instances, bigints) without a typify() call
852
+ // per value, which dominated clone's cost.
853
+ const replacer: any = (_k: any, v: any) => {
854
+ const t = typeof v
855
+ if (
856
+ S_function === t ||
857
+ 'bigint' === t ||
858
+ (S_object === t &&
859
+ null !== v &&
860
+ !Array.isArray(v) &&
861
+ v.constructor instanceof Function &&
862
+ 'Object' !== v.constructor.name &&
863
+ 'Array' !== v.constructor.name)
864
+ ) {
865
+ return (refs.push(v), '`$REF:' + (refs.length - 1) + '`')
866
+ }
867
+ return v
868
+ }
869
+ const reviver: any = (_k: any, v: any, m: any) =>
870
+ S_string === typeof v ? ((m = re_find(R_CLONE_REF, v)), m ? refs[m[1]] : v) : v
871
+ if (NONE === val) {
872
+ return NONE
873
+ }
874
+ const json = JSON.stringify(val, replacer)
875
+ // A reviver forces V8 off its native JSON.parse fast path (a large cost). It
876
+ // is only needed to swap `$REF:n` placeholders back for the copied-by-ref
877
+ // functions/instances the replacer captured — so when none were captured,
878
+ // parse without it.
879
+ return 0 === refs.length ? JSON.parse(json) : JSON.parse(json, reviver)
796
880
  }
797
881
 
798
-
799
882
  // Define a JSON Object using function arguments.
800
883
  function jm(...kv: any[]): Record<string, any> {
801
884
  const kvsize = size(kv)
802
885
  const o: any = {}
803
886
  for (let i = 0; i < kvsize; i += 2) {
804
- let k = getprop(kv, i, '$KEY' + i)
887
+ // Builders are Group B (value processing): preserve literal stored
888
+ // values, including null. Direct array index distinguishes "slot exists
889
+ // with value null" from "slot beyond end of kv".
890
+ let k = i < kv.length ? kv[i] : '$KEY' + i
805
891
  k = 'string' === typeof k ? k : stringify(k)
806
- o[k] = getprop(kv, i + 1, null)
892
+ o[k] = i + 1 < kv.length ? kv[i + 1] : null
807
893
  }
808
894
  return o
809
895
  }
810
896
 
811
-
812
897
  // Define a JSON Array using function arguments.
813
898
  function jt(...v: any[]): any[] {
814
899
  const vsize = size(v)
@@ -819,8 +904,7 @@ function jt(...v: any[]): any[] {
819
904
  return a
820
905
  }
821
906
 
822
-
823
- // Safely delete a property from an object or array element.
907
+ // Safely delete a property from an object or array element.
824
908
  // Undefined arguments and invalid keys are ignored.
825
909
  // Returns the (possibly modified) parent.
826
910
  // For objects, the property is deleted using the delete operator.
@@ -831,11 +915,25 @@ function delprop<PARENT>(parent: PARENT, key: any): PARENT {
831
915
  return parent
832
916
  }
833
917
 
918
+ // A condensed structure is a DAG: one subtree can be reached by many
919
+ // references, so an in-place write would be visible through all of them.
920
+ // Raising is the decided contract (design Q1) - it can be relaxed to
921
+ // copy-on-write later without breaking any caller, where the reverse is not
922
+ // true. expand() gives an ordinary mutable copy.
923
+ //
924
+ // Checked AFTER the key validation: both helpers define an invalid key as
925
+ // ignored, so a call that cannot write must not start throwing merely
926
+ // because the store is condensed.
927
+ if (iscondensed(parent as any)) {
928
+ throw new Error(
929
+ 'struct: condensed structures are immutable - ' + 'expand() first, then modify the copy',
930
+ )
931
+ }
932
+
834
933
  if (ismap(parent)) {
835
934
  key = strkey(key)
836
935
  delete (parent as any)[key]
837
- }
838
- else if (islist(parent)) {
936
+ } else if (islist(parent)) {
839
937
  // Ensure key is an integer.
840
938
  let keyI = +key
841
939
 
@@ -859,7 +957,6 @@ function delprop<PARENT>(parent: PARENT, key: any): PARENT {
859
957
  return parent
860
958
  }
861
959
 
862
-
863
960
  // Safely set a property. Undefined arguments and invalid keys are ignored.
864
961
  // Returns the (possibly modified) parent.
865
962
  // If the parent is a list, and the key is negative, prepend the value.
@@ -870,12 +967,26 @@ function setprop<PARENT>(parent: PARENT, key: any, val: any): PARENT {
870
967
  return parent
871
968
  }
872
969
 
970
+ // A condensed structure is a DAG: one subtree can be reached by many
971
+ // references, so an in-place write would be visible through all of them.
972
+ // Raising is the decided contract (design Q1) - it can be relaxed to
973
+ // copy-on-write later without breaking any caller, where the reverse is not
974
+ // true. expand() gives an ordinary mutable copy.
975
+ //
976
+ // Checked AFTER the key validation: both helpers define an invalid key as
977
+ // ignored, so a call that cannot write must not start throwing merely
978
+ // because the store is condensed.
979
+ if (iscondensed(parent as any)) {
980
+ throw new Error(
981
+ 'struct: condensed structures are immutable - ' + 'expand() first, then modify the copy',
982
+ )
983
+ }
984
+
873
985
  if (ismap(parent)) {
874
986
  key = S_MT + key
875
987
  const pany = parent as any
876
988
  pany[key] = val
877
- }
878
- else if (islist(parent)) {
989
+ } else if (islist(parent)) {
879
990
  // Ensure key is an integer.
880
991
  let keyI = +key
881
992
 
@@ -901,8 +1012,12 @@ function setprop<PARENT>(parent: PARENT, key: any, val: any): PARENT {
901
1012
  return parent
902
1013
  }
903
1014
 
904
-
905
1015
  // Walk a data structure depth first, applying a function to each value.
1016
+ // The `path` argument passed to the before/after callbacks is a single
1017
+ // mutable array per depth, shared across all callback invocations for the
1018
+ // lifetime of this top-level walk call. Callbacks that need to store the
1019
+ // path MUST clone it (e.g. `path.slice()`); the contents will otherwise
1020
+ // be overwritten by subsequent visits.
906
1021
  function walk(
907
1022
  // These arguments are the public interface.
908
1023
  val: any,
@@ -919,25 +1034,51 @@ function walk(
919
1034
  // These areguments are used for recursive state.
920
1035
  key?: string | number,
921
1036
  parent?: any,
922
- path?: string[]
1037
+ path?: string[],
1038
+ pool?: string[][],
923
1039
  ): any {
1040
+ if (NONE === pool) {
1041
+ pool = [[]]
1042
+ }
924
1043
  if (NONE === path) {
925
- path = []
1044
+ path = pool[0]
926
1045
  }
927
1046
 
1047
+ const depth = path.length
1048
+
928
1049
  let out = null == before ? val : before(key, val, parent, path)
929
1050
 
930
1051
  maxdepth = null != maxdepth && 0 <= maxdepth ? maxdepth : MAXDEPTH
931
- if (0 === maxdepth || (null != path && 0 < maxdepth && maxdepth <= path.length)) {
1052
+ if (0 === maxdepth || (0 < maxdepth && maxdepth <= depth)) {
932
1053
  return out
933
1054
  }
934
1055
 
935
1056
  if (isnode(out)) {
936
- for (let [ckey, child] of items(out)) {
937
- setprop(out, ckey, walk(
938
- child, before, after, maxdepth, ckey, out,
939
- flatten([getdef(path, []), S_MT + ckey])
940
- ))
1057
+ const childDepth = depth + 1
1058
+ let childPath = pool[childDepth]
1059
+ if (NONE === childPath) {
1060
+ childPath = new Array(childDepth)
1061
+ pool[childDepth] = childPath
1062
+ }
1063
+ // Sync prefix [0..depth-1] from the current path. Only needed once per
1064
+ // parent: siblings share the same prefix and will each overwrite slot
1065
+ // [depth] below.
1066
+ for (let i = 0; i < depth; i++) {
1067
+ childPath[i] = path[i]
1068
+ }
1069
+
1070
+ // Iterate the sorted keys directly (as items() would order them) without
1071
+ // building the intermediate [key, value] tuple array items() allocates for
1072
+ // every node.
1073
+ const ckeys = keysof(out)
1074
+ for (let cI = 0; cI < ckeys.length; cI++) {
1075
+ const ckey = ckeys[cI]
1076
+ childPath[depth] = S_MT + ckey
1077
+ setprop(
1078
+ out,
1079
+ ckey,
1080
+ walk((out as any)[ckey], before, after, maxdepth, ckey, out, childPath, pool),
1081
+ )
941
1082
  }
942
1083
  }
943
1084
 
@@ -946,7 +1087,6 @@ function walk(
946
1087
  return out
947
1088
  }
948
1089
 
949
-
950
1090
  // Merge a list of values into each other. Later values have
951
1091
  // precedence. Nodes override scalars. Node kinds (list or map)
952
1092
  // override each other, and do *not* merge. The first element is
@@ -966,8 +1106,7 @@ function merge(val: any, maxdepth?: number): any {
966
1106
 
967
1107
  if (0 === lenlist) {
968
1108
  return NONE
969
- }
970
- else if (1 === lenlist) {
1109
+ } else if (1 === lenlist) {
971
1110
  return list[0]
972
1111
  }
973
1112
 
@@ -975,25 +1114,19 @@ function merge(val: any, maxdepth?: number): any {
975
1114
  out = getprop(list, 0, {})
976
1115
 
977
1116
  for (let oI = 1; oI < lenlist; oI++) {
978
- let obj = list[oI]
1117
+ const obj = list[oI]
979
1118
 
980
1119
  if (!isnode(obj)) {
981
1120
  // Nodes win.
982
1121
  out = obj
983
- }
984
- else {
1122
+ } else {
985
1123
  // Current value at path end in overriding node.
986
- let cur: any[] = [out]
1124
+ const cur: any[] = [out]
987
1125
 
988
1126
  // Current value at path end in destination node.
989
- let dst: any[] = [out]
990
-
991
- function before(
992
- key: string | number | undefined,
993
- val: any,
994
- _parent: any,
995
- path: string[]
996
- ) {
1127
+ const dst: any[] = [out]
1128
+
1129
+ function before(key: string | number | undefined, val: any, _parent: any, path: string[]) {
997
1130
  const pI = size(path)
998
1131
 
999
1132
  if (md <= pI) {
@@ -1007,18 +1140,19 @@ function merge(val: any, maxdepth?: number): any {
1007
1140
 
1008
1141
  // Descend into override node - Set up correct target in `after` function.
1009
1142
  else {
1010
-
1011
1143
  // Descend into destination node using same key.
1012
1144
  dst[pI] = 0 < pI ? getprop(dst[pI - 1], key) : dst[pI]
1013
1145
  const tval = dst[pI]
1014
1146
 
1147
+ const vtype = typify(val)
1148
+
1015
1149
  // Destination empty, so create node (unless override is class instance).
1016
- if (NONE === tval && 0 === (T_instance & typify(val))) {
1150
+ if (NONE === tval && 0 === (T_instance & vtype)) {
1017
1151
  cur[pI] = islist(val) ? [] : {}
1018
1152
  }
1019
1153
 
1020
1154
  // Matching override and destination so continue with their values.
1021
- else if (typify(val) === typify(tval)) {
1155
+ else if (vtype === typify(tval)) {
1022
1156
  cur[pI] = tval
1023
1157
  }
1024
1158
 
@@ -1038,12 +1172,7 @@ function merge(val: any, maxdepth?: number): any {
1038
1172
  return val
1039
1173
  }
1040
1174
 
1041
- function after(
1042
- key: string | number | undefined,
1043
- _val: any,
1044
- _parent: any,
1045
- path: string[]
1046
- ) {
1175
+ function after(key: string | number | undefined, _val: any, _parent: any, path: string[]) {
1047
1176
  const cI = size(path)
1048
1177
  const target = cur[cI - 1]
1049
1178
  const value = cur[cI]
@@ -1069,20 +1198,31 @@ function merge(val: any, maxdepth?: number): any {
1069
1198
  return out
1070
1199
  }
1071
1200
 
1072
-
1073
1201
  // Set a value using a path. Missing path parts are created.
1074
1202
  // String paths create only maps. Use a string list to create list parts.
1075
1203
  function setpath(
1076
1204
  store: any,
1077
1205
  path: number | string | string[],
1078
1206
  val: any,
1079
- injdef?: Partial<Injection>
1207
+ injdef?: Partial<Injection>,
1080
1208
  ) {
1209
+ // Same contract as setprop/delprop: a condensed store is immutable.
1210
+ if (iscondensed(store)) {
1211
+ throw new Error(
1212
+ 'struct: condensed structures are immutable - ' + 'expand() first, then modify the copy',
1213
+ )
1214
+ }
1215
+
1081
1216
  const pathType = typify(path)
1082
1217
 
1083
- const parts = 0 < (T_list & pathType) ? path :
1084
- 0 < (T_string & pathType) ? (path as string).split(S_DT) :
1085
- 0 < (T_number & pathType) ? [path] : NONE
1218
+ const parts =
1219
+ 0 < (T_list & pathType)
1220
+ ? path
1221
+ : 0 < (T_string & pathType)
1222
+ ? (path as string).split(S_DT)
1223
+ : 0 < (T_number & pathType)
1224
+ ? [path]
1225
+ : NONE
1086
1226
 
1087
1227
  if (NONE === parts) {
1088
1228
  return NONE
@@ -1104,26 +1244,47 @@ function setpath(
1104
1244
 
1105
1245
  if (DELETE === val) {
1106
1246
  delprop(parent, getelem(parts, -1))
1107
- }
1108
- else {
1247
+ } else {
1109
1248
  setprop(parent, getelem(parts, -1), val)
1110
1249
  }
1111
1250
 
1112
1251
  return parent
1113
1252
  }
1114
1253
 
1115
-
1116
1254
  function getpath(store: any, path: number | string | string[], injdef?: Partial<Injection>) {
1117
-
1118
1255
  // Operate on a string array.
1119
- const parts = islist(path) ? path :
1120
- 'string' === typeof path ? path.split(S_DT) :
1121
- 'number' === typeof path ? [strkey(path)] : NONE
1256
+ const parts = islist(path)
1257
+ ? path
1258
+ : 'string' === typeof path
1259
+ ? path.split(S_DT)
1260
+ : 'number' === typeof path
1261
+ ? [strkey(path)]
1262
+ : NONE
1122
1263
 
1123
1264
  if (NONE === parts) {
1124
1265
  return NONE
1125
1266
  }
1126
1267
 
1268
+ // TRANSPARENT CONDENSED READ. A condensed store answers the same path with
1269
+ // the same value as the ordinary node it was built from - the caller does
1270
+ // not need to know which it holds. Only the nodes ON the path are touched,
1271
+ // so this never materialises the whole structure.
1272
+ if (iscondensed(store)) {
1273
+ // The fast path applies only when there are no injection options to
1274
+ // honour. base/dparent/dpath/meta change what the walk below resolves
1275
+ // against, and silently ignoring them would make a condensed store
1276
+ // answer differently from the node it was built from - the opposite of
1277
+ // the transparent-read contract. Rare enough to be worth materialising.
1278
+ if (null == injdef || null == getprop(injdef, S_base)) {
1279
+ const found = condenseResolve(store, (store as any)[S_condroot], parts)
1280
+ const val = 0 > found ? undefined : condenseMaterialise(store, found)
1281
+ // Group A null-as-absent: an ordinary getpath observes a stored null
1282
+ // through getprop and reports it as absent, so this must too.
1283
+ return null == val ? undefined : val
1284
+ }
1285
+ store = expand(store)
1286
+ }
1287
+
1127
1288
  // let root = store
1128
1289
  let val = store
1129
1290
  const base = getprop(injdef, S_base)
@@ -1134,9 +1295,7 @@ function getpath(store: any, path: number | string | string[], injdef?: Partial<
1134
1295
  // An empty path (incl empty string) just finds the store.
1135
1296
  if (null == path || null == store || (1 === numparts && S_MT === parts[0])) {
1136
1297
  val = src
1137
- }
1138
- else if (0 < numparts) {
1139
-
1298
+ } else if (0 < numparts) {
1140
1299
  // Check for $ACTIONs
1141
1300
  if (1 === numparts) {
1142
1301
  val = getprop(store, parts[0])
@@ -1145,7 +1304,7 @@ function getpath(store: any, path: number | string | string[], injdef?: Partial<
1145
1304
  if (!isfunc(val)) {
1146
1305
  val = src
1147
1306
 
1148
- const m = parts[0].match(R_META_PATH)
1307
+ const m = re_find(R_META_PATH, parts[0])
1149
1308
  if (m && injdef && injdef.meta) {
1150
1309
  val = getprop(injdef.meta, m[1])
1151
1310
  parts[0] = m[3]
@@ -1158,25 +1317,23 @@ function getpath(store: any, path: number | string | string[], injdef?: Partial<
1158
1317
 
1159
1318
  if (injdef && S_DKEY === part) {
1160
1319
  part = getprop(injdef, S_key)
1161
- }
1162
- else if (injdef && part.startsWith('$GET:')) {
1320
+ } else if (injdef && part.startsWith('$GET:')) {
1163
1321
  // $GET:path$ -> get store value, use as path part (string)
1164
1322
  part = stringify(getpath(src, slice(part, 5, -1)))
1165
- }
1166
- else if (injdef && part.startsWith('$REF:')) {
1323
+ } else if (injdef && part.startsWith('$REF:')) {
1167
1324
  // $REF:refpath$ -> get spec value, use as path part (string)
1168
1325
  part = stringify(getpath(getprop(store, S_DSPEC), slice(part, 5, -1)))
1169
- }
1170
- else if (injdef && part.startsWith('$META:')) {
1326
+ } else if (injdef && part.startsWith('$META:')) {
1171
1327
  // $META:metapath$ -> get meta value, use as path part (string)
1172
1328
  part = stringify(getpath(getprop(injdef, 'meta'), slice(part, 6, -1)))
1173
1329
  }
1174
1330
 
1175
- // $$ escapes $
1176
- part = part.replace(R_DOUBLE_DOLLAR, '$')
1331
+ // $$ escapes $ (skip the regex for the common no-`$$` segment).
1332
+ if (-1 !== part.indexOf('$$')) {
1333
+ part = re_replace(R_DOUBLE_DOLLAR, part, '$')
1334
+ }
1177
1335
 
1178
1336
  if (S_MT === part) {
1179
-
1180
1337
  let ascends = 0
1181
1338
  while (S_MT === parts[1 + pI]) {
1182
1339
  ascends++
@@ -1190,26 +1347,22 @@ function getpath(store: any, path: number | string | string[], injdef?: Partial<
1190
1347
 
1191
1348
  if (0 === ascends) {
1192
1349
  val = dparent
1193
- }
1194
- else {
1350
+ } else {
1195
1351
  // const fullpath = slice(dpath, 0 - ascends).concat(parts.slice(pI + 1))
1196
1352
  const fullpath = flatten([slice(dpath, 0 - ascends), parts.slice(pI + 1)])
1197
1353
 
1198
1354
  if (ascends <= size(dpath)) {
1199
1355
  val = getpath(store, fullpath)
1200
- }
1201
- else {
1356
+ } else {
1202
1357
  val = NONE
1203
1358
  }
1204
1359
 
1205
1360
  break
1206
1361
  }
1207
- }
1208
- else {
1362
+ } else {
1209
1363
  val = dparent
1210
1364
  }
1211
- }
1212
- else {
1365
+ } else {
1213
1366
  val = getprop(val, part)
1214
1367
  }
1215
1368
  }
@@ -1228,16 +1381,11 @@ function getpath(store: any, path: number | string | string[], injdef?: Partial<
1228
1381
  return val
1229
1382
  }
1230
1383
 
1231
-
1232
1384
  // Inject values from a data store into a node recursively, resolving
1233
1385
  // paths against the store, or current if they are local. The modify
1234
1386
  // argument allows custom modification of the result. The inj
1235
1387
  // (Injection) argument is used to maintain recursive state.
1236
- function inject(
1237
- val: any,
1238
- store: any,
1239
- injdef?: Partial<Injection>,
1240
- ) {
1388
+ function inject(val: any, store: any, injdef?: Partial<Injection>) {
1241
1389
  const valtype = typeof val
1242
1390
  let inj: Injection = injdef as Injection
1243
1391
 
@@ -1265,7 +1413,6 @@ function inject(
1265
1413
 
1266
1414
  // Descend into node.
1267
1415
  if (isnode(val)) {
1268
-
1269
1416
  // Keys are sorted alphanumerically to ensure determinism.
1270
1417
  // Injection transforms ($FOO) are processed *after* other keys.
1271
1418
  // NOTE: the optional digits suffix of the transform can thus be
@@ -1276,11 +1423,10 @@ function inject(
1276
1423
 
1277
1424
  if (ismap(val)) {
1278
1425
  nodekeys = flatten([
1279
- filter(nodekeys, (n => !n[1].includes(S_DS))),
1280
- filter(nodekeys, (n => n[1].includes(S_DS))),
1426
+ filter(nodekeys, (n) => !n[1].includes(S_DS)),
1427
+ filter(nodekeys, (n) => n[1].includes(S_DS)),
1281
1428
  ])
1282
- }
1283
- else {
1429
+ } else {
1284
1430
  nodekeys = keysof(val)
1285
1431
  }
1286
1432
 
@@ -1289,7 +1435,6 @@ function inject(
1289
1435
  // 2. inj.mode=M_VAL - The child value is injected.
1290
1436
  // 3. inj.mode=M_KEYPOST - Key string is injected again, allowing child mutation.
1291
1437
  for (let nkI = 0; nkI < nodekeys.length; nkI++) {
1292
-
1293
1438
  const childinj = inj.child(nkI, nodekeys)
1294
1439
  const nodekey = childinj.key
1295
1440
  childinj.mode = M_KEYPRE
@@ -1336,17 +1481,11 @@ function inject(
1336
1481
 
1337
1482
  // Custom modification.
1338
1483
  if (inj.modify && SKIP !== val) {
1339
- let mkey = inj.key
1340
- let mparent = inj.parent
1341
- let mval = getprop(mparent, mkey)
1342
-
1343
- inj.modify(
1344
- mval,
1345
- mkey,
1346
- mparent,
1347
- inj,
1348
- store
1349
- )
1484
+ const mkey = inj.key
1485
+ const mparent = inj.parent
1486
+ const mval = getprop(mparent, mkey)
1487
+
1488
+ inj.modify(mval, mkey, mparent, inj, store)
1350
1489
  }
1351
1490
 
1352
1491
  // console.log('INJ-VAL', val)
@@ -1355,10 +1494,9 @@ function inject(
1355
1494
 
1356
1495
  // Original val reference may no longer be correct.
1357
1496
  // This return value is only used as the top level result.
1358
- return getprop(inj.parent, S_DTOP)
1497
+ return _lookup(inj.parent, S_DTOP)
1359
1498
  }
1360
1499
 
1361
-
1362
1500
  // The transform_* functions are special command inject handlers (see Injector).
1363
1501
 
1364
1502
  // Delete a key from a map or list.
@@ -1367,7 +1505,6 @@ const transform_DELETE: Injector = (inj: Injection) => {
1367
1505
  return NONE
1368
1506
  }
1369
1507
 
1370
-
1371
1508
  // Copy value from source data.
1372
1509
  const transform_COPY: Injector = (inj: Injection, _val: any) => {
1373
1510
  const ijname = 'COPY'
@@ -1376,13 +1513,12 @@ const transform_COPY: Injector = (inj: Injection, _val: any) => {
1376
1513
  return NONE
1377
1514
  }
1378
1515
 
1379
- let out = getprop(inj.dparent, inj.key)
1516
+ const out = _lookup(inj.dparent, inj.key)
1380
1517
  inj.setval(out)
1381
1518
 
1382
1519
  return out
1383
1520
  }
1384
1521
 
1385
-
1386
1522
  // As a value, inject the key of the parent node.
1387
1523
  // As a key, defined the name of the key property in the source object.
1388
1524
  const transform_KEY: Injector = (inj: Injection) => {
@@ -1394,7 +1530,7 @@ const transform_KEY: Injector = (inj: Injection) => {
1394
1530
  }
1395
1531
 
1396
1532
  // Key is defined by $KEY meta property.
1397
- const keyspec = getprop(parent, S_BKEY)
1533
+ const keyspec = _lookup(parent, S_BKEY)
1398
1534
  if (NONE !== keyspec) {
1399
1535
  delprop(parent, S_BKEY)
1400
1536
  return getprop(inj.dparent, keyspec)
@@ -1402,10 +1538,9 @@ const transform_KEY: Injector = (inj: Injection) => {
1402
1538
 
1403
1539
  // Key is defined within general purpose $META object.
1404
1540
  // return getprop(getprop(parent, S_BANNO), S_KEY, getprop(path, path.length - 2))
1405
- return getprop(getprop(parent, S_BANNO), S_KEY, getelem(path, -2))
1541
+ return _lookup(_lookup(parent, S_BANNO), S_KEY) ?? getelem(path, -2)
1406
1542
  }
1407
1543
 
1408
-
1409
1544
  // Annotate node. Does nothing itself, just used by
1410
1545
  // other injectors, and is removed when called.
1411
1546
  const transform_ANNO: Injector = (inj: Injection) => {
@@ -1414,10 +1549,9 @@ const transform_ANNO: Injector = (inj: Injection) => {
1414
1549
  return NONE
1415
1550
  }
1416
1551
 
1417
-
1418
- // Merge a list of objects into the current object.
1552
+ // Merge a list of objects into the current object.
1419
1553
  // Must be a key in an object. The value is merged over the current object.
1420
- // If the value is an array, the elements are first merged using `merge`.
1554
+ // If the value is an array, the elements are first merged using `merge`.
1421
1555
  // If the value is the empty string, merge the top level store.
1422
1556
  // Format: { '`$MERGE`': '`source-path`' | ['`source-paths`', ...] }
1423
1557
  const transform_MERGE: Injector = (inj: Injection) => {
@@ -1450,15 +1584,9 @@ const transform_MERGE: Injector = (inj: Injection) => {
1450
1584
  return out
1451
1585
  }
1452
1586
 
1453
-
1454
1587
  // Convert a node to a list.
1455
1588
  // Format: ['`$EACH`', '`source-path-of-node`', child-template]
1456
- const transform_EACH: Injector = (
1457
- inj: Injection,
1458
- _val: any,
1459
- _ref: string,
1460
- store: any
1461
- ) => {
1589
+ const transform_EACH: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
1462
1590
  const ijname = 'EACH'
1463
1591
 
1464
1592
  if (!checkPlacement(M_VAL, ijname, T_list, inj)) {
@@ -1487,24 +1615,28 @@ const transform_EACH: Injector = (
1487
1615
  let tval: any = []
1488
1616
 
1489
1617
  const tkey = getelem(inj.path, -2)
1490
- const target = getelem(inj.nodes, - 2, () => getelem(inj.nodes, -1))
1618
+ const target = getelem(inj.nodes, -2, () => getelem(inj.nodes, -1))
1491
1619
 
1492
1620
  // Create clones of the child template for each value of the current soruce.
1493
1621
  if (0 < (T_list & srctype)) {
1494
1622
  tval = items(src, () => clone(child))
1495
- }
1496
- else if (0 < (T_map & srctype)) {
1497
- tval = items(src, (n => merge([
1498
- clone(child),
1499
- // Make a note of the key for $KEY transforms.
1500
- { [S_BANNO]: { KEY: n[0] } }
1501
- ], 1)))
1623
+ } else if (0 < (T_map & srctype)) {
1624
+ tval = items(src, (n) =>
1625
+ merge(
1626
+ [
1627
+ clone(child),
1628
+ // Make a note of the key for $KEY transforms.
1629
+ { [S_BANNO]: { KEY: n[0] } },
1630
+ ],
1631
+ 1,
1632
+ ),
1633
+ )
1502
1634
  }
1503
1635
 
1504
1636
  let rval = []
1505
1637
 
1506
1638
  if (0 < size(tval)) {
1507
- tcur = null == src ? NONE : Object.values(src)
1639
+ tcur = null == src ? NONE : items(src, (n: [string, any]) => n[1])
1508
1640
 
1509
1641
  const ckey = getelem(inj.path, -2)
1510
1642
 
@@ -1542,16 +1674,10 @@ const transform_EACH: Injector = (
1542
1674
  return rval[0]
1543
1675
  }
1544
1676
 
1545
-
1546
1677
  // Convert a node to a map.
1547
1678
  // Format: { '`$PACK`':['source-path', child-template]}
1548
- const transform_PACK: Injector = (
1549
- inj: Injection,
1550
- _val: any,
1551
- _ref: string,
1552
- store: any
1553
- ) => {
1554
- const { mode, key, path, parent, nodes } = inj
1679
+ const transform_PACK: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
1680
+ const { key, path, parent, nodes } = inj
1555
1681
 
1556
1682
  const ijname = 'EACH'
1557
1683
 
@@ -1583,8 +1709,7 @@ const transform_PACK: Injector = (
1583
1709
  setprop(item[1], S_BANNO, { KEY: item[0] })
1584
1710
  return item[1]
1585
1711
  })
1586
- }
1587
- else {
1712
+ } else {
1588
1713
  src = NONE
1589
1714
  }
1590
1715
  }
@@ -1600,7 +1725,7 @@ const transform_PACK: Injector = (
1600
1725
  const child = getprop(childspec, S_BVAL, childspec)
1601
1726
 
1602
1727
  // Build parallel target object.
1603
- let tval: any = {}
1728
+ const tval: any = {}
1604
1729
 
1605
1730
  items(src, (item: [string, any]) => {
1606
1731
  const srckey = item[0]
@@ -1610,8 +1735,7 @@ const transform_PACK: Injector = (
1610
1735
  if (NONE !== keypath) {
1611
1736
  if (keypath.startsWith('`')) {
1612
1737
  key = inject(keypath, merge([{}, store, { $TOP: srcnode }], 1))
1613
- }
1614
- else {
1738
+ } else {
1615
1739
  key = getpath(srcnode, keypath, inj)
1616
1740
  }
1617
1741
  }
@@ -1622,8 +1746,7 @@ const transform_PACK: Injector = (
1622
1746
  const anno = getprop(srcnode, S_BANNO)
1623
1747
  if (NONE === anno) {
1624
1748
  delprop(tchild, S_BANNO)
1625
- }
1626
- else {
1749
+ } else {
1627
1750
  setprop(tchild, S_BANNO, anno)
1628
1751
  }
1629
1752
  })
@@ -1631,14 +1754,15 @@ const transform_PACK: Injector = (
1631
1754
  let rval = {}
1632
1755
 
1633
1756
  if (!isempty(tval)) {
1634
-
1635
1757
  // Build parallel source object.
1636
- let tsrc: any = {}
1758
+ const tsrc: any = {}
1637
1759
  src.reduce((a: any, n: any, i: any) => {
1638
- let kn = null == keypath ? i :
1639
- keypath.startsWith('`') ?
1640
- inject(keypath, merge([{}, store, { $TOP: n }], 1)) :
1641
- getpath(n, keypath, inj)
1760
+ const kn =
1761
+ null == keypath
1762
+ ? i
1763
+ : keypath.startsWith('`')
1764
+ ? inject(keypath, merge([{}, store, { $TOP: n }], 1))
1765
+ : getpath(n, keypath, inj)
1642
1766
 
1643
1767
  setprop(a, kn, n)
1644
1768
  return a
@@ -1678,16 +1802,10 @@ const transform_PACK: Injector = (
1678
1802
  return NONE
1679
1803
  }
1680
1804
 
1681
-
1682
1805
  // TODO: not found ref should removed key (setprop NONE)
1683
1806
  // Reference original spec (enables recursice transformations)
1684
1807
  // Format: ['`$REF`', '`spec-path`']
1685
- const transform_REF: Injector = (
1686
- inj: Injection,
1687
- val: any,
1688
- _ref: string,
1689
- store: any
1690
- ) => {
1808
+ const transform_REF: Injector = (inj: Injection, val: any, _ref: string, store: any) => {
1691
1809
  const { nodes } = inj
1692
1810
 
1693
1811
  if (M_VAL !== inj.mode) {
@@ -1695,7 +1813,7 @@ const transform_REF: Injector = (
1695
1813
  }
1696
1814
 
1697
1815
  // Get arguments: ['`$REF`', 'ref-path'].
1698
- const refpath = getprop(inj.parent, 1)
1816
+ const refpath = _lookup(inj.parent, 1)
1699
1817
  inj.keyI = size(inj.keys)
1700
1818
 
1701
1819
  // Spec reference.
@@ -1720,12 +1838,12 @@ const transform_REF: Injector = (
1720
1838
  })
1721
1839
  }
1722
1840
 
1723
- let tref = clone(ref)
1841
+ const tref = clone(ref)
1724
1842
 
1725
1843
  const cpath = slice(inj.path, -3)
1726
1844
  const tpath = slice(inj.path, -1)
1727
- let tcur = getpath(store, cpath)
1728
- let tval = getpath(store, tpath)
1845
+ const tcur = getpath(store, cpath)
1846
+ const tval = getpath(store, tpath)
1729
1847
  let rval = NONE
1730
1848
 
1731
1849
  if (!hasSubRef || NONE !== tval) {
@@ -1742,8 +1860,7 @@ const transform_REF: Injector = (
1742
1860
  inject(tref, store, tinj)
1743
1861
 
1744
1862
  rval = tinj.val
1745
- }
1746
- else {
1863
+ } else {
1747
1864
  rval = NONE
1748
1865
  }
1749
1866
 
@@ -1756,13 +1873,7 @@ const transform_REF: Injector = (
1756
1873
  return val
1757
1874
  }
1758
1875
 
1759
-
1760
- const transform_FORMAT: Injector = (
1761
- inj: Injection,
1762
- _val: any,
1763
- _ref: string,
1764
- store: any
1765
- ) => {
1876
+ const transform_FORMAT: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
1766
1877
  // console.log('FORMAT-START', inj, _val)
1767
1878
 
1768
1879
  // Remove remaining keys to avoid spurious processing.
@@ -1774,24 +1885,24 @@ const transform_FORMAT: Injector = (
1774
1885
 
1775
1886
  // Get arguments: ['`$FORMAT`', 'name', child].
1776
1887
  // TODO: EACH and PACK should accept customm functions too
1777
- const name = getprop(inj.parent, 1)
1778
- const child = getprop(inj.parent, 2)
1888
+ const name = _lookup(inj.parent, 1)
1889
+ const child = _lookup(inj.parent, 2)
1779
1890
 
1780
1891
  // Source data.
1781
1892
  const tkey = getelem(inj.path, -2)
1782
- const target = getelem(inj.nodes, - 2, () => getelem(inj.nodes, -1))
1893
+ const target = getelem(inj.nodes, -2, () => getelem(inj.nodes, -1))
1783
1894
 
1784
1895
  const cinj = injectChild(child, store, inj)
1785
1896
  const resolved = cinj.val
1786
1897
 
1787
- let formatter = 0 < (T_function & typify(name)) ? name : getprop(FORMATTER, name)
1898
+ const formatter = 0 < (T_function & typify(name)) ? name : getprop(FORMATTER, name)
1788
1899
 
1789
1900
  if (NONE === formatter) {
1790
1901
  inj.errs.push('$FORMAT: unknown format: ' + name + '.')
1791
1902
  return NONE
1792
1903
  }
1793
1904
 
1794
- let out = walk(resolved, formatter)
1905
+ const out = walk(resolved, formatter)
1795
1906
 
1796
1907
  setprop(target, tkey, out)
1797
1908
  // _updateAncestors(inj, target, tkey, out)
@@ -1799,17 +1910,15 @@ const transform_FORMAT: Injector = (
1799
1910
  return out
1800
1911
  }
1801
1912
 
1802
-
1803
1913
  const FORMATTER: Record<string, WalkApply> = {
1804
1914
  identity: (_k: any, v: any) => v,
1805
- upper: (_k: any, v: any) => isnode(v) ? v : ('' + v).toUpperCase(),
1806
- lower: (_k: any, v: any) => isnode(v) ? v : ('' + v).toLowerCase(),
1807
- string: (_k: any, v: any) => isnode(v) ? v : ('' + v),
1915
+ upper: (_k: any, v: any) => (isnode(v) ? v : ('' + v).toUpperCase()),
1916
+ lower: (_k: any, v: any) => (isnode(v) ? v : ('' + v).toLowerCase()),
1917
+ string: (_k: any, v: any) => (isnode(v) ? v : '' + v),
1808
1918
  number: (_k: any, v: any) => {
1809
1919
  if (isnode(v)) {
1810
1920
  return v
1811
- }
1812
- else {
1921
+ } else {
1813
1922
  let n = Number(v)
1814
1923
  if (isNaN(n)) {
1815
1924
  n = 0
@@ -1820,8 +1929,7 @@ const FORMATTER: Record<string, WalkApply> = {
1820
1929
  integer: (_k: any, v: any) => {
1821
1930
  if (isnode(v)) {
1822
1931
  return v
1823
- }
1824
- else {
1932
+ } else {
1825
1933
  let n = Number(v)
1826
1934
  if (isNaN(n)) {
1827
1935
  n = 0
@@ -1830,17 +1938,15 @@ const FORMATTER: Record<string, WalkApply> = {
1830
1938
  }
1831
1939
  },
1832
1940
  concat: (k: any, v: any) =>
1833
- null == k && islist(v) ? join(items(v, (n => isnode(n[1]) ? S_MT : (S_MT + n[1]))), S_MT) : v
1941
+ null == k && islist(v)
1942
+ ? join(
1943
+ items(v, (n) => (isnode(n[1]) ? S_MT : S_MT + n[1])),
1944
+ S_MT,
1945
+ )
1946
+ : v,
1834
1947
  }
1835
1948
 
1836
-
1837
-
1838
- const transform_APPLY: Injector = (
1839
- inj: Injection,
1840
- _val: any,
1841
- _ref: string,
1842
- store: any
1843
- ) => {
1949
+ const transform_APPLY: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
1844
1950
  const ijname = 'APPLY'
1845
1951
 
1846
1952
  if (!checkPlacement(M_VAL, ijname, T_list, inj)) {
@@ -1855,7 +1961,7 @@ const transform_APPLY: Injector = (
1855
1961
  }
1856
1962
 
1857
1963
  const tkey = getelem(inj.path, -2)
1858
- const target = getelem(inj.nodes, - 2, () => getelem(inj.nodes, -1))
1964
+ const target = getelem(inj.nodes, -2, () => getelem(inj.nodes, -1))
1859
1965
 
1860
1966
  const cinj = injectChild(child, store, inj)
1861
1967
  const resolved = cinj.val
@@ -1867,14 +1973,13 @@ const transform_APPLY: Injector = (
1867
1973
  return out
1868
1974
  }
1869
1975
 
1870
-
1871
1976
  // Transform data using spec.
1872
1977
  // Only operates on static JSON-like data.
1873
1978
  // Arrays are treated as if they are objects with indices as keys.
1874
1979
  function transform(
1875
1980
  data: any, // Source data to transform into new data (original not mutated)
1876
1981
  spec: any, // Transform specification; output follows this shape
1877
- injdef?: Partial<Injection>
1982
+ injdef?: Partial<Injection>,
1878
1983
  ) {
1879
1984
  // Clone the spec so that the clone can be modified in place as the transform result.
1880
1985
  const origspec = spec
@@ -1886,57 +1991,64 @@ function transform(
1886
1991
  const errs = injdef?.errs || []
1887
1992
 
1888
1993
  const extraTransforms: any = {}
1889
- const extraData = null == extra ? NONE : items(extra)
1890
- .reduce((a: any, n: any[]) =>
1891
- (n[0].startsWith(S_DS) ? extraTransforms[n[0]] = n[1] : (a[n[0]] = n[1]), a), {})
1892
-
1893
- const dataClone = merge([
1894
- isempty(extraData) ? NONE : clone(extraData),
1895
- clone(data),
1896
- ])
1994
+ const extraData =
1995
+ null == extra
1996
+ ? NONE
1997
+ : items(extra).reduce(
1998
+ (a: any, n: any[]) => (
1999
+ n[0].startsWith(S_DS) ? (extraTransforms[n[0]] = n[1]) : (a[n[0]] = n[1]),
2000
+ a
2001
+ ),
2002
+ {},
2003
+ )
2004
+
2005
+ const dataClone = merge([isempty(extraData) ? NONE : clone(extraData), clone(data)])
1897
2006
 
1898
2007
  // Define a top level store that provides transform operations.
1899
- const store = merge([
1900
- {
1901
- // The inject function recognises this special location for the root of the source data.
1902
- // NOTE: to escape data that contains "`$FOO`" keys at the top level,
1903
- // place that data inside a holding map: { myholder: mydata }.
1904
- $TOP: dataClone,
1905
-
1906
- $SPEC: () => origspec,
1907
-
1908
- // Escape backtick (this also works inside backticks).
1909
- $BT: () => S_BT,
1910
-
1911
- // Escape dollar sign (this also works inside backticks).
1912
- $DS: () => S_DS,
1913
-
1914
- // Insert current date and time as an ISO string.
1915
- $WHEN: () => new Date().toISOString(),
1916
-
1917
- $DELETE: transform_DELETE,
1918
- $COPY: transform_COPY,
1919
- $KEY: transform_KEY,
1920
- $ANNO: transform_ANNO,
1921
- $MERGE: transform_MERGE,
1922
- $EACH: transform_EACH,
1923
- $PACK: transform_PACK,
1924
- $REF: transform_REF,
1925
- $FORMAT: transform_FORMAT,
1926
- $APPLY: transform_APPLY,
1927
- },
1928
-
1929
- // Custom extra transforms, if any.
1930
- extraTransforms,
1931
-
1932
- {
1933
- $ERRS: errs,
1934
- }
1935
- ], 1)
2008
+ const store = merge(
2009
+ [
2010
+ {
2011
+ // The inject function recognises this special location for the root of the source data.
2012
+ // NOTE: to escape data that contains "`$FOO`" keys at the top level,
2013
+ // place that data inside a holding map: { myholder: mydata }.
2014
+ $TOP: dataClone,
2015
+
2016
+ $SPEC: () => origspec,
2017
+
2018
+ // Escape backtick (this also works inside backticks).
2019
+ $BT: () => S_BT,
2020
+
2021
+ // Escape dollar sign (this also works inside backticks).
2022
+ $DS: () => S_DS,
2023
+
2024
+ // Insert current date and time as an ISO string.
2025
+ $WHEN: () => new Date().toISOString(),
2026
+
2027
+ $DELETE: transform_DELETE,
2028
+ $COPY: transform_COPY,
2029
+ $KEY: transform_KEY,
2030
+ $ANNO: transform_ANNO,
2031
+ $MERGE: transform_MERGE,
2032
+ $EACH: transform_EACH,
2033
+ $PACK: transform_PACK,
2034
+ $REF: transform_REF,
2035
+ $FORMAT: transform_FORMAT,
2036
+ $APPLY: transform_APPLY,
2037
+ },
2038
+
2039
+ // Custom extra transforms, if any.
2040
+ extraTransforms,
2041
+
2042
+ {
2043
+ $ERRS: errs,
2044
+ },
2045
+ ],
2046
+ 1,
2047
+ )
1936
2048
 
1937
2049
  const out = inject(spec, store, injdef)
1938
2050
 
1939
- const generr = (0 < size(errs) && !collect)
2051
+ const generr = 0 < size(errs) && !collect
1940
2052
  if (generr) {
1941
2053
  throw new Error(join(errs, ' | '))
1942
2054
  }
@@ -1944,20 +2056,19 @@ function transform(
1944
2056
  return out
1945
2057
  }
1946
2058
 
1947
-
1948
2059
  // A required string value. NOTE: Rejects empty strings.
1949
2060
  const validate_STRING: Injector = (inj: Injection) => {
1950
- let out = getprop(inj.dparent, inj.key)
2061
+ const out = _lookup(inj.dparent, inj.key)
1951
2062
 
1952
2063
  const t = typify(out)
1953
2064
  if (0 === (T_string & t)) {
1954
- let msg = _invalidTypeMsg(inj.path, S_string, t, out, 'V1010')
2065
+ const msg = _invalidTypeMsg(inj.path, S_string, t, out, 'V1010')
1955
2066
  inj.errs.push(msg)
1956
2067
  return NONE
1957
2068
  }
1958
2069
 
1959
2070
  if (S_MT === out) {
1960
- let msg = 'Empty string at ' + pathify(inj.path, 1)
2071
+ const msg = 'Empty string at ' + pathify(inj.path, 1)
1961
2072
  inj.errs.push(msg)
1962
2073
  return NONE
1963
2074
  }
@@ -1965,13 +2076,10 @@ const validate_STRING: Injector = (inj: Injection) => {
1965
2076
  return out
1966
2077
  }
1967
2078
 
1968
-
1969
-
1970
-
1971
2079
  const validate_TYPE: Injector = (inj: Injection, _val: any, ref: string) => {
1972
2080
  const tname = slice(ref, 1).toLowerCase()
1973
2081
  const typev = 1 << (31 - TYPENAME.indexOf(tname))
1974
- let out = getprop(inj.dparent, inj.key)
2082
+ const out = _lookup(inj.dparent, inj.key)
1975
2083
 
1976
2084
  const t = typify(out)
1977
2085
 
@@ -1985,15 +2093,12 @@ const validate_TYPE: Injector = (inj: Injection, _val: any, ref: string) => {
1985
2093
  return out
1986
2094
  }
1987
2095
 
1988
-
1989
2096
  // Allow any value.
1990
2097
  const validate_ANY: Injector = (inj: Injection) => {
1991
- let out = getprop(inj.dparent, inj.key)
2098
+ const out = _lookup(inj.dparent, inj.key)
1992
2099
  return out
1993
2100
  }
1994
2101
 
1995
-
1996
-
1997
2102
  // Specify child values for map or list.
1998
2103
  // Map syntax: {'`$CHILD`': child-template }
1999
2104
  // List syntax: ['`$CHILD`', child-template ]
@@ -2012,15 +2117,13 @@ const validate_CHILD: Injector = (inj: Injection) => {
2012
2117
 
2013
2118
  if (NONE == tval) {
2014
2119
  tval = {}
2015
- }
2016
- else if (!ismap(tval)) {
2017
- inj.errs.push(_invalidTypeMsg(
2018
- slice(inj.path, -1), S_object, typify(tval), tval), 'V0220')
2120
+ } else if (!ismap(tval)) {
2121
+ inj.errs.push(_invalidTypeMsg(slice(inj.path, -1), S_object, typify(tval), tval), 'V0220')
2019
2122
  return NONE
2020
2123
  }
2021
2124
 
2022
2125
  const ckeys = keysof(tval)
2023
- for (let ckey of ckeys) {
2126
+ for (const ckey of ckeys) {
2024
2127
  setprop(parent, ckey, clone(childtm))
2025
2128
 
2026
2129
  // NOTE: modifying inj! This extends the child value loop in inject.
@@ -2034,14 +2137,13 @@ const validate_CHILD: Injector = (inj: Injection) => {
2034
2137
 
2035
2138
  // List syntax.
2036
2139
  if (M_VAL === mode) {
2037
-
2038
2140
  if (!islist(parent)) {
2039
2141
  // $CHILD was not inside a list.
2040
2142
  inj.errs.push('Invalid $CHILD as value')
2041
2143
  return NONE
2042
2144
  }
2043
2145
 
2044
- const childtm = getprop(parent, 1)
2146
+ const childtm = _lookup(parent, 1)
2045
2147
 
2046
2148
  if (NONE === inj.dparent) {
2047
2149
  // Empty list as default.
@@ -2052,46 +2154,58 @@ const validate_CHILD: Injector = (inj: Injection) => {
2052
2154
 
2053
2155
  if (!islist(inj.dparent)) {
2054
2156
  const msg = _invalidTypeMsg(
2055
- slice(inj.path, -1), S_list, typify(inj.dparent), inj.dparent, 'V0230')
2157
+ slice(inj.path, -1),
2158
+ S_list,
2159
+ typify(inj.dparent),
2160
+ inj.dparent,
2161
+ 'V0230',
2162
+ )
2056
2163
  inj.errs.push(msg)
2057
2164
  inj.keyI = size(parent)
2058
2165
  return inj.dparent
2059
2166
  }
2060
2167
 
2061
- // Clone children abd reset inj key index.
2168
+ // Clone children and reset inj key index.
2062
2169
  // The inject child loop will now iterate over the cloned children,
2063
- // validating them againt the current list values.
2170
+ // validating them against the current list values.
2064
2171
  items(inj.dparent, (n) => setprop(parent, n[0], clone(childtm)))
2065
2172
  slice(parent, 0, inj.dparent.length, true)
2066
- inj.keyI = 0
2067
2173
 
2068
- const out = getprop(inj.dparent, 0)
2069
- return out
2174
+ // NOTE: modifying inj! This extends the child value loop in inject
2175
+ // to cover every cloned child.
2176
+ for (let ckeyI = size(keys); ckeyI < size(parent); ckeyI++) {
2177
+ keys.push(strkey(ckeyI))
2178
+ }
2179
+
2180
+ // Restart the child value loop at the first element (the loop
2181
+ // increments keyI on resume) so that the first element is also
2182
+ // validated against the child template.
2183
+ inj.keyI = -1
2184
+
2185
+ // SKIP leaves the cloned child template in place at the first
2186
+ // element so the resumed loop can validate it.
2187
+ return SKIP
2070
2188
  }
2071
2189
 
2072
2190
  return NONE
2073
2191
  }
2074
2192
 
2075
-
2076
2193
  // TODO: implement SOME, ALL
2077
2194
  // FIX: ONE should mean exactly one, not at least one (=SOME)
2078
2195
  // TODO: implement a generate validate_ALT to do all of these
2079
2196
  // Match at least one of the specified shapes.
2080
2197
  // Syntax: ['`$ONE`', alt0, alt1, ...]
2081
- const validate_ONE: Injector = (
2082
- inj: Injection,
2083
- _val: any,
2084
- _ref: string,
2085
- store: any
2086
- ) => {
2198
+ const validate_ONE: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
2087
2199
  const { mode, parent, keyI } = inj
2088
2200
 
2089
2201
  // Only operate in val mode, since parent is a list.
2090
2202
  if (M_VAL === mode) {
2091
2203
  if (!islist(parent) || 0 !== keyI) {
2092
- inj.errs.push('The $ONE validator at field ' +
2093
- pathify(inj.path, 1, 1) +
2094
- ' must be the first element of an array.')
2204
+ inj.errs.push(
2205
+ 'The $ONE validator at field ' +
2206
+ pathify(inj.path, 1, 1) +
2207
+ ' must be the first element of an array.',
2208
+ )
2095
2209
  return
2096
2210
  }
2097
2211
 
@@ -2103,19 +2217,20 @@ const validate_ONE: Injector = (
2103
2217
  inj.path = slice(inj.path, -1)
2104
2218
  inj.key = getelem(inj.path, -1)
2105
2219
 
2106
- let tvals = slice(parent, 1)
2220
+ const tvals = slice(parent, 1)
2107
2221
  if (0 === size(tvals)) {
2108
- inj.errs.push('The $ONE validator at field ' +
2109
- pathify(inj.path, 1, 1) +
2110
- ' must have at least one argument.')
2222
+ inj.errs.push(
2223
+ 'The $ONE validator at field ' +
2224
+ pathify(inj.path, 1, 1) +
2225
+ ' must have at least one argument.',
2226
+ )
2111
2227
  return
2112
2228
  }
2113
2229
 
2114
2230
  // See if we can find a match.
2115
- for (let tval of tvals) {
2116
-
2231
+ for (const tval of tvals) {
2117
2232
  // If match, then errs.length = 0
2118
- let terrs: any[] = []
2233
+ const terrs: any[] = []
2119
2234
 
2120
2235
  const vstore = merge([{}, store], 1)
2121
2236
  vstore.$TOP = inj.dparent
@@ -2135,27 +2250,38 @@ const validate_ONE: Injector = (
2135
2250
  }
2136
2251
 
2137
2252
  // There was no match.
2138
- const valdesc =
2139
- replace(join(items(tvals, (n) => stringify(n[1])), ', '),
2140
- R_TRANSFORM_NAME, (_m: any, p1: string) => p1.toLowerCase())
2253
+ const valdesc = replace(
2254
+ join(
2255
+ items(tvals, (n) => stringify(n[1])),
2256
+ ', ',
2257
+ ),
2258
+ R_TRANSFORM_NAME,
2259
+ (_m: any, p1: string) => p1.toLowerCase(),
2260
+ )
2141
2261
 
2142
- inj.errs.push(_invalidTypeMsg(
2143
- inj.path,
2144
- (1 < size(tvals) ? 'one of ' : '') + valdesc,
2145
- typify(inj.dparent), inj.dparent, 'V0210'))
2262
+ inj.errs.push(
2263
+ _invalidTypeMsg(
2264
+ inj.path,
2265
+ (1 < size(tvals) ? 'one of ' : '') + valdesc,
2266
+ typify(inj.dparent),
2267
+ inj.dparent,
2268
+ 'V0210',
2269
+ ),
2270
+ )
2146
2271
  }
2147
2272
  }
2148
2273
 
2149
-
2150
2274
  const validate_EXACT: Injector = (inj: Injection) => {
2151
2275
  const { mode, parent, key, keyI } = inj
2152
2276
 
2153
2277
  // Only operate in val mode, since parent is a list.
2154
2278
  if (M_VAL === mode) {
2155
2279
  if (!islist(parent) || 0 !== keyI) {
2156
- inj.errs.push('The $EXACT validator at field ' +
2157
- pathify(inj.path, 1, 1) +
2158
- ' must be the first element of an array.')
2280
+ inj.errs.push(
2281
+ 'The $EXACT validator at field ' +
2282
+ pathify(inj.path, 1, 1) +
2283
+ ' must be the first element of an array.',
2284
+ )
2159
2285
  return
2160
2286
  }
2161
2287
 
@@ -2168,17 +2294,19 @@ const validate_EXACT: Injector = (inj: Injection) => {
2168
2294
  inj.path = slice(inj.path, 0, -1)
2169
2295
  inj.key = getelem(inj.path, -1)
2170
2296
 
2171
- let tvals = slice(parent, 1)
2297
+ const tvals = slice(parent, 1)
2172
2298
  if (0 === size(tvals)) {
2173
- inj.errs.push('The $EXACT validator at field ' +
2174
- pathify(inj.path, 1, 1) +
2175
- ' must have at least one argument.')
2299
+ inj.errs.push(
2300
+ 'The $EXACT validator at field ' +
2301
+ pathify(inj.path, 1, 1) +
2302
+ ' must have at least one argument.',
2303
+ )
2176
2304
  return
2177
2305
  }
2178
2306
 
2179
2307
  // See if we can find an exact value match.
2180
2308
  let currentstr: string | undefined = undefined
2181
- for (let tval of tvals) {
2309
+ for (const tval of tvals) {
2182
2310
  let exactmatch = tval === inj.dparent
2183
2311
 
2184
2312
  if (!exactmatch && isnode(tval)) {
@@ -2193,31 +2321,35 @@ const validate_EXACT: Injector = (inj: Injection) => {
2193
2321
  }
2194
2322
 
2195
2323
  // There was no match.
2196
- const valdesc =
2197
- replace(join(items(tvals, (n) => stringify(n[1])), ', '),
2198
- R_TRANSFORM_NAME, (_m: any, p1: string) => p1.toLowerCase())
2324
+ const valdesc = replace(
2325
+ join(
2326
+ items(tvals, (n) => stringify(n[1])),
2327
+ ', ',
2328
+ ),
2329
+ R_TRANSFORM_NAME,
2330
+ (_m: any, p1: string) => p1.toLowerCase(),
2331
+ )
2199
2332
 
2200
- inj.errs.push(_invalidTypeMsg(
2201
- inj.path,
2202
- (1 < size(inj.path) ? '' : 'value ') +
2203
- 'exactly equal to ' + (1 === size(tvals) ? '' : 'one of ') + valdesc,
2204
- typify(inj.dparent), inj.dparent, 'V0110'))
2205
- }
2206
- else {
2333
+ inj.errs.push(
2334
+ _invalidTypeMsg(
2335
+ inj.path,
2336
+ (1 < size(inj.path) ? '' : 'value ') +
2337
+ 'exactly equal to ' +
2338
+ (1 === size(tvals) ? '' : 'one of ') +
2339
+ valdesc,
2340
+ typify(inj.dparent),
2341
+ inj.dparent,
2342
+ 'V0110',
2343
+ ),
2344
+ )
2345
+ } else {
2207
2346
  delprop(parent, key)
2208
2347
  }
2209
2348
  }
2210
2349
 
2211
-
2212
2350
  // This is the "modify" argument to inject. Use this to perform
2213
2351
  // generic validation. Runs *after* any special commands.
2214
- const _validation: Modify = (
2215
- pval: any,
2216
- key?: any,
2217
- parent?: any,
2218
- inj?: Injection,
2219
- ) => {
2220
-
2352
+ const _validation: Modify = (pval: any, key?: any, parent?: any, inj?: Injection) => {
2221
2353
  if (NONE === inj) {
2222
2354
  return
2223
2355
  }
@@ -2263,40 +2395,37 @@ const _validation: Modify = (
2263
2395
  // Empty spec object {} means object can be open (any keys).
2264
2396
  if (0 < size(pkeys) && true !== getprop(pval, '`$OPEN`')) {
2265
2397
  const badkeys = []
2266
- for (let ckey of ckeys) {
2267
- if (!haskey(pval, ckey)) {
2398
+ for (const ckey of ckeys) {
2399
+ // Literal presence: _validation needs to know if the SHAPE declares
2400
+ // this key, regardless of whether the validator stored null in that
2401
+ // slot. The Group A haskey would miss null-valued slots.
2402
+ if (NONE === _lookup(pval, ckey)) {
2268
2403
  badkeys.push(ckey)
2269
2404
  }
2270
2405
  }
2271
2406
 
2272
2407
  // Closed object, so reject extra keys not in shape.
2273
2408
  if (0 < size(badkeys)) {
2274
- const msg =
2275
- 'Unexpected keys at field ' + pathify(inj.path, 1) + S_VIZ + join(badkeys, ', ')
2409
+ const msg = 'Unexpected keys at field ' + pathify(inj.path, 1) + S_VIZ + join(badkeys, ', ')
2276
2410
  inj.errs.push(msg)
2277
2411
  }
2278
- }
2279
- else {
2412
+ } else {
2280
2413
  // Object is open, so merge in extra keys.
2281
2414
  merge([pval, cval])
2282
2415
  if (isnode(pval)) {
2283
2416
  delprop(pval, '`$OPEN`')
2284
2417
  }
2285
2418
  }
2286
- }
2287
- else if (islist(cval)) {
2419
+ } else if (islist(cval)) {
2288
2420
  if (!islist(pval)) {
2289
2421
  inj.errs.push(_invalidTypeMsg(inj.path, typename(ptype), ctype, cval, 'V0030'))
2290
2422
  }
2291
- }
2292
- else if (exact) {
2423
+ } else if (exact) {
2293
2424
  if (cval !== pval) {
2294
2425
  const pathmsg = 1 < size(inj.path) ? 'at field ' + pathify(inj.path, 1) + S_VIZ : S_MT
2295
- inj.errs.push('Value ' + pathmsg + cval +
2296
- ' should equal ' + pval + S_DT)
2426
+ inj.errs.push('Value ' + pathmsg + cval + ' should equal ' + pval + S_DT)
2297
2427
  }
2298
- }
2299
- else {
2428
+ } else {
2300
2429
  // Spec value was a default, copy over data
2301
2430
  setprop(parent, key, cval)
2302
2431
  }
@@ -2304,8 +2433,6 @@ const _validation: Modify = (
2304
2433
  return
2305
2434
  }
2306
2435
 
2307
-
2308
-
2309
2436
  // Validate a data structure against a shape specification. The shape
2310
2437
  // specification follows the "by example" principle. Plain data in
2311
2438
  // teh shape is treated as default values that also specify the
@@ -2319,51 +2446,54 @@ const _validation: Modify = (
2319
2446
  function validate(
2320
2447
  data: any, // Source data to transform into new data (original not mutated)
2321
2448
  spec: any, // Transform specification; output follows this shape
2322
- injdef?: Partial<Injection>
2449
+ injdef?: Partial<Injection>,
2323
2450
  ) {
2324
2451
  const extra = injdef?.extra
2325
2452
 
2326
2453
  const collect = null != injdef?.errs
2327
2454
  const errs = injdef?.errs || []
2328
2455
 
2329
- const store = merge([
2330
- {
2331
- // Remove the transform commands.
2332
- $DELETE: null,
2333
- $COPY: null,
2334
- $KEY: null,
2335
- $META: null,
2336
- $MERGE: null,
2337
- $EACH: null,
2338
- $PACK: null,
2339
-
2340
- $STRING: validate_STRING,
2341
- $NUMBER: validate_TYPE,
2342
- $INTEGER: validate_TYPE,
2343
- $DECIMAL: validate_TYPE,
2344
- $BOOLEAN: validate_TYPE,
2345
- $NULL: validate_TYPE,
2346
- $NIL: validate_TYPE,
2347
- $MAP: validate_TYPE,
2348
- $LIST: validate_TYPE,
2349
- $FUNCTION: validate_TYPE,
2350
- $INSTANCE: validate_TYPE,
2351
- $ANY: validate_ANY,
2352
- $CHILD: validate_CHILD,
2353
- $ONE: validate_ONE,
2354
- $EXACT: validate_EXACT,
2355
- },
2356
-
2357
- getdef(extra, {}),
2358
-
2359
- // A special top level value to collect errors.
2360
- // NOTE: collecterrs parameter always wins.
2361
- {
2362
- $ERRS: errs,
2363
- }
2364
- ], 1)
2365
-
2366
- let meta = getprop(injdef, 'meta', {})
2456
+ const store = merge(
2457
+ [
2458
+ {
2459
+ // Remove the transform commands.
2460
+ $DELETE: null,
2461
+ $COPY: null,
2462
+ $KEY: null,
2463
+ $META: null,
2464
+ $MERGE: null,
2465
+ $EACH: null,
2466
+ $PACK: null,
2467
+
2468
+ $STRING: validate_STRING,
2469
+ $NUMBER: validate_TYPE,
2470
+ $INTEGER: validate_TYPE,
2471
+ $DECIMAL: validate_TYPE,
2472
+ $BOOLEAN: validate_TYPE,
2473
+ $NULL: validate_TYPE,
2474
+ $NIL: validate_TYPE,
2475
+ $MAP: validate_TYPE,
2476
+ $LIST: validate_TYPE,
2477
+ $FUNCTION: validate_TYPE,
2478
+ $INSTANCE: validate_TYPE,
2479
+ $ANY: validate_ANY,
2480
+ $CHILD: validate_CHILD,
2481
+ $ONE: validate_ONE,
2482
+ $EXACT: validate_EXACT,
2483
+ },
2484
+
2485
+ getdef(extra, {}),
2486
+
2487
+ // A special top level value to collect errors.
2488
+ // NOTE: collecterrs parameter always wins.
2489
+ {
2490
+ $ERRS: errs,
2491
+ },
2492
+ ],
2493
+ 1,
2494
+ )
2495
+
2496
+ const meta = getprop(injdef, 'meta', {})
2367
2497
  setprop(meta, S_BEXACT, getprop(meta, S_BEXACT, false))
2368
2498
 
2369
2499
  const out = transform(data, spec, {
@@ -2374,7 +2504,7 @@ function validate(
2374
2504
  errs,
2375
2505
  })
2376
2506
 
2377
- const generr = (0 < size(errs) && !collect)
2507
+ const generr = 0 < size(errs) && !collect
2378
2508
  if (generr) {
2379
2509
  throw new Error(join(errs, ' | '))
2380
2510
  }
@@ -2382,10 +2512,9 @@ function validate(
2382
2512
  return out
2383
2513
  }
2384
2514
 
2385
-
2386
2515
  const select_AND: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
2387
2516
  if (M_KEYPRE === inj.mode) {
2388
- const terms = getprop(inj.parent, inj.key)
2517
+ const terms = _lookup(inj.parent, inj.key)
2389
2518
 
2390
2519
  const ppath = slice(inj.path, -1)
2391
2520
  const point = getpath(store, ppath)
@@ -2393,8 +2522,8 @@ const select_AND: Injector = (inj: Injection, _val: any, _ref: string, store: an
2393
2522
  const vstore = merge([{}, store], 1)
2394
2523
  vstore.$TOP = point
2395
2524
 
2396
- for (let term of terms) {
2397
- let terrs: any[] = []
2525
+ for (const term of terms) {
2526
+ const terrs: any[] = []
2398
2527
 
2399
2528
  validate(point, term, {
2400
2529
  extra: vstore,
@@ -2404,7 +2533,8 @@ const select_AND: Injector = (inj: Injection, _val: any, _ref: string, store: an
2404
2533
 
2405
2534
  if (0 != size(terrs)) {
2406
2535
  inj.errs.push(
2407
- 'AND:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(terms))
2536
+ 'AND:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(terms),
2537
+ )
2408
2538
  }
2409
2539
  }
2410
2540
 
@@ -2414,10 +2544,9 @@ const select_AND: Injector = (inj: Injection, _val: any, _ref: string, store: an
2414
2544
  }
2415
2545
  }
2416
2546
 
2417
-
2418
2547
  const select_OR: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
2419
2548
  if (M_KEYPRE === inj.mode) {
2420
- const terms = getprop(inj.parent, inj.key)
2549
+ const terms = _lookup(inj.parent, inj.key)
2421
2550
 
2422
2551
  const ppath = slice(inj.path, -1)
2423
2552
  const point = getpath(store, ppath)
@@ -2425,8 +2554,8 @@ const select_OR: Injector = (inj: Injection, _val: any, _ref: string, store: any
2425
2554
  const vstore = merge([{}, store], 1)
2426
2555
  vstore.$TOP = point
2427
2556
 
2428
- for (let term of terms) {
2429
- let terrs: any[] = []
2557
+ for (const term of terms) {
2558
+ const terrs: any[] = []
2430
2559
 
2431
2560
  validate(point, term, {
2432
2561
  extra: vstore,
@@ -2443,15 +2572,13 @@ const select_OR: Injector = (inj: Injection, _val: any, _ref: string, store: any
2443
2572
  }
2444
2573
  }
2445
2574
 
2446
- inj.errs.push(
2447
- 'OR:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(terms))
2575
+ inj.errs.push('OR:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(terms))
2448
2576
  }
2449
2577
  }
2450
2578
 
2451
-
2452
2579
  const select_NOT: Injector = (inj: Injection, _val: any, _ref: string, store: any) => {
2453
2580
  if (M_KEYPRE === inj.mode) {
2454
- const term = getprop(inj.parent, inj.key)
2581
+ const term = _lookup(inj.parent, inj.key)
2455
2582
 
2456
2583
  const ppath = slice(inj.path, -1)
2457
2584
  const point = getpath(store, ppath)
@@ -2459,7 +2586,7 @@ const select_NOT: Injector = (inj: Injection, _val: any, _ref: string, store: an
2459
2586
  const vstore = merge([{}, store], 1)
2460
2587
  vstore.$TOP = point
2461
2588
 
2462
- let terrs: any[] = []
2589
+ const terrs: any[] = []
2463
2590
 
2464
2591
  validate(point, term, {
2465
2592
  extra: vstore,
@@ -2468,8 +2595,7 @@ const select_NOT: Injector = (inj: Injection, _val: any, _ref: string, store: an
2468
2595
  })
2469
2596
 
2470
2597
  if (0 == size(terrs)) {
2471
- inj.errs.push(
2472
- 'NOT:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(term))
2598
+ inj.errs.push('NOT:' + pathify(ppath) + S_VIZ + stringify(point) + ' fail:' + stringify(term))
2473
2599
  }
2474
2600
 
2475
2601
  const gkey = getelem(inj.path, -2)
@@ -2478,10 +2604,9 @@ const select_NOT: Injector = (inj: Injection, _val: any, _ref: string, store: an
2478
2604
  }
2479
2605
  }
2480
2606
 
2481
-
2482
2607
  const select_CMP: Injector = (inj: Injection, _val: any, ref: string, store: any) => {
2483
2608
  if (M_KEYPRE === inj.mode) {
2484
- const term = getprop(inj.parent, inj.key)
2609
+ const term = _lookup(inj.parent, inj.key)
2485
2610
  // const src = getprop(store, inj.base, store)
2486
2611
  const gkey = getelem(inj.path, -2)
2487
2612
 
@@ -2494,17 +2619,13 @@ const select_CMP: Injector = (inj: Injection, _val: any, ref: string, store: any
2494
2619
 
2495
2620
  if ('$GT' === ref && point > term) {
2496
2621
  pass = true
2497
- }
2498
- else if ('$LT' === ref && point < term) {
2622
+ } else if ('$LT' === ref && point < term) {
2499
2623
  pass = true
2500
- }
2501
- else if ('$GTE' === ref && point >= term) {
2624
+ } else if ('$GTE' === ref && point >= term) {
2502
2625
  pass = true
2503
- }
2504
- else if ('$LTE' === ref && point <= term) {
2626
+ } else if ('$LTE' === ref && point <= term) {
2505
2627
  pass = true
2506
- }
2507
- else if ('$LIKE' === ref && stringify(point).match(RegExp(term))) {
2628
+ } else if ('$LIKE' === ref && re_test(re_compile(term), stringify(point))) {
2508
2629
  pass = true
2509
2630
  }
2510
2631
 
@@ -2512,17 +2633,23 @@ const select_CMP: Injector = (inj: Injection, _val: any, ref: string, store: any
2512
2633
  // Update spec to match found value so that _validate does not complain.
2513
2634
  const gp = getelem(inj.nodes, -2)
2514
2635
  setprop(gp, gkey, point)
2515
- }
2516
- else {
2517
- inj.errs.push('CMP: ' + pathify(ppath) + S_VIZ + stringify(point) +
2518
- ' fail:' + ref + ' ' + stringify(term))
2636
+ } else {
2637
+ inj.errs.push(
2638
+ 'CMP: ' +
2639
+ pathify(ppath) +
2640
+ S_VIZ +
2641
+ stringify(point) +
2642
+ ' fail:' +
2643
+ ref +
2644
+ ' ' +
2645
+ stringify(term),
2646
+ )
2519
2647
  }
2520
2648
  }
2521
2649
 
2522
2650
  return NONE
2523
2651
  }
2524
2652
 
2525
-
2526
2653
  // Select children from a top-level object that match a MongoDB-style query.
2527
2654
  // Supports $and, $or, and equality comparisons.
2528
2655
  // For arrays, children are elements; for objects, children are values.
@@ -2533,12 +2660,11 @@ function select(children: any, query: any): any[] {
2533
2660
  }
2534
2661
 
2535
2662
  if (ismap(children)) {
2536
- children = items(children, n => {
2663
+ children = items(children, (n) => {
2537
2664
  setprop(n[1], S_DKEY, n[0])
2538
2665
  return n[1]
2539
2666
  })
2540
- }
2541
- else {
2667
+ } else {
2542
2668
  children = items(children, (n) => (setprop(n[1], S_DKEY, +n[0]), n[1]))
2543
2669
  }
2544
2670
 
@@ -2555,7 +2681,7 @@ function select(children: any, query: any): any[] {
2555
2681
  $GTE: select_CMP,
2556
2682
  $LTE: select_CMP,
2557
2683
  $LIKE: select_CMP,
2558
- }
2684
+ },
2559
2685
  }
2560
2686
 
2561
2687
  const q = clone(query)
@@ -2580,26 +2706,25 @@ function select(children: any, query: any): any[] {
2580
2706
  return results
2581
2707
  }
2582
2708
 
2583
-
2584
2709
  // Injection state used for recursive injection into JSON - like data structures.
2585
2710
  class Injection {
2586
- mode: InjectMode // Injection mode: M_KEYPRE, M_VAL, M_KEYPOST.
2587
- full: boolean // Transform escape was full key name.
2588
- keyI: number // Index of parent key in list of parent keys.
2589
- keys: string[] // List of parent keys.
2590
- key: string // Current parent key.
2591
- val: any // Current child value.
2592
- parent: any // Current parent (in transform specification).
2593
- path: string[] // Path to current node.
2594
- nodes: any[] // Stack of ancestor nodes.
2595
- handler: Injector // Custom handler for injections.
2596
- errs: any[] // Error collector.
2711
+ mode: InjectMode // Injection mode: M_KEYPRE, M_VAL, M_KEYPOST.
2712
+ full: boolean // Transform escape was full key name.
2713
+ keyI: number // Index of parent key in list of parent keys.
2714
+ keys: string[] // List of parent keys.
2715
+ key: string // Current parent key.
2716
+ val: any // Current child value.
2717
+ parent: any // Current parent (in transform specification).
2718
+ path: string[] // Path to current node.
2719
+ nodes: any[] // Stack of ancestor nodes.
2720
+ handler: Injector // Custom handler for injections.
2721
+ errs: any[] // Error collector.
2597
2722
  meta: Record<string, any> // Custom meta data. NOTE: do not merge, values must remain as-is.
2598
- dparent: any // Current data parent node (contains current data value).
2599
- dpath: string[] // Current data value path
2600
- base?: string // Base key for data in store, if any.
2601
- modify?: Modify // Modify injection output.
2602
- prior?: Injection // Parent (aka prior) injection.
2723
+ dparent: any // Current data parent node (contains current data value).
2724
+ dpath: string[] // Current data value path
2725
+ base?: string // Base key for data in store, if any.
2726
+ modify?: Modify // Modify injection output.
2727
+ prior?: Injection // Parent (aka prior) injection.
2603
2728
  extra?: any
2604
2729
 
2605
2730
  constructor(val: any, parent: any) {
@@ -2622,42 +2747,56 @@ class Injection {
2622
2747
  this.meta = {}
2623
2748
  }
2624
2749
 
2625
-
2626
2750
  toString(prefix?: string) {
2627
- return 'INJ' + (null == prefix ? '' : S_FS + prefix) + S_CN +
2751
+ return (
2752
+ 'INJ' +
2753
+ (null == prefix ? '' : S_FS + prefix) +
2754
+ S_CN +
2628
2755
  pad(pathify(this.path, 1)) +
2629
- MODENAME[this.mode] + (this.full ? '/full' : '') + S_CN +
2630
- 'key=' + this.keyI + S_FS + this.key + S_FS + S_OS + this.keys + S_CS +
2631
- ' p=' + stringify(this.parent, -1, 1) +
2632
- ' m=' + stringify(this.meta, -1, 1) +
2633
- ' d/' + pathify(this.dpath, 1) + '=' + stringify(this.dparent, -1, 1) +
2634
- ' r=' + stringify(this.nodes[0]?.[S_DTOP], -1, 1)
2756
+ MODENAME[this.mode] +
2757
+ (this.full ? '/full' : '') +
2758
+ S_CN +
2759
+ 'key=' +
2760
+ this.keyI +
2761
+ S_FS +
2762
+ this.key +
2763
+ S_FS +
2764
+ S_OS +
2765
+ this.keys +
2766
+ S_CS +
2767
+ ' p=' +
2768
+ stringify(this.parent, -1, 1) +
2769
+ ' m=' +
2770
+ stringify(this.meta, -1, 1) +
2771
+ ' d/' +
2772
+ pathify(this.dpath, 1) +
2773
+ '=' +
2774
+ stringify(this.dparent, -1, 1) +
2775
+ ' r=' +
2776
+ stringify(this.nodes[0]?.[S_DTOP], -1, 1)
2777
+ )
2635
2778
  }
2636
2779
 
2637
-
2638
2780
  descend() {
2639
2781
  this.meta.__d++
2640
2782
  const parentkey = getelem(this.path, -2)
2641
2783
 
2642
2784
  // Resolve current node in store for local paths.
2643
2785
  if (NONE === this.dparent) {
2644
-
2645
2786
  // Even if there's no data, dpath should continue to match path, so that
2646
2787
  // relative paths work properly.
2647
2788
  if (1 < size(this.dpath)) {
2648
2789
  this.dpath = flatten([this.dpath, parentkey])
2649
2790
  }
2650
- }
2651
- else {
2791
+ } else {
2652
2792
  // this.dparent is the containing node of the current store value.
2653
2793
  if (null != parentkey) {
2654
2794
  this.dparent = getprop(this.dparent, parentkey)
2655
2795
 
2656
- let lastpart = getelem(this.dpath, -1)
2796
+ const lastpart = getelem(this.dpath, -1)
2657
2797
  if (lastpart === '$:' + parentkey) {
2658
2798
  this.dpath = slice(this.dpath, -1)
2659
- }
2660
- else {
2799
+ } else {
2661
2800
  this.dpath = flatten([this.dpath, parentkey])
2662
2801
  }
2663
2802
  }
@@ -2667,7 +2806,6 @@ class Injection {
2667
2806
  return this.dparent
2668
2807
  }
2669
2808
 
2670
-
2671
2809
  child(keyI: number, keys: string[]) {
2672
2810
  const key = strkey(keys[keyI])
2673
2811
  const val = this.val
@@ -2694,20 +2832,17 @@ class Injection {
2694
2832
  return cinj
2695
2833
  }
2696
2834
 
2697
-
2698
2835
  setval(val: any, ancestor?: number) {
2699
2836
  let parent = NONE
2700
2837
  if (null == ancestor || ancestor < 2) {
2701
- parent = NONE === val ?
2702
- this.parent = delprop(this.parent, this.key) :
2703
- setprop(this.parent, this.key, val)
2704
- }
2705
- else {
2838
+ parent =
2839
+ NONE === val
2840
+ ? (this.parent = delprop(this.parent, this.key))
2841
+ : setprop(this.parent, this.key, val)
2842
+ } else {
2706
2843
  const aval = getelem(this.nodes, 0 - ancestor)
2707
2844
  const akey = getelem(this.path, 0 - ancestor)
2708
- parent = NONE === val ?
2709
- delprop(aval, akey) :
2710
- setprop(aval, akey, val)
2845
+ parent = NONE === val ? delprop(aval, akey) : setprop(aval, akey, val)
2711
2846
  }
2712
2847
 
2713
2848
  // console.log('SETVAL', val, this.key, this.parent)
@@ -2715,42 +2850,36 @@ class Injection {
2715
2850
  }
2716
2851
  }
2717
2852
 
2718
-
2719
2853
  // Internal utilities
2720
2854
  // ==================
2721
2855
 
2722
-
2723
2856
  // // Update all references to target in inj.nodes.
2724
2857
  // function _updateAncestors(_inj: Injection, target: any, tkey: any, tval: any) {
2725
2858
  // // SetProp is sufficient in TypeScript as target reference remains consistent even for lists.
2726
2859
  // setprop(target, tkey, tval)
2727
2860
  // }
2728
2861
 
2729
-
2730
2862
  // Build a type validation error message.
2731
2863
  function _invalidTypeMsg(path: any, needtype: string, vt: number, v: any, _whence?: string) {
2732
- let vs = null == v ? 'no value' : stringify(v)
2733
-
2734
- return 'Expected ' +
2735
- (1 < size(path) ? ('field ' + pathify(path, 1) + ' to be ') : '') +
2736
- needtype + ', but found ' +
2737
- (null != v ? typename(vt) + S_VIZ : '') + vs +
2738
-
2864
+ const vs = null == v ? 'no value' : stringify(v)
2865
+
2866
+ return (
2867
+ 'Expected ' +
2868
+ (1 < size(path) ? 'field ' + pathify(path, 1) + ' to be ' : '') +
2869
+ needtype +
2870
+ ', but found ' +
2871
+ (null != v ? typename(vt) + S_VIZ : '') +
2872
+ vs +
2739
2873
  // Uncomment to help debug validation errors.
2740
2874
  // ' [' + _whence + ']' +
2741
2875
 
2742
2876
  '.'
2877
+ )
2743
2878
  }
2744
2879
 
2745
-
2746
2880
  // Default inject handler for transforms. If the path resolves to a function,
2747
2881
  // call the function passing the injection inj. This is how transforms operate.
2748
- const _injecthandler: Injector = (
2749
- inj: Injection,
2750
- val: any,
2751
- ref: string,
2752
- store: any
2753
- ): any => {
2882
+ const _injecthandler: Injector = (inj: Injection, val: any, ref: string, store: any): any => {
2754
2883
  let out = val
2755
2884
  const iscmd = isfunc(val) && (NONE === ref || ref.startsWith(S_DS))
2756
2885
 
@@ -2769,37 +2898,28 @@ const _injecthandler: Injector = (
2769
2898
  return out
2770
2899
  }
2771
2900
 
2772
-
2773
- const _validatehandler: Injector = (
2774
- inj: Injection,
2775
- val: any,
2776
- ref: string,
2777
- store: any
2778
- ): any => {
2901
+ const _validatehandler: Injector = (inj: Injection, val: any, ref: string, store: any): any => {
2779
2902
  let out = val
2780
2903
 
2781
- const m = ref.match(R_META_PATH)
2904
+ const m = re_find(R_META_PATH, ref)
2782
2905
  const ismetapath = null != m
2783
2906
 
2784
2907
  if (ismetapath) {
2785
2908
  if ('=' === m[2]) {
2786
2909
  inj.setval([S_BEXACT, val])
2787
- }
2788
- else {
2910
+ } else {
2789
2911
  inj.setval(val)
2790
2912
  }
2791
2913
  inj.keyI = -1
2792
2914
 
2793
2915
  out = SKIP
2794
- }
2795
- else {
2916
+ } else {
2796
2917
  out = _injecthandler(inj, val, ref, store)
2797
2918
  }
2798
2919
 
2799
2920
  return out
2800
2921
  }
2801
2922
 
2802
-
2803
2923
  // Inject values from a data store into a string. Not a public utility - used by
2804
2924
  // `inject`. Inject are marked with `path` where path is resolved
2805
2925
  // with getpath against the store or current (if defined)
@@ -2809,11 +2929,7 @@ const _validatehandler: Injector = (
2809
2929
  // upper case letters only, and 999 is any digits, which are
2810
2930
  // discarded. This syntax specifies the name of a transform, and
2811
2931
  // optionally allows transforms to be ordered by alphanumeric sorting.
2812
- function _injectstr(
2813
- val: string,
2814
- store: any,
2815
- inj?: Injection
2816
- ): any {
2932
+ function _injectstr(val: string, store: any, inj?: Injection): any {
2817
2933
  // Can't inject into non-strings
2818
2934
  if (S_string !== typeof val || S_MT === val) {
2819
2935
  return S_MT
@@ -2822,7 +2938,7 @@ function _injectstr(
2822
2938
  let out: any = val
2823
2939
 
2824
2940
  // Pattern examples: "`a.b.c`", "`$NAME`", "`$NAME1`"
2825
- const m = val.match(R_INJECTION_FULL)
2941
+ const m = re_find(R_INJECTION_FULL, val)
2826
2942
 
2827
2943
  // Full string of the val is an injection.
2828
2944
  if (m) {
@@ -2833,20 +2949,18 @@ function _injectstr(
2833
2949
 
2834
2950
  // Special escapes inside injection.
2835
2951
  if (3 < size(pathref)) {
2836
- pathref = pathref.replace(R_BT_ESCAPE, S_BT).replace(R_DS_ESCAPE, S_DS)
2952
+ pathref = re_replace(R_DS_ESCAPE, re_replace(R_BT_ESCAPE, pathref, S_BT), S_DS)
2837
2953
  }
2838
2954
 
2839
2955
  // Get the extracted path reference.
2840
2956
  out = getpath(store, pathref, inj)
2841
- }
2842
-
2843
- else {
2957
+ } else {
2844
2958
  // Check for injections within the string.
2845
2959
  const partial = (_m: string, ref: string) => {
2846
2960
  // Special escapes inside injection.
2847
2961
 
2848
2962
  if (3 < size(ref)) {
2849
- ref = ref.replace(R_BT_ESCAPE, S_BT).replace(R_DS_ESCAPE, S_DS)
2963
+ ref = re_replace(R_DS_ESCAPE, re_replace(R_BT_ESCAPE, ref, S_BT), S_DS)
2850
2964
  }
2851
2965
 
2852
2966
  if (inj) {
@@ -2859,7 +2973,7 @@ function _injectstr(
2859
2973
  return NONE === found ? S_MT : S_string === typeof found ? found : JSON.stringify(found)
2860
2974
  }
2861
2975
 
2862
- out = val.replace(R_INJECTION_PARTIAL, partial)
2976
+ out = re_replace(R_INJECTION_PARTIAL, val, (m) => partial('', m[1]))
2863
2977
 
2864
2978
  // Also call the inj handler on the entire string, providing the
2865
2979
  // option for custom injection.
@@ -2872,11 +2986,9 @@ function _injectstr(
2872
2986
  return out
2873
2987
  }
2874
2988
 
2875
-
2876
2989
  // Handler Utilities
2877
2990
  // =================
2878
2991
 
2879
-
2880
2992
  const MODENAME: any = {
2881
2993
  [M_VAL]: 'val',
2882
2994
  [M_KEYPRE]: 'key:pre',
@@ -2893,28 +3005,44 @@ function checkPlacement(
2893
3005
  modes: InjectMode,
2894
3006
  ijname: string,
2895
3007
  parentTypes: number,
2896
- inj: Injection
3008
+ inj: Injection,
2897
3009
  ): boolean {
2898
3010
  if (0 === (modes & inj.mode)) {
2899
- inj.errs.push('$' + ijname + ': invalid placement as ' + PLACEMENT[inj.mode] +
2900
- ', expected: ' + join(items(
2901
- [M_KEYPRE, M_KEYPOST, M_VAL].filter(m => modes & m),
2902
- (n: any) => PLACEMENT[n[1]]), ',') + '.')
3011
+ inj.errs.push(
3012
+ '$' +
3013
+ ijname +
3014
+ ': invalid placement as ' +
3015
+ PLACEMENT[inj.mode] +
3016
+ ', expected: ' +
3017
+ join(
3018
+ items(
3019
+ [M_KEYPRE, M_KEYPOST, M_VAL].filter((m) => modes & m),
3020
+ (n: any) => PLACEMENT[n[1]],
3021
+ ),
3022
+ ',',
3023
+ ) +
3024
+ '.',
3025
+ )
2903
3026
  return false
2904
3027
  }
2905
3028
  if (!isempty(parentTypes)) {
2906
3029
  const ptype = typify(inj.parent)
2907
3030
  if (0 === (parentTypes & ptype)) {
2908
- inj.errs.push('$' + ijname + ': invalid placement in parent ' + typename(ptype) +
2909
- ', expected: ' + typename(parentTypes) + '.')
3031
+ inj.errs.push(
3032
+ '$' +
3033
+ ijname +
3034
+ ': invalid placement in parent ' +
3035
+ typename(ptype) +
3036
+ ', expected: ' +
3037
+ typename(parentTypes) +
3038
+ '.',
3039
+ )
2910
3040
  return false
2911
-
2912
3041
  }
2913
3042
  }
2914
3043
  return true
2915
3044
  }
2916
3045
 
2917
-
2918
3046
  // function injectorArgs(argTypes: number[], inj: Injection): any {
2919
3047
  function injectorArgs(argTypes: number[], args: any[]): any {
2920
3048
  const numargs = size(argTypes)
@@ -2925,9 +3053,16 @@ function injectorArgs(argTypes: number[], args: any[]): any {
2925
3053
  const arg = args[argI]
2926
3054
  const argType = typify(arg)
2927
3055
  if (0 === (argTypes[argI] & argType)) {
2928
- found[0] = 'invalid argument: ' + stringify(arg, 22) +
2929
- ' (' + typename(argType) + ' at position ' + (1 + argI) +
2930
- ') is not of type: ' + typename(argTypes[argI]) + '.'
3056
+ found[0] =
3057
+ 'invalid argument: ' +
3058
+ stringify(arg, 22) +
3059
+ ' (' +
3060
+ typename(argType) +
3061
+ ' at position ' +
3062
+ (1 + argI) +
3063
+ ') is not of type: ' +
3064
+ typename(argTypes[argI]) +
3065
+ '.'
2931
3066
  break
2932
3067
  }
2933
3068
  found[1 + argI] = arg
@@ -2935,7 +3070,6 @@ function injectorArgs(argTypes: number[], args: any[]): any {
2935
3070
  return found
2936
3071
  }
2937
3072
 
2938
-
2939
3073
  function injectChild(child: any, store: any, inj: Injection): Injection {
2940
3074
  let cinj = inj
2941
3075
 
@@ -2945,8 +3079,7 @@ function injectChild(child: any, store: any, inj: Injection): Injection {
2945
3079
  cinj = inj.prior.prior.child(inj.prior.keyI, inj.prior.keys)
2946
3080
  cinj.val = child
2947
3081
  setprop(cinj.parent, inj.prior.key, child)
2948
- }
2949
- else {
3082
+ } else {
2950
3083
  cinj = inj.prior.child(inj.keyI, inj.keys)
2951
3084
  cinj.val = child
2952
3085
  setprop(cinj.parent, inj.key, child)
@@ -2959,12 +3092,14 @@ function injectChild(child: any, store: any, inj: Injection): Injection {
2959
3092
  return cinj
2960
3093
  }
2961
3094
 
2962
-
2963
3095
  class StructUtility {
2964
3096
  clone = clone
3097
+ condense = condense
3098
+ condenseview = condenseview
2965
3099
  delprop = delprop
2966
3100
  escre = escre
2967
3101
  escurl = escurl
3102
+ expand = expand
2968
3103
  filter = filter
2969
3104
  flatten = flatten
2970
3105
  getdef = getdef
@@ -2974,6 +3109,7 @@ class StructUtility {
2974
3109
  haskey = haskey
2975
3110
  inject = inject
2976
3111
  isempty = isempty
3112
+ iscondensed = iscondensed
2977
3113
  isfunc = isfunc
2978
3114
  iskey = iskey
2979
3115
  islist = islist
@@ -2999,6 +3135,13 @@ class StructUtility {
2999
3135
  validate = validate
3000
3136
  walk = walk
3001
3137
 
3138
+ re_compile = re_compile
3139
+ re_find = re_find
3140
+ re_find_all = re_find_all
3141
+ re_replace = re_replace
3142
+ re_test = re_test
3143
+ re_escape = re_escape
3144
+
3002
3145
  SKIP = SKIP
3003
3146
  DELETE = DELETE
3004
3147
 
@@ -3027,9 +3170,481 @@ class StructUtility {
3027
3170
  injectChild = injectChild
3028
3171
  }
3029
3172
 
3173
+ // ---------------------------------------------------------------------------
3174
+ // CONDENSED STRUCTURES
3175
+ //
3176
+ // A condensed structure holds the same information as a JSON-shaped node in a
3177
+ // fraction of the space, and can be read WITHOUT materialising it. Three
3178
+ // mechanisms, all in one format:
3179
+ //
3180
+ // intern a symbol table holds each distinct map key and string value once
3181
+ // share identical subtrees become ONE node with many references (a DAG)
3182
+ // lazy a reader resolves a path by walking integer indexes, touching
3183
+ // only the nodes on that path
3184
+ //
3185
+ // The shape:
3186
+ //
3187
+ // { "$condense": 1, format version
3188
+ // "sym": [ ... ], distinct strings, sorted (so lookup can bisect)
3189
+ // "node": [ ... ], integer-encoded nodes, children before parents
3190
+ // "root": 12 } index into node[]
3191
+ //
3192
+ // Each node is one of:
3193
+ //
3194
+ // map [0, k0, v0, k1, v1, ...] k ascending symbol ids, v node refs
3195
+ // list [1, v0, v1, ...] node refs
3196
+ // string [2, symId]
3197
+ // number [3, value]
3198
+ // true [4] false [5] null [6]
3199
+ //
3200
+ // Two properties are load-bearing rather than incidental:
3201
+ //
3202
+ // Map keys ascend, because keysof() sorts. That makes output byte-stable
3203
+ // (generators depend on it) and lets a key lookup bisect rather than scan.
3204
+ //
3205
+ // References are integer indexes into a post-order table, so every ref
3206
+ // points BACKWARDS. Sharing is therefore free — two identical subtrees
3207
+ // intern to one node and two refs, with no dedup pass at read time — and a
3208
+ // reader can never loop.
3209
+ //
3210
+ // The transport is JSON arrays of integers on purpose. The FORMAT is defined
3211
+ // here, but TOKENIZING is left to each language's own JSON parser: mature,
3212
+ // fast, memory-safe, already present, and nothing to keep in parity across
3213
+ // ports. A binary profile ($condense: 2) is a future option, deliberately
3214
+ // deferred - it is worth perhaps 2-3x and costs a hand-written decoder per
3215
+ // language.
3216
+ //
3217
+ // IMMUTABILITY. Sharing makes a condensed structure a DAG, so a mutation
3218
+ // through one reference would be visible through every other. Rather than
3219
+ // make that hazard the caller's problem, it is designed out: mutating
3220
+ // helpers RAISE on a condensed node, and expand()/value() always return an
3221
+ // independent copy. ref() is the one opt-in escape hatch and is named to be
3222
+ // alarming.
3223
+
3224
+ const CONDENSE_VERSION = 1
3225
+
3226
+ const CONDENSE_MAP = 0
3227
+ const CONDENSE_LIST = 1
3228
+ const CONDENSE_STR = 2
3229
+ const CONDENSE_NUM = 3
3230
+ const CONDENSE_TRUE = 4
3231
+ const CONDENSE_FALSE = 5
3232
+ const CONDENSE_NULL = 6
3233
+
3234
+ const S_condense = '$condense'
3235
+ const S_condsym = 'sym'
3236
+ const S_condnode = 'node'
3237
+ const S_condroot = 'root'
3238
+
3239
+ // Is this a condensed structure? Checked structurally rather than by a marker
3240
+ // alone, so an ordinary map that happens to carry a `$condense` key is not
3241
+ // mistaken for one.
3242
+ // Compare by UNICODE CODE POINT, not by UTF-16 code unit.
3243
+ //
3244
+ // JavaScript's default string sort compares UTF-16 code units, which orders
3245
+ // an astral character BEFORE a high-BMP one; Python, Go and Rust all compare
3246
+ // by code point (equivalently, by UTF-8 bytes). A symbol table sorted the
3247
+ // JavaScript way would make two conforming ports emit different bytes for the
3248
+ // same input, which is exactly the byte-stability the format promises. Code
3249
+ // point order is the portable choice, so it is specified rather than
3250
+ // inherited.
3251
+ function condenseCmp(a: string, b: string): number {
3252
+ let i = 0
3253
+ let j = 0
3254
+ while (i < a.length && j < b.length) {
3255
+ const ca = a.codePointAt(i) as number
3256
+ const cb = b.codePointAt(j) as number
3257
+ if (ca !== cb) {
3258
+ return ca < cb ? -1 : 1
3259
+ }
3260
+ i += 0xffff < ca ? 2 : 1
3261
+ j += 0xffff < cb ? 2 : 1
3262
+ }
3263
+ return a.length - i - (b.length - j)
3264
+ }
3265
+
3266
+ // Assign a key that may be `__proto__`.
3267
+ //
3268
+ // Plain assignment to `__proto__` invokes the inherited setter instead of
3269
+ // creating an own property, so a model carrying that key or string value
3270
+ // would be silently corrupted - it is valid JSON and Seneca-adjacent data
3271
+ // really does use `$`-ish and `__`-ish names.
3272
+ function condenseSet(obj: any, key: string, val: any) {
3273
+ if ('__proto__' === key) {
3274
+ Object.defineProperty(obj, key, {
3275
+ value: val,
3276
+ enumerable: true,
3277
+ writable: true,
3278
+ configurable: true,
3279
+ })
3280
+ } else {
3281
+ obj[key] = val
3282
+ }
3283
+ }
3284
+
3285
+ function iscondensed(val: any): boolean {
3286
+ // The version must MATCH, not merely be numeric: a future profile would
3287
+ // otherwise be decoded under this one's schema.
3288
+ return (
3289
+ ismap(val) &&
3290
+ CONDENSE_VERSION === (val as any)[S_condense] &&
3291
+ islist((val as any)[S_condsym]) &&
3292
+ islist((val as any)[S_condnode]) &&
3293
+ 'number' === typeof (val as any)[S_condroot]
3294
+ )
3295
+ }
3296
+
3297
+ // Condense any JSON-shaped node. Pure: the same input always produces a
3298
+ // byte-identical result, on every port.
3299
+ function condense(val: any): any {
3300
+ // Pass one: every distinct map key and string value, once, sorted.
3301
+ const symset: { [sym: string]: boolean } = Object.create(null)
3302
+
3303
+ const collect = (n: any) => {
3304
+ if (ismap(n)) {
3305
+ const keys = keysof(n)
3306
+ for (let i = 0; i < keys.length; i++) {
3307
+ symset[keys[i]] = true
3308
+ collect((n as any)[keys[i]])
3309
+ }
3310
+ } else if (islist(n)) {
3311
+ for (let i = 0; i < n.length; i++) {
3312
+ collect(n[i])
3313
+ }
3314
+ } else if ('string' === typeof n) {
3315
+ symset[n] = true
3316
+ }
3317
+ }
3318
+ collect(val)
3319
+
3320
+ const sym = Object.keys(symset).sort(condenseCmp)
3321
+ const symid: { [sym: string]: number } = Object.create(null)
3322
+ for (let i = 0; i < sym.length; i++) {
3323
+ symid[sym[i]] = i
3324
+ }
3325
+
3326
+ // Pass two: build post-order, interning on the canonical encoding. Because
3327
+ // children are built first, identical subtrees have already been interned
3328
+ // by the time a parent references them, so sharing costs nothing extra.
3329
+ const node: any[][] = []
3330
+ const seen: { [enc: string]: number } = Object.create(null)
3331
+
3332
+ const intern = (enc: any[]): number => {
3333
+ // The kind tag leads every encoding, so a string node and a number node
3334
+ // can never collide on the same joined key.
3335
+ const enckey = enc.join(',')
3336
+ const hit = seen[enckey]
3337
+ if (undefined !== hit) {
3338
+ return hit
3339
+ }
3340
+ const idx = node.length
3341
+ node.push(enc)
3342
+ seen[enckey] = idx
3343
+ return idx
3344
+ }
3345
+
3346
+ const build = (n: any): number => {
3347
+ if (ismap(n)) {
3348
+ const enc: any[] = [CONDENSE_MAP]
3349
+ // keysof sorts, so the key ids ascend and lookup can bisect.
3350
+ const keys = keysof(n)
3351
+ for (let i = 0; i < keys.length; i++) {
3352
+ enc.push(symid[keys[i]])
3353
+ enc.push(build((n as any)[keys[i]]))
3354
+ }
3355
+ return intern(enc)
3356
+ }
3357
+ if (islist(n)) {
3358
+ const enc: any[] = [CONDENSE_LIST]
3359
+ for (let i = 0; i < n.length; i++) {
3360
+ enc.push(build(n[i]))
3361
+ }
3362
+ return intern(enc)
3363
+ }
3364
+ if ('string' === typeof n) {
3365
+ return intern([CONDENSE_STR, symid[n]])
3366
+ }
3367
+ if ('number' === typeof n) {
3368
+ return intern([CONDENSE_NUM, n])
3369
+ }
3370
+ if (true === n) {
3371
+ return intern([CONDENSE_TRUE])
3372
+ }
3373
+ if (false === n) {
3374
+ return intern([CONDENSE_FALSE])
3375
+ }
3376
+ return intern([CONDENSE_NULL])
3377
+ }
3378
+
3379
+ const root = build(val)
3380
+
3381
+ const out: any = {}
3382
+ out[S_condense] = CONDENSE_VERSION
3383
+ out[S_condsym] = sym
3384
+ out[S_condnode] = node
3385
+ out[S_condroot] = root
3386
+ return out
3387
+ }
3388
+
3389
+ // Resolve a symbol to its id by bisecting the sorted symbol table. -1 when
3390
+ // the string does not occur anywhere in the structure, which is itself a fast
3391
+ // negative answer for a key lookup.
3392
+ function condenseSymId(cond: any, key: string): number {
3393
+ const sym = (cond as any)[S_condsym]
3394
+ let lo = 0
3395
+ let hi = sym.length - 1
3396
+ while (lo <= hi) {
3397
+ const mid = (lo + hi) >> 1
3398
+ if (sym[mid] === key) {
3399
+ return mid
3400
+ }
3401
+ if (0 > condenseCmp(sym[mid], key)) {
3402
+ lo = mid + 1
3403
+ } else {
3404
+ hi = mid - 1
3405
+ }
3406
+ }
3407
+ return -1
3408
+ }
3409
+
3410
+ // The node index of `key` inside node `idx`, or -1. Touches only this node.
3411
+ function condenseChild(cond: any, idx: number, key: any): number {
3412
+ const enc = (cond as any)[S_condnode][idx]
3413
+ if (null == enc) {
3414
+ return -1
3415
+ }
3416
+
3417
+ if (CONDENSE_MAP === enc[0]) {
3418
+ const symid = condenseSymId(cond, S_MT + key)
3419
+ if (0 > symid) {
3420
+ return -1
3421
+ }
3422
+ // Pairs live at [1,2], [3,4], ... with keys ascending: bisect the pairs.
3423
+ let lo = 0
3424
+ let hi = (enc.length - 1) / 2 - 1
3425
+ while (lo <= hi) {
3426
+ const mid = (lo + hi) >> 1
3427
+ const k = enc[1 + mid * 2]
3428
+ if (k === symid) {
3429
+ return enc[2 + mid * 2]
3430
+ }
3431
+ if (k < symid) {
3432
+ lo = mid + 1
3433
+ } else {
3434
+ hi = mid - 1
3435
+ }
3436
+ }
3437
+ return -1
3438
+ }
3439
+
3440
+ if (CONDENSE_LIST === enc[0]) {
3441
+ // Canonical integer keys ONLY. Unary + would accept "01", "1e0", " 1"
3442
+ // and "+1", which an ordinary list lookup treats as plain (absent)
3443
+ // properties - condensing must not change which value a path selects.
3444
+ const k = S_MT + key
3445
+ if (!/^(0|[1-9][0-9]*)$/.test(k)) {
3446
+ return -1
3447
+ }
3448
+ const i = +k
3449
+ if (i >= enc.length - 1) {
3450
+ return -1
3451
+ }
3452
+ return enc[1 + i]
3453
+ }
3454
+
3455
+ return -1
3456
+ }
3457
+
3458
+ // Walk a path from a starting node, returning the node index or -1.
3459
+ function condenseResolve(cond: any, idx: number, path: any): number {
3460
+ const parts = islist(path)
3461
+ ? path
3462
+ : 'string' === typeof path
3463
+ ? '' === path
3464
+ ? []
3465
+ : path.split(S_DT)
3466
+ : null == path
3467
+ ? []
3468
+ : [S_MT + path]
3469
+
3470
+ let at = idx
3471
+ for (let i = 0; i < parts.length; i++) {
3472
+ if (0 > at) {
3473
+ return -1
3474
+ }
3475
+ if (S_MT === parts[i]) {
3476
+ continue
3477
+ }
3478
+ at = condenseChild(cond, at, parts[i])
3479
+ }
3480
+ return at
3481
+ }
3482
+
3483
+ // Materialise node `idx` as an ordinary JSON-shaped value.
3484
+ //
3485
+ // Containers are rebuilt on every call and never memoised: sharing is an
3486
+ // implementation detail, and handing the same object out twice would let a
3487
+ // caller mutating one reference corrupt every other. The cost is that
3488
+ // expansion reproduces the original tree - which is exactly the size it was
3489
+ // before condensing, so it is bounded by the input.
3490
+ function condenseMaterialise(cond: any, idx: number): any {
3491
+ const sym = (cond as any)[S_condsym]
3492
+ const node = (cond as any)[S_condnode]
3493
+
3494
+ const rebuild = (i: number): any => {
3495
+ const enc = node[i]
3496
+ if (null == enc) {
3497
+ return undefined
3498
+ }
3499
+ const kind = enc[0]
3500
+ if (CONDENSE_MAP === kind) {
3501
+ const out: any = {}
3502
+ for (let j = 1; j < enc.length; j += 2) {
3503
+ condenseSet(out, sym[enc[j]], rebuild(enc[j + 1]))
3504
+ }
3505
+ return out
3506
+ }
3507
+ if (CONDENSE_LIST === kind) {
3508
+ const out: any[] = []
3509
+ for (let j = 1; j < enc.length; j++) {
3510
+ out.push(rebuild(enc[j]))
3511
+ }
3512
+ return out
3513
+ }
3514
+ if (CONDENSE_STR === kind) {
3515
+ return sym[enc[1]]
3516
+ }
3517
+ if (CONDENSE_NUM === kind) {
3518
+ return enc[1]
3519
+ }
3520
+ if (CONDENSE_TRUE === kind) {
3521
+ return true
3522
+ }
3523
+ if (CONDENSE_FALSE === kind) {
3524
+ return false
3525
+ }
3526
+ return null
3527
+ }
3528
+
3529
+ return 0 > idx ? undefined : rebuild(idx)
3530
+ }
3531
+
3532
+ // Fully materialise a condensed structure. The escape hatch: correct for any
3533
+ // caller, but it gives up every benefit of condensing, so prefer a view or a
3534
+ // path read.
3535
+ function expand(cond: any): any {
3536
+ if (!iscondensed(cond)) {
3537
+ return cond
3538
+ }
3539
+ return condenseMaterialise(cond, (cond as any)[S_condroot])
3540
+ }
3541
+
3542
+ // The keys of node `idx`, WITHOUT materialising any of its values. This is
3543
+ // the point of the whole exercise for enumeration: listing the names in a
3544
+ // large collection should cost a symbol-table read, not a full decode.
3545
+ function condenseKeys(cond: any, idx: number): string[] {
3546
+ const enc = (cond as any)[S_condnode][idx]
3547
+ if (null == enc) {
3548
+ return []
3549
+ }
3550
+ const sym = (cond as any)[S_condsym]
3551
+ const out: string[] = []
3552
+ if (CONDENSE_MAP === enc[0]) {
3553
+ for (let j = 1; j < enc.length; j += 2) {
3554
+ out.push(sym[enc[j]])
3555
+ }
3556
+ return out
3557
+ }
3558
+ if (CONDENSE_LIST === enc[0]) {
3559
+ for (let j = 1; j < enc.length; j++) {
3560
+ out.push(S_MT + (j - 1))
3561
+ }
3562
+ return out
3563
+ }
3564
+ return []
3565
+ }
3566
+
3567
+ // A cursor into a condensed structure. Reads resolve by integer index and
3568
+ // touch only the nodes on the path, so the cost of a lookup is proportional
3569
+ // to path depth and result size, never to total structure size.
3570
+ type CondenseView = {
3571
+ get: (path?: any) => any
3572
+ keys: (path?: any) => string[]
3573
+ has: (path?: any) => boolean
3574
+ at: (path?: any) => CondenseView
3575
+ value: () => any
3576
+ size: () => number
3577
+ exists: () => boolean
3578
+ ref: () => any
3579
+ }
3580
+
3581
+ function condenseview(cond: any, idx?: number, memo?: any): CondenseView {
3582
+ const start = null == idx ? (iscondensed(cond) ? (cond as any)[S_condroot] : -1) : idx
3583
+
3584
+ // Shared materialisations, handed out ONLY by ref(). Threaded through at()
3585
+ // so every cursor into the same structure shares one memo - otherwise
3586
+ // ref() would hand out a different object per cursor and quietly stop
3587
+ // being the shared-reference escape hatch it claims to be.
3588
+ const refmemo: { [idx: string]: any } = null == memo ? {} : memo
3589
+
3590
+ const view: CondenseView = {
3591
+ // The value at path, materialised. Scalars come back directly; a
3592
+ // container comes back as an independent copy.
3593
+ get: (path?: any) => condenseMaterialise(cond, condenseResolve(cond, start, path)),
3594
+
3595
+ // Key names at path, with no value decoding at all.
3596
+ keys: (path?: any) => {
3597
+ const at = condenseResolve(cond, start, path)
3598
+ return 0 > at ? [] : condenseKeys(cond, at)
3599
+ },
3600
+
3601
+ has: (path?: any) => 0 <= condenseResolve(cond, start, path),
3602
+
3603
+ // Chainable cursor. Cheap: no decoding happens here.
3604
+ at: (path?: any) => condenseview(cond, condenseResolve(cond, start, path), refmemo),
3605
+
3606
+ value: () => condenseMaterialise(cond, start),
3607
+
3608
+ size: () => {
3609
+ const enc = 0 > start ? null : (cond as any)[S_condnode][start]
3610
+ if (null == enc) {
3611
+ return 0
3612
+ }
3613
+ if (CONDENSE_MAP === enc[0]) {
3614
+ return (enc.length - 1) / 2
3615
+ }
3616
+ if (CONDENSE_LIST === enc[0]) {
3617
+ return enc.length - 1
3618
+ }
3619
+ return 0
3620
+ },
3621
+
3622
+ exists: () => 0 <= start,
3623
+
3624
+ // DANGER, and named to say so. Returns a SHARED materialisation: the same
3625
+ // object for the same node, every time. Mutating it corrupts every other
3626
+ // reader of that node, including ones reached by a different path,
3627
+ // because the structure is a DAG. Use value() unless a profile says this
3628
+ // matters.
3629
+ ref: () => {
3630
+ const k = S_MT + start
3631
+ if (undefined === refmemo[k]) {
3632
+ refmemo[k] = condenseMaterialise(cond, start)
3633
+ }
3634
+ return refmemo[k]
3635
+ },
3636
+ }
3637
+
3638
+ return view
3639
+ }
3640
+
3030
3641
  export {
3031
3642
  StructUtility,
3032
3643
  clone,
3644
+ condense,
3645
+ condenseview,
3646
+ expand,
3647
+ iscondensed,
3033
3648
  delprop,
3034
3649
  escre,
3035
3650
  escurl,
@@ -3066,13 +3681,16 @@ export {
3066
3681
  typename,
3067
3682
  validate,
3068
3683
  walk,
3069
-
3684
+ re_compile,
3685
+ re_find,
3686
+ re_find_all,
3687
+ re_replace,
3688
+ re_test,
3689
+ re_escape,
3070
3690
  SKIP,
3071
3691
  DELETE,
3072
-
3073
3692
  jm,
3074
3693
  jt,
3075
-
3076
3694
  T_any,
3077
3695
  T_noval,
3078
3696
  T_boolean,
@@ -3088,20 +3706,13 @@ export {
3088
3706
  T_instance,
3089
3707
  T_scalar,
3090
3708
  T_node,
3091
-
3092
3709
  M_KEYPRE,
3093
3710
  M_KEYPOST,
3094
3711
  M_VAL,
3095
-
3096
3712
  MODENAME,
3097
-
3098
3713
  checkPlacement,
3099
3714
  injectorArgs,
3100
3715
  injectChild,
3101
3716
  }
3102
3717
 
3103
- export type {
3104
- Injection,
3105
- Injector,
3106
- WalkApply
3107
- }
3718
+ export type { Injection, Injector, WalkApply }