jtlt 0.17.0 → 0.19.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.
@@ -59,8 +59,9 @@ Common to both entry points unless noted.
59
59
  | ------ | ---- | ----------- |
60
60
  | `data` | object \| primitive | Root JSON/JS value (or DOM `Document`/`Element` for the XPath engine). Required unless `ajaxData` provided. |
61
61
  | `ajaxData` | string | URL to fetch JSON (async start). |
62
- | `templates` | Array<TemplateObject> | Template declarations; see below. |
62
+ | `templates` | Array<TemplateObject> \| TemplateObject \| [path, Function\|Array] | Template declarations; see below. A bare `TemplateObject` or `[path, template]` tuple (not wrapped in an outer array) is also accepted directly. |
63
63
  | `template` | Function \| TemplateObject | Single root template convenience. |
64
+ | `defaultTemplateFormat` | 'json' \| 'javascript' | JSONPath engine only. Jamilih validation strictness applied to a declarative (`Array`) `template` when its own `format` isn't set. Default `'json'`. See "Declarative (jamilih-shaped) templates" below. |
64
65
  | `query` | Function | Root template convenience (wrapped as `path: '$'`). |
65
66
  | `forQuery` | [select, cb] | One-off query (like FLWOR `for`). Auto-wrapped as root template. |
66
67
  | `success` | Function(result) | **`JTLT` only** — required callback; also the return of `.transform()`. Not accepted by `jtlt()`. |
@@ -97,6 +98,79 @@ Edge cases:
97
98
  - For XPath: `//item`, `/root/item`, `//*[@id='x']`.
98
99
  - Root template: path `$` (JSONPath) or `/` (XPath).
99
100
 
