@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,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