jtlt 0.13.0 → 0.14.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.
Files changed (38) hide show
  1. package/CHANGES.md +10 -0
  2. package/README.md +99 -85
  3. package/demo/calltemplate-params-demo.js +13 -22
  4. package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +17 -17
  5. package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +60 -61
  6. package/demo/vendor/jhtml/src/jhtml-browser.js +1 -0
  7. package/demo/vendor/jhtml/src/jhtml-node.js +1 -0
  8. package/demo/vendor/jhtml/src/jhtml.js +18 -15
  9. package/dist/AbstractJoiningTransformer.d.ts +5 -5
  10. package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
  11. package/dist/JSONPathTransformer.d.ts +13 -12
  12. package/dist/JSONPathTransformer.d.ts.map +1 -1
  13. package/dist/JSONPathTransformerContext.d.ts +50 -20
  14. package/dist/JSONPathTransformerContext.d.ts.map +1 -1
  15. package/dist/XPathTransformer.d.ts +9 -2
  16. package/dist/XPathTransformer.d.ts.map +1 -1
  17. package/dist/XPathTransformerContext.d.ts +46 -9
  18. package/dist/XPathTransformerContext.d.ts.map +1 -1
  19. package/dist/index.d.ts +104 -10
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/indexedDB.d.ts +107 -0
  22. package/dist/indexedDB.d.ts.map +1 -0
  23. package/dist/maybeAsync.d.ts +15 -0
  24. package/dist/maybeAsync.d.ts.map +1 -0
  25. package/docs/API.expanded.md +70 -20
  26. package/docs/API.md +34 -4
  27. package/docs/TO-DO.md +2 -2
  28. package/eslint.config.js +11 -3
  29. package/package.json +7 -5
  30. package/pnpm-workspace.yaml +9 -0
  31. package/src/AbstractJoiningTransformer.js +3 -3
  32. package/src/JSONPathTransformer.js +45 -7
  33. package/src/JSONPathTransformerContext.js +157 -22
  34. package/src/XPathTransformer.js +33 -1
  35. package/src/XPathTransformerContext.js +136 -16
  36. package/src/index.js +85 -8
  37. package/src/indexedDB.js +530 -0
  38. package/src/maybeAsync.js +44 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jtlt",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "author": "Brett Zamir",
6
6
  "contributors": [],
@@ -33,6 +33,7 @@
33
33
  "@lezer/highlight": "^1.2.3",
34
34
  "codemirror": "^6.0.2",
35
35
  "fontoxpath": "^3.34.0",
36
+ "idb": "^8.0.3",
36
37
  "jamilih": "0.69.0",
37
38
  "jhtml": "0.7.3",
38
39
  "jsdom": "30.0.1",
@@ -50,14 +51,15 @@
50
51
  "@types/chai": "^5.2.3",
51
52
  "@types/jsdom": "^30.0.0",
52
53
  "@types/mocha": "^10.0.10",
53
- "baseline-browser-mapping": "^2.11.19",
54
+ "baseline-browser-mapping": "^2.11.20",
54
55
  "c8": "^12.0.0",
55
56
  "chai": "^6.2.2",
56
57
  "eslint": "^10.9.1",
57
- "eslint-config-ash-nazg": "^42.6.0",
58
+ "eslint-config-ash-nazg": "^43.1.0",
59
+ "indexeddbshim": "^17.4.3",
58
60
  "mocha": "^11.8.0",
59
- "open": "^11.0.1",
60
- "rollup": "^4.63.0",
61
+ "open": "^11.0.2",
62
+ "rollup": "^4.63.1",
61
63
  "typescript": "^7.0.2"
62
64
  },
63
65
  "bugs": "https://github.com/brettz9/jtlt/issues",
@@ -1,6 +1,9 @@
1
1
  allowBuilds:
2
+ better-sqlite3: true
3
+ canvas: true
2
4
  is-hidden-file: true
3
5
  libxmljs: true
6
+ sqlite3: true
4
7
  unrs-resolver: true
5
8
  minimumReleaseAgeExclude:
6
9
  - '@rollup/rollup-android-arm-eabi@4.63.0'
@@ -30,3 +33,9 @@ minimumReleaseAgeExclude:
30
33
  - '@rollup/rollup-win32-x64-msvc@4.63.0'
31
34
  - rollup@4.63.0
32
35
  - jhtml@0.7.2 || 0.7.3