101
+ ## Declarative (jamilih-shaped) templates
102
+
103
+ JSONPath engine only. A `TemplateObject.template` may be an `Array` of
104
+ declarative nodes — modeled on [jamilih](https://github.com/brettz9/jamilih)'s
105
+ own JSON/JS-array HTML/XML construction syntax — instead of a `Function`.
106
+ `compileJSONTemplate()` compiles it once, up front, into an ordinary
107
+ `TemplateFunction`; every other API (`applyTemplates`, `callTemplate`, modes,
108
+ priority, …) works the same regardless of which form a matched template uses.
109
+
110
+ ```js
111
+ const out = await jtlt({
112
+ data: {name: 'Ada'},
113
+ outputType: 'string',
114
+ templates: {
115
+ path: '$',
116
+ template: [
117
+ ['h1', ['Hello']],
118
+ [{$valueOf: '$.name'}]
119
+ ]
120
+ }
121
+ });
122
+ // -> <h1>Hello</h1>Ada
123
+ ```
124
+
125
+ A node is one of:
126
+
127
+ | Node | Emits |
128
+ | --- | --- |
129
+ | `"text"` (a plain string) | a text node |
130
+ | `[name, attrs?, children?]` | an element — `attrs` is omitted when there are none, in which case the second item is the children array directly |
131
+ | `[{$text: value}]` | `this.text(value)` — jamilih's own native text-node form; bare only, no `$select` |
132
+ | `[{$jtltText: value, $select?: sel}]` | `this.text(value)` (no `$select`) or `this.valueOf(sel)` (with `$select`) — the general/canonical form of `$text` |
133
+ | `[{$string: value, $select?: sel}]` | `this.string(value)` or, with `$select`, the selected value stringified |
134
+ | `[{$valueOf: sel}]` | `this.valueOf(sel)` |
135
+ | `[{$applyTemplates: sel, $jtltMode?: mode, $sort?: sort}]` | `this.applyTemplates(sel, mode, sort)` |
136
+ | `[{$if: sel}, thenNodes, elseNodes?]` | `this.if(sel, cb)` or, with an else, `this.choose(sel, whenCb, otherwiseCb)` |
137
+ | `[{$forEach: sel, $sort?: sort}, childNodes]` | `this.forEach(sel, cb, sort)` |
138
+ | `[{$variable: name, $select: sel}]` | `this.variable(name, sel)`, readable back later via a bare `$name` reference |
139
+ | `[{$indexedDB: {db, store, options?}, $as?: name}, childNodes?]` | fetches rows; with `$as`, binds them via `this.variable(name, {value: rows})`; without it, swaps the data context (`$`) to the rows for `childNodes` |
140
+ | `[{$renderDefault: true}]` | `this.appendOutput(await this.renderDefault())` (an `extensions.renderDefault` your config supplies) |
141
+
142
+ Every operation node's leading object is entirely `$`-prefixed — jamilih's
143
+ own validator requires this of any first-position plain object. `$jtltMode`/
144
+ `$jtltText` are namespaced because jamilih itself reserves `$mode` outright
145
+ and gives `$text` its own (bare-only) native meaning; see the README's
146
+ ["Differences between an exact equivalence with
147
+ XSLT"](../README.md#differences-between-an-exact-equivalence-with-xslt)
148
+ section for why `template`, an `$if`/`$forEach` body, and an operation
149
+ node's own trailing arguments are each *arrays* — including the
150
+ easy-to-miss case of a single node or a single sibling, which still needs
151
+ its wrapping array.
152
+
153
+ `$indexedDB`/`$renderDefault` may appear nested anywhere a node is
154
+ allowed — inside an element's children, or a `$if`/`$forEach` body — not
155
+ just at a template's top level; see the README section above for why.
156
+
157
+ **Validation**: `format: 'json'` (the default) rejects an embedded live
158
+ function/DOM node anywhere in the tree — the intent is a safely
159
+ serializable structure (e.g. round-tripped through IndexedDB), not
160
+ arbitrary JavaScript. Pass `format: 'javascript'` on the `TemplateObject`
161
+ (or set `defaultTemplateFormat: 'javascript'` on the config to relax every
162
+ entry lacking its own `format`) to allow one.
163
+
164
+ **Exports** (from `'jtlt'`):
165
+ - `compileJSONTemplate(nodes, {format?}) => TemplateFunction` — throws a
166
+ `TypeError` with every problem found if `nodes` is invalid.
167
+ - `validateJSONTemplate(nodes, {format?}) => {valid, errors}` — the same
168
+ checks, non-executing and non-throwing; for a "validate before save"
169
+ editor workflow.
170
+ - `isJSONTemplateNodeArray(x) => boolean` — `Array.isArray`, exported for
171
+ callers that need to detect the declarative form themselves (e.g. before
172
+ deciding whether to call `compileJSONTemplate`).
173
+
100
174
  ## Engines
101
175
 
102
176
  ### `JSONPathTransformer`
package/docs/API.md CHANGED
@@ -181,6 +181,26 @@ this.applyTemplates('$.items[*]');
181
181
 
182
182
  This is useful for strict template matching where ambiguity should be an error rather than silently choosing a template, or for development where warnings help identify potential issues.
183
183
 
184
+ ## Declarative (jamilih-shaped) templates
185
+
186
+ JSONPath engine only. `template` may be an `Array` of jamilih-shaped nodes
187
+ instead of a `Function` — compiled once via `compileJSONTemplate()`
188
+ (also exported, along with `validateJSONTemplate()` for non-executing
189
+ validation and `isJSONTemplateNodeArray()`):
190
+
191
+ ```js
192
+ const templates = {path: '$', template: [
193
+ ['h1', ['Hello']],
194
+ [{$valueOf: '$.name'}]
195
+ ]};
196
+ // -> <h1>Hello</h1>Ada
197
+ ```
198
+
199
+ `format: 'json'` (default) rejects an embedded live function/DOM node
200
+ anywhere in the tree; `format: 'javascript'` (per-entry, or
201
+ `defaultTemplateFormat` on the config) allows one. See
202
+ `docs/API.expanded.md` for the full node vocabulary.
203
+
184
204
  ## Sorting
185
205
 
186
206
  Path string (ascending text), comparator function, object spec
package/docs/TO-DO.md CHANGED
@@ -4,10 +4,12 @@
4
4
 
5
5
  1. Document
6
6
 
7
+ 1. Write a specification for declarative jamilih / jtlt?
8
+
7
9
  1. Implement and demo equivalent to applying and calling templates, and
8
10
  root template
9
11
 
10
- 2. Demo chaining of methods, including [equivalents](https://www.saxonica.com/papers/XTech2005/mhkpaper.html#S4.)
12
+ 1. Demo chaining of methods, including [equivalents](https://www.saxonica.com/papers/XTech2005/mhkpaper.html#S4.)
11
13
  to XQuery's FLWOR expressions (see also Promises to-do), perhaps
12
14
  even making aliases so that XQuery's friendlier terms can be used
13
15
  instead of XSLT's.
@@ -41,10 +43,12 @@
41
43
  through [HTTPQuery](https://github.com/brettz9/httpquery) (and also
42
44
  supply to JSONEditor, etc.). Utilize updating by reference.
43
45
 
44
- 8. Demo narrowing to subset of JavaScript (as with `jsep`) to make
46
+ ## Possible to-dos
47
+
48
+ 1. Demo narrowing to subset of JavaScript (as with `jsep`) to make
45
49
  JTLT truly "declarative" as far as freedom from scripting
46
50
 
47
- ## Possible to-dos
51
+ 1. Support XPath and Declarative Jamilih
48
52
 
49
53
  1. Make schema-aware so that templates could target types. Most reusable
50
54
  application may be having a type-driven view of a JSON Schema instance
@@ -112,6 +116,7 @@ We'd otherwise have to refactor a great deal to maintain a buffer.
112
116
  xsl:break
113
117
  xsl:catch
114
118
  xsl:context-item
119
+ ~~"xsl:document",~~ (not yet exposed)
115
120
  xsl:evaluate
116
121
  xsl:expose
117
122
  "xsl:fallback",
@@ -139,6 +144,8 @@ We'd otherwise have to refactor a great deal to maintain a buffer.
139
144
  xsl:use-package
140
145
  xsl:where-populated
141
146
 
147
+ // Todo: Some of these may be implemented but not yet exposed
148
+
142
149
  ~~"xsl:analyze-string",~~
143
150
  ~~"xsl:apply-templates",~~
144
151
  ~~xsl:assert~~
@@ -151,7 +158,6 @@ We'd otherwise have to refactor a great deal to maintain a buffer.
151
158
  ~~"xsl:copy",~~
152
159
  ~~"xsl:copy-of",~~
153
160
  ~~"xsl:decimal-format",~~
154
- ~~"xsl:document",~~
155
161
  ~~"xsl:element",~~
156
162
  ~~"xsl:for-each",~~
157
163
  ~~"xsl:for-each-group",~~
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jtlt",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "author": "Brett Zamir",
6
6
  "contributors": [],
@@ -39,7 +39,7 @@
39
39
  "codemirror": "^6.0.2",
40
40
  "fontoxpath": "^3.34.0",
41
41
  "idb": "^8.0.3",
42
- "jamilih": "0.69.1",
42
+ "jamilih": "0.70.0",
43
43
  "jhtml": "0.7.3",
44
44
  "jsdom": "30.0.1",
45
45
  "jsonpath-plus": "10.4.0",
@@ -51,17 +51,17 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@arethetypeswrong/cli": "^0.18.5",
54
- "@node-static/node-static": "^0.9.1",
54
+ "@node-static/node-static": "^0.9.2",
55
55
  "@rollup/plugin-node-resolve": "^16.0.3",
56
56
  "@types/chai": "^5.2.3",
57
57
  "@types/jsdom": "^30.0.0",
58
58
  "@types/mocha": "^10.0.10",
59
- "baseline-browser-mapping": "^2.11.21",
59
+ "baseline-browser-mapping": "^2.11.22",
60
60
  "c8": "^12.0.0",
61
61
  "chai": "^6.2.2",
62
62
  "eslint": "^10.10.0",
63
- "eslint-config-ash-nazg": "^43.1.7",
64
- "indexeddbshim": "^19.0.1",
63
+ "eslint-config-ash-nazg": "^43.1.8",
64
+ "indexeddbshim": "^19.0.2",
65
65
  "mocha": "^12.0.0",
66
66
  "open": "^11.0.2",
67
67
  "rollup": "^4.63.1",
@@ -35,7 +35,10 @@ minimumReleaseAgeExclude:
35
35
  - eslint-config-ash-nazg@43.1.0 || 43.1.6
36
36
  - eslint-plugin-escompat@3.12.0
37
37
  - eslint-plugin-jsdoc@64.3.0 || 64.3.3
38
- - indexeddbshim@17.4.1 || 17.4.2 || 17.4.3 || 17.5.0 || 17.6.0
38
+ - indexeddbshim@17.4.1 || 17.4.2 || 17.4.3 || 17.5.0 || 17.6.0 || 19.0.2
39
39
  - open@11.0.2
40
40
  - powershell-utils@0.2.1
41
41
  - websql-configurable@4.0.0
42
+ - '@node-static/node-static@0.9.2'
43
+ - baseline-browser-mapping@2.11.22
44
+ - jamilih@0.70.0
@@ -300,7 +300,7 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
300
300
  * @overload
301
301
  * @param {Element|string} elName
302
302
  * @param {(this: DOMJoiningTransformer) => void} cb
303
- * @returns {DOMJoiningTransformer}
303
+ * @returns {DOMJoiningTransformer|Promise<DOMJoiningTransformer>}
304
304
  */
305
305
  /**
306
306
  * @overload
@@ -314,9 +314,15 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
314
314
  * childNodes, or callback
315
315
  * @param {(Node|string)[]|((this: DOMJoiningTransformer) => void)
316
316
  * } [childNodes] - Child nodes or callback
317
- * @param {(this: DOMJoiningTransformer) => void} [cb] - Callback
317
+ * @param {(this: DOMJoiningTransformer) => void} [cb] -
318
+ * Callback; may be async (e.g. to `await` a `$indexedDB` fetch), in
319
+ * which case `element()` itself returns a `Promise` instead of `this`.
320
+ * (Typed as returning plain `void`, not `void|Promise<void>` — a union
321
+ * there would lose TypeScript's usual "a void-returning callback
322
+ * parameter accepts any actual return value" leniency, which existing
323
+ * callers rely on for e.g. `() => this.text(...)` concise arrows.)
318
324
  * @param {string[]} [useAttributeSets] - Attribute set names to apply
319
- * @returns {DOMJoiningTransformer}
325
+ * @returns {DOMJoiningTransformer|Promise<DOMJoiningTransformer>}
320
326
  */
321
327
  element (elem, atts, childNodes, cb, useAttributeSets) {
322
328
  // Handle argument overloading like other transformers
@@ -440,12 +446,24 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
440
446
  }
441
447
  }
442
448
 
449
+ const finishRoot = () => {
450
+ this._dom = oldDOM;
451
+ return this;
452
+ };
443
453
  if (cb) {
444
- cb.call(this._context || this);
454
+ // `cb`'s declared return type is plain `void` (see the parameter's
455
+ // JSDoc), preserving TypeScript's void-return leniency for
456
+ // callers — cast here to duck-type the real value.
457
+ const cbResult = /** @type {any} */ (cb.call(this._context || this));
458
+ // `cb` may be an async function (e.g. one that awaits a
459
+ // `$indexedDB` fetch); resume only once it settles, so `_dom` is
460
+ // restored in the right order.
461
+ if (cbResult && typeof cbResult.then === 'function') {
462
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
463
+ return cbResult.then(() => finishRoot());
464
+ }
445
465
  }
446
- this._dom = oldDOM;
447
-
448
- return this;
466
+ return finishRoot();
449
467
  }
450
468
 
451
469
  // Non-root elements
@@ -494,12 +512,20 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
494
512
  }
495
513
  }
