jtlt 0.18.0 → 0.20.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,91 @@ 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. Includes `extractReads()`'s errors, below — an
170
+ unresolvable `$indexedDB` target makes the whole template invalid, not
171
+ just a `reads` omission.
172
+ - `extractReads(nodes) => {reads: {db, store}[], errors}` — statically
173
+ derives the `{db, store}` targets every `$indexedDB` node in the tree
174
+ touches (walking element children, `$if`/`$forEach` bodies, and another
175
+ `$indexedDB` node's own children — not just the top level), deduplicated.
176
+ Total: an `$indexedDB` node whose `db`/`store` isn't a literal,
177
+ non-empty string is reported as an error rather than silently omitted
178
+ from `reads` — the point is to drive a data-access allowlist, so an
179
+ unresolvable target must never read as "no read happens here." Useful on
180
+ its own (not just via `validateJSONTemplate`) for computing the actual
181
+ `reads` value to store alongside a saved template.
182
+ - `isJSONTemplateNodeArray(x) => boolean` — `Array.isArray`, exported for
183
+ callers that need to detect the declarative form themselves (e.g. before
184
+ deciding whether to call `compileJSONTemplate`).
185
+
100
186
  ## Engines
101
187
 
102
188
  ### `JSONPathTransformer`
package/docs/API.md CHANGED
@@ -181,6 +181,27 @@ 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, `extractReads()` for statically deriving the `{db, store}`
190
+ targets its `$indexedDB` nodes touch, and `isJSONTemplateNodeArray()`):
191
+
192
+ ```js
193
+ const templates = {path: '$', template: [
194
+ ['h1', ['Hello']],
195
+ [{$valueOf: '$.name'}]
196
+ ]};
197
+ // -> <h1>Hello</h1>Ada
198
+ ```
199
+
200
+ `format: 'json'` (default) rejects an embedded live function/DOM node
201
+ anywhere in the tree; `format: 'javascript'` (per-entry, or
202
+ `defaultTemplateFormat` on the config) allows one. See
203
+ `docs/API.expanded.md` for the full node vocabulary.
204
+
184
205
  ## Sorting
185
206
 
186
207
  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.18.0",
3
+ "version": "0.20.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
@@ -2,9 +2,11 @@ import * as JHTML from 'jhtml';
2
2
  import AbstractJoiningTransformer from './AbstractJoiningTransformer.js';
3
3
 
4
4
  /**
5
- * @callback SimpleCallback
6
- * @this {DOMJoiningTransformer}
7
- * @returns {void}
5
+ * A `@callback` tag combined with `@this` mis-emits in `dist/*.d.ts` (the
6
+ * `@this` type lands as raw, unparsed JSDoc text) — see the equivalent note
7
+ * in `JSONJoiningTransformer.js`; this inline `@typedef {(this: ...) =>
8
+ * ...}` function-type form emits correctly instead.
9
+ * @typedef {(this: DOMJoiningTransformer) => void} SimpleCallback
8
10
  */
9
11
 
10
12
  /* eslint-disable @stylistic/max-len -- Long */
@@ -300,7 +302,7 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
300
302
  * @overload
301
303
  * @param {Element|string} elName
302
304
  * @param {(this: DOMJoiningTransformer) => void} cb
303
- * @returns {DOMJoiningTransformer}
305
+ * @returns {DOMJoiningTransformer|Promise<DOMJoiningTransformer>}
304
306
  */
305
307
  /**
306
308
  * @overload
@@ -314,9 +316,15 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
314
316
  * childNodes, or callback
315
317
  * @param {(Node|string)[]|((this: DOMJoiningTransformer) => void)
316
318
  * } [childNodes] - Child nodes or callback
317
- * @param {(this: DOMJoiningTransformer) => void} [cb] - Callback
319
+ * @param {(this: DOMJoiningTransformer) => void} [cb] -
320
+ * Callback; may be async (e.g. to `await` a `$indexedDB` fetch), in
321
+ * which case `element()` itself returns a `Promise` instead of `this`.
322
+ * (Typed as returning plain `void`, not `void|Promise<void>` — a union
323
+ * there would lose TypeScript's usual "a void-returning callback
324
+ * parameter accepts any actual return value" leniency, which existing
325
+ * callers rely on for e.g. `() => this.text(...)` concise arrows.)
318
326
  * @param {string[]} [useAttributeSets] - Attribute set names to apply
319
- * @returns {DOMJoiningTransformer}
327
+ * @returns {DOMJoiningTransformer|Promise<DOMJoiningTransformer>}
320
328
  */
321
329
  element (elem, atts, childNodes, cb, useAttributeSets) {
322
330
  // Handle argument overloading like other transformers
@@ -440,12 +448,24 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
440
448
  }
