jtlt 0.1.0
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/.editorconfig +16 -0
- package/CHANGES.md +5 -0
- package/LICENSE-MIT.txt +21 -0
- package/README.md +534 -0
- package/dist/AbstractJoiningTransformer.d.ts +42 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -0
- package/dist/DOMJoiningTransformer.d.ts +113 -0
- package/dist/DOMJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONJoiningTransformer.d.ts +160 -0
- package/dist/JSONJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformer.d.ts +95 -0
- package/dist/JSONPathTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformerContext.d.ts +263 -0
- package/dist/JSONPathTransformerContext.d.ts.map +1 -0
- package/dist/StringJoiningTransformer.d.ts +168 -0
- package/dist/StringJoiningTransformer.d.ts.map +1 -0
- package/dist/XPathTransformer.d.ts +51 -0
- package/dist/XPathTransformer.d.ts.map +1 -0
- package/dist/XPathTransformerContext.d.ts +260 -0
- package/dist/XPathTransformerContext.d.ts.map +1 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts +16 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts.map +1 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.d.ts.map +1 -0
- package/docs/API.expanded.md +263 -0
- package/docs/API.md +69 -0
- package/eslint.config.js +30 -0
- package/package.json +53 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/AbstractJoiningTransformer.js +73 -0
- package/src/DOMJoiningTransformer.js +237 -0
- package/src/JSONJoiningTransformer.js +472 -0
- package/src/JSONPathTransformer.js +159 -0
- package/src/JSONPathTransformerContext.js +807 -0
- package/src/StringJoiningTransformer.js +589 -0
- package/src/XPathTransformer.js +94 -0
- package/src/XPathTransformerContext.js +496 -0
- package/src/XSLTStyleJSONPathResolver.js +39 -0
- package/src/index.js +299 -0
- package/src/types/xpath2-js.d.ts +2 -0
- package/tsconfig-prod.json +19 -0
- package/tsconfig.json +14 -0
- package/typings/xpath2-js.d.ts +2 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
; EditorConfig file: https://EditorConfig.org
|
|
2
|
+
; Install the "EditorConfig" plugin into your editor to use
|
|
3
|
+
|
|
4
|
+
root = true
|
|
5
|
+
|
|
6
|
+
[*]
|
|
7
|
+
charset = utf-8
|
|
8
|
+
end_of_line = lf
|
|
9
|
+
insert_final_newline = true
|
|
10
|
+
indent_style = space
|
|
11
|
+
indent_size = 2
|
|
12
|
+
trim_trailing_whitespace = true
|
|
13
|
+
|
|
14
|
+
; [app/public/css/**.styl]
|
|
15
|
+
; indent_style = tab
|
|
16
|
+
; indent_size = 2
|
package/CHANGES.md
ADDED
package/LICENSE-MIT.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Brett Zamir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
# jtlt
|
|
2
|
+
|
|
3
|
+
JavaScript Template Language Transformations (JTLT, pronounced as
|
|
4
|
+
"Jetlet")—a JavaScript equivalent of [XSLT](https://www.w3.org/Style/XSL/),
|
|
5
|
+
for JSON/JavaScript object data sources.
|
|
6
|
+
|
|
7
|
+
As with XSLT, allows for declarative, linear declaration of
|
|
8
|
+
(recursive) templates and can be transformed into different
|
|
9
|
+
formats (e.g., HTML strings, JSON, DOM objects, etc.).
|
|
10
|
+
|
|
11
|
+
***Beta state!!!***
|
|
12
|
+
|
|
13
|
+
## Credits
|
|
14
|
+
|
|
15
|
+
Packaged with [JSONPath Plus](https://github.com/s3u/JSONPath).
|
|
16
|
+
|
|
17
|
+
The sample file is from <https://goessner.net/articles/JsonPath/>
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
npm install .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
In the browser, you will also need to include the dependencies.
|
|
26
|
+
See the [test file](test/test.html).
|
|
27
|
+
|
|
28
|
+
## Basic usage
|
|
29
|
+
|
|
30
|
+
### Node
|
|
31
|
+
|
|
32
|
+
### Browser
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
See the [docs](docs/API.md). A high‑level overview is below.
|
|
37
|
+
|
|
38
|
+
## API overview
|
|
39
|
+
|
|
40
|
+
JTLT has two layers:
|
|
41
|
+
|
|
42
|
+
- Engine (template application):
|
|
43
|
+
- JSONPathTransformer: Applies templates to JSON by matching JSONPath selectors (and optional modes), resolving priority, and invoking the winning template. Falls back to built‑in default rules when no user template matches.
|
|
44
|
+
- JSONPathTransformerContext: The execution context passed to templates. It mirrors the joiner API (e.g., string(), object(), array()) so templates can emit results. It also provides helpers like applyTemplates(), callTemplate(), valueOf(), variable(), and forEach().
|
|
45
|
+
- XPathTransformer (experimental): Applies templates to
|
|
46
|
+
XML/HTML DOM by matching XPath selectors (and optional modes).
|
|
47
|
+
Supports two evaluation modes: version 1 (native
|
|
48
|
+
XPathEvaluator) and version 2 (via xpath2.js). Falls back to
|
|
49
|
+
built‑in default rules when no template matches.
|
|
50
|
+
- XPathTransformerContext (experimental): Execution context for
|
|
51
|
+
XPath. Offers get(), forEach(), valueOf(), variable(), key()
|
|
52
|
+
and the same joiner helpers as the JSONPath context.
|
|
53
|
+
|
|
54
|
+
- Joiners (output builders):
|
|
55
|
+
- StringJoiningTransformer: Builds a string. Context‑aware append() routes into objects/arrays when inside object()/array() scopes, otherwise concatenates to a buffer. Includes element(), attribute(), and text() helpers for HTML/XML emission.
|
|
56
|
+
- DOMJoiningTransformer: Builds a DocumentFragment/Element tree. element()/attribute()/text() add real nodes; primitives append as text nodes.
|
|
57
|
+
- JSONJoiningTransformer: Builds real JS values (objects/arrays/primitives) without serialization.
|
|
58
|
+
|
|
59
|
+
### Common joiner methods
|
|
60
|
+
|
|
61
|
+
- append(value): Central sink. Based on context, concatenates to string, pushes to array, or assigns to an object property.
|
|
62
|
+
- get(): Return the accumulated result.
|
|
63
|
+
- object(obj?, cb?, usePropertySets?, propSets?): Enter object context; optionally seed from an object or build via cb.
|
|
64
|
+
- array(arr?, cb?): Enter array context; optionally seed from an array or build via cb.
|
|
65
|
+
- string(str, cb?): Emit a string value (no HTML escaping). In String joiner, optional cb lets you compose nested fragments before emitting.
|
|
66
|
+
- number(num), boolean(bool), null(), undefined() (JS mode only), nonfiniteNumber(NaN|Infinity), function(fn) (JS mode only): Emit primitives/functions.
|
|
67
|
+
- element(name, attrs?, children?, cb?): Build elements (String and DOM joiners). In String joiner, uses Jamilih under the hood to serialize; in DOM joiner, creates Elements.
|
|
68
|
+
- attribute(name, value, avoidEscape?): Add attributes to the most recently opened element (String joiner) or to the current Element (DOM joiner).
|
|
69
|
+
- text(txt): Emit text content. In String joiner, escapes & and <, and closes an open start tag if needed.
|
|
70
|
+
- plainText(str): Raw, no‑escape append that bypasses context routing in the String joiner (always writes to top‑level buffer). In DOM/JSON joiners, it maps to text()/string() respectively.
|
|
71
|
+
|
|
72
|
+
### string() vs text() vs plainText() (String joiner)
|
|
73
|
+
|
|
74
|
+
- text(): Escapes &, < and closes an open start tag. Use for safe text nodes in markup.
|
|
75
|
+
- string(): No HTML escaping or JSON stringify; routes via append() so it participates in object()/array()/propOnly() states. Optional cb to build a composite string before emitting.
|
|
76
|
+
- plainText(): Always writes directly to the top‑level string buffer with no escaping, ignoring object/array state. Useful for deliberate raw insertion.
|
|
77
|
+
|
|
78
|
+
### Configuration quick reference
|
|
79
|
+
|
|
80
|
+
Provide joiningConfig when constructing JTLT:
|
|
81
|
+
|
|
82
|
+
- joiningConfig.mode: 'JavaScript' or 'JSON' controls allowance of undefined/functions/non‑finite numbers in the String joiner.
|
|
83
|
+
- joiningConfig.JHTMLForJSON: If true, object()/array() serialize via JHTML instead of JSON.
|
|
84
|
+
- joiningConfig.xmlElements: Switch element() to XML serialization mode in the String joiner.
|
|
85
|
+
- joiningConfig.preEscapedAttributes: Skip escaping attribute values in the String joiner.
|
|
86
|
+
|
|
87
|
+
### XPath (experimental)
|
|
88
|
+
|
|
89
|
+
You can run templates against XML/HTML using XPath instead of JSONPath.
|
|
90
|
+
|
|
91
|
+
- Construct with `new XPathTransformer({data, templates,
|
|
92
|
+
joiningTransformer, xpathVersion})`.
|
|
93
|
+
- `data` should be a Document or Element (e.g., from DOMParser with
|
|
94
|
+
`text/xml`).
|
|
95
|
+
- `xpathVersion`: `1` uses native XPath (browser like). `2` uses
|
|
96
|
+
`xpath2.js` for XPath 2.0‑style evaluation.
|
|
97
|
+
- In version 2, some functions may be missing; prefer simple path
|
|
98
|
+
expressions. Use version 1 for wide XPath 1.0 function support.
|
|
99
|
+
|
|
100
|
+
Example (string output):
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import {JSDOM} from 'jsdom';
|
|
104
|
+
import {StringJoiningTransformer, XPathTransformer} from 'jtlt';
|
|
105
|
+
|
|
106
|
+
const {window} = new JSDOM('<!doctype><html><body></body></html>');
|
|
107
|
+
const parser = new window.DOMParser();
|
|
108
|
+
const doc = parser.parseFromString(
|
|
109
|
+
'<root><item>a</item><item>b</item></root>', 'text/xml'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const joiner = new StringJoiningTransformer('', {document: doc});
|
|
113
|
+
const templates = [
|
|
114
|
+
{name: 'root', path: '/', template () {
|
|
115
|
+
this.applyTemplates('//item');
|
|
116
|
+
}},
|
|
117
|
+
{name: 'item', path: '//item', template (node) {
|
|
118
|
+
this.element('li', {}, [], () => this.text(node.textContent));
|
|
119
|
+
}}
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const out = new XPathTransformer({
|
|
123
|
+
data: doc,
|
|
124
|
+
templates,
|
|
125
|
+
joiningTransformer: joiner,
|
|
126
|
+
xpathVersion: 1 // or 2
|
|
127
|
+
}).transform('');
|
|
128
|
+
// -> <li>a</li><li>b</li>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Using the JTLT facade with XPath (no manual joiner needed):
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import {JSDOM} from 'jsdom';
|
|
135
|
+
import JTLT from 'jtlt';
|
|
136
|
+
|
|
137
|
+
const {window} = new JSDOM('<!doctype><html><body></body></html>');
|
|
138
|
+
const parser = new window.DOMParser();
|
|
139
|
+
const doc = parser.parseFromString(
|
|
140
|
+
'<root><item>a</item><item>b</item></root>', 'text/xml'
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const templates = [
|
|
144
|
+
{
|
|
145
|
+
path: '/',
|
|
146
|
+
template () {
|
|
147
|
+
this.applyTemplates('//item');
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
path: '//item',
|
|
152
|
+
template (n) {
|
|
153
|
+
this.string('<li>', () => this.text(n.textContent));
|
|
154
|
+
this.string('</li>');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const out = new JTLT({
|
|
160
|
+
data: doc,
|
|
161
|
+
templates,
|
|
162
|
+
outputType: 'string',
|
|
163
|
+
engineType: 'xpath',
|
|
164
|
+
xpathVersion: 1, // or 2
|
|
165
|
+
success: (res) => res
|
|
166
|
+
}).transform('');
|
|
167
|
+
// -> <li>a</li><li>b</li>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Quick start
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import JTLT from 'jtlt';
|
|
174
|
+
|
|
175
|
+
const data = {title: 'Hello', items: ['a', 'b']};
|
|
176
|
+
|
|
177
|
+
const templates = [
|
|
178
|
+
{path: '$', template () {
|
|
179
|
+
this.applyTemplates({mode: 'html'});
|
|
180
|
+
}},
|
|
181
|
+
{mode: 'html', path: '$.title', template (v) {
|
|
182
|
+
this.string('<h1>', () => this.text(v));
|
|
183
|
+
this.string('</h1>');
|
|
184
|
+
}},
|
|
185
|
+
{mode: 'html', path: '$.items[*]', template (v) {
|
|
186
|
+
this.element('li', {}, [], () => this.text(v));
|
|
187
|
+
}}
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const out = new JTLT({data, templates, outputType: 'string'}).
|
|
191
|
+
transform('html');
|
|
192
|
+
|
|
193
|
+
console.log(out);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Notes:
|
|
197
|
+
|
|
198
|
+
- Modes let you organize multiple passes or output targets.
|
|
199
|
+
- You can also call templates by name via this.callTemplate('name').
|
|
200
|
+
- For DOM output, use outputType: 'dom'. For JSON output, use 'json'.
|
|
201
|
+
|
|
202
|
+
## One-off queries with forQuery (XQuery-like)
|
|
203
|
+
|
|
204
|
+
If you just want to run a single, non-recursive query (similar to an XQuery "for … where … return …"), you can skip defining templates and use `forQuery` to seed a root function that iterates a JSONPath and emits results.
|
|
205
|
+
|
|
206
|
+
- `forQuery` takes the same arguments you’d pass to `this.forEach(select, cb)`: an absolute JSONPath selector and a callback invoked for each match.
|
|
207
|
+
- You can set variables via `this.variable(name, select)` and use plain JavaScript `if` for conditions (there is no dedicated `this.if`).
|
|
208
|
+
|
|
209
|
+
Example: collect item names whose price meets a threshold, using a variable sourced from the root.
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
import JTLT from 'jtlt';
|
|
213
|
+
|
|
214
|
+
const data = {
|
|
215
|
+
threshold: 10,
|
|
216
|
+
items: [
|
|
217
|
+
{name: 'A', price: 8},
|
|
218
|
+
{name: 'B', price: 12},
|
|
219
|
+
{name: 'C', price: 10}
|
|
220
|
+
]
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const jtlt = new JTLT({
|
|
224
|
+
data,
|
|
225
|
+
outputType: 'json', // Top-level result will be a JSON array
|
|
226
|
+
// forQuery mirrors: this.forEach(select, cb)
|
|
227
|
+
forQuery: [
|
|
228
|
+
'$.items[*]',
|
|
229
|
+
function (item) {
|
|
230
|
+
// Set a reusable variable from the root context
|
|
231
|
+
this.variable('threshold', '$.threshold');
|
|
232
|
+
const {threshold} = this.vars;
|
|
233
|
+
|
|
234
|
+
// Use normal JS conditionals (no this.if helper)
|
|
235
|
+
if (item.price >= threshold) {
|
|
236
|
+
// In JSON output mode, appending a string pushes into
|
|
237
|
+
// the top-level array
|
|
238
|
+
this.string(item.name);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
// success receives the final result; return it for convenience
|
|
243
|
+
success: (out) => out
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const result = jtlt.transform();
|
|
247
|
+
// result => ['B', 'C']
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Tips:
|
|
251
|
+
|
|
252
|
+
- For string output, set `outputType: 'string'` and emit with `this.text()`/`this.string()` in the callback.
|
|
253
|
+
- `this.variable(name, select)` evaluates the JSONPath against the current context (root for `forQuery`), storing it in `this.vars[name]`.
|
|
254
|
+
- If you need multiple passes or richer logic, switch to named templates and modes.
|
|
255
|
+
|
|
256
|
+
## FLWOR-style (XQuery) example
|
|
257
|
+
|
|
258
|
+
You can express the essentials of a FLWOR expression (For, Let, Where, Order by, Return) using a template with `forEach()` and the new `sort` support:
|
|
259
|
+
|
|
260
|
+
Scenario: list book titles whose price is at/above a threshold, ordered by price descending and then title ascending.
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
import JTLT from 'jtlt';
|
|
264
|
+
|
|
265
|
+
const data = {
|
|
266
|
+
threshold: 10,
|
|
267
|
+
store: {
|
|
268
|
+
book: [
|
|
269
|
+
{title: 'A Tale', price: 8},
|
|
270
|
+
{title: 'Brave New', price: 12},
|
|
271
|
+
{title: 'Cobalt', price: 12},
|
|
272
|
+
{title: 'Delta', price: 10}
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const templates = [
|
|
278
|
+
// Root template builds an HTML list
|
|
279
|
+
{path: '$', mode: 'html', template () {
|
|
280
|
+
// Let: bind a reusable variable from root
|
|
281
|
+
this.variable('threshold', '$.threshold');
|
|
282
|
+
|
|
283
|
+
this.element('ul', {}, [], () => {
|
|
284
|
+
// For + Order by: iterate books with multi-key sort
|
|
285
|
+
this.forEach('$.store.book[*]', function (b) {
|
|
286
|
+
// Where: filter in JS
|
|
287
|
+
if (b.price >= this.vars.threshold) {
|
|
288
|
+
// Return: emit a list item for each match
|
|
289
|
+
this.element('li', {}, [], () => this.text(b.title));
|
|
290
|
+
}
|
|
291
|
+
}, [
|
|
292
|
+
{select: '$.price', type: 'number', order: 'descending'},
|
|
293
|
+
{select: '$.title', type: 'text', order: 'ascending'}
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
}}
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const out = new JTLT({data, templates, outputType: 'string'}).
|
|
300
|
+
transform('html');
|
|
301
|
+
|
|
302
|
+
// -> <ul><li>Brave New</li><li>Cobalt</li><li>Delta</li></ul>
|
|
303
|
+
console.log(out);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Notes:
|
|
307
|
+
|
|
308
|
+
- You can also drive a FLWOR-like flow with `applyTemplates({select, mode}, sort)` and a dedicated template `mode` instead of using an inline `forEach()` callback.
|
|
309
|
+
- The `sort` parameter accepts:
|
|
310
|
+
- a JSONPath string relative to each item (e.g., `$.name` or `.`)
|
|
311
|
+
- a comparator function `(aValue, bValue, ctx) => number`
|
|
312
|
+
- an object `{select, order, type, locale, localeOptions}`
|
|
313
|
+
- an array of such strings/objects for multi-key sorting
|
|
314
|
+
|
|
315
|
+
## FLWOR-style join (two forEach loops)
|
|
316
|
+
|
|
317
|
+
You can model a join across two arrays (e.g., orders ↔ customers) using two `forEach()` passes: the first builds a lookup (an index), the second consumes it to emit joined rows. This mirrors a FLWOR-style join while keeping intent explicit and fast.
|
|
318
|
+
|
|
319
|
+
Example: render an HTML list of orders annotated with customer names.
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
import JTLT from 'jtlt';
|
|
323
|
+
|
|
324
|
+
const data = {
|
|
325
|
+
customers: [
|
|
326
|
+
{id: 1, name: 'Alice'},
|
|
327
|
+
{id: 2, name: 'Bob'}
|
|
328
|
+
],
|
|
329
|
+
orders: [
|
|
330
|
+
{id: 'o-10', customerId: 2, item: 'Keyboard', date: '2024-10-01'},
|
|
331
|
+
{id: 'o-11', customerId: 1, item: 'Mouse', date: '2024-09-20'}
|
|
332
|
+
]
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const templates = [
|
|
336
|
+
{path: '$', mode: 'html', template () {
|
|
337
|
+
// 1) Build an index by id (first forEach)
|
|
338
|
+
const byId = {};
|
|
339
|
+
this.forEach('$.customers[*]', function (c) {
|
|
340
|
+
byId[c.id] = c;
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// 2) Emit joined rows (second forEach)
|
|
344
|
+
this.element('ul', {}, [], () => {
|
|
345
|
+
this.forEach('$.orders[*]', function (o) {
|
|
346
|
+
const c = byId[o.customerId];
|
|
347
|
+
if (!c) {
|
|
348
|
+
return; // skip if no matching customer
|
|
349
|
+
}
|
|
350
|
+
this.element('li', {}, [], () => {
|
|
351
|
+
this.text(`${c.name} — ${o.item}`);
|
|
352
|
+
});
|
|
353
|
+
}, {select: '$.date', type: 'text', order: 'ascending'}); // optional sort
|
|
354
|
+
});
|
|
355
|
+
}}
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const out = new JTLT({data, templates, outputType: 'string'}).transform('html');
|
|
359
|
+
// -> <ul><li>Bob — Keyboard</li><li>Alice — Mouse</li></ul>
|
|
360
|
+
console.log(out);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Notes:
|
|
364
|
+
|
|
365
|
+
- This pattern uses two `forEach()` calls rather than nesting them, which avoids repeatedly scanning the second array for each outer item.
|
|
366
|
+
- If you already maintain keys in your data, you can skip the first pass and derive `byId` with `Object.fromEntries` or similar.
|
|
367
|
+
- For locale-aware or numeric ordering of the second pass, use the `sort` parameter (string/comparator/object/array as shown above).
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
## Joins with key()/getKey() (xsl:key-like)
|
|
371
|
+
|
|
372
|
+
Define an index once, then perform O(1) lookups from another sequence when rendering. If no match is found, `getKey()` returns the current context (`this`) as a sentinel; check for that to skip safely.
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
import JTLT from 'jtlt';
|
|
376
|
+
|
|
377
|
+
const data = {
|
|
378
|
+
customers: [
|
|
379
|
+
{id: 1, name: 'Alice'},
|
|
380
|
+
{id: 2, name: 'Bob'}
|
|
381
|
+
],
|
|
382
|
+
orders: [
|
|
383
|
+
{id: 'o-10', customerId: 2, item: 'Keyboard'},
|
|
384
|
+
{id: 'o-11', customerId: 3, item: 'Cable'} // no matching customer
|
|
385
|
+
]
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const templates = [
|
|
389
|
+
{path: '$', mode: 'html', template () {
|
|
390
|
+
// Define an index by id: key(name, match, use)
|
|
391
|
+
this.key('customerById', '$.customers[*]', 'id');
|
|
392
|
+
|
|
393
|
+
this.element('ul', {}, [], () => {
|
|
394
|
+
this.forEach('$.orders[*]', function (o) {
|
|
395
|
+
const c = this.getKey('customerById', o.customerId);
|
|
396
|
+
// getKey returns `this` if no match; skip such rows
|
|
397
|
+
if (c === this) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.element('li', {}, [], () => this.text(`${c.name}: ${o.item}`));
|
|
401
|
+
}, {select: '$.id', order: 'ascending'});
|
|
402
|
+
});
|
|
403
|
+
}}
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
const out = new JTLT({data, templates, outputType: 'string'}).transform('html');
|
|
407
|
+
// -> <ul><li>Bob: Keyboard</li></ul>
|
|
408
|
+
console.log(out);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Tips:
|
|
412
|
+
|
|
413
|
+
- You can define multiple keys with different `use` properties (e.g., lookup by `email`, `id`, etc.).
|
|
414
|
+
- The `match` expression can target nested arrays (e.g., `$.stores[*].customers[*]`).
|
|
415
|
+
- For JSON output joins, switch `outputType: 'json'` and use `object()`/`array()` to build structured results.
|
|
416
|
+
|
|
417
|
+
## How this compares to XSLT: pros and cons
|
|
418
|
+
|
|
419
|
+
Advantages (strong parallels with XSLT):
|
|
420
|
+
|
|
421
|
+
- Template matching by path and mode: templates use JSONPath selectors and optional `mode`, with priority resolution and an option to error on equal priority.
|
|
422
|
+
- Built‑in default rules: when no template matches, defaults traverse and render objects, arrays, scalars, property names, and functions, similar to XSLT’s built‑in templates.
|
|
423
|
+
- applyTemplates/forEach and sorting: `applyTemplates(select, mode, sort)` and `forEach(select, cb, sort)` mirror `xsl:apply-templates`/`xsl:for-each` and `xsl:sort`.
|
|
424
|
+
- Named templates and parameters: `callTemplate(name, withParam)` reflects `xsl:call-template` + `xsl:with-param`.
|
|
425
|
+
- Keys and lookups: `key(name, match, use)` + `getKey(name, value)` provide `xsl:key`-style indexing for joins and fast lookups.
|
|
426
|
+
- Multiple output forms: string, DOM, and JSON builders ("joiners") allow emitting different result trees like XSLT’s result tree model.
|
|
427
|
+
|
|
428
|
+
Differences / current limitations:
|
|
429
|
+
|
|
430
|
+
- Expression language: XPath 2.0 implementation is not fully feature complete.
|
|
431
|
+
- Stylesheet composition/precedence: no `xsl:import`/`xsl:include` equivalents; only basic priority and modes.
|
|
432
|
+
- Schema awareness: no type-aware processing (a major XSLT/XQuery feature).
|
|
433
|
+
- Multi-output (`xsl:result-document`): not built-in; pick one output type per transform.
|
|
434
|
+
|
|
435
|
+
## Differences between an exact equivalence with XSLT
|
|
436
|
+
|
|
437
|
+
JTLT, having the freedom to start a new pattern from XSLT, and though
|
|
438
|
+
seeking to learn from it, deviates somewhat from making an exact
|
|
439
|
+
equivalence with XSLT (to the extent JTLT and JSONPath implement
|
|
440
|
+
what could possibly be transferred to JSON-based transformations
|
|
441
|
+
from XSLT):
|
|
442
|
+
|
|
443
|
+
1. Although the option is given for throwing errors upon finding
|
|
444
|
+
templates of equal priority, the default behavior is to give
|
|
445
|
+
preference to the last template (unlike XSLT which makes it an
|
|
446
|
+
error by default).
|
|
447
|
+
|
|
448
|
+
## To-dos
|
|
449
|
+
|
|
450
|
+
1. Document and add test cases to cover current features! (Could adapt an XSLT/XQuery test suite)
|
|
451
|
+
|
|
452
|
+
1. Implement and demo equivalent to applying and calling templates, and
|
|
453
|
+
root template
|
|
454
|
+
|
|
455
|
+
2. Demo chaining of methods, including [equivalents](https://www.saxonica.com/papers/XTech2005/mhkpaper.html#S4.)
|
|
456
|
+
to XQuery's FLWOR expressions (see also Promises to-do), perhaps
|
|
457
|
+
even making aliases so that XQuery's friendlier terms can be used
|
|
458
|
+
instead of XSLT's.
|
|
459
|
+
|
|
460
|
+
2. Support processing of JSON documents with `$jtlt-stylesheet` to let
|
|
461
|
+
documents define their own targeted stylesheets.
|
|
462
|
+
|
|
463
|
+
3. When sufficiently documented, add as example library/tool to
|
|
464
|
+
JSONPath wiki.
|
|
465
|
+
|
|
466
|
+
4. Allow alternative to `element()`, `array()`, etc. methods by just
|
|
467
|
+
detecting those types from return values (and generic of each
|
|
468
|
+
type like `dom()` and `json()`).
|
|
469
|
+
|
|
470
|
+
5. Allow, depending on mode, containers to contain containers of other
|
|
471
|
+
types (e.g., a JS container containing DOM objects, or temporary use
|
|
472
|
+
of a string container, etc.).
|
|
473
|
+
|
|
474
|
+
1. Support XML and add [hXML](https://github.com/brettz9/hxml) methods.
|
|
475
|
+
|
|
476
|
+
2. Support [JHTML](https://github.com/brettz9/jhtml).
|
|
477
|
+
|
|
478
|
+
3. Support `appendJSON()`/`appendDOM()` and
|
|
479
|
+
`appendType('json', ...)` (allowing type extensions).
|
|
480
|
+
|
|
481
|
+
6. Add `appendResult(function () {return result})`.
|
|
482
|
+
|
|
483
|
+
7. Add JSON update functions (equivalent to Xquery Update Facility for
|
|
484
|
+
XML ([overview](http://www.xmlplease.com/xquery-update))) and create
|
|
485
|
+
JSON serialization (as with XSLT expressed itself in declarative XML)
|
|
486
|
+
so one can submit and evaluate
|
|
487
|
+
through [HTTPQuery](https://github.com/brettz9/httpquery) (and also
|
|
488
|
+
supply to JSONEditor, etc.). Utilize updating by reference.
|
|
489
|
+
|
|
490
|
+
8. Demo narrowing to subset of JavaScript (as with `jspe`) to make
|
|
491
|
+
JTLT truly "declarative" as far as freedom from scripting
|
|
492
|
+
|
|
493
|
+
## Possible to-dos
|
|
494
|
+
|
|
495
|
+
1. Make schema-aware so that templates could target types. Most reusable
|
|
496
|
+
application may be having a type-driven view of a JSON Schema instance
|
|
497
|
+
(e.g., dates could be shown inside a calendar widget). Perhaps this
|
|
498
|
+
schema-awareness could also drive a JSON editor (as with other existing
|
|
499
|
+
projects) (even using same API as JSONEditor?) (or type-aware filtered
|
|
500
|
+
search/raw queries). This would help not only for editors which edit a
|
|
501
|
+
JSON file in full, but also for providing schema paths or other identifiers
|
|
502
|
+
so that a transformed/queried subset of a file (or joining of multiple
|
|
503
|
+
files) could point the way for edited contents to be saved back to the
|
|
504
|
+
correct JSON file and position in the JSON file.
|
|
505
|
+
|
|
506
|
+
2. Add a [non-eval PR for JSONPath](https://github.com/s3u/JSONPath/pull/4).
|
|
507
|
+
The OR condition (outside of filters) is another important feature as
|
|
508
|
+
would be schema-aware path results.
|
|
509
|
+
|
|
510
|
+
3. Allow hybrid JSON/[Jamilih](https://github.com/brettz9/jamilih) or
|
|
511
|
+
JSON/(X)HTML/XML so that one can add
|
|
512
|
+
XPath or query into HTML in a relevant manner
|
|
513
|
+
|
|
514
|
+
4. Support pull parsing/streaming? Pass `done()` function to templates to
|
|
515
|
+
signal completion?
|
|
516
|
+
|
|
517
|
+
5. Support Promise API in addition to callbacks (reconcile with
|
|
518
|
+
current chaining; see also XQuery FLWOR to-do)
|
|
519
|
+
|
|
520
|
+
6. Add [XQuery Functions](https://code.google.com/p/jsxqueryparser/source/browse/trunk/jsxqueryparser/XQueryParser.js#1768)
|
|
521
|
+
(also supporting DOM and JSON where possible) as plug-in (also any
|
|
522
|
+
missing from XSLT/XQuery 3.0). Also add, if not present among these
|
|
523
|
+
functions (or in XQuery), add equivalents to XSLT's
|
|
524
|
+
`document()` and `unparsed-text()` for allowing non-JSON file
|
|
525
|
+
retrieval (as well as variables/parameters) and also methods for
|
|
526
|
+
iterating or retrieving IndexedDB, `localStorage`, and cookies
|
|
527
|
+
(names, keys and values).
|
|
528
|
+
|
|
529
|
+
7. Add `outputType` which uses a DOM joiner but allows specialized
|
|
530
|
+
serialized output (e.g., pretty-printed HTML) so the users
|
|
531
|
+
don't need to build it themselves (likewise with stringified
|
|
532
|
+
JSON output).
|
|
533
|
+
|
|
534
|
+
8. See code for other possible to-dos
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default AbstractJoiningTransformer;
|
|
2
|
+
/**
|
|
3
|
+
* Base class for joining transformers.
|
|
4
|
+
*
|
|
5
|
+
* A "joining transformer" is the sink that receives template outputs and
|
|
6
|
+
* accumulates them into a particular representation (string, DOM, JSON).
|
|
7
|
+
* Subclasses implement a consistent set of methods (string, number, object,
|
|
8
|
+
* array, element, text, etc.) but may interpret them differently according
|
|
9
|
+
* to their target representation.
|
|
10
|
+
*
|
|
11
|
+
* Common patterns supported by all joiners:
|
|
12
|
+
* - append(): central method that either concatenates, pushes, or assigns
|
|
13
|
+
* based on the current state.
|
|
14
|
+
* - get(): returns the accumulated result.
|
|
15
|
+
* - config(): temporarily tweak a config flag for the duration of a callback.
|
|
16
|
+
*/
|
|
17
|
+
declare class AbstractJoiningTransformer {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} [cfg] - Configuration object
|
|
20
|
+
*/
|
|
21
|
+
constructor(cfg?: object);
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} [cfg] - Configuration object
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
setConfig(cfg?: any): void;
|
|
27
|
+
_cfg: any;
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} type - Type name
|
|
30
|
+
* @param {string} embedType - Embed type name
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
_requireSameChildren(type: string, embedType: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} prop - Configuration property name
|
|
36
|
+
* @param {*} val - Configuration property value
|
|
37
|
+
* @param {Function} [cb] - Callback function
|
|
38
|
+
* @returns {void}
|
|
39
|
+
*/
|
|
40
|
+
config(prop: string, val: any, cb?: Function): void;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=AbstractJoiningTransformer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AbstractJoiningTransformer.d.ts","sourceRoot":"","sources":["../src/AbstractJoiningTransformer.js"],"names":[],"mappings":";AAIA;;;;;;;;;;;;;;GAcG;AACH;IACE;;OAEG;IACH,kBAFW,MAAM,EAKhB;IAED;;;OAGG;IACH,gBAHW,GAAG,GACD,IAAI,CAIhB;IADC,UAAe;IAGjB;;;;OAIG;IACH,2BAJW,MAAM,aACN,MAAM,GACJ,IAAI,CAUhB;IAED;;;;;OAKG;IACH,aALW,MAAM,OACN,GAAC,kBAEC,IAAI,CAchB;CACF"}
|