496
514
 
515
+ const finish = () => {
516
+ this._dom = oldDOM;
517
+ return this;
518
+ };
497
519
  if (cb) {
498
- cb.call(this._context || this);
520
+ // `cb`'s declared return type is plain `void` — cast here to
521
+ // duck-type the real value; see the note above the other branch.
522
+ const cbResult = /** @type {any} */ (cb.call(this._context || this));
523
+ if (cbResult && typeof cbResult.then === 'function') {
524
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
525
+ return cbResult.then(() => finish());
526
+ }
499
527
  }
500
- this._dom = oldDOM;
501
-
502
- return this;
528
+ return finish();
503
529
  }
504
530
 
505
531
  /**
@@ -30,6 +30,12 @@ function _makeDatasetAttribute (n0) {
30
30
  * : import('./DOMJoiningTransformer.js').default}
31
31
  * @returns {void}
32
32
  */
33
+ // Kept as plain `void` (not `void|Promise<void>`) rather than a union:
34
+ // TypeScript's "a void-returning callback parameter accepts any actual
35
+ // return value" leniency — relied on throughout the test suite for
36
+ // concise-body arrows like `() => this.text(...)` — only applies to a
37
+ // literal `void` return type, not a union. An async callback (returning
38
+ // `Promise<void>`) is still assignable here for the same reason.
33
39
 