441
449
  }
442
450
 
451
+ const finishRoot = () => {
452
+ this._dom = oldDOM;
453
+ return this;
454
+ };
443
455
  if (cb) {
444
- cb.call(this._context || this);
456
+ // `cb`'s declared return type is plain `void` (see the parameter's
457
+ // JSDoc), preserving TypeScript's void-return leniency for
458
+ // callers — cast here to duck-type the real value.
459
+ const cbResult = /** @type {any} */ (cb.call(this._context || this));
460
+ // `cb` may be an async function (e.g. one that awaits a
461
+ // `$indexedDB` fetch); resume only once it settles, so `_dom` is
462
+ // restored in the right order.
463
+ if (cbResult && typeof cbResult.then === 'function') {
464
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
465
+ return cbResult.then(() => finishRoot());
466
+ }
445
467
  }
446
- this._dom = oldDOM;
447
-
448
- return this;
468
+ return finishRoot();
449
469
  }
450
470
 
451
471
  // Non-root elements
@@ -494,12 +514,20 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
494
514
  }
495
515
  }
496
516
 
517
+ const finish = () => {
518
+ this._dom = oldDOM;
519
+ return this;
520
+ };
497
521
  if (cb) {
498
- cb.call(this._context || this);
522
+ // `cb`'s declared return type is plain `void` — cast here to
523
+ // duck-type the real value; see the note above the other branch.
524
+ const cbResult = /** @type {any} */ (cb.call(this._context || this));
525
+ if (cbResult && typeof cbResult.then === 'function') {
526
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
527
+ return cbResult.then(() => finish());
528
+ }
499
529
  }
500
- this._dom = oldDOM;
501
-
502
- return this;
530
+ return finish();
503
531
  }
504
532
 
505
533
  /**
@@ -11,25 +11,33 @@ function _makeDatasetAttribute (n0) {
11
11
  }
12
12
 
13
13
  /**
14
- * @callback ObjectCallback
15
- * @this {JSONJoiningTransformer}
16
- * @param {Record<string, unknown>} obj
17
- * @returns {void}
14
+ * A `@callback` tag combined with `@this` mis-emits in `dist/*.d.ts`
15
+ * (verified against `tsc`'s declaration output — the `@this` type lands as
16
+ * raw, unparsed JSDoc text rather than being converted to a proper `this:`
17
+ * function-type parameter); the equivalent inline `@typedef {(this: ...) =>
18
+ * ...}` function-type form (already used elsewhere, e.g. `TemplateFunction`
19
+ * in `index.js`) emits correctly, so these three use that form instead.
20
+ * @typedef {(
21
+ * this: JSONJoiningTransformer, obj: Record<string, unknown>
22
+ * ) => void} ObjectCallback
18
23
  */
19
24
  /**
20
- * @callback ArrayCallback
21
- * @this {JSONJoiningTransformer}
22
- * @param {any[]} arr
23
- * @returns {void}
25
+ * @typedef {(this: JSONJoiningTransformer, arr: any[]) => void} ArrayCallback
24
26
  */
25
27
  /**
26
28
  * @template [T = "json"]
27
- * @callback SimpleCallback
28
- * @this {T extends "json" ? JSONJoiningTransformer :
29
- * T extends "string" ? import('./StringJoiningTransformer.js').default
30
- * : import('./DOMJoiningTransformer.js').default}
31
- * @returns {void}
29
+ * @typedef {(
30
+ * this: T extends "json" ? JSONJoiningTransformer :
31
+ * T extends "string" ? import('./StringJoiningTransformer.js').default
32
+ * : import('./DOMJoiningTransformer.js').default
33
+ * ) => void} SimpleCallback
32
34
  */
35
+ // Kept as plain `void` (not `void|Promise<void>`) rather than a union:
36
+ // TypeScript's "a void-returning callback parameter accepts any actual
37
+ // return value" leniency — relied on throughout the test suite for
38
+ // concise-body arrows like `() => this.text(...)` — only applies to a
39
+ // literal `void` return type, not a union. An async callback (returning
40
+ // `Promise<void>`) is still assignable here for the same reason.
33
41
 
34
42
  /**
35
43
  * Attributes object for element() allowing standard string attributes
@@ -392,9 +400,11 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
392
400
  * @param {ElementAttributes|any[]|SimpleCallback} [atts]
393
401
  * Attrs, children, or cb.
394
402
  * @param {any[]|SimpleCallback} [childNodes] Children or cb.
395
- * @param {SimpleCallback} [cb] Builder callback.
403
+ * @param {SimpleCallback} [cb] Builder callback; may be async (e.g. to
404
+ * `await` a `$indexedDB` fetch), in which case `element()` itself
405
+ * returns a `Promise` instead of `this`.
396
406
  * @param {string[]} [useAttributeSets] - Attribute set names to apply
397
- * @returns {JSONJoiningTransformer}
407
+ * @returns {JSONJoiningTransformer|Promise<JSONJoiningTransformer>}
398
408
  */
