@jarenjs/json 0.9.2

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 (57) hide show
  1. package/ARCHITECTURE.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +471 -0
  4. package/dist/types/basic.d.ts +32 -0
  5. package/dist/types/index.d.ts +4 -0
  6. package/dist/types/jslt/dispatch.d.ts +11 -0
  7. package/dist/types/jslt/errors.d.ts +18 -0
  8. package/dist/types/jslt/index.d.ts +53 -0
  9. package/dist/types/jslt/stylesheet.d.ts +8 -0
  10. package/dist/types/jtlt/desugar.d.ts +19 -0
  11. package/dist/types/jtlt/errors.d.ts +18 -0
  12. package/dist/types/jtlt/index.d.ts +57 -0
  13. package/dist/types/jtlt/template.d.ts +8 -0
  14. package/dist/types/jtlt/writer.d.ts +6 -0
  15. package/dist/types/path.d.ts +235 -0
  16. package/dist/types/pointer.d.ts +114 -0
  17. package/dist/types/query/compile.d.ts +21 -0
  18. package/dist/types/query/errors.d.ts +18 -0
  19. package/dist/types/query/index.d.ts +70 -0
  20. package/dist/types/query/normalize.d.ts +68 -0
  21. package/dist/types/query/operators.d.ts +424 -0
  22. package/dist/types/query/runtime.d.ts +93 -0
  23. package/dist/types/segments.d.ts +62 -0
  24. package/dist/types/xquery/index.d.ts +19 -0
  25. package/dist/types/xquery/parse.d.ts +20 -0
  26. package/docs/JSLT-FORMAT.md +861 -0
  27. package/docs/JSLT-PRELUDE.md +159 -0
  28. package/docs/JTLT-FORMAT.md +659 -0
  29. package/docs/QUERY-FORMAT.md +1221 -0
  30. package/docs/XQUERY-FRONTEND.md +321 -0
  31. package/package.json +81 -0
  32. package/schemas/jaren-jslt.draft-07.schema.json +776 -0
  33. package/schemas/jaren-jslt.schema.json +776 -0
  34. package/schemas/jaren-query.draft-07.schema.json +613 -0
  35. package/schemas/jaren-query.schema.json +375 -0
  36. package/src/basic.js +300 -0
  37. package/src/index.js +4 -0
  38. package/src/jslt/dispatch.js +934 -0
  39. package/src/jslt/errors.js +34 -0
  40. package/src/jslt/index.js +121 -0
  41. package/src/jslt/stylesheet.js +234 -0
  42. package/src/jtlt/desugar.js +231 -0
  43. package/src/jtlt/errors.js +34 -0
  44. package/src/jtlt/index.js +155 -0
  45. package/src/jtlt/template.js +130 -0
  46. package/src/jtlt/writer.js +110 -0
  47. package/src/path.js +977 -0
  48. package/src/pointer.js +453 -0
  49. package/src/query/compile.js +817 -0
  50. package/src/query/errors.js +33 -0
  51. package/src/query/index.js +150 -0
  52. package/src/query/normalize.js +1047 -0
  53. package/src/query/operators.js +1253 -0
  54. package/src/query/runtime.js +233 -0
  55. package/src/segments.js +627 -0
  56. package/src/xquery/index.js +35 -0
  57. package/src/xquery/parse.js +1647 -0
