@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,34 @@
|
|
|
1
|
+
//#region Jaren JSLT errors
|
|
2
|
+
// Error classes for the Jaren JSLT engine. Every error carries a stable
|
|
3
|
+
// `code` and a `docPath`, an RFC 6901 JSON Pointer into the stylesheet
|
|
4
|
+
// document. Wrapped parser/query/hook errors are exposed through `cause`.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown when a JSLT stylesheet is rejected at compile time
|
|
8
|
+
* (`JT0xxx` codes).
|
|
9
|
+
*/
|
|
10
|
+
export class JsltCompileError extends Error {
|
|
11
|
+
constructor(code, message, docPath, cause = undefined) {
|
|
12
|
+
super(`${code}: ${message} at ${docPath}`,
|
|
13
|
+
cause === undefined ? undefined : { cause });
|
|
14
|
+
this.name = 'JsltCompileError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.docPath = docPath;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error thrown when evaluating a compiled JSLT stylesheet fails
|
|
22
|
+
* (`JT2xxx` codes).
|
|
23
|
+
*/
|
|
24
|
+
export class JsltRuntimeError extends Error {
|
|
25
|
+
constructor(code, message, docPath, cause = undefined) {
|
|
26
|
+
super(`${code}: ${message} at ${docPath}`,
|
|
27
|
+
cause === undefined ? undefined : { cause });
|
|
28
|
+
this.name = 'JsltRuntimeError';
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.docPath = docPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//#region Jaren JSLT public API
|
|
2
|
+
// A compiled JSON stylesheet layer over the Jaren JSON Query engine.
|
|
3
|
+
// Stylesheets normalize once, compile to ranked dispatch closures once,
|
|
4
|
+
// and may then transform any number of input documents.
|
|
5
|
+
|
|
6
|
+
import { deepFreezeCopy } from '../query/normalize.js';
|
|
7
|
+
import { EMPTY, Seq } from '../query/runtime.js';
|
|
8
|
+
import { normalizeJsltStylesheet } from './stylesheet.js';
|
|
9
|
+
import { compileJsltDispatch } from './dispatch.js';
|
|
10
|
+
|
|
11
|
+
export { JsltCompileError, JsltRuntimeError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compile a Jaren JSLT 0.1 stylesheet into a reusable transformation.
|
|
15
|
+
*
|
|
16
|
+
* The returned function maps the internal sequence result to plain JSON:
|
|
17
|
+
* `undefined` for the empty sequence, the item itself for a singleton,
|
|
18
|
+
* and an array of items for a longer sequence. Metadata:
|
|
19
|
+
*
|
|
20
|
+
* - `transform.externals` - user parameter names in first-appearance order
|
|
21
|
+
* (`root` and `path` are engine-bound and excluded)
|
|
22
|
+
* - `transform.doc` - an independent, deeply frozen stylesheet copy
|
|
23
|
+
*
|
|
24
|
+
* @param {any} doc - a bare rule array or `{"$jslt":"0.1","rules":[]}`
|
|
25
|
+
* stylesheet envelope
|
|
26
|
+
* @param {object} [options] - compile options
|
|
27
|
+
* @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
|
|
28
|
+
* [options.compileTypeTest] - validator-agnostic hook compiling schema
|
|
29
|
+
* match conditions and schema literals inside query bodies
|
|
30
|
+
* @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
|
|
31
|
+
* @returns {function} reusable `transform(data, externals?)` function
|
|
32
|
+
* @throws {import('./errors.js').JsltCompileError} when compilation fails
|
|
33
|
+
* @example
|
|
34
|
+
* const transform = compileJsltStylesheet([
|
|
35
|
+
* { match: '$..price', body: { $mul: ['$', 1.21] } }
|
|
36
|
+
* ]);
|
|
37
|
+
* transform({ item: { price: 10 } });
|
|
38
|
+
* // { item: { price: 12.1 } }
|
|
39
|
+
*/
|
|
40
|
+
export function compileJsltStylesheet(doc, options = {}) {
|
|
41
|
+
const frozenDoc = deepFreezeCopy(doc);
|
|
42
|
+
const model = normalizeJsltStylesheet(frozenDoc);
|
|
43
|
+
const runtime = compileJsltDispatch(model, options);
|
|
44
|
+
|
|
45
|
+
const transform = (data, externals) => {
|
|
46
|
+
const value = runtime.evaluate(data, externals);
|
|
47
|
+
if (value === EMPTY)
|
|
48
|
+
return undefined;
|
|
49
|
+
return value instanceof Seq ? value.items : value;
|
|
50
|
+
};
|
|
51
|
+
transform.externals = runtime.externals;
|
|
52
|
+
transform.doc = frozenDoc;
|
|
53
|
+
return transform;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const STYLESHEET_CACHE = new WeakMap();
|
|
57
|
+
|
|
58
|
+
function cachedTransform(stylesheet, options) {
|
|
59
|
+
let record = STYLESHEET_CACHE.get(stylesheet);
|
|
60
|
+
if (record === undefined) {
|
|
61
|
+
record = {
|
|
62
|
+
defaultTransform: null,
|
|
63
|
+
variants: null,
|
|
64
|
+
};
|
|
65
|
+
STYLESHEET_CACHE.set(stylesheet, record);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const compileTypeTest = typeof options?.compileTypeTest === 'function'
|
|
69
|
+
? options.compileTypeTest
|
|
70
|
+
: null;
|
|
71
|
+
const maxDepth = options?.maxDepth === undefined ? 1024 : options.maxDepth;
|
|
72
|
+
if (compileTypeTest === null && maxDepth === 1024) {
|
|
73
|
+
if (record.defaultTransform === null)
|
|
74
|
+
record.defaultTransform = compileJsltStylesheet(stylesheet);
|
|
75
|
+
return record.defaultTransform;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let variants = record.variants;
|
|
79
|
+
if (variants === null) {
|
|
80
|
+
variants = [];
|
|
81
|
+
record.variants = variants;
|
|
82
|
+
}
|
|
83
|
+
for (let i = 0; i < variants.length; i++) {
|
|
84
|
+
const variant = variants[i];
|
|
85
|
+
if (variant.compileTypeTest === compileTypeTest && variant.maxDepth === maxDepth)
|
|
86
|
+
return variant.transform;
|
|
87
|
+
}
|
|
88
|
+
const transform = compileJsltStylesheet(stylesheet, options);
|
|
89
|
+
variants.push({ compileTypeTest, maxDepth, transform });
|
|
90
|
+
return transform;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Transform a JSON value with a JSLT stylesheet in one call.
|
|
95
|
+
* Object/array stylesheet documents are compiled once and cached by
|
|
96
|
+
* identity in a WeakMap.
|
|
97
|
+
* @param {any} stylesheet - JSLT stylesheet document
|
|
98
|
+
* @param {any} data - input JSON value
|
|
99
|
+
* @param {object} [externals] - user parameter bindings
|
|
100
|
+
* @param {object} [options] - compile options used on a cache miss
|
|
101
|
+
* @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
|
|
102
|
+
* [options.compileTypeTest] - schema type-test compiler
|
|
103
|
+
* @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
|
|
104
|
+
* @returns {any} `undefined`, one JSON item, or an array of result items
|
|
105
|
+
* @throws {import('./errors.js').JsltCompileError} when compilation fails
|
|
106
|
+
* @throws {import('./errors.js').JsltRuntimeError} when dispatch fails
|
|
107
|
+
* @example
|
|
108
|
+
* transformJson([], { value: 1 }); // returns the input object by reference
|
|
109
|
+
*/
|
|
110
|
+
export function transformJson(stylesheet, data, externals, options = undefined) {
|
|
111
|
+
let transform;
|
|
112
|
+
if (typeof stylesheet === 'object' && stylesheet !== null) {
|
|
113
|
+
transform = cachedTransform(stylesheet, options);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
transform = compileJsltStylesheet(stylesheet, options);
|
|
117
|
+
}
|
|
118
|
+
return transform(data, externals);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
//#region Jaren JSLT stylesheet normalizer
|
|
2
|
+
// Validates the closed stylesheet vocabulary, partitions rules by mode,
|
|
3
|
+
// and precomputes conflict order. The output is frozen data only; path,
|
|
4
|
+
// schema, and body closures are compiled by dispatch.js.
|
|
5
|
+
|
|
6
|
+
import { JsltCompileError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
const hasOwn = Object.hasOwn;
|
|
9
|
+
const ENVELOPE_KEYS = new Set(['$jslt', 'rules', 'unmatched', 'modes']);
|
|
10
|
+
const RULE_KEYS = new Set(['match', 'mode', 'priority', 'body']);
|
|
11
|
+
const MATCH_KEYS = new Set(['path', 'schema']);
|
|
12
|
+
const MODE_KEYS = new Set(['unmatched']);
|
|
13
|
+
|
|
14
|
+
function isObject(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function escapeToken(token) {
|
|
19
|
+
if (token.indexOf('~') < 0 && token.indexOf('/') < 0)
|
|
20
|
+
return token;
|
|
21
|
+
return token.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fail(code, message, docPath) {
|
|
25
|
+
throw new JsltCompileError(code, message, docPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isDisposition(value) {
|
|
29
|
+
return value === 'share' || value === 'fresh' || value === 'error';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeModes(value, docPath) {
|
|
33
|
+
if (!isObject(value))
|
|
34
|
+
fail('JT0001', "'modes' must be an object", docPath);
|
|
35
|
+
const modes = new Map();
|
|
36
|
+
const names = Object.keys(value);
|
|
37
|
+
for (let i = 0; i < names.length; i++) {
|
|
38
|
+
const name = names[i];
|
|
39
|
+
const modePath = docPath + '/' + escapeToken(name);
|
|
40
|
+
const config = value[name];
|
|
41
|
+
if (!isObject(config))
|
|
42
|
+
fail('JT0001', `mode '${name}' must be an object`, modePath);
|
|
43
|
+
const keys = Object.keys(config);
|
|
44
|
+
for (let j = 0; j < keys.length; j++) {
|
|
45
|
+
if (!MODE_KEYS.has(keys[j]))
|
|
46
|
+
fail('JT0001', `unknown mode member '${keys[j]}'`,
|
|
47
|
+
modePath + '/' + escapeToken(keys[j]));
|
|
48
|
+
}
|
|
49
|
+
if (!hasOwn(config, 'unmatched'))
|
|
50
|
+
fail('JT0001', `mode '${name}' requires 'unmatched'`, modePath + '/unmatched');
|
|
51
|
+
if (!isDisposition(config.unmatched))
|
|
52
|
+
fail('JT0001', `invalid unmatched disposition ${JSON.stringify(config.unmatched)}`,
|
|
53
|
+
modePath + '/unmatched');
|
|
54
|
+
modes.set(name, Object.freeze({
|
|
55
|
+
unmatched: config.unmatched,
|
|
56
|
+
unmatchedPath: modePath + '/unmatched',
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
return modes;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeMatch(value, matchPath) {
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
path: value,
|
|
66
|
+
pathDocPath: matchPath,
|
|
67
|
+
schema: null,
|
|
68
|
+
schemaPresent: false,
|
|
69
|
+
schemaDocPath: '',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (!isObject(value))
|
|
73
|
+
fail('JT0003', "'match' must be a JSONPath string or an object", matchPath);
|
|
74
|
+
|
|
75
|
+
const keys = Object.keys(value);
|
|
76
|
+
if (keys.length === 0)
|
|
77
|
+
fail('JT0003', "'match' cannot be an empty object", matchPath);
|
|
78
|
+
for (let i = 0; i < keys.length; i++) {
|
|
79
|
+
if (!MATCH_KEYS.has(keys[i]))
|
|
80
|
+
fail('JT0003', `unknown match member '${keys[i]}'`,
|
|
81
|
+
matchPath + '/' + escapeToken(keys[i]));
|
|
82
|
+
}
|
|
83
|
+
const hasPath = hasOwn(value, 'path');
|
|
84
|
+
const hasSchema = hasOwn(value, 'schema');
|
|
85
|
+
if (!hasPath && !hasSchema)
|
|
86
|
+
fail('JT0003', "'match' requires 'path' and/or 'schema'", matchPath);
|
|
87
|
+
if (hasPath && typeof value.path !== 'string')
|
|
88
|
+
fail('JT0003', "'match.path' must be a JSONPath string", matchPath + '/path');
|
|
89
|
+
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
path: hasPath ? value.path : null,
|
|
92
|
+
pathDocPath: hasPath ? matchPath + '/path' : '',
|
|
93
|
+
schema: hasSchema ? value.schema : null,
|
|
94
|
+
schemaPresent: hasSchema,
|
|
95
|
+
schemaDocPath: hasSchema ? matchPath + '/schema' : '',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeRule(value, index, rulesPath) {
|
|
100
|
+
const rulePath = rulesPath + '/' + index;
|
|
101
|
+
if (!isObject(value))
|
|
102
|
+
fail('JT0002', 'a stylesheet rule must be an object', rulePath);
|
|
103
|
+
|
|
104
|
+
const keys = Object.keys(value);
|
|
105
|
+
for (let i = 0; i < keys.length; i++) {
|
|
106
|
+
if (!RULE_KEYS.has(keys[i]))
|
|
107
|
+
fail('JT0002', `unknown rule member '${keys[i]}'`,
|
|
108
|
+
rulePath + '/' + escapeToken(keys[i]));
|
|
109
|
+
}
|
|
110
|
+
if (!hasOwn(value, 'body'))
|
|
111
|
+
fail('JT0002', "a stylesheet rule requires 'body'", rulePath + '/body');
|
|
112
|
+
if (hasOwn(value, 'mode') && typeof value.mode !== 'string')
|
|
113
|
+
fail('JT0002', "'mode' must be a string", rulePath + '/mode');
|
|
114
|
+
if (hasOwn(value, 'priority')
|
|
115
|
+
&& (typeof value.priority !== 'number' || !Number.isFinite(value.priority)))
|
|
116
|
+
fail('JT0002', "'priority' must be a finite JSON number", rulePath + '/priority');
|
|
117
|
+
|
|
118
|
+
const match = hasOwn(value, 'match')
|
|
119
|
+
? normalizeMatch(value.match, rulePath + '/match')
|
|
120
|
+
: null;
|
|
121
|
+
const pathCount = match !== null && match.path !== null ? 1 : 0;
|
|
122
|
+
const schemaCount = match !== null && match.schemaPresent ? 1 : 0;
|
|
123
|
+
const defaultPriority = pathCount + schemaCount === 2
|
|
124
|
+
? 1
|
|
125
|
+
: (pathCount + schemaCount === 1 ? 0 : -1);
|
|
126
|
+
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
index,
|
|
129
|
+
docPath: rulePath,
|
|
130
|
+
bodyDocPath: rulePath + '/body',
|
|
131
|
+
body: value.body,
|
|
132
|
+
mode: hasOwn(value, 'mode') ? value.mode : '',
|
|
133
|
+
priority: hasOwn(value, 'priority') ? value.priority : defaultPriority,
|
|
134
|
+
match,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Normalize a frozen JSLT stylesheet document into a frozen, closure-free
|
|
140
|
+
* model with ranked per-mode rule arrays.
|
|
141
|
+
* @param {any} doc - deeply frozen stylesheet document
|
|
142
|
+
* @returns {object} frozen stylesheet model
|
|
143
|
+
* @throws {JsltCompileError} on JT0001-JT0004 shape errors
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeJsltStylesheet(doc) {
|
|
146
|
+
let sourceRules;
|
|
147
|
+
let rulesPath;
|
|
148
|
+
let unmatched = 'share';
|
|
149
|
+
let unmatchedPath = '';
|
|
150
|
+
let declaredModes = new Map();
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(doc)) {
|
|
153
|
+
sourceRules = doc;
|
|
154
|
+
rulesPath = '';
|
|
155
|
+
}
|
|
156
|
+
else if (isObject(doc)) {
|
|
157
|
+
const keys = Object.keys(doc);
|
|
158
|
+
for (let i = 0; i < keys.length; i++) {
|
|
159
|
+
if (!ENVELOPE_KEYS.has(keys[i]))
|
|
160
|
+
fail('JT0001', `unknown stylesheet member '${keys[i]}'`,
|
|
161
|
+
'/' + escapeToken(keys[i]));
|
|
162
|
+
}
|
|
163
|
+
if (!hasOwn(doc, '$jslt'))
|
|
164
|
+
fail('JT0001', "the stylesheet envelope requires '$jslt'", '/$jslt');
|
|
165
|
+
if (doc.$jslt !== '0.1')
|
|
166
|
+
fail('JT0004', `unknown JSLT format version ${JSON.stringify(doc.$jslt)}`, '/$jslt');
|
|
167
|
+
if (!hasOwn(doc, 'rules'))
|
|
168
|
+
fail('JT0001', "the stylesheet envelope requires 'rules'", '/rules');
|
|
169
|
+
if (!Array.isArray(doc.rules))
|
|
170
|
+
fail('JT0001', "'rules' must be an array", '/rules');
|
|
171
|
+
if (hasOwn(doc, 'unmatched')) {
|
|
172
|
+
if (!isDisposition(doc.unmatched))
|
|
173
|
+
fail('JT0001', `invalid unmatched disposition ${JSON.stringify(doc.unmatched)}`,
|
|
174
|
+
'/unmatched');
|
|
175
|
+
unmatched = doc.unmatched;
|
|
176
|
+
unmatchedPath = '/unmatched';
|
|
177
|
+
}
|
|
178
|
+
if (hasOwn(doc, 'modes'))
|
|
179
|
+
declaredModes = normalizeModes(doc.modes, '/modes');
|
|
180
|
+
sourceRules = doc.rules;
|
|
181
|
+
rulesPath = '/rules';
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
fail('JT0001', 'a stylesheet must be a rule array or a version envelope', '');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rules = new Array(sourceRules.length);
|
|
188
|
+
const rulesByMode = new Map();
|
|
189
|
+
rulesByMode.set('', []);
|
|
190
|
+
let anyPathRule = false;
|
|
191
|
+
for (let i = 0; i < sourceRules.length; i++) {
|
|
192
|
+
const rule = normalizeRule(sourceRules[i], i, rulesPath);
|
|
193
|
+
rules[i] = rule;
|
|
194
|
+
let modeRules = rulesByMode.get(rule.mode);
|
|
195
|
+
if (modeRules === undefined) {
|
|
196
|
+
modeRules = [];
|
|
197
|
+
rulesByMode.set(rule.mode, modeRules);
|
|
198
|
+
}
|
|
199
|
+
modeRules.push(rule);
|
|
200
|
+
if (rule.match !== null && rule.match.path !== null)
|
|
201
|
+
anyPathRule = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const name of declaredModes.keys()) {
|
|
205
|
+
if (!rulesByMode.has(name))
|
|
206
|
+
rulesByMode.set(name, []);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const modes = [];
|
|
210
|
+
for (const [name, modeRules] of rulesByMode) {
|
|
211
|
+
modeRules.sort((a, b) => b.priority - a.priority || b.index - a.index);
|
|
212
|
+
const declared = declaredModes.get(name);
|
|
213
|
+
modes.push(Object.freeze({
|
|
214
|
+
name,
|
|
215
|
+
unmatched: declared !== undefined
|
|
216
|
+
? declared.unmatched
|
|
217
|
+
: unmatched,
|
|
218
|
+
unmatchedPath: declared !== undefined
|
|
219
|
+
? declared.unmatchedPath
|
|
220
|
+
: unmatchedPath,
|
|
221
|
+
rules: Object.freeze(modeRules),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
rules: Object.freeze(rules),
|
|
227
|
+
modes: Object.freeze(modes),
|
|
228
|
+
unmatched,
|
|
229
|
+
unmatchedPath,
|
|
230
|
+
anyPathRule,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
//#endregion
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
//#region Jaren JTLT desugarer
|
|
2
|
+
// Compiles the template model down to an ordinary JSLT 0.1 stylesheet.
|
|
3
|
+
// Each rule body becomes an array constructor of tagged segment pairs:
|
|
4
|
+
//
|
|
5
|
+
// SegTree := Pair | Array<SegTree>
|
|
6
|
+
// Pair := ['r<docPath?>', text] raw literal text
|
|
7
|
+
// | ['e<docPath?>', ...values] interpolated, method-escaped
|
|
8
|
+
// | ['w<docPath?>', ...values] interpolated, never escaped
|
|
9
|
+
// | ['j<docPath?>', ...values] JSON.stringify'd, method-escaped
|
|
10
|
+
//
|
|
11
|
+
// The tag is a compile-time string constant: one kind character followed
|
|
12
|
+
// by the segment's docPath in the *template* document, so runtime errors
|
|
13
|
+
// point at the author's source. Literal pairs are wrapped in $const and
|
|
14
|
+
// allocate nothing per render.
|
|
15
|
+
//
|
|
16
|
+
// A matchless catch-all rule is appended per mode at the reserved
|
|
17
|
+
// priority. It guarantees every dispatched node fires *some* rule, so
|
|
18
|
+
// the stream grammar above is closed: data values only ever appear as
|
|
19
|
+
// pair payloads, never bare, and `typeof x[0] === 'string'` identifies a
|
|
20
|
+
// pair unambiguously.
|
|
21
|
+
|
|
22
|
+
import { JtltCompileError } from './errors.js';
|
|
23
|
+
|
|
24
|
+
const DOLLAR = 0x24;
|
|
25
|
+
const BUILTIN_PRIORITY = -1e308;
|
|
26
|
+
|
|
27
|
+
function fail(code, message, docPath) {
|
|
28
|
+
throw new JtltCompileError(code, message, docPath);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Collect every statically declared `$apply` target mode inside a plain
|
|
32
|
+
// JSON expression tree. `$apply` mode arguments are literal strings by
|
|
33
|
+
// JSLT contract, so this walk cannot under-collect; over-collection
|
|
34
|
+
// (e.g. an `$apply` shape quoted inside `$const`) only compiles a spare
|
|
35
|
+
// catch-all rule that is never dispatched.
|
|
36
|
+
function collectApplyModes(node, modes) {
|
|
37
|
+
if (Array.isArray(node)) {
|
|
38
|
+
for (let i = 0; i < node.length; i++)
|
|
39
|
+
collectApplyModes(node[i], modes);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (typeof node !== 'object' || node === null)
|
|
43
|
+
return;
|
|
44
|
+
const keys = Object.keys(node);
|
|
45
|
+
for (let i = 0; i < keys.length; i++) {
|
|
46
|
+
const value = node[keys[i]];
|
|
47
|
+
if (keys[i] === '$apply' && Array.isArray(value)
|
|
48
|
+
&& value.length === 2 && typeof value[1] === 'string')
|
|
49
|
+
modes.add(value[1]);
|
|
50
|
+
collectApplyModes(value, modes);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function desugarSegment(seg, path, modes) {
|
|
55
|
+
if (typeof seg === 'string') {
|
|
56
|
+
if (seg.charCodeAt(0) !== DOLLAR)
|
|
57
|
+
return { $const: ['r', seg] };
|
|
58
|
+
if (seg.charCodeAt(1) === DOLLAR) // '$$x' is the literal text '$x'
|
|
59
|
+
return { $const: ['r', seg.slice(1)] };
|
|
60
|
+
return ['e' + path, seg];
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(seg)) {
|
|
63
|
+
const out = new Array(seg.length);
|
|
64
|
+
for (let j = 0; j < seg.length; j++)
|
|
65
|
+
out[j] = desugarSegment(seg[j], path + '/' + j, modes);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
if (typeof seg === 'object' && seg !== null) {
|
|
69
|
+
const keys = Object.keys(seg);
|
|
70
|
+
if (keys.length === 1) {
|
|
71
|
+
const key = keys[0];
|
|
72
|
+
if (key === '$raw') {
|
|
73
|
+
collectApplyModes(seg.$raw, modes);
|
|
74
|
+
return ['w' + path, seg.$raw];
|
|
75
|
+
}
|
|
76
|
+
if (key === '$json') {
|
|
77
|
+
collectApplyModes(seg.$json, modes);
|
|
78
|
+
return ['j' + path, seg.$json];
|
|
79
|
+
}
|
|
80
|
+
if (key === '$apply') { // passes through: its output is spliced verbatim
|
|
81
|
+
collectApplyModes(seg, modes);
|
|
82
|
+
return seg;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let anyDollar = false;
|
|
86
|
+
for (let j = 0; j < keys.length; j++) {
|
|
87
|
+
if (keys[j].charCodeAt(0) === DOLLAR) {
|
|
88
|
+
anyDollar = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!anyDollar) {
|
|
93
|
+
fail('TL0004',
|
|
94
|
+
'a plain object is not a template segment (map constructors cannot be serialized; use an operator phrase, $json, or $apply)',
|
|
95
|
+
path);
|
|
96
|
+
}
|
|
97
|
+
collectApplyModes(seg, modes);
|
|
98
|
+
return ['e' + path, seg];
|
|
99
|
+
}
|
|
100
|
+
fail('TL0004',
|
|
101
|
+
`${seg === null ? 'null' : typeof seg} is not a template segment; write literal text as a string`,
|
|
102
|
+
path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The built-in rule per mode: containers apply templates to every child
|
|
106
|
+
// in document order, atoms interpolate their string value (the XSLT
|
|
107
|
+
// built-in template rules, restated for JSON).
|
|
108
|
+
function builtinRule(mode) {
|
|
109
|
+
const rule = {
|
|
110
|
+
priority: BUILTIN_PRIORITY,
|
|
111
|
+
body: {
|
|
112
|
+
$if: [
|
|
113
|
+
{ $or: [{ '$is-object': '$' }, { '$is-array': '$' }] },
|
|
114
|
+
[{ $apply: '$[*]' }],
|
|
115
|
+
['e', '$'],
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
if (mode !== '')
|
|
120
|
+
rule.mode = mode;
|
|
121
|
+
return rule;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Desugar a normalized template model into a JSLT 0.1 stylesheet
|
|
126
|
+
* document. User rules keep their index; built-in rules are appended.
|
|
127
|
+
* @param {object} model - result of normalizeJtltTemplate
|
|
128
|
+
* @returns {object} a JSLT stylesheet envelope
|
|
129
|
+
* @throws {JtltCompileError} on TL0004 segment errors
|
|
130
|
+
*/
|
|
131
|
+
export function desugarTemplate(model) {
|
|
132
|
+
const modes = new Set(['']);
|
|
133
|
+
const rules = [];
|
|
134
|
+
const templateRules = model.rules;
|
|
135
|
+
for (let i = 0; i < templateRules.length; i++) {
|
|
136
|
+
const rule = templateRules[i];
|
|
137
|
+
modes.add(rule.mode);
|
|
138
|
+
const body = new Array(rule.body.length);
|
|
139
|
+
for (let j = 0; j < rule.body.length; j++)
|
|
140
|
+
body[j] = desugarSegment(rule.body[j], rule.bodyDocPath + '/' + j, modes);
|
|
141
|
+
const out = {};
|
|
142
|
+
if (rule.hasMatch)
|
|
143
|
+
out.match = rule.match;
|
|
144
|
+
if (rule.mode !== '')
|
|
145
|
+
out.mode = rule.mode;
|
|
146
|
+
if (rule.hasPriority)
|
|
147
|
+
out.priority = rule.priority;
|
|
148
|
+
out.body = body;
|
|
149
|
+
rules.push(out);
|
|
150
|
+
}
|
|
151
|
+
for (const mode of modes)
|
|
152
|
+
rules.push(builtinRule(mode));
|
|
153
|
+
return { $jslt: '0.1', rules };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#region docPath remapping
|
|
157
|
+
|
|
158
|
+
function appendRest(out, tokens, k) {
|
|
159
|
+
return k < tokens.length ? out + '/' + tokens.slice(k).join('/') : out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Translate a docPath into the desugared JSLT stylesheet back into the
|
|
164
|
+
* author's template document. Best-effort: paths that cannot be walked
|
|
165
|
+
* (built-in rules, envelope members the template does not have) map to
|
|
166
|
+
* '' - the whole document - and `render.stylesheet` stays available for
|
|
167
|
+
* inspection.
|
|
168
|
+
* @param {object} model - result of normalizeJtltTemplate
|
|
169
|
+
* @param {string} docPath - pointer into the desugared stylesheet
|
|
170
|
+
* @returns {string} pointer into the template document
|
|
171
|
+
*/
|
|
172
|
+
export function remapDocPath(model, docPath) {
|
|
173
|
+
if (typeof docPath !== 'string' || docPath === '')
|
|
174
|
+
return '';
|
|
175
|
+
const tokens = docPath.split('/');
|
|
176
|
+
if (tokens.length < 3 || tokens[1] !== 'rules')
|
|
177
|
+
return '';
|
|
178
|
+
const index = Number(tokens[2]);
|
|
179
|
+
const rules = model.rules;
|
|
180
|
+
if (!Number.isInteger(index) || index < 0 || index >= rules.length)
|
|
181
|
+
return ''; // a built-in rule: point at the whole document
|
|
182
|
+
const base = model.rulesPrefix + '/' + index;
|
|
183
|
+
if (tokens.length === 3)
|
|
184
|
+
return base;
|
|
185
|
+
if (tokens[3] !== 'body') // match/mode/priority pass through verbatim
|
|
186
|
+
return appendRest(base, tokens, 3);
|
|
187
|
+
|
|
188
|
+
let segs = rules[index].body;
|
|
189
|
+
let out = base + '/body';
|
|
190
|
+
let k = 4;
|
|
191
|
+
while (k < tokens.length) {
|
|
192
|
+
const j = Number(tokens[k]);
|
|
193
|
+
if (!Number.isInteger(j) || j < 0 || j >= segs.length)
|
|
194
|
+
return appendRest(out, tokens, k);
|
|
195
|
+
const seg = segs[j];
|
|
196
|
+
out += '/' + j;
|
|
197
|
+
k++;
|
|
198
|
+
if (typeof seg === 'string') {
|
|
199
|
+
// an expression string desugars to a pair; drop the payload index
|
|
200
|
+
if (seg.charCodeAt(0) === DOLLAR && seg.charCodeAt(1) !== DOLLAR
|
|
201
|
+
&& k < tokens.length && tokens[k] === '1')
|
|
202
|
+
k++;
|
|
203
|
+
return appendRest(out, tokens, k);
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(seg)) { // nested segment list: keep walking
|
|
206
|
+
segs = seg;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (typeof seg === 'object' && seg !== null) {
|
|
210
|
+
const keys = Object.keys(seg);
|
|
211
|
+
if (keys.length === 1 && (keys[0] === '$raw' || keys[0] === '$json')) {
|
|
212
|
+
if (k < tokens.length && tokens[k] === '1') {
|
|
213
|
+
k++;
|
|
214
|
+
out += '/' + keys[0];
|
|
215
|
+
}
|
|
216
|
+
return appendRest(out, tokens, k);
|
|
217
|
+
}
|
|
218
|
+
if (keys.length === 1 && keys[0] === '$apply') // verbatim
|
|
219
|
+
return appendRest(out, tokens, k);
|
|
220
|
+
if (k < tokens.length && tokens[k] === '1') // pair-wrapped expression
|
|
221
|
+
k++;
|
|
222
|
+
return appendRest(out, tokens, k);
|
|
223
|
+
}
|
|
224
|
+
return appendRest(out, tokens, k);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
|
|
231
|
+
//#endregion
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region Jaren JTLT errors
|
|
2
|
+
// Error classes for the Jaren JTLT template engine. Every error carries a
|
|
3
|
+
// stable `code` and a `docPath`, an RFC 6901 JSON Pointer into the template
|
|
4
|
+
// document. Wrapped JSLT/query errors are exposed through `cause`.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown when a JTLT template is rejected at compile time
|
|
8
|
+
* (`TL0xxx` codes).
|
|
9
|
+
*/
|
|
10
|
+
export class JtltCompileError extends Error {
|
|
11
|
+
constructor(code, message, docPath, cause = undefined) {
|
|
12
|
+
super(`${code}: ${message} at ${docPath}`,
|
|
13
|
+
cause === undefined ? undefined : { cause });
|
|
14
|
+
this.name = 'JtltCompileError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.docPath = docPath;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error thrown when rendering with a compiled JTLT template fails
|
|
22
|
+
* (`TL2xxx` codes).
|
|
23
|
+
*/
|
|
24
|
+
export class JtltRuntimeError extends Error {
|
|
25
|
+
constructor(code, message, docPath, cause = undefined) {
|
|
26
|
+
super(`${code}: ${message} at ${docPath}`,
|
|
27
|
+
cause === undefined ? undefined : { cause });
|
|
28
|
+
this.name = 'JtltRuntimeError';
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.docPath = docPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|