@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,155 @@
1
+ //#region Jaren JTLT public API
2
+ // JTLT: template-driven text output over the Jaren JSLT dispatcher.
3
+ // A JTLT template is a JSLT-shaped rule document whose bodies are
4
+ // segment lists (literal text, interpolated queries, $apply splices)
5
+ // and whose result is a string. It compiles down to an ordinary JSLT
6
+ // 0.1 stylesheet - inspectable as `render.stylesheet` - so dispatch,
7
+ // modes, conflict resolution, and schema matching are inherited, not
8
+ // reimplemented.
9
+
10
+ import { deepFreezeCopy } from '../query/normalize.js';
11
+ import {
12
+ compileJsltStylesheet,
13
+ JsltCompileError,
14
+ JsltRuntimeError,
15
+ } from '../jslt/index.js';
16
+ import { JtltCompileError, JtltRuntimeError } from './errors.js';
17
+ import { normalizeJtltTemplate } from './template.js';
18
+ import { desugarTemplate, remapDocPath } from './desugar.js';
19
+ import { createWriter } from './writer.js';
20
+
21
+ export { JtltCompileError, JtltRuntimeError } from './errors.js';
22
+
23
+ /**
24
+ * Compile a Jaren JTLT 0.1 template into a reusable renderer.
25
+ *
26
+ * The returned function renders any input document to a string in the
27
+ * template's output method ('text' by default, 'xml' for markup with
28
+ * escaped interpolation). Metadata:
29
+ *
30
+ * - `render.externals` - user parameter names in first-appearance order
31
+ * (`root` and `path` are engine-bound and excluded)
32
+ * - `render.output` - the resolved output method
33
+ * - `render.doc` - an independent, deeply frozen template copy
34
+ * - `render.stylesheet` - the frozen JSLT stylesheet it compiled to
35
+ *
36
+ * @param {any} doc - a bare rule array or `{"$jtlt":"0.1","output":...,
37
+ * "rules":[]}` template envelope
38
+ * @param {object} [options] - compile options, passed to the JSLT layer
39
+ * @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
40
+ * [options.compileTypeTest] - validator-agnostic hook for schema
41
+ * matches and schema operators inside segment expressions
42
+ * @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
43
+ * @returns {function} reusable `render(data, externals?)` function
44
+ * @throws {JtltCompileError} when compilation fails
45
+ * @example
46
+ * const render = compileJtltStylesheet([
47
+ * { match: '$.items[*]', body: ['- ', '$.name', '\n'] },
48
+ * ]);
49
+ * render({ items: [{ name: 'a' }, { name: 'b' }] });
50
+ * // '- a\n- b\n'
51
+ */
52
+ export function compileJtltStylesheet(doc, options = {}) {
53
+ const frozenDoc = deepFreezeCopy(doc);
54
+ const model = normalizeJtltTemplate(frozenDoc);
55
+ const stylesheet = desugarTemplate(model);
56
+ let transform;
57
+ try {
58
+ transform = compileJsltStylesheet(stylesheet, options);
59
+ }
60
+ catch (error) {
61
+ if (!(error instanceof JsltCompileError))
62
+ throw error;
63
+ throw new JtltCompileError('TL0005', error.message,
64
+ remapDocPath(model, error.docPath), error);
65
+ }
66
+ const write = createWriter(model.output);
67
+
68
+ const render = (data, externals) => {
69
+ let value;
70
+ try {
71
+ value = transform(data, externals);
72
+ }
73
+ catch (error) {
74
+ if (!(error instanceof JsltRuntimeError))
75
+ throw error;
76
+ throw new JtltRuntimeError('TL2003', error.message,
77
+ remapDocPath(model, error.docPath), error);
78
+ }
79
+ return write(value);
80
+ };
81
+ render.externals = transform.externals;
82
+ render.output = model.output;
83
+ render.doc = frozenDoc;
84
+ render.stylesheet = transform.doc;
85
+ return render;
86
+ }
87
+
88
+ const TEMPLATE_CACHE = new WeakMap();
89
+
90
+ function cachedRender(template, options) {
91
+ let record = TEMPLATE_CACHE.get(template);
92
+ if (record === undefined) {
93
+ record = {
94
+ defaultRender: null,
95
+ variants: null,
96
+ };
97
+ TEMPLATE_CACHE.set(template, record);
98
+ }
99
+
100
+ const compileTypeTest = typeof options?.compileTypeTest === 'function'
101
+ ? options.compileTypeTest
102
+ : null;
103
+ const maxDepth = options?.maxDepth === undefined ? 1024 : options.maxDepth;
104
+ if (compileTypeTest === null && maxDepth === 1024) {
105
+ if (record.defaultRender === null)
106
+ record.defaultRender = compileJtltStylesheet(template);
107
+ return record.defaultRender;
108
+ }
109
+
110
+ let variants = record.variants;
111
+ if (variants === null) {
112
+ variants = [];
113
+ record.variants = variants;
114
+ }
115
+ for (let i = 0; i < variants.length; i++) {
116
+ const variant = variants[i];
117
+ if (variant.compileTypeTest === compileTypeTest && variant.maxDepth === maxDepth)
118
+ return variant.render;
119
+ }
120
+ const render = compileJtltStylesheet(template, options);
121
+ variants.push({ compileTypeTest, maxDepth, render });
122
+ return render;
123
+ }
124
+
125
+ /**
126
+ * Render a JSON value with a JTLT template in one call. Object/array
127
+ * template documents are compiled once and cached by identity in a
128
+ * WeakMap.
129
+ * @param {any} template - JTLT template document
130
+ * @param {any} data - input JSON value
131
+ * @param {object} [externals] - user parameter bindings
132
+ * @param {object} [options] - compile options used on a cache miss
133
+ * @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
134
+ * [options.compileTypeTest] - schema type-test compiler
135
+ * @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
136
+ * @returns {string} the rendered output text
137
+ * @throws {JtltCompileError} when compilation fails
138
+ * @throws {JtltRuntimeError} when rendering fails
139
+ * @example
140
+ * renderText([{ match: '$.name', body: ['Hello ', '$', '!'] }],
141
+ * { name: 'world' });
142
+ * // 'Hello world!'
143
+ */
144
+ export function renderText(template, data, externals, options = undefined) {
145
+ let render;
146
+ if (typeof template === 'object' && template !== null) {
147
+ render = cachedRender(template, options);
148
+ }
149
+ else {
150
+ render = compileJtltStylesheet(template, options);
151
+ }
152
+ return render(data, externals);
153
+ }
154
+
155
+ //#endregion
@@ -0,0 +1,130 @@
1
+ //#region Jaren JTLT template normalizer
2
+ // Validates the template envelope and rule shape. Segment bodies are
3
+ // validated during desugaring (desugar.js); everything the JSLT layer
4
+ // checks itself (match details, mode strings inside $apply, query
5
+ // vocabulary) is deliberately left to it so the two vocabularies cannot
6
+ // drift apart.
7
+
8
+ import { JtltCompileError } from './errors.js';
9
+
10
+ const hasOwn = Object.hasOwn;
11
+ const ENVELOPE_KEYS = new Set(['$jtlt', 'output', 'rules']);
12
+ const RULE_KEYS = new Set(['match', 'mode', 'priority', 'body']);
13
+ const OUTPUT_METHODS = new Set(['text', 'xml']);
14
+
15
+ // Priorities at or below the floor are reserved for the injected
16
+ // built-in rules, which sit at -1e308 - beneath every user rule.
17
+ export const RESERVED_PRIORITY_FLOOR = -1e307;
18
+
19
+ function isObject(value) {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
21
+ }
22
+
23
+ function escapeToken(token) {
24
+ if (token.indexOf('~') < 0 && token.indexOf('/') < 0)
25
+ return token;
26
+ return token.replace(/~/g, '~0').replace(/\//g, '~1');
27
+ }
28
+
29
+ function fail(code, message, docPath) {
30
+ throw new JtltCompileError(code, message, docPath);
31
+ }
32
+
33
+ function normalizeRule(value, index, rulesPath) {
34
+ const rulePath = rulesPath + '/' + index;
35
+ if (!isObject(value))
36
+ fail('TL0002', 'a template rule must be an object', rulePath);
37
+
38
+ const keys = Object.keys(value);
39
+ for (let i = 0; i < keys.length; i++) {
40
+ if (!RULE_KEYS.has(keys[i]))
41
+ fail('TL0002', `unknown rule member '${keys[i]}'`,
42
+ rulePath + '/' + escapeToken(keys[i]));
43
+ }
44
+ if (!hasOwn(value, 'body'))
45
+ fail('TL0002', "a template rule requires 'body'", rulePath + '/body');
46
+ if (!Array.isArray(value.body))
47
+ fail('TL0002', "'body' must be a segment array", rulePath + '/body');
48
+ if (hasOwn(value, 'mode') && typeof value.mode !== 'string')
49
+ fail('TL0002', "'mode' must be a string", rulePath + '/mode');
50
+ const hasPriority = hasOwn(value, 'priority');
51
+ if (hasPriority) {
52
+ if (typeof value.priority !== 'number' || !Number.isFinite(value.priority))
53
+ fail('TL0002', "'priority' must be a finite JSON number", rulePath + '/priority');
54
+ if (value.priority <= RESERVED_PRIORITY_FLOOR)
55
+ fail('TL0003', `priorities at or below ${RESERVED_PRIORITY_FLOOR} are reserved for the built-in rules`,
56
+ rulePath + '/priority');
57
+ }
58
+ const hasMatch = hasOwn(value, 'match');
59
+ if (hasMatch && typeof value.match !== 'string' && !isObject(value.match))
60
+ fail('TL0002', "'match' must be a JSONPath string or an object", rulePath + '/match');
61
+
62
+ return Object.freeze({
63
+ index,
64
+ docPath: rulePath,
65
+ bodyDocPath: rulePath + '/body',
66
+ body: value.body,
67
+ hasMatch,
68
+ match: hasMatch ? value.match : null,
69
+ mode: hasOwn(value, 'mode') ? value.mode : '',
70
+ hasPriority,
71
+ priority: hasPriority ? value.priority : 0,
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Normalize a frozen JTLT template document into a frozen model.
77
+ * @param {any} doc - deeply frozen template document
78
+ * @returns {object} frozen template model
79
+ * @throws {JtltCompileError} on TL0001-TL0003 and TL0006 shape errors
80
+ */
81
+ export function normalizeJtltTemplate(doc) {
82
+ let sourceRules;
83
+ let rulesPath;
84
+ let output = 'text';
85
+
86
+ if (Array.isArray(doc)) {
87
+ sourceRules = doc;
88
+ rulesPath = '';
89
+ }
90
+ else if (isObject(doc)) {
91
+ const keys = Object.keys(doc);
92
+ for (let i = 0; i < keys.length; i++) {
93
+ if (!ENVELOPE_KEYS.has(keys[i]))
94
+ fail('TL0001', `unknown template member '${keys[i]}'`,
95
+ '/' + escapeToken(keys[i]));
96
+ }
97
+ if (!hasOwn(doc, '$jtlt'))
98
+ fail('TL0001', "the template envelope requires '$jtlt'", '/$jtlt');
99
+ if (doc.$jtlt !== '0.1')
100
+ fail('TL0006', `unknown JTLT format version ${JSON.stringify(doc.$jtlt)}`, '/$jtlt');
101
+ if (hasOwn(doc, 'output') && !OUTPUT_METHODS.has(doc.output)) {
102
+ fail('TL0001',
103
+ `unknown output method ${JSON.stringify(doc.output)} (supported: "text", "xml")`,
104
+ '/output');
105
+ }
106
+ if (hasOwn(doc, 'output'))
107
+ output = doc.output;
108
+ if (!hasOwn(doc, 'rules'))
109
+ fail('TL0001', "the template envelope requires 'rules'", '/rules');
110
+ if (!Array.isArray(doc.rules))
111
+ fail('TL0001', "'rules' must be an array", '/rules');
112
+ sourceRules = doc.rules;
113
+ rulesPath = '/rules';
114
+ }
115
+ else {
116
+ fail('TL0001', 'a template must be a rule array or a version envelope', '');
117
+ }
118
+
119
+ const rules = new Array(sourceRules.length);
120
+ for (let i = 0; i < sourceRules.length; i++)
121
+ rules[i] = normalizeRule(sourceRules[i], i, rulesPath);
122
+
123
+ return Object.freeze({
124
+ output,
125
+ rulesPrefix: rulesPath,
126
+ rules: Object.freeze(rules),
127
+ });
128
+ }
129
+
130
+ //#endregion
@@ -0,0 +1,110 @@
1
+ //#region Jaren JTLT writer
2
+ // Serializes the tagged segment stream a desugared stylesheet produces
3
+ // (see desugar.js for the closed grammar) into the output text. The
4
+ // writer is chosen once per compiled template; escaping applies to
5
+ // interpolated data only - literal template text is the author's markup
6
+ // and passes through raw, exactly as in XSLT and T4.
7
+
8
+ import { JtltRuntimeError } from './errors.js';
9
+
10
+ const XML_TEST = /[&<>"']/;
11
+ const XML_PATTERN = /[&<>"']/g;
12
+
13
+ function xmlEntity(c) {
14
+ switch (c) {
15
+ case '&': return '&amp;';
16
+ case '<': return '&lt;';
17
+ case '>': return '&gt;';
18
+ case '"': return '&quot;';
19
+ default: return '&#39;';
20
+ }
21
+ }
22
+
23
+ function escapeXml(s) {
24
+ return XML_TEST.test(s) ? s.replace(XML_PATTERN, xmlEntity) : s;
25
+ }
26
+
27
+ // The text value of one interpolated item: null is empty (text
28
+ // serialization, not the $string cast), containers are a template error
29
+ // pointing at the segment that produced them.
30
+ function stringifyItem(v, docPath) {
31
+ switch (typeof v) {
32
+ case 'string':
33
+ return v;
34
+ case 'number':
35
+ return String(v);
36
+ case 'boolean':
37
+ return v ? 'true' : 'false';
38
+ default:
39
+ if (v === null)
40
+ return '';
41
+ throw new JtltRuntimeError('TL2001',
42
+ `cannot interpolate ${Array.isArray(v) ? 'an array' : 'an object'} into text output; dispatch into it with $apply, or embed it with $json`,
43
+ docPath);
44
+ }
45
+ }
46
+
47
+ // Multi-item payloads (a sequence-valued expression) join with a single
48
+ // space, the XSLT value-of separator default.
49
+ function pushPayload(pair, parts, escape, asJson) {
50
+ const tag = pair[0];
51
+ const docPath = tag.length > 1 ? tag.slice(1) : '';
52
+ for (let i = 1; i < pair.length; i++) {
53
+ if (i > 1)
54
+ parts.push(' ');
55
+ const s = asJson ? JSON.stringify(pair[i]) : stringifyItem(pair[i], docPath);
56
+ parts.push(escape === null ? s : escape(s));
57
+ }
58
+ }
59
+
60
+ function walk(x, parts, escape) {
61
+ if (!Array.isArray(x)) {
62
+ // every dispatched node fires a rule and every rule body is a
63
+ // segment constructor, so a bare value here is an engine-contract
64
+ // break, never author error
65
+ throw new JtltRuntimeError('TL2002',
66
+ 'malformed segment stream (engine contract violation)', '');
67
+ }
68
+ const head = x.length === 0 ? null : x[0];
69
+ if (typeof head === 'string') {
70
+ const kind = head.charCodeAt(0);
71
+ if (kind === 0x72) { // 'r'
72
+ parts.push(x[1]);
73
+ return;
74
+ }
75
+ if (kind === 0x65) { // 'e'
76
+ pushPayload(x, parts, escape, false);
77
+ return;
78
+ }
79
+ if (kind === 0x77) { // 'w'
80
+ pushPayload(x, parts, null, false);
81
+ return;
82
+ }
83
+ if (kind === 0x6A) { // 'j'
84
+ pushPayload(x, parts, escape, true);
85
+ return;
86
+ }
87
+ throw new JtltRuntimeError('TL2002',
88
+ `unknown segment tag ${JSON.stringify(head)}`, '');
89
+ }
90
+ for (let i = 0; i < x.length; i++)
91
+ walk(x[i], parts, escape);
92
+ }
93
+
94
+ /**
95
+ * Create the serializer for one output method.
96
+ * @param {'text' | 'xml'} method - the template's output method
97
+ * @returns {(value: any) => string} segment-stream serializer
98
+ */
99
+ export function createWriter(method) {
100
+ const escape = method === 'xml' ? escapeXml : null;
101
+ return (value) => {
102
+ if (value === undefined)
103
+ return '';
104
+ const parts = [];
105
+ walk(value, parts, escape);
106
+ return parts.join('');
107
+ };
108
+ }
109
+
110
+ //#endregion