34
40
  /**
35
41
  * Attributes object for element() allowing standard string attributes
@@ -392,9 +398,11 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
392
398
  * @param {ElementAttributes|any[]|SimpleCallback} [atts]
393
399
  * Attrs, children, or cb.
394
400
  * @param {any[]|SimpleCallback} [childNodes] Children or cb.
395
- * @param {SimpleCallback} [cb] Builder callback.
401
+ * @param {SimpleCallback} [cb] Builder callback; may be async (e.g. to
402
+ * `await` a `$indexedDB` fetch), in which case `element()` itself
403
+ * returns a `Promise` instead of `this`.
396
404
  * @param {string[]} [useAttributeSets] - Attribute set names to apply
397
- * @returns {JSONJoiningTransformer}
405
+ * @returns {JSONJoiningTransformer|Promise<JSONJoiningTransformer>}
398
406
  */
399
407
  element (elem, atts, childNodes, cb, useAttributeSets) {
400
408
  this._requireSameChildren('json', 'element');
@@ -485,77 +493,96 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
485
493
  }));
486
494
  }
487
495
 
488
- // Callback-driven building (attribute/text/nested element mutate stack)
489
- if (cb) {
490
- // Push current state onto a stack
491
- this._elementStack.push({attsObj, jmlChildren});
492
- cb.call(this._context || this);
493
- const state = /** @type {ElementInfo} */ (this._elementStack.pop());
494
- ({attsObj} = state);
495
- // Children may have been mutated by nested element()/text();
496
- // already in jmlChildren
497
- }
498
-
499
- // Build Jamilih array
500
- /** @type {any[]} */
501
- const jmlEl = [elementName];
502
- if (Object.keys(attsObj).length) {
503
- jmlEl.push(attsObj, jmlChildren);
504
- } else {
505
- jmlEl.push(jmlChildren);
506
- }
507
-
508
- if (isRoot) {
509
- // todo: indent, cdataSectionElements
510
- const {
511
- omitXmlDeclaration, bareDoctype, doctypePublic, doctypeSystem, method
512
- } = this._outputConfig ?? {};
513
-
514
- const dtd = bareDoctype !== false
515
- ? [{$DOCTYPE: {
516
- name: elementName,
517
- publicId: doctypePublic ?? null, // Public ID (optional)
518
- systemId: doctypeSystem ?? null // System ID (optional)
519
- }}]
520
- : [];
521
-
522
- let xmlDeclaration;
523
- /* c8 ignore start -- third OR condition short-circuits */
524
- if (!omitXmlDeclaration && (
525
- method === 'xml' || method === 'xhtml' || omitXmlDeclaration === false)
526
- ) {
527
- const {version, encoding, standalone} = this._outputConfig ?? {};
528
-
529
- xmlDeclaration = {
530
- version,
531
- encoding,
532
- standalone
533
- };
496
+ // Everything from here on depends on `attsObj`/`jmlChildren` having
497
+ // been fully populated by `cb` (when given) — deferred into a
498
+ // continuation so an async `cb` (e.g. one that awaits a `$indexedDB`
499
+ // fetch) can be awaited first, keeping output correctly ordered.
500
+ const finishElement = () => {
501
+ // Build Jamilih array
502
+ /** @type {any[]} */
503
+ const jmlEl = [elementName];
504
+ if (Object.keys(attsObj).length) {
505
+ jmlEl.push(attsObj, jmlChildren);
506
+ } else {
507
+ jmlEl.push(jmlChildren);
534
508
  }
535
- /* c8 ignore stop */
536
509
 
537
- const doc = {$document: {
538
- ...(xmlDeclaration ? {xmlDeclaration} : {}),
539
- childNodes: [
540
- ...(method === 'xml' || method === 'xhtml' ? dtd : []),
541
- jmlEl
542
- ]
543
- }};
510
+ if (isRoot) {
511
+ // todo: indent, cdataSectionElements
512
+ const {
513
+ omitXmlDeclaration, bareDoctype, doctypePublic, doctypeSystem, method
514
+ } = this._outputConfig ?? {};
515
+
516
+ const dtd = bareDoctype !== false
517
+ ? [{$DOCTYPE: {
518
+ name: elementName,
519
+ publicId: doctypePublic ?? null, // Public ID (optional)
520
+ systemId: doctypeSystem ?? null // System ID (optional)
521
+ }}]
522
+ : [];
523
+
524
+ let xmlDeclaration;
525
+ /* c8 ignore start -- third OR condition short-circuits */
526
+ if (!omitXmlDeclaration && (
527
+ method === 'xml' || method === 'xhtml' ||
528
+ omitXmlDeclaration === false)
529
+ ) {
530
+ const {version, encoding, standalone} = this._outputConfig ?? {};
531
+
532
+ xmlDeclaration = {
533
+ version,
534
+ encoding,
535
+ standalone
536
+ };
537
+ }
538
+ /* c8 ignore stop */
539
+
540
+ const doc = {$document: {
541
+ ...(xmlDeclaration ? {xmlDeclaration} : {}),
542
+ childNodes: [
543
+ ...(method === 'xml' || method === 'xhtml' ? dtd : []),
544
+ jmlEl
545
+ ]
546
+ }};
547
+
548
+ // Removed this._doc; use this._docs only
549
+ if (this._cfg.exposeDocuments) {
550
+ this._docs.push(doc);
551
+ }
552
+ }
544
553
 
545
- // Removed this._doc; use this._docs only
546
- if (this._cfg.exposeDocuments) {
547
- this._docs.push(doc);
554
+ // If inside a parent element, append as its child; otherwise
555
+ // append to root
556
+ if (this._elementStack.length) {
557
+ const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
558
+ top.jmlChildren.push(jmlEl);
559
+ } else {
560
+ this.append(jmlEl);
548
561
  }
549
- }
562
+ return this;
563
+ };
550
564
 
551
- // If inside a parent element, append as its child; otherwise append to root
552
- if (this._elementStack.length) {
553
- const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
554
- top.jmlChildren.push(jmlEl);
555
- } else {
556
- this.append(jmlEl);
565
+ // Callback-driven building (attribute/text/nested element mutate stack)
566
+ if (cb) {
567
+ // Push current state onto a stack
568
+ this._elementStack.push({attsObj, jmlChildren});
569
+ // `cb`'s declared return type is plain `void` (see `SimpleCallback`)
570
+ // — cast here to duck-type the real value.
571
+ const cbResult = /** @type {any} */ (cb.call(this._context || this));
572
+ const finishWithStack = () => {
573
+ const state = /** @type {ElementInfo} */ (this._elementStack.pop());
574
+ ({attsObj} = state);
575
+ // Children may have been mutated by nested element()/text();
576
+ // already in jmlChildren
577
+ return finishElement();
578
+ };
579
+ if (cbResult && typeof cbResult.then === 'function') {
580
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
581
+ return cbResult.then(() => finishWithStack());
582
+ }
583
+ return finishWithStack();
557
584
  }
558
- return this;
585
+ return finishElement();
559
586
  }
560
587
 
561
588
  /**
@@ -1,4 +1,7 @@
1
1
  import JSONPathTransformerContext from './JSONPathTransformerContext.js';
2
+ import {
3
+ compileJSONTemplate, isJSONTemplateNodeArray
4
+ } from './jsonTemplate.js';
2
5
 
3
6
  /**
4
7
  * Applies named JSONPath-driven templates to JSON data.
@@ -106,18 +109,35 @@ class JSONPathTransformer {
106
109
  throw new TypeError('config.templates is required');
107
110
  }
108
111
  this.templates = config.templates.map(function (template) {
112
+ /** @type {import('./index.js').JSONPathTemplateObject<T>} */
113
+ let normalized;
109
114
  if (Array.isArray(template)) {
110
115
  // Todo: We could allow a third argument (at beginning or
111
116
  // end?) to represent template name
112
- return /** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
113
- {path: template[0], template: template[1]}
114
- );
117
+ normalized = /**
118
+ * @type {import('./index.js').
119
+ JSONPathTemplateObject<T>} */ (
120
+ {path: template[0], template: template[1]}
121
+ );
122
+ } else if (template.match && !template.path) {
123
+ // Normalize 'match' to 'path' for XSLT compatibility
124
+ normalized = {...template, path: template.match};
125
+ } else {
126
+ normalized = template;
115
127
  }
116
- // Normalize 'match' to 'path' for XSLT compatibility
117
- if (template.match && !template.path) {
118
- return {...template, path: template.match};
128
+ // A declarative (jamilih-shaped) node array, as opposed to a
129
+ // `TemplateFunction` — compile it once, up front, so every later
130
+ // dispatch site (root, applyTemplates, callTemplate) only ever sees
131
+ // a plain function.
132
+ if (isJSONTemplateNodeArray(normalized.template)) {
133
+ const format = normalized.format || config.defaultTemplateFormat ||
134
+ 'json';
135
+ normalized = {
136
+ ...normalized,
137
+ template: compileJSONTemplate(normalized.template, {format})
138
+ };
119
139
  }
120
- return template;
140
+ return normalized;
121
141
  });
122
142
  this.templates.forEach((template, i, templates) => {
123
143
  // eslint-disable-next-line @stylistic/max-len -- Long
@@ -164,16 +184,26 @@ class JSONPathTransformer {
164
184
  }
165
185
  // Set up parameter context for valueOf() access in root template
166
186
  jte._params = {0: jte._contextObj};
187
+ // `template` is always a function here: a declarative (jamilih-shaped)
188
+ // Array is compiled to one up front, in the constructor.
189
+ const {template: rootTemplateFn} =
190
+ /**
191
+ * @type {import('./index.js').JSONPathTemplateObject<T> &
192
+ * {template: import('./index.js').
193
+ * TemplateFunction<T, "json", JSONPathTransformerContext<T>>}}
194
+ */ (
195
+ templateObj
196
+ );
167
197
  /**
168
198
  * The template may return a value synchronously or a Promise (e.g. from
169
199
  * `await this.indexedDB(...)`), which is awaited unless `config.sync`.
170
200
  * @type {any}
171
201
  */
172
- const ret = /** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
173
- templateObj
202
+ const ret = (
174
203
  // `this` carries runtime `extensions`; a consumer's `ContextExtensions`
175
204
  // augmentation would otherwise reject the bare context here.
176
- ).template.call(/** @type {any} */ (jte), undefined, {mode});
205
+ rootTemplateFn
206
+ ).call(/** @type {any} */ (jte), undefined, {mode});
177
207
 
178
208
  if (ret !== null && typeof ret !== 'undefined' &&
179
209
  typeof ret.then === 'function') {