36
+ - eslint-config-ash-nazg@43.1.0
37
+ - eslint-plugin-escompat@3.12.0
38
+ - eslint-plugin-jsdoc@64.3.0
39
+ - indexeddbshim@17.4.1 || 17.4.2 || 17.4.3
40
+ - open@11.0.2
41
+ - powershell-utils@0.2.1
@@ -258,8 +258,8 @@ class AbstractJoiningTransformer {
258
258
  /**
259
259
  * Invoke a registered stylesheet function with positional arguments.
260
260
  * @param {string} name - Function name (with namespace)
261
- * @param {any[]} args - Positional arguments
262
- * @returns {any} Function return value
261
+ * @param {unknown[]} args - Positional arguments
262
+ * @returns {unknown} Function return value
263
263
  */
264
264
  invokeFunctionByArity (name, args = []) {
265
265
  const arity = args.length;
@@ -436,7 +436,7 @@ class AbstractJoiningTransformer {
436
436
 
437
437
  /**
438
438
  * @param {string} prop - Configuration property name
439
- * @param {any} val - Configuration property value
439
+ * @param {unknown} val - Configuration property value
440
440
  * @param {(this: AbstractJoiningTransformer<T>) => void} [cb]
441
441
  * Optional callback invoked with this instance
442
442
  * @returns {void}
@@ -16,7 +16,7 @@ class JSONPathTransformer {
16
16
  /**
17
17
  * @template {"json"|"string"|"dom"} U
18
18
  * @this {JSONPathTransformerContext<U>}
19
- * @param {any} value - Value
19
+ * @param {unknown} value - Value
20
20
  * @param {{mode?: string}} cfg - Configuration
21
21
  * @returns {void}
22
22
  */
@@ -26,8 +26,8 @@ class JSONPathTransformer {
26
26
  },
27
27
  transformPropertyNames: {
28
28
  /**
29
- * @param {any} value - Current context value
30
- * @returns {any}
29
+ * @param {unknown} value - Current context value
30
+ * @returns {unknown}
31
31
  */
32
32
  template (value) {
33
33
  // Emit property names for the current object context
@@ -40,7 +40,7 @@ class JSONPathTransformer {
40
40
  transformObjects: {
41
41
  /**
42
42
  * @this {JSONPathTransformerContext}
43
- * @param {any} value - Value
43
+ * @param {unknown} value - Value
44
44
  * @param {{mode?: string}} cfg - Configuration
45
45
  * @returns {void}
46
46
  */
@@ -51,7 +51,7 @@ class JSONPathTransformer {
51
51
  transformArrays: {
52
52
  /**
53
53
  * @this {JSONPathTransformerContext}
54
- * @param {any} value - Value
54
+ * @param {unknown} value - Value
55
55
  * @param {{mode?: string}} cfg - Configuration
56
56
  * @returns {void}
57
57
  */
@@ -70,8 +70,9 @@ class JSONPathTransformer {
70
70
  },
71
71
  transformFunctions: {
72
72
  /**
73
- * @param {( ...args: any[]) => any} value - Function at current context
74
- * @returns {any}
73
+ * @param {(...args: unknown[]) => unknown} value - Function at current
74
+ * context
75
+ * @returns {unknown}
75
76
  */
76
77
  template (value) {
77
78
  // Call the function and return its result
@@ -163,9 +164,46 @@ class JSONPathTransformer {
163
164
  }
164
165
  // Set up parameter context for valueOf() access in root template
165
166
  jte._params = {0: jte._contextObj};
167
+ /**
168
+ * The template may return a value synchronously or a Promise (e.g. from
169
+ * `await this.indexedDB(...)`), which is awaited unless `config.sync`.
170
+ * @type {any}
171
+ */
166
172
  const ret = /** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
167
173
  templateObj
168
174
  ).template.call(jte, undefined, {mode});
175
+
176
+ if (ret !== null && typeof ret !== 'undefined' &&
177
+ typeof ret.then === 'function') {
178
+ if (this._config.sync) {
179
+ throw new Error(
180
+ 'A template returned a Promise but JTLT is configured with ' +
181
+ '`sync: true`.'
182
+ );
183
+ }
184
+ return /** @type {any} */ (
185
+ // eslint-disable-next-line @stylistic/max-len -- Long
186
+ // eslint-disable-next-line promise/prefer-await-to-then -- intentional dynamic sync/async
187
+ ret.then((/** @type {any} */ resolvedRet) => {
188
+ if (typeof resolvedRet !== 'undefined') {
189
+ const joiner = jte._getJoiningTransformer();
190
+ if (typeof resolvedRet === 'string' ||
191
+ (typeof resolvedRet === 'object' && resolvedRet !== null &&
192
+ 'nodeType' in resolvedRet)) {
193
+ joiner.append(/** @type {string|Node} */ (resolvedRet));
194
+ } else {
195
+ /** @type {import('./JSONJoiningTransformer.js').default} */ (
196
+ joiner
197
+ ).append(resolvedRet);
198
+ }
199
+ }
200
+ return /** @type {import('./index.js').ResultType<T>} */ (
201
+ jte.getOutput()
202
+ );
203
+ })
204
+ );
205
+ }
206
+
169
207
  if (typeof ret !== 'undefined') {
170
208
  // Will vary by jte._config.outputType
171
209
  // After the undefined check, ret is ResultType<T>
@@ -1,5 +1,9 @@
1
1
  import {JSONPath as jsonpath} from 'jsonpath-plus';
2
2
  import JSONPathTransformer from './JSONPathTransformer.js';
3
+ import {maybeAsyncLoop} from './maybeAsync.js';
4
+ import {
5
+ parseIndexedDBExpression, queryIndexedDB, resolveIndexedDBQuery
6
+ } from './indexedDB.js';
3
7
 
4
8
  /**
5
9
  * @param {string} string
@@ -52,7 +56,7 @@ const escapeRegexReplacement = (string) => {
52
56
  * ctx: JSONPathTransformerContext
53
57
  * ) => number} SortComparator
54
58
  * @typedef {string | SortObject | SortComparator |
55
- * Array<string|SortObject>} SortSpec
59
+ * Array<string|SortObject> | null} SortSpec
56
60
  */
57
61
 
58
62
  /**
@@ -88,6 +92,8 @@ const escapeRegexReplacement = (string) => {
88
92
  * @property {JoiningTransformerMap[T]} joiningTransformer - Joining transformer
89
93
  * @property {boolean} [preventEval] - Whether to prevent eval in
90
94
  * JSONPath
95
+ * @property {boolean} [sync] - When true, throw if a template returns a
96
+ * Promise instead of awaiting it (disables `indexedDB()`)
91
97
  * @property {(path: string) => number} [specificityPriorityResolver]
92
98
  * Priority resolver function
93
99
  * @property {import('./index.js').JSONPathTemplateObject<T>[]|
@@ -195,7 +201,7 @@ class JSONPathTransformerContext {
195
201
 
196
202
  /**
197
203
  * Gets the current output.
198
- * @returns {any} The output from the joining transformer
204
+ * @returns {unknown} The output from the joining transformer
199
205
  */
200
206
  getOutput () {
201
207
  return this._getJoiningTransformer().get();
@@ -221,7 +227,7 @@ class JSONPathTransformerContext {
221
227
  }
222
228
 
223
229
  /**
224
- * @param {any} v - Value to set
230
+ * @param {unknown} v - Value to set
225
231
  * @returns {this}
226
232
  */
227
233
  set (v) {
@@ -403,7 +409,7 @@ class JSONPathTransformerContext {
403
409
  const prevCurrPath = that._currPath;
404
410
 
405
411
  // Process in (sorted) order
406
- for (const o of matches) {
412
+ const loopResult = maybeAsyncLoop(matches, (o) => {
407
413
  const {value, parent, parentProperty, path} = o;
408
414
  const _oldPath = that._currPath;
409
415
  that._currPath += path.replace(/^\$/v, '');
@@ -454,17 +460,19 @@ class JSONPathTransformerContext {
454
460
  }
455
461
  if (onNoMatch === 'deep-skip') {
456
462
  // Skip this node and its descendants entirely
457
- continue;
463
+ return;
458
464
  }
459
465
  if (onNoMatch === 'shallow-copy') {
460
466
  // Output the value as-is without processing children
461
467
  joiner.append(value);
462
- continue;
468
+ that._currPath = _oldPath;
469
+ return;
463
470
  }
464
471
  if (onNoMatch === 'deep-copy') {
465
472
  // Output the value and all descendants as-is
466
473
  joiner.append(JSON.stringify(value));
467
- continue;
474
+ that._currPath = _oldPath;
475
+ return;
468
476
  }
469
477
  if (onNoMatch === 'text-only-copy') {
470
478
  // Output only text content (primitives)
@@ -472,7 +480,8 @@ class JSONPathTransformerContext {
472
480
  typeof value === 'boolean') {
473
481
  joiner.append(String(value));
474
482
  }
475
- continue;
483
+ that._currPath = _oldPath;
484
+ return;
476
485
  }
477
486
  // 'apply-templates', 'shallow-skip', or other:
478
487
  // use default template rules
@@ -573,6 +582,11 @@ class JSONPathTransformerContext {
573
582
  const prevTemplateParams = that._params;
574
583
  that._params = {0: value};
575
584
 
585
+ /**
586
+ * The template may return synchronously or return a Promise (e.g. from
587
+ * `await this.indexedDB(...)`), which is awaited unless `config.sync`.
588
+ * @type {any}
589
+ */
576
590
  const ret =
577
591
  /** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
578
592
  templateObj
@@ -582,6 +596,42 @@ class JSONPathTransformerContext {
582
596
 
583
597
  // Restore previous parameter context
584
598
  that._params = prevTemplateParams;
599
+ if (ret !== null && typeof ret !== 'undefined' &&
600
+ typeof ret.then === 'function') {
601
+ if (that._config.sync) {
602
+ throw new Error(
603
+ 'A template returned a Promise but JTLT is configured with ' +
604
+ '`sync: true`.'
605
+ );
606
+ }
607
+ // The loop body deliberately mixes value and no-value returns so
608
+ // `maybeAsyncLoop` can stay synchronous unless a template awaits.
609
+ // eslint-disable-next-line @stylistic/max-len -- Long
610
+ // eslint-disable-next-line promise/prefer-await-to-then, consistent-return -- intentional dynamic sync/async
611
+ return ret.then((/** @type {any} */ resolvedRet) => {
612
+ that._params = prevTemplateParams;
613
+ if (typeof resolvedRet !== 'undefined') {
614
+ const joiner = that._getJoiningTransformer();
615
+ /* c8 ignore start -- _openTagState only on
616
+ StringJoiningTransformer; string-output defensive check */
617
+ // @ts-expect-error -- _openTagState: StringJoiningTransformer only
618
+ if (joiner._openTagState) {
619
+ joiner.append('>');
620
+ // @ts-expect-error -- _openTagState: StringJoiningTransformer
621
+ joiner._openTagState = false;
622
+ }
623
+ /* c8 ignore stop */
624
+ joiner.append(resolvedRet);
625
+ }
626
+ that._parent = parent;
627
+ // Matches reached here always carry a parentProperty; the sync
628
+ // path covers the root-node fallback.
629
+ /* c8 ignore next */
630
+ that._parentProperty = (parentProperty ?? that._parentProperty);
631
+ that._currPath = _oldPath;
632
+ return undefined;
633
+ });
634
+ }
585
635
  if (typeof ret !== 'undefined') {
586
636
  // After the undefined check, ret is ResultType<T>
587
637
  const joiner = that._getJoiningTransformer();
@@ -604,12 +654,27 @@ class JSONPathTransformerContext {
604
654
  that._parent = parent;
605
655
  that._parentProperty = (parentProperty ?? that._parentProperty);
606
656
  that._currPath = _oldPath;
657
+ });
658
+
659
+ if (typeof loopResult?.then === 'function') {
660
+ // Returns a Promise<this> when a nested template ran asynchronously.
661
+ return /** @type {any} */ (
662
+ // eslint-disable-next-line @stylistic/max-len -- Long
663
+ // eslint-disable-next-line promise/prefer-await-to-then -- intentional dynamic sync/async
664
+ loopResult.then(() => {
665
+ this._contextObj = prevContext;
666
+ this._parent = prevParent;
667
+ this._parentProperty = prevParentProp;
668
+ this._currPath = prevCurrPath;
669
+ return this;
670
+ })
671
+ );
607
672
  }
608
- // Restore outer context
609
- that._contextObj = prevContext;
610
- that._parent = prevParent;
611
- that._parentProperty = prevParentProp;
612
- that._currPath = prevCurrPath;
673
+
674
+ this._contextObj = prevContext;
675
+ this._parent = prevParent;
676
+ this._parentProperty = prevParentProp;
677
+ this._currPath = prevCurrPath;
613
678
  return this;
614
679
  }
615
680
 
@@ -819,9 +884,12 @@ class JSONPathTransformerContext {
819
884
  * expression matches
820
885
  * @param {string} [options.groupEndingWith] - Ends group when expression
821
886
  * matches
822
- * @param {any} [options.sort] - Sort specification (same as forEach)
887
+ * @param {SortSpec<V>} [options.sort] - Sort specification (same as forEach)
823
888
  * @param {(
824
- * this: JSONPathTransformerContext<T>, key: any, items: any[], ctx: any
889
+ * this: JSONPathTransformerContext<T>,
890
+ * key: unknown,
891
+ * items: unknown[],
892
+ * ctx: JSONPathTransformerContext<T>
825
893
  * ) => void} cb - Callback receives (groupingKey, groupItems, context)
826
894
  * @returns {this}
827
895
  */
@@ -1135,7 +1203,7 @@ class JSONPathTransformerContext {
1135
1203
 
1136
1204
  /**
1137
1205
  * Returns the current group (for use within forEachGroup callback).
1138
- * @returns {any[]|undefined}
1206
+ * @returns {unknown[]|undefined}
1139
1207
  */
1140
1208
  currentGroup () {
1141
1209
  return /** @type {any} */ (this)._currentGroup;
@@ -1143,12 +1211,48 @@ class JSONPathTransformerContext {
1143
1211
 
1144
1212
  /**
1145
1213
  * Returns the current grouping key (for use within forEachGroup callback).
1146
- * @returns {any}
1214
+ * @returns {unknown}
1147
1215
  */
1148
1216
  currentGroupingKey () {
1149
1217
  return /** @type {any} */ (this)._currentGroupingKey;
1150
1218
  }
1151
1219
 
1220
+ /**
1221
+ * Directly query IndexedDB from within a template, e.g.
1222
+ * `await this.indexedDB('myDB', 'myStore', {index: 'byAge'})`.
1223
+ *
1224
+ * Since IndexedDB access is asynchronous, this is unavailable when JTLT is
1225
+ * configured with `sync: true`.
1226
+ * @param {string} dbName - Database name
1227
+ * @param {string} storeName - Object store name
1228
+ * @param {import('./indexedDB.js').QueryOptions} [options] - Query options
1229
+ * @returns {Promise<unknown[]>} The matching records
1230
+ */
1231
+ indexedDB (dbName, storeName, options) {
1232
+ if (this._config.sync) {
1233
+ throw new Error(
1234
+ 'The `indexedDB()` API is unavailable when JTLT is configured with ' +
1235
+ '`sync: true`.'
1236
+ );
1237
+ }
1238
+ return queryIndexedDB(dbName, storeName, options);
1239
+ }
1240
+
1241
+ /**
1242
+ * Await a parsed `indexedDB(...)` expression and append its (stringified)
1243
+ * value to the output. Used by {@link valueOf}. Callers reject `config.sync`.
1244
+ * @param {import('./indexedDB.js').ParsedIndexedDBExpression} parsed
1245
+ * @param {any} results - The joining transformer
1246
+ * @returns {Promise<this>}
1247
+ */
1248
+ async _appendIndexedDBValue (parsed, results) {
1249
+ const value = await resolveIndexedDBQuery(parsed, {
1250
+ preventEval: this._config.preventEval
1251
+ });
1252
+ results.text(String(value));
1253
+ return this;
1254
+ }
1255
+
1152
1256
  /**
1153
1257
  * @param {string|object} [select] - JSONPath selector
1154
1258
  * @returns {this}
@@ -1159,6 +1263,26 @@ class JSONPathTransformerContext {
1159
1263
  const results = this._getJoiningTransformer();
1160
1264
  let result;
1161
1265
 
1266
+ const selectString = select && typeof select === 'object'
1267
+ ? /** @type {{select?: string}} */ (select).select
1268
+ : select;
1269
+
1270
+ // Intercept `indexedDB('db', 'store')<trailing JSONPath>` expressions and
1271
+ // resolve them asynchronously before appending. Returns a Promise so
1272
+ // templates can `await this.valueOf(...)`.
1273
+ if (typeof selectString === 'string') {
1274
+ const parsed = parseIndexedDBExpression(selectString);
1275
+ if (parsed) {
1276
+ if (this._config.sync) {
1277
+ throw new Error(
1278
+ 'The `indexedDB()` function is unavailable when JTLT is ' +
1279
+ 'configured with `sync: true`.'
1280
+ );
1281
+ }
1282
+ return /** @type {any} */ (this._appendIndexedDBValue(parsed, results));
1283
+ }
1284
+ }
1285
+
1162
1286
  if (select && typeof select === 'object' &&
1163
1287
  /** @type {{select?: string}} */ (select).select === '.') {
1164
1288
  result = this._contextObj;
@@ -2079,8 +2203,8 @@ class JSONPathTransformerContext {
2079
2203
  /**
2080
2204
  * Invoke a registered stylesheet function with positional arguments.
2081
2205
  * @param {string} name - Function name (with namespace)
2082
- * @param {any[]} args - Positional arguments
2083
- * @returns {any} Function return value
2206
+ * @param {unknown[]} args - Positional arguments
2207
+ * @returns {unknown} Function return value
2084
2208
  */
2085
2209
  invokeFunctionByArity (name, args = []) {
2086
2210
  return this._getJoiningTransformer().invokeFunctionByArity(name, args);
@@ -2233,12 +2357,23 @@ class JSONPathTransformerContext {
2233
2357
 
2234
2358
  /**
2235
2359
  * @param {string} name - Key name
2236
- * @param {any} value - Value to match
2237
- * @returns {any}
2360
+ * @param {unknown} value - Value to match
2361
+ * @returns {unknown}
2238
2362
  */
2239
2363
  getKey (name, value) {
2240
2364
  const key = this.keys[name];
2241
- const matches = this.get(key.match, true);
2365
+ // Keys are document-global (like `xsl:key`): resolve `match` against the
2366
+ // root, not the current context (which may be a `forEach`/`applyTemplates`
2367
+ // item).
2368
+ const matches = /** @type {any[]} */ (
2369
+ (/** @type {any} */ (jsonpath))({
2370
+ path: JSONPathTransformer.makeJSONPathAbsolute(key.match),
2371
+ json: this._origObj,
2372
+ preventEval: this._config.preventEval,
2373
+ wrap: true,
2374
+ returnType: 'value'
2375
+ })
2376
+ );
2242
2377
  for (const match of matches) { // For objects or arrays
2243
2378
  if (match && typeof match === 'object' &&
2244
2379
  match[key.use] === value) {
@@ -7,6 +7,8 @@ import XPathTransformerContext from './XPathTransformerContext.js';
7
7
  * @property {import('./index.js').
8
8
  * XPathTemplateArray<T>} templates Template objects
9
9
  * @property {number} [xpathVersion] XPath version (1|2|3.1)
10
+ * @property {boolean} [sync] When true, throw if a template returns a Promise
11
+ * instead of awaiting it (disables `indexedDB()` and other async features)
10
12
  */
11
13
 
12
14
  /**
@@ -21,7 +23,7 @@ class XPathTransformer {
21
23
  static DefaultTemplateRules = {
22
24
  transformRoot: {
23
25
  /**
24
- * @param {any} node Node
26
+ * @param {unknown} node Node
25
27
  * @param {{mode:string}} cfg Config
26
28
  * @returns {void}
27
29
  */
@@ -106,7 +108,37 @@ class XPathTransformer {
106
108
  }
107
109
  // Set up parameter context for valueOf() access in root template
108
110
  xte._params = {0: xte._contextNode};
111
+ /**
112
+ * The template may return synchronously or return a Promise (e.g. from
113
+ * `await this.indexedDB(...)`), which is awaited unless `config.sync`.
114
+ * @type {any}
115
+ */
109
116
  const ret = templateObj.template.call(xte, undefined, {mode});
117
+
118
+ if (ret !== null && typeof ret !== 'undefined' &&
119
+ typeof ret.then === 'function') {
120
+ if (this._config.sync) {
121
+ throw new Error(
122
+ 'A template returned a Promise but JTLT is configured with ' +
123
+ '`sync: true`.'
124
+ );
125
+ }
126
+ return /** @type {any} */ (
127
+ // eslint-disable-next-line @stylistic/max-len -- Long
128
+ // eslint-disable-next-line promise/prefer-await-to-then -- intentional dynamic sync/async
129
+ ret.then((/** @type {any} */ resolvedRet) => {
130
+ if (typeof resolvedRet !== 'undefined') {
131
+ /** @type {any} */ (xte)._getJoiningTransformer().append(
132
+ resolvedRet
133
+ );
134
+ }
135
+ return /** @type {import('./index.js').ResultType<T>} */ (
136
+ xte.getOutput()
137
+ );
138
+ })
139
+ );
140
+ }
141
+
110
142
  if (typeof ret !== 'undefined') {
111
143
  /** @type {any} */ (xte)._getJoiningTransformer().append(ret);
112
144
  }