399
409
  element (elem, atts, childNodes, cb, useAttributeSets) {
400
410
  this._requireSameChildren('json', 'element');
@@ -485,77 +495,96 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
485
495
  }));
486
496
  }
487
497
 
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
- };
498
+ // Everything from here on depends on `attsObj`/`jmlChildren` having
499
+ // been fully populated by `cb` (when given) — deferred into a
500
+ // continuation so an async `cb` (e.g. one that awaits a `$indexedDB`
501
+ // fetch) can be awaited first, keeping output correctly ordered.
502
+ const finishElement = () => {
503
+ // Build Jamilih array
504
+ /** @type {any[]} */
505
+ const jmlEl = [elementName];
506
+ if (Object.keys(attsObj).length) {
507
+ jmlEl.push(attsObj, jmlChildren);
508
+ } else {
509
+ jmlEl.push(jmlChildren);
534
510
  }
535
- /* c8 ignore stop */
536
511
 
537
- const doc = {$document: {
538
- ...(xmlDeclaration ? {xmlDeclaration} : {}),
539
- childNodes: [
540
- ...(method === 'xml' || method === 'xhtml' ? dtd : []),
541
- jmlEl
542
- ]
543
- }};
512
+ if (isRoot) {
513
+ // todo: indent, cdataSectionElements
514
+ const {
515
+ omitXmlDeclaration, bareDoctype, doctypePublic, doctypeSystem, method
516
+ } = this._outputConfig ?? {};
517
+
518
+ const dtd = bareDoctype !== false
519
+ ? [{$DOCTYPE: {
520
+ name: elementName,
521
+ publicId: doctypePublic ?? null, // Public ID (optional)
522
+ systemId: doctypeSystem ?? null // System ID (optional)
523
+ }}]
524
+ : [];
525
+
526
+ let xmlDeclaration;
527
+ /* c8 ignore start -- third OR condition short-circuits */
528
+ if (!omitXmlDeclaration && (
529
+ method === 'xml' || method === 'xhtml' ||
530
+ omitXmlDeclaration === false)
531
+ ) {
532
+ const {version, encoding, standalone} = this._outputConfig ?? {};
533
+
534
+ xmlDeclaration = {
535
+ version,
536
+ encoding,
537
+ standalone
538
+ };
539
+ }
540
+ /* c8 ignore stop */
541
+
542
+ const doc = {$document: {
543
+ ...(xmlDeclaration ? {xmlDeclaration} : {}),
544
+ childNodes: [
545
+ ...(method === 'xml' || method === 'xhtml' ? dtd : []),
546
+ jmlEl
547
+ ]
548
+ }};
549
+
550
+ // Removed this._doc; use this._docs only
551
+ if (this._cfg.exposeDocuments) {
552
+ this._docs.push(doc);
553
+ }
554
+ }
544
555
 
545
- // Removed this._doc; use this._docs only
546
- if (this._cfg.exposeDocuments) {
547
- this._docs.push(doc);
556
+ // If inside a parent element, append as its child; otherwise
557
+ // append to root
558
+ if (this._elementStack.length) {
559
+ const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
560
+ top.jmlChildren.push(jmlEl);
561
+ } else {
562
+ this.append(jmlEl);
548
563
  }
549
- }
564
+ return this;
565
+ };
550
566
 
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);
567
+ // Callback-driven building (attribute/text/nested element mutate stack)
568
+ if (cb) {
569
+ // Push current state onto a stack
570
+ this._elementStack.push({attsObj, jmlChildren});
571
+ // `cb`'s declared return type is plain `void` (see `SimpleCallback`)
572
+ // — cast here to duck-type the real value.
573
+ const cbResult = /** @type {any} */ (cb.call(this._context || this));
574
+ const finishWithStack = () => {
575
+ const state = /** @type {ElementInfo} */ (this._elementStack.pop());
576
+ ({attsObj} = state);
577
+ // Children may have been mutated by nested element()/text();
578
+ // already in jmlChildren
579
+ return finishElement();
580
+ };
581
+ if (cbResult && typeof cbResult.then === 'function') {
582
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
583
+ return cbResult.then(() => finishWithStack());
584
+ }
585
+ return finishWithStack();
557
586
  }
558
- return this;
587
+ return finishElement();
559
588
  }
560
589
 
561
590
  /**
@@ -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') {