@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.
- package/ARCHITECTURE.md +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- 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
|