@@ -0,0 +1,934 @@
1
+ //#region Jaren JSLT dispatcher
2
+ // Compiles match paths, schema predicates, and query bodies once, then
3
+ // evaluates transformations through ranked per-mode dispatch tables.
4
+
5
+ import {
6
+ compileJSONPath,
7
+ JSONPathSyntaxError,
8
+ } from '../path.js';
9
+ import {
10
+ appendName,
11
+ compileSegmentP,
12
+ runSegmentsP,
13
+ } from '../segments.js';
14
+ import {
15
+ CARD_MANY,
16
+ normalizeQuery,
17
+ } from '../query/normalize.js';
18
+ import {
19
+ compileNode,
20
+ UNBOUND,
21
+ } from '../query/compile.js';
22
+ import {
23
+ JsonQueryCompileError,
24
+ JsonQueryRuntimeError,
25
+ } from '../query/errors.js';
26
+ import {
27
+ EMPTY,
28
+ Seq,
29
+ appendItem,
30
+ seqOf,
31
+ } from '../query/runtime.js';
32
+ import {
33
+ JsltCompileError,
34
+ JsltRuntimeError,
35
+ } from './errors.js';
36
+
37
+ const hasOwn = Object.hasOwn;
38
+ const NO_RULE = Symbol('Jslt.NoRule');
39
+ const NO_EXTERNAL_VALUES = Object.freeze([]);
40
+ // The rebuild prune set of a matched-but-unfired location in an
41
+ // all-path-rule share mode: no child continues toward a match. Never
42
+ // mutated; rebuild walkers only test membership.
43
+ const NO_CHILDREN = new Set();
44
+
45
+ function composeDocPath(base, inner) {
46
+ return inner.length === 0 ? base : base + inner;
47
+ }
48
+
49
+ function errorText(error) {
50
+ return error instanceof Error ? error.message : String(error);
51
+ }
52
+
53
+ // One compiled query per distinct match-path source string: rules across
54
+ // modes matching the same path share the object, so the per-call
55
+ // `.paths(root)` enumeration is computed once per transform (see
56
+ // getQueryPaths), not once per mode.
57
+ function compileMatchPath(rule, pathQueryCache) {
58
+ const match = rule.match;
59
+ if (match === null || match.path === null)
60
+ return null;
61
+ const cached = pathQueryCache.get(match.path);
62
+ if (cached !== undefined)
63
+ return cached;
64
+ try {
65
+ const query = compileJSONPath(match.path);
66
+ pathQueryCache.set(match.path, query);
67
+ return query;
68
+ }
69
+ catch (error) {
70
+ if (!(error instanceof JSONPathSyntaxError))
71
+ throw error;
72
+ throw new JsltCompileError('JT0003',
73
+ `invalid match path: ${error.message}`, match.pathDocPath, error);
74
+ }
75
+ }
76
+
77
+ function compileMatchSchema(rule, compileTypeTest) {
78
+ const match = rule.match;
79
+ if (match === null || !match.schemaPresent)
80
+ return null;
81
+ if (compileTypeTest === null) {
82
+ throw new JsltCompileError('JT0006',
83
+ 'schema matches require a type-test compiler (options.compileTypeTest)',
84
+ match.schemaDocPath);
85
+ }
86
+ let test;
87
+ try {
88
+ test = compileTypeTest(match.schema, match.schemaDocPath);
89
+ }
90
+ catch (error) {
91
+ throw new JsltCompileError('JT0005',
92
+ `invalid match schema: ${errorText(error)}`, match.schemaDocPath, error);
93
+ }
94
+ if (typeof test !== 'function') {
95
+ const cause = new TypeError('the type-test compiler did not return a predicate function');
96
+ throw new JsltCompileError('JT0005', cause.message, match.schemaDocPath, cause);
97
+ }
98
+ return test;
99
+ }
100
+
101
+ function appendDispatched(acc, selected, targetMode, depth, tctx, dispatch) {
102
+ if (selected === EMPTY)
103
+ return;
104
+ if (selected instanceof Seq) {
105
+ const items = selected.items;
106
+ for (let i = 0; i < items.length; i++)
107
+ appendItem(acc, dispatch(items[i], null, targetMode, depth, tctx));
108
+ return;
109
+ }
110
+ appendItem(acc, dispatch(selected, null, targetMode, depth, tctx));
111
+ }
112
+
113
+ function createApplyEntry(ruleBox, tableBox, targetModes) {
114
+ return {
115
+ result: () => CARD_MANY,
116
+ normalize(arg, docPath, opPath, scope, ctx, helpers) {
117
+ let selector;
118
+ let targetMode = ruleBox.mode;
119
+ if (!Array.isArray(arg)) {
120
+ selector = helpers.normalizeExpr(arg, opPath, scope, ctx);
121
+ }
122
+ else {
123
+ if (arg.length < 1 || arg.length > 2)
124
+ helpers.fail('JQ0003', "'$apply' takes [selector] or [selector, mode]", opPath);
125
+ selector = helpers.normalizeExpr(arg[0], opPath + '/0', scope, ctx);
126
+ if (arg.length === 2) {
127
+ if (typeof arg[1] !== 'string')
128
+ helpers.fail('JQ0003', "the '$apply' mode must be a literal string", opPath + '/1');
129
+ targetMode = arg[1];
130
+ }
131
+ }
132
+ targetModes.add(targetMode);
133
+ const args = [selector];
134
+ if (Array.isArray(arg) && arg.length === 2) // the explicit-mode form
135
+ args.push(helpers.makeRaw(targetMode, opPath + '/1'));
136
+ return { args, card: CARD_MANY };
137
+ },
138
+ compile(gets, args) {
139
+ const selector = args[0];
140
+ const get = gets[0];
141
+ const targetMode = args.length === 2 ? args[1].value : ruleBox.mode;
142
+ // slots are final once normalizeQuery returns; snapshot them here
143
+ const locSlot = ruleBox.locSlot;
144
+ const depthSlot = ruleBox.depthSlot;
145
+ const tctxSlot = ruleBox.tctxSlot;
146
+ const currentPath = selector.kind === 'path'
147
+ && selector.rootSlot === 0
148
+ && selector.external === false;
149
+ const rootPath = selector.kind === 'path'
150
+ && selector.name === 'root'
151
+ && selector.external === true;
152
+ const locatedPath = currentPath || rootPath;
153
+ const segs = locatedPath
154
+ ? selector.segments.map(compileSegmentP)
155
+ : null;
156
+
157
+ return (frame) => {
158
+ const tctx = frame[tctxSlot];
159
+ const depth = frame[depthSlot] + 1;
160
+ const dispatch = tableBox.dispatch;
161
+ const acc = [];
162
+
163
+ if (tableBox.needsLoc && locatedPath) {
164
+ const baseLoc = currentPath ? frame[locSlot] : '$';
165
+ if (baseLoc !== null) {
166
+ const baseValue = currentPath ? frame[0] : frame[selector.rootSlot];
167
+ const result = runSegmentsP(segs, baseValue, baseLoc, frame[0]);
168
+ const vals = result.vals;
169
+ const paths = result.paths;
170
+ for (let i = 0; i < vals.length; i++)
171
+ appendItem(acc, dispatch(vals[i], paths[i], targetMode, depth, tctx));
172
+ return seqOf(acc);
173
+ }
174
+ }
175
+
176
+ appendDispatched(acc, get(frame), targetMode, depth, tctx, dispatch);
177
+ return seqOf(acc);
178
+ };
179
+ },
180
+ };
181
+ }
182
+
183
+ function wrapBodyCompileError(rule, error) {
184
+ throw new JsltCompileError('JT0007',
185
+ `rule body failed to compile: ${error.message}`,
186
+ composeDocPath(rule.bodyDocPath, error.docPath), error);
187
+ }
188
+
189
+ function compileBody(rule, compileTypeTest, tableBox, targetModes) {
190
+ const ruleBox = {
191
+ mode: rule.mode,
192
+ locSlot: -1,
193
+ depthSlot: -1,
194
+ tctxSlot: -1,
195
+ };
196
+ const applyEntry = createApplyEntry(ruleBox, tableBox, targetModes);
197
+ let normalized;
198
+ let bodyGet;
199
+ try {
200
+ normalized = normalizeQuery(rule.body, {
201
+ compileTypeTest,
202
+ extensions: { '$apply': applyEntry },
203
+ });
204
+ ruleBox.locSlot = normalized.frameSize;
205
+ ruleBox.depthSlot = normalized.frameSize + 1;
206
+ ruleBox.tctxSlot = normalized.frameSize + 2;
207
+ bodyGet = compileNode(normalized.root);
208
+ }
209
+ catch (error) {
210
+ if (error instanceof JsonQueryCompileError)
211
+ wrapBodyCompileError(rule, error);
212
+ throw error;
213
+ }
214
+
215
+ const userExternals = [];
216
+ let rootSlot = -1;
217
+ let pathSlot = -1;
218
+ let readsPath = false;
219
+ const externals = normalized.externals;
220
+ for (let i = 0; i < externals.length; i++) {
221
+ const external = externals[i];
222
+ if (external.name === 'root') {
223
+ rootSlot = external.slot;
224
+ }
225
+ else if (external.name === 'path') {
226
+ pathSlot = external.slot;
227
+ readsPath = true;
228
+ }
229
+ else {
230
+ userExternals.push(external);
231
+ }
232
+ }
233
+
234
+ return {
235
+ bodyGet,
236
+ frameSize: normalized.frameSize,
237
+ locSlot: ruleBox.locSlot,
238
+ depthSlot: ruleBox.depthSlot,
239
+ tctxSlot: ruleBox.tctxSlot,
240
+ rootSlot,
241
+ pathSlot,
242
+ readsPath,
243
+ userExternals,
244
+ };
245
+ }
246
+
247
+ function makeBodyEvaluator(rule, body, userSlots) {
248
+ const {
249
+ bodyGet,
250
+ frameSize,
251
+ locSlot,
252
+ depthSlot,
253
+ tctxSlot,
254
+ rootSlot,
255
+ pathSlot,
256
+ } = body;
257
+ const ruleIndex = rule.index;
258
+ const bodyDocPath = rule.bodyDocPath;
259
+ const ruleDocPath = rule.docPath;
260
+ const userCount = userSlots.length;
261
+ const fullSize = frameSize + 3;
262
+
263
+ return (value, loc, depth, tctx) => {
264
+ const frame = new Array(fullSize);
265
+ frame[0] = value;
266
+ const externalValues = tctx.ruleExternalValues[ruleIndex];
267
+ for (let i = 0; i < userCount; i++)
268
+ frame[userSlots[i]] = externalValues[i];
269
+ if (rootSlot >= 0)
270
+ frame[rootSlot] = tctx.root;
271
+ if (pathSlot >= 0)
272
+ frame[pathSlot] = loc;
273
+ frame[locSlot] = loc;
274
+ frame[depthSlot] = depth;
275
+ frame[tctxSlot] = tctx;
276
+
277
+ try {
278
+ return bodyGet(frame);
279
+ }
280
+ catch (error) {
281
+ if (error instanceof JsltRuntimeError)
282
+ throw error;
283
+ if (!(error instanceof JsonQueryRuntimeError))
284
+ throw error;
285
+ const location = loc === null ? 'a location-less value' : `location ${loc}`;
286
+ throw new JsltRuntimeError('JT2004',
287
+ `rule ${ruleDocPath} failed at ${location}: ${error.message}`,
288
+ composeDocPath(bodyDocPath, error.docPath), error);
289
+ }
290
+ };
291
+ }
292
+
293
+ function compileRules(model, compileTypeTest, tableBox, targetModes) {
294
+ const rules = model.rules;
295
+ const temporary = new Array(rules.length);
296
+ const externalNames = [];
297
+ const externalNameSet = new Set();
298
+ let readsPath = false;
299
+
300
+ const pathQueryCache = new Map();
301
+ let pathRuleCount = 0;
302
+ for (let i = 0; i < rules.length; i++) {
303
+ const rule = rules[i];
304
+ const pathQuery = compileMatchPath(rule, pathQueryCache);
305
+ if (pathQuery !== null)
306
+ pathRuleCount++;
307
+ const test = compileMatchSchema(rule, compileTypeTest);
308
+ const body = compileBody(rule, compileTypeTest, tableBox, targetModes);
309
+ readsPath = readsPath || body.readsPath;
310
+ const userExternals = body.userExternals;
311
+ for (let j = 0; j < userExternals.length; j++) {
312
+ const name = userExternals[j].name;
313
+ if (!externalNameSet.has(name)) {
314
+ externalNameSet.add(name);
315
+ externalNames.push(name);
316
+ }
317
+ }
318
+ temporary[i] = { rule, pathQuery, test, body };
319
+ }
320
+
321
+ const externalIndexes = new Map();
322
+ for (let i = 0; i < externalNames.length; i++)
323
+ externalIndexes.set(externalNames[i], i);
324
+
325
+ const compiled = new Array(rules.length);
326
+ for (let i = 0; i < temporary.length; i++) {
327
+ const item = temporary[i];
328
+ const userExternals = item.body.userExternals;
329
+ const userSlots = new Array(userExternals.length);
330
+ const userIndexes = new Array(userExternals.length);
331
+ for (let j = 0; j < userExternals.length; j++) {
332
+ userSlots[j] = userExternals[j].slot;
333
+ userIndexes[j] = externalIndexes.get(userExternals[j].name);
334
+ }
335
+ const frozenSlots = Object.freeze(userSlots);
336
+ const frozenIndexes = Object.freeze(userIndexes);
337
+ compiled[i] = Object.freeze({
338
+ index: item.rule.index,
339
+ pathQuery: item.pathQuery,
340
+ test: item.test,
341
+ bodyEval: makeBodyEvaluator(item.rule, item.body, frozenSlots),
342
+ userSlots: frozenSlots,
343
+ userIndexes: frozenIndexes,
344
+ });
345
+ }
346
+
347
+ return {
348
+ rules: Object.freeze(compiled),
349
+ externals: Object.freeze(externalNames),
350
+ readsPath,
351
+ // rules sharing one match path (across modes, the TOC/render idiom)
352
+ // enumerate it once per transform call through tctx.queryPaths
353
+ sharedQueries: pathRuleCount > pathQueryCache.size,
354
+ };
355
+ }
356
+
357
+ //#region match pre-pass
358
+
359
+ // A mode's pre-pass is ONE Map from normalized path to a match entry.
360
+ // A matched location holds its rule bits directly (a nonzero mask, or a
361
+ // Set of ordinals past 32 path rules); a location on the spine of a
362
+ // deeper match holds a SpineEntry carrying both its own match (0 or
363
+ // undefined when none) and the set of child keys continuing toward a
364
+ // match. One lookup per dispatched node answers "does a rule match
365
+ // here", "can a rule match below", and "through which children" - so a
366
+ // share rebuild copies every other child by reference without building
367
+ // its path string. Invariant: every present key's proper ancestors are
368
+ // present with their child links, so prefix insertion stops at the
369
+ // first present prefix.
370
+
371
+ class SpineEntry {
372
+ constructor(match, childKey) {
373
+ this.match = match; // 0 (bit mode) / undefined (Set mode) when unmatched
374
+ this.children = new Set([childKey]);
375
+ }
376
+ }
377
+
378
+ // Decode one normalized-path segment `['name']` (with RFC 9535 section
379
+ // 2.7 escapes) into the raw member name, or `[123]` into the integer
380
+ // index - the exact key forms the built-in rebuild walks with.
381
+ function parseSegmentKey(path, start, end) {
382
+ if (path.charCodeAt(start + 1) === 0x27) { // single quote: a member name
383
+ const from = start + 2;
384
+ const to = end - 2;
385
+ let i = from;
386
+ while (i < to && path.charCodeAt(i) !== 0x5C) // backslash
387
+ i++;
388
+ if (i === to)
389
+ return path.slice(from, to);
390
+ let out = path.slice(from, i);
391
+ while (i < to) {
392
+ if (path.charCodeAt(i) !== 0x5C) {
393
+ out += path[i];
394
+ i++;
395
+ continue;
396
+ }
397
+ const esc = path.charCodeAt(i + 1);
398
+ if (esc === 0x75) { // 'u': the \u00XX control-character form
399
+ out += String.fromCharCode(parseInt(path.slice(i + 2, i + 6), 16));
400
+ i += 6;
401
+ continue;
402
+ }
403
+ if (esc === 0x62) out += '\b';
404
+ else if (esc === 0x74) out += '\t';
405
+ else if (esc === 0x6E) out += '\n';
406
+ else if (esc === 0x66) out += '\f';
407
+ else if (esc === 0x72) out += '\r';
408
+ else out += path[i + 1]; // a quote or backslash escapes itself
409
+ i += 2;
410
+ }
411
+ return out;
412
+ }
413
+ let index = 0;
414
+ for (let i = start + 1; i < end - 1; i++)
415
+ index = index * 10 + (path.charCodeAt(i) - 0x30);
416
+ return index;
417
+ }
418
+
419
+ // Link `prefix -> childKey` into the pre-pass map. Returns true when the
420
+ // prefix was already present (its ancestors are then already linked).
421
+ function addSpineLink(matchMap, prefix, childKey, bitMode) {
422
+ const entry = matchMap.get(prefix);
423
+ if (entry === undefined) {
424
+ matchMap.set(prefix, new SpineEntry(bitMode ? 0 : undefined, childKey));
425
+ return false;
426
+ }
427
+ if (entry instanceof SpineEntry) {
428
+ entry.children.add(childKey);
429
+ return true;
430
+ }
431
+ // a matched-only entry becomes a spine entry keeping its match
432
+ matchMap.set(prefix, new SpineEntry(entry, childKey));
433
+ return true;
434
+ }
435
+
436
+ // Add every proper ancestor of one matched RFC 9535 normalized path,
437
+ // with its child link. Segment starts are '[' characters outside a
438
+ // quoted name; backslash escapes inside `['...']` skip the escaped code
439
+ // unit. Ancestors are linked longest-first so the presence invariant
440
+ // makes repeated spines O(1).
441
+ function addSpinePrefixes(path, matchMap, bitMode) {
442
+ let positions = null;
443
+ let quoted = false;
444
+ for (let i = 1; i < path.length; i++) {
445
+ const c = path.charCodeAt(i);
446
+ if (quoted) {
447
+ if (c === 0x5C) // backslash
448
+ i++;
449
+ else if (c === 0x27) // single quote
450
+ quoted = false;
451
+ }
452
+ else if (c === 0x27) { // single quote
453
+ quoted = true;
454
+ }
455
+ else if (c === 0x5B) { // '['
456
+ if (positions === null)
457
+ positions = [i];
458
+ else
459
+ positions.push(i);
460
+ }
461
+ }
462
+ if (positions === null)
463
+ return;
464
+ let end = path.length;
465
+ for (let j = positions.length - 1; j >= 0; j--) {
466
+ const start = positions[j];
467
+ const childKey = parseSegmentKey(path, start, end);
468
+ if (addSpineLink(matchMap, path.slice(0, start), childKey, bitMode))
469
+ return;
470
+ end = start;
471
+ }
472
+ }
473
+
474
+ // Every distinct match query enumerates the input once per transform
475
+ // call, however many modes (or ranked slots) reference it. Stylesheets
476
+ // without duplicate match paths bypass the cache (queryPaths === false).
477
+ function getQueryPaths(query, tctx) {
478
+ let cache = tctx.queryPaths;
479
+ if (cache === false)
480
+ return query.paths(tctx.root);
481
+ if (cache === null) {
482
+ cache = new Map();
483
+ tctx.queryPaths = cache;
484
+ }
485
+ const cached = cache.get(query);
486
+ if (cached !== undefined)
487
+ return cached;
488
+ const paths = query.paths(tctx.root);
489
+ cache.set(query, paths);
490
+ return paths;
491
+ }
492
+
493
+ function buildBitPrepass(mode, tctx) {
494
+ const matchMap = new Map();
495
+ const queries = mode.pathQueries;
496
+ for (let ordinal = 0; ordinal < queries.length; ordinal++) {
497
+ const paths = getQueryPaths(queries[ordinal], tctx);
498
+ const bit = 1 << ordinal;
499
+ for (let i = 0; i < paths.length; i++) {
500
+ const path = paths[i];
501
+ const entry = matchMap.get(path);
502
+ if (entry === undefined)
503
+ matchMap.set(path, bit);
504
+ else if (entry instanceof SpineEntry)
505
+ entry.match |= bit;
506
+ else
507
+ matchMap.set(path, entry | bit);
508
+ addSpinePrefixes(path, matchMap, true);
509
+ }
510
+ }
511
+ return matchMap;
512
+ }
513
+
514
+ function buildSetPrepass(mode, tctx) {
515
+ const matchMap = new Map();
516
+ const queries = mode.pathQueries;
517
+ for (let ordinal = 0; ordinal < queries.length; ordinal++) {
518
+ const paths = getQueryPaths(queries[ordinal], tctx);
519
+ for (let i = 0; i < paths.length; i++) {
520
+ const path = paths[i];
521
+ const entry = matchMap.get(path);
522
+ if (entry === undefined) {
523
+ matchMap.set(path, new Set([ordinal]));
524
+ }
525
+ else if (entry instanceof SpineEntry) {
526
+ if (entry.match === undefined)
527
+ entry.match = new Set([ordinal]);
528
+ else
529
+ entry.match.add(ordinal);
530
+ }
531
+ else {
532
+ entry.add(ordinal);
533
+ }
534
+ addSpinePrefixes(path, matchMap, false);
535
+ }
536
+ }
537
+ return matchMap;
538
+ }
539
+
540
+ function buildPrepass(mode, tctx) {
541
+ const prepass = mode.bitMasks
542
+ ? buildBitPrepass(mode, tctx)
543
+ : buildSetPrepass(mode, tctx);
544
+ tctx.prepasses[mode.id] = prepass;
545
+ return prepass;
546
+ }
547
+
548
+ //#region roadmap
549
+ // A single-walk multi-pattern path matcher would replace the per-rule
550
+ // `.paths(root)` loop here. Schema-aware pruning can join the same mode
551
+ // metadata once predicates expose safe descendant impossibility facts.
552
+ // Location tracking is intentionally global today; a future reachability
553
+ // analysis can specialize it per connected set of modes.
554
+ //#endregion
555
+
556
+ //#endregion
557
+
558
+ function scanBitRules(mode, value, loc, depth, tctx, mask) {
559
+ const pathBits = mode.pathBits;
560
+ const tests = mode.tests;
561
+ const bodyEvals = mode.bodyEvals;
562
+ for (let i = 0; i < bodyEvals.length; i++) {
563
+ // a zero mask also covers location-less values: no bit can be set
564
+ const bit = pathBits[i];
565
+ if (bit !== 0 && (mask & bit) === 0)
566
+ continue;
567
+ const test = tests[i];
568
+ if (test !== null && !test(value))
569
+ continue;
570
+ return bodyEvals[i](value, loc, depth, tctx);
571
+ }
572
+ return NO_RULE;
573
+ }
574
+
575
+ function scanSetRules(mode, value, loc, depth, tctx, matched) {
576
+ const pathOrdinals = mode.pathOrdinals;
577
+ const tests = mode.tests;
578
+ const bodyEvals = mode.bodyEvals;
579
+ for (let i = 0; i < bodyEvals.length; i++) {
580
+ const ordinal = pathOrdinals[i];
581
+ if (ordinal >= 0 && (matched === undefined || !matched.has(ordinal)))
582
+ continue;
583
+ const test = tests[i];
584
+ if (test !== null && !test(value))
585
+ continue;
586
+ return bodyEvals[i](value, loc, depth, tctx);
587
+ }
588
+ return NO_RULE;
589
+ }
590
+
591
+ function setObjectMember(out, name, value) {
592
+ if (name === '__proto__') {
593
+ Object.defineProperty(out, name, {
594
+ value,
595
+ enumerable: true,
596
+ configurable: true,
597
+ writable: true,
598
+ });
599
+ }
600
+ else {
601
+ out[name] = value;
602
+ }
603
+ }
604
+
605
+ function describeLocation(loc) {
606
+ return loc === null ? 'a location-less value' : loc;
607
+ }
608
+
609
+ // `children` (a share-mode all-path-rules prune set, null otherwise)
610
+ // lists the only keys through which a deeper rule can still match:
611
+ // every other child is copied by reference without a dispatch and
612
+ // without building its normalized path.
613
+ function rebuildObject(value, loc, mode, depth, tctx, dispatchMode, fresh, children) {
614
+ const out = {};
615
+ let changed = fresh;
616
+ for (const key in value) {
617
+ if (!hasOwn(value, key))
618
+ continue;
619
+ if (children !== null && !children.has(key)) {
620
+ setObjectMember(out, key, value[key]);
621
+ continue;
622
+ }
623
+ const childLoc = loc === null ? null : appendName(loc, key);
624
+ const child = dispatchMode(value[key], childLoc, mode, depth + 1, tctx);
625
+ if (child === EMPTY) {
626
+ changed = true;
627
+ continue;
628
+ }
629
+ if (child instanceof Seq) {
630
+ throw new JsltRuntimeError('JT2002',
631
+ `member '${key}' at ${describeLocation(childLoc)} produced ${child.items.length} items`,
632
+ mode.unmatchedPath);
633
+ }
634
+ setObjectMember(out, key, child);
635
+ if (child !== value[key])
636
+ changed = true;
637
+ }
638
+ return changed ? out : value;
639
+ }
640
+
641
+ function rebuildArray(value, loc, mode, depth, tctx, dispatchMode, fresh, children) {
642
+ const out = [];
643
+ let changed = fresh;
644
+ for (let i = 0; i < value.length; i++) {
645
+ if (children !== null && !children.has(i)) {
646
+ out.push(value[i]);
647
+ continue;
648
+ }
649
+ const childLoc = loc === null ? null : loc + '[' + i + ']';
650
+ const child = dispatchMode(value[i], childLoc, mode, depth + 1, tctx);
651
+ if (child === EMPTY) {
652
+ changed = true;
653
+ }
654
+ else if (child instanceof Seq) {
655
+ appendItem(out, child);
656
+ changed = true;
657
+ }
658
+ else {
659
+ out.push(child);
660
+ if (child !== value[i])
661
+ changed = true;
662
+ }
663
+ }
664
+ return changed ? out : value;
665
+ }
666
+
667
+ // The share pruning decision lives in dispatchMode (one pre-pass lookup
668
+ // yields the match and the prune set); by the time the built-in rule
669
+ // rebuilds, descending is already known to be required or harmless.
670
+ function builtIn(value, loc, mode, depth, tctx, dispatchMode, children) {
671
+ if (mode.unmatched === 'error') {
672
+ throw new JsltRuntimeError('JT2003',
673
+ `no rule in mode ${JSON.stringify(mode.name)} matched ${describeLocation(loc)}`,
674
+ mode.unmatchedPath);
675
+ }
676
+
677
+ if (typeof value !== 'object' || value === null)
678
+ return value;
679
+
680
+ const fresh = mode.unmatched === 'fresh';
681
+ return Array.isArray(value)
682
+ ? rebuildArray(value, loc, mode, depth, tctx, dispatchMode, fresh, children)
683
+ : rebuildObject(value, loc, mode, depth, tctx, dispatchMode, fresh, children);
684
+ }
685
+
686
+ function compileMode(modelMode, compiledRules, id, fallbackUnmatched, fallbackPath) {
687
+ const rankedRules = modelMode === null ? NO_EXTERNAL_VALUES : modelMode.rules;
688
+ const name = modelMode === null ? '' : modelMode.name;
689
+ const unmatched = modelMode === null ? fallbackUnmatched : modelMode.unmatched;
690
+ const unmatchedPath = modelMode === null ? fallbackPath : modelMode.unmatchedPath;
691
+ let pathCount = 0;
692
+ let allPathRules = true;
693
+ for (let i = 0; i < rankedRules.length; i++) {
694
+ const compiled = compiledRules[rankedRules[i].index];
695
+ if (compiled.pathQuery === null)
696
+ allPathRules = false;
697
+ else
698
+ pathCount++;
699
+ }
700
+
701
+ const bitMasks = pathCount <= 32;
702
+ const pathBits = bitMasks ? new Array(rankedRules.length) : null;
703
+ const pathOrdinals = bitMasks ? null : new Array(rankedRules.length);
704
+ const pathQueries = new Array(pathCount);
705
+ const tests = new Array(rankedRules.length);
706
+ const bodyEvals = new Array(rankedRules.length);
707
+ const ruleIndexes = new Array(rankedRules.length);
708
+ let ordinal = 0;
709
+ for (let i = 0; i < rankedRules.length; i++) {
710
+ const compiled = compiledRules[rankedRules[i].index];
711
+ if (compiled.pathQuery === null) {
712
+ if (bitMasks)
713
+ pathBits[i] = 0;
714
+ else
715
+ pathOrdinals[i] = -1;
716
+ }
717
+ else {
718
+ pathQueries[ordinal] = compiled.pathQuery;
719
+ if (bitMasks)
720
+ pathBits[i] = 1 << ordinal;
721
+ else
722
+ pathOrdinals[i] = ordinal;
723
+ ordinal++;
724
+ }
725
+ tests[i] = compiled.test;
726
+ bodyEvals[i] = compiled.bodyEval;
727
+ ruleIndexes[i] = compiled.index;
728
+ }
729
+
730
+ return Object.freeze({
731
+ id,
732
+ name,
733
+ unmatched,
734
+ unmatchedPath,
735
+ // Every rule of a share-mode chain requires a positional match: a
736
+ // value without a match entry (off-spine, or location-less) can fire
737
+ // nothing here or below and returns by reference without a scan.
738
+ // Covers the zero-rule share mode (vacuously all-path).
739
+ shareAllPaths: unmatched === 'share' && allPathRules,
740
+ bitMasks,
741
+ pathQueries: Object.freeze(pathQueries),
742
+ pathBits: pathBits === null ? null : Object.freeze(pathBits),
743
+ pathOrdinals: pathOrdinals === null ? null : Object.freeze(pathOrdinals),
744
+ tests: Object.freeze(tests),
745
+ bodyEvals: Object.freeze(bodyEvals),
746
+ ruleIndexes: Object.freeze(ruleIndexes),
747
+ });
748
+ }
749
+
750
+ function buildModes(model, compiledRules, targetModes) {
751
+ const modelModes = new Map();
752
+ const names = [];
753
+ for (let i = 0; i < model.modes.length; i++) {
754
+ const mode = model.modes[i];
755
+ modelModes.set(mode.name, mode);
756
+ names.push(mode.name);
757
+ }
758
+ for (const name of targetModes) {
759
+ if (!modelModes.has(name)) {
760
+ modelModes.set(name, null);
761
+ names.push(name);
762
+ }
763
+ }
764
+
765
+ const modes = new Map();
766
+ for (let i = 0; i < names.length; i++) {
767
+ const name = names[i];
768
+ const modelMode = modelModes.get(name);
769
+ const runtime = modelMode === null
770
+ ? Object.freeze({
771
+ ...compileMode(null, compiledRules, i, model.unmatched, model.unmatchedPath),
772
+ name,
773
+ })
774
+ : compileMode(modelMode, compiledRules, i, model.unmatched, model.unmatchedPath);
775
+ modes.set(name, runtime);
776
+ }
777
+ return { modes, count: names.length };
778
+ }
779
+
780
+ function resolveExternalValues(externalNames, compiledRules, ext) {
781
+ const globalValues = new Array(externalNames.length);
782
+ for (let i = 0; i < externalNames.length; i++) {
783
+ const name = externalNames[i];
784
+ globalValues[i] = ext != null && hasOwn(ext, name) ? ext[name] : UNBOUND;
785
+ }
786
+
787
+ const ruleValues = new Array(compiledRules.length);
788
+ for (let i = 0; i < compiledRules.length; i++) {
789
+ const indexes = compiledRules[i].userIndexes;
790
+ if (indexes.length === 0) {
791
+ ruleValues[i] = NO_EXTERNAL_VALUES;
792
+ continue;
793
+ }
794
+ const values = new Array(indexes.length);
795
+ for (let j = 0; j < indexes.length; j++)
796
+ values[j] = globalValues[indexes[j]];
797
+ ruleValues[i] = values;
798
+ }
799
+ return ruleValues;
800
+ }
801
+
802
+ /**
803
+ * Compile a normalized stylesheet model into the reusable transformation
804
+ * evaluator and its user-external metadata.
805
+ * @param {object} model - result of normalizeJsltStylesheet
806
+ * @param {object} [options] - compile options
807
+ * @returns {{evaluate: Function, externals: readonly string[]}}
808
+ */
809
+ export function compileJsltDispatch(model, options = {}) {
810
+ const compileTypeTest = typeof options.compileTypeTest === 'function'
811
+ ? options.compileTypeTest
812
+ : null;
813
+ const maxDepth = options.maxDepth === undefined ? 1024 : options.maxDepth;
814
+ if (!Number.isInteger(maxDepth) || maxDepth < 0)
815
+ throw new TypeError('options.maxDepth must be a non-negative integer');
816
+
817
+ const tableBox = {
818
+ dispatch: null,
819
+ needsLoc: false,
820
+ };
821
+ const targetModes = new Set();
822
+ const compiled = compileRules(model, compileTypeTest, tableBox, targetModes);
823
+ tableBox.needsLoc = model.anyPathRule || compiled.readsPath;
824
+ const built = buildModes(model, compiled.rules, targetModes);
825
+ const modes = built.modes;
826
+ const rootMode = modes.get('');
827
+
828
+ function dispatchMode(value, loc, mode, depth, tctx) {
829
+ if (depth > maxDepth) {
830
+ throw new JsltRuntimeError('JT2001',
831
+ `dispatch depth ${depth} exceeded maxDepth ${maxDepth}`,
832
+ mode.unmatchedPath);
833
+ }
834
+ if (depth > tctx.highestDepth)
835
+ tctx.highestDepth = depth;
836
+
837
+ // location-less values can never match a path rule; modes reached
838
+ // only through location-less items never pay for a pre-pass
839
+ let match; // rule bit mask (bit mode) or Set of ordinals, or undefined
840
+ let children = null;
841
+ if (loc !== null && mode.pathQueries.length !== 0) {
842
+ let prepass = tctx.prepasses[mode.id];
843
+ if (prepass === undefined)
844
+ prepass = buildPrepass(mode, tctx);
845
+ const entry = prepass.get(loc);
846
+ if (entry !== undefined) {
847
+ if (entry instanceof SpineEntry) {
848
+ match = entry.match;
849
+ children = entry.children;
850
+ }
851
+ else {
852
+ match = entry;
853
+ }
854
+ }
855
+ }
856
+ if (mode.shareAllPaths) {
857
+ // every rule needs a positional match: values without one (and all
858
+ // location-less values) can fire nothing here or anywhere below
859
+ if (children === null) {
860
+ if (match === undefined)
861
+ return value;
862
+ }
863
+ else if (match === undefined || match === 0) {
864
+ // on the spine of a deeper match only: descend without a scan
865
+ return builtIn(value, loc, mode, depth, tctx, dispatchMode, children);
866
+ }
867
+ }
868
+ const matched = mode.bitMasks
869
+ ? scanBitRules(mode, value, loc, depth, tctx, match === undefined ? 0 : match)
870
+ : scanSetRules(mode, value, loc, depth, tctx, match);
871
+ if (matched !== NO_RULE)
872
+ return matched;
873
+ return builtIn(value, loc, mode, depth, tctx, dispatchMode,
874
+ mode.shareAllPaths ? (children === null ? NO_CHILDREN : children) : null);
875
+ }
876
+
877
+ function dispatch(value, loc, modeName, depth, tctx) {
878
+ const mode = modes.get(modeName);
879
+ /* c8 ignore next 2 -- every static $apply target is registered at compile time */
880
+ if (mode === undefined)
881
+ throw new Error(`JSLT internal error: mode ${JSON.stringify(modeName)} was not compiled`);
882
+ return dispatchMode(value, loc, mode, depth, tctx);
883
+ }
884
+
885
+ tableBox.dispatch = dispatch;
886
+ Object.freeze(tableBox);
887
+
888
+ if (rootMode.bodyEvals.length === 0 && rootMode.unmatched === 'share') {
889
+ return Object.freeze({
890
+ evaluate: (data) => data,
891
+ externals: compiled.externals,
892
+ });
893
+ }
894
+
895
+ const modeCount = built.count;
896
+ const externalNames = compiled.externals;
897
+ const compiledRules = compiled.rules;
898
+ const sharedQueries = compiled.sharedQueries;
899
+ const rootLoc = tableBox.needsLoc ? '$' : null;
900
+ // the all-unbound resolution is a compile-time constant; calls without
901
+ // user bindings (the common case) share it instead of re-resolving
902
+ const unboundRuleValues = resolveExternalValues(externalNames, compiledRules, null);
903
+ return Object.freeze({
904
+ evaluate(data, ext) {
905
+ const tctx = {
906
+ root: data,
907
+ ruleExternalValues: ext == null || externalNames.length === 0
908
+ ? unboundRuleValues
909
+ : resolveExternalValues(externalNames, compiledRules, ext),
910
+ prepasses: new Array(modeCount),
911
+ queryPaths: sharedQueries ? null : false,
912
+ highestDepth: 0,
913
+ };
914
+ try {
915
+ return dispatchMode(data, rootLoc, rootMode, 0, tctx);
916
+ }
917
+ catch (error) {
918
+ // A synchronous JavaScript stack can be shallower than the
919
+ // language's default 1024-dispatch guard. Never expose the host
920
+ // RangeError for a recursive dispatch chain; preserve JT2001 as
921
+ // the public resource-guard condition.
922
+ if (error instanceof RangeError && tctx.highestDepth > 32) {
923
+ throw new JsltRuntimeError('JT2001',
924
+ `dispatch recursion exhausted the host call stack before maxDepth ${maxDepth}`,
925
+ rootMode.unmatchedPath);
926
+ }
927
+ throw error;
928
+ }
929
+ },
930
+ externals: externalNames,
931
+ });
932
+ }
933
+
934
+ //#endregion