jtlt 0.18.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.
@@ -99,6 +99,10 @@ const escapeRegexReplacement = (string) => {
99
99
  * Priority resolver function
100
100
  * @property {import('./index.js').JSONPathTemplateObject<T>[]|
101
101
  * import('./index.js').JSONPathTemplateArray<T>[]} [templates]
102
+ * @property {'json'|'javascript'} [defaultTemplateFormat] Config-wide
103
+ * default for the jamilih validation strictness `compileJSONTemplate`
104
+ * applies to a declarative (Array) `template`, used when an entry has no
105
+ * `format` of its own
102
106
  * @property {Record<string, unknown>} [params] Runtime parameter values
103
107
  * (like an XSLT processor's stylesheet parameters); a `param()` with a
104
108
  * matching name uses this value instead of its declared default
@@ -609,26 +613,40 @@ class JSONPathTransformerContext {
609
613
  that._parent = parent;
610
614
  that._parentProperty = (parentProperty ?? that._parentProperty);
611
615
 
612
- // Set up parameter context for valueOf() access in templates
616
+ // Set up parameter context for valueOf() access in templates. `vars`
617
+ // is reset too — matching XSLT's `xsl:variable`/`xsl:param` scoping,
618
+ // a `variable()` set in the calling template must not leak into (or
619
+ // be leaked into by) a separately applied/matched template.
613
620
  const prevTemplateParams = that._params;
621
+ const prevTemplateVars = that.vars;
614
622
  that._params = {0: value, ...appliedParams};
615
-
623
+ that.vars = {};
624
+
625
+ // `template` is always a function here: a declarative (jamilih-shaped)
626
+ // Array is compiled to one up front, by JSONPathTransformer's
627
+ // constructor.
628
+ const {template: matchedTemplateFn} =
629
+ /**
630
+ * @type {import('./index.js').JSONPathTemplateObject<T> &
631
+ * {template: import('./index.js').
632
+ * TemplateFunction<T, "json", JSONPathTransformerContext<T>>}}
633
+ */ (
634
+ templateObj
635
+ );
616
636
  /**
617
637
  * The template may return synchronously or return a Promise (e.g. from
618
638
  * `await this.indexedDB(...)`), which is awaited unless `config.sync`.
619
639
  * @type {any}
620
640
  */
621
- const ret =
622
- /** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
623
- templateObj
624
- ).template.call(
625
- // `this` carries runtime `extensions`; a consumer's
626
- // `ContextExtensions` augmentation would otherwise reject `that`.
627
- /** @type {any} */ (that), value, {mode, parent, parentProperty}
628
- );
641
+ const ret = matchedTemplateFn.call(
642
+ // `this` carries runtime `extensions`; a consumer's
643
+ // `ContextExtensions` augmentation would otherwise reject `that`.
644
+ /** @type {any} */ (that), value, {mode, parent, parentProperty}
645
+ );
629
646
 
630
- // Restore previous parameter context
647
+ // Restore previous parameter/variable context
631
648
  that._params = prevTemplateParams;
649
+ that.vars = prevTemplateVars;
632
650
  if (ret !== null && typeof ret !== 'undefined' &&
633
651
  typeof ret.then === 'function') {
634
652
  if (that._config.sync) {
@@ -643,6 +661,7 @@ class JSONPathTransformerContext {
643
661
  // eslint-disable-next-line promise/prefer-await-to-then, consistent-return -- intentional dynamic sync/async
644
662
  return ret.then((/** @type {any} */ resolvedRet) => {
645
663
  that._params = prevTemplateParams;
664
+ that.vars = prevTemplateVars;
646
665
  if (typeof resolvedRet !== 'undefined') {
647
666
  const joiner = that._getJoiningTransformer();
648
667
  /* c8 ignore start -- _openTagState only on
@@ -726,11 +745,16 @@ class JSONPathTransformerContext {
726
745
  }
727
746
  withParams ||= [];
728
747
 
729
- // Store parameters in a temporary context for valueOf() access
748
+ // Store parameters in a temporary context for valueOf() access. `vars`
749
+ // is reset too, matching XSLT's `xsl:variable`/`xsl:param` scoping: a
750
+ // called template gets a fresh scope, not the caller's `variable()`
751
+ // values, and its own don't leak back once it returns.
730
752
  const prevParams = this._params;
753
+ const prevVars = this.vars;
731
754
  /** @type {Record<string, any>} */
732
755
  const params = {};
733
756
  this._params = params;
757
+ this.vars = {};
734
758
 
735
759
  // Params staged via `this.withParam()` seed the set; explicit `withParam`
736
760
  // entries below override any of the same name (`xsl:with-param`).
@@ -759,7 +783,16 @@ class JSONPathTransformerContext {
759
783
  );
760
784
  }
761
785
 
762
- const result = templateObj.template.call(
786
+ // `template` is always a function here: a declarative (jamilih-shaped)
787
+ // Array is compiled to one up front, by JSONPathTransformer's
788
+ // constructor.
789
+ const {template: namedTemplateFn} =
790
+ /**
791
+ * @type {import('./index.js').JSONPathTemplateObject<T> &
792
+ * {template: import('./index.js').
793
+ * TemplateFunction<T, "json", JSONPathTransformerContext<T>>}}
794
+ */ (templateObj);
795
+ const result = namedTemplateFn.call(
763
796
  // `this` carries runtime `extensions`; a consumer's `ContextExtensions`
764
797
  // augmentation would otherwise reject the bare context.
765
798
  /** @type {any} */ (this), this._contextObj, {}
@@ -768,8 +801,9 @@ class JSONPathTransformerContext {
768
801
  /** @type {any} */ (results).append(result);
769
802
  }
770
803
 
771
- // Restore previous parameter context
804
+ // Restore previous parameter/variable context
772
805
  this._params = prevParams;
806
+ this.vars = prevVars;
773
807
 
774
808
  return this;
775
809
  }
@@ -781,21 +815,38 @@ class JSONPathTransformerContext {
781
815
  * @param {string} select - JSONPath selector
782
816
  * @param {(this: JSONPathTransformerContext<T>,
783
817
  * value: unknown
784
- * ) => void} cb - Callback function
818
+ * ) => void} cb - Callback function; may be async, in which case
819
+ * `forEach()` itself returns a `Promise<this>` instead of `this` from
820
+ * the iteration where that first happens onward. (Typed as plain
821
+ * `void`, not `void|Promise<void>` — see the note on `SimpleCallback`
822
+ * in JSONJoiningTransformer.js.)
785
823
  * @param {SortSpec<V>} [sort] - Sort spec
786
- * @returns {this}
824
+ * @returns {this|Promise<this>}
787
825
  */
788
826
  forEach (select, cb, sort) {
789
827
  // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
790
828
  const that = this;
829
+ // A bare `$name` (matching `if()`/`valueOf()`/comparisons' own
830
+ // convention — no trailing path) resolves against the param/var scope
831
+ // and iterates its value directly: jsonpath-plus has no notion of a
832
+ // named root to continue a path from (`$name[*]` is not `$.name[*]`),
833
+ // and XPath's data model treats a non-array value as a length-1
834
+ // sequence, so a scalar/object here becomes a single iteration.
835
+ const paramRef = select.trim().match(/^\$(?<name>[\w\-]+)$/v);
836
+ const param = paramRef && paramRef.groups
837
+ ? this._lookupParam(paramRef.groups.name)
838
+ : {has: false, value: undefined};
791
839
  /** @type {{value: any}[]} */
792
- const matches = /** @type {any} */ (jsonpath)({
793
- path: select,
794
- json: this._contextObj,
795
- preventEval: this._config.preventEval,
796
- wrap: true,
797
- resultType: 'all'
798
- });
840
+ const matches = param.has
841
+ ? (Array.isArray(param.value) ? param.value : [param.value]).
842
+ map((value) => ({value}))
843
+ : /** @type {any} */ (jsonpath)({
844
+ path: select,
845
+ json: this._contextObj,
846
+ preventEval: this._config.preventEval,
847
+ wrap: true,
848
+ resultType: 'all'
849
+ });
799
850
 
800
851
  /**
801
852
  * @param {string} expr
@@ -896,19 +947,70 @@ class JSONPathTransformerContext {
896
947
 
897
948
  const comparator = feBuildComparator(sort);
898
949
  const list = comparator ? [...matches].toSorted(comparator) : matches;
899
- for (const m of list) {
900
- // Set up parameter context for valueOf() access
950
+ for (const [idx, m] of list.entries()) {
951
+ // Set up parameter context for valueOf() access. `vars` gets a fresh
952
+ // scope per iteration too, matching `xsl:for-each`: a `variable()` set
953
+ // for one item must not leak into (or be seen by) the next.
901
954
  const prevParams = that._params;
902
955
  const prevContext = that._contextObj;
956
+ const prevVars = that.vars;
903
957
  that._params = {0: m.value};
904
958
  that._contextObj = m.value;
959
+ that.vars = {};
960
+ // `cb`'s declared return type is plain `void` (see the parameter's
961
+ // JSDoc) — cast here to duck-type the real value; see the note on
962
+ // `SimpleCallback` in JSONJoiningTransformer.js.
963
+ /** @type {any} */
964
+ let cbResult;
905
965
  try {
906
- cb.call(that, m.value);
907
- } finally {
908
- // Restore previous parameter context
966
+ cbResult = cb.call(that, m.value);
967
+ } catch (err) {
909
968
  that._params = prevParams;
910
969
  that._contextObj = prevContext;
970
+ that.vars = prevVars;
971
+ throw err;
911
972
  }
973
+ if (cbResult && typeof cbResult.then === 'function') {
974
+ // `cb` turned out to be async (e.g. it awaits a `$indexedDB`
975
+ // fetch): finish the remaining items sequentially, awaiting each,
976
+ // rather than the plain synchronous loop above — which is used for
977
+ // as long as every `cb` call keeps returning synchronously.
978
+ const remaining = list.slice(idx + 1);
979
+ // eslint-disable-next-line promise/prefer-await-to-then -- see above
980
+ return cbResult.then(async () => {
981
+ that._params = prevParams;
982
+ that._contextObj = prevContext;
983
+ that.vars = prevVars;
984
+ for (const next of remaining) {
985
+ const pp = that._params;
986
+ const pc = that._contextObj;
987
+ const pv = that.vars;
988
+ that._params = {0: next.value};
989
+ that._contextObj = next.value;
990
+ that.vars = {};
991
+ try {
992
+ // eslint-disable-next-line no-await-in-loop -- Sequential order
993
+ await cb.call(that, next.value);
994
+ // eslint-disable-next-line promise/always-return -- see above
995
+ } finally {
996
+ that._params = pp;
997
+ that._contextObj = pc;
998
+ that.vars = pv;
999
+ }
1000
+ }
1001
+ return that;
1002
+ // eslint-disable-next-line promise/prefer-await-to-then -- see above
1003
+ }).catch((/** @type {any} */ err) => {
1004
+ that._params = prevParams;
1005
+ that._contextObj = prevContext;
1006
+ that.vars = prevVars;
1007
+ throw err;
1008
+ });
1009
+ }
1010
+ // Restore previous parameter/variable context
1011
+ that._params = prevParams;
1012
+ that._contextObj = prevContext;
1013
+ that.vars = prevVars;
912
1014
  }
913
1015
  return this;
914
1016
  }
@@ -1000,6 +1102,8 @@ class JSONPathTransformerContext {
1000
1102
  const actualKey = key === null && keyStr === 'null' ? undefined : key;
1001
1103
  const prevContext = this._contextObj;
1002
1104
  const prevParams = this._params;
1105
+ const prevVars = this.vars;
1106
+ this.vars = {};
1003
1107
  try {
1004
1108
  this._contextObj = items;
1005
1109
  // Provide currentGroup() and currentGroupingKey() via context
@@ -1009,6 +1113,7 @@ class JSONPathTransformerContext {
1009
1113
  } finally {
1010
1114
  this._contextObj = prevContext;
1011
1115
  this._params = prevParams;
1116
+ this.vars = prevVars;
1012
1117
  delete /** @type {any} */ (this)._currentGroup;
1013
1118
  delete /** @type {any} */ (this)._currentGroupingKey;
1014
1119
  }
@@ -1027,6 +1132,8 @@ class JSONPathTransformerContext {
1027
1132
  if (currentGroup.length > 0) {
1028
1133
  const prevContext = this._contextObj;
1029
1134
  const prevParams = this._params;
1135
+ const prevVars = this.vars;
1136
+ this.vars = {};
1030
1137
  try {
1031
1138
  this._contextObj = currentGroup;
1032
1139
  /** @type {any} */ (this)._currentGroup = currentGroup;
@@ -1041,6 +1148,7 @@ class JSONPathTransformerContext {
1041
1148
  } finally {
1042
1149
  this._contextObj = prevContext;
1043
1150
  this._params = prevParams;
1151
+ this.vars = prevVars;
1044
1152
  delete /** @type {any} */ (this)._currentGroup;
1045
1153
  delete /** @type {any} */ (this)._currentGroupingKey;
1046
1154
  }
@@ -1056,6 +1164,8 @@ class JSONPathTransformerContext {
1056
1164
  if (currentGroup.length > 0) {
1057
1165
  const prevContext = this._contextObj;
1058
1166
  const prevParams = this._params;
1167
+ const prevVars = this.vars;
1168
+ this.vars = {};
1059
1169
  try {
1060
1170
  this._contextObj = currentGroup;
1061
1171
  /** @type {any} */ (this)._currentGroup = currentGroup;
@@ -1070,6 +1180,7 @@ class JSONPathTransformerContext {
1070
1180
  } finally {
1071
1181
  this._contextObj = prevContext;
1072
1182
  this._params = prevParams;
1183
+ this.vars = prevVars;
1073
1184
  delete /** @type {any} */ (this)._currentGroup;
1074
1185
  delete /** @type {any} */ (this)._currentGroupingKey;
1075
1186
  }
@@ -1084,6 +1195,8 @@ class JSONPathTransformerContext {
1084
1195
  if (startMatch && currentGroup.length > 0) {
1085
1196
  const prevContext = this._contextObj;
1086
1197
  const prevParams = this._params;
1198
+ const prevVars = this.vars;
1199
+ this.vars = {};
1087
1200
  try {
1088
1201
  this._contextObj = currentGroup;
1089
1202
  /** @type {any} */ (this)._currentGroup = currentGroup;
@@ -1091,6 +1204,7 @@ class JSONPathTransformerContext {
1091
1204
  } finally {
1092
1205
  this._contextObj = prevContext;
1093
1206
  this._params = prevParams;
1207
+ this.vars = prevVars;
1094
1208
  delete /** @type {any} */ (this)._currentGroup;
1095
1209
  }
1096
1210
  currentGroup = [];
@@ -1102,6 +1216,8 @@ class JSONPathTransformerContext {
1102
1216
  if (currentGroup.length > 0) {
1103
1217
  const prevContext = this._contextObj;
1104
1218
  const prevParams = this._params;
1219
+ const prevVars = this.vars;
1220
+ this.vars = {};
1105
1221
  try {
1106
1222
  this._contextObj = currentGroup;
1107
1223
  /** @type {any} */ (this)._currentGroup = currentGroup;
@@ -1109,6 +1225,7 @@ class JSONPathTransformerContext {
1109
1225
  } finally {
1110
1226
  this._contextObj = prevContext;
1111
1227
  this._params = prevParams;
1228
+ this.vars = prevVars;
1112
1229
  delete /** @type {any} */ (this)._currentGroup;
1113
1230
  }
1114
1231
  }
@@ -1123,6 +1240,8 @@ class JSONPathTransformerContext {
1123
1240
  if (endMatch) {
1124
1241
  const prevContext = this._contextObj;
1125
1242
  const prevParams = this._params;
1243
+ const prevVars = this.vars;
1244
+ this.vars = {};
1126
1245
  try {
1127
1246
  this._contextObj = currentGroup;
1128
1247
  /** @type {any} */ (this)._currentGroup = currentGroup;
@@ -1130,6 +1249,7 @@ class JSONPathTransformerContext {
1130
1249
  } finally {
1131
1250
  this._contextObj = prevContext;
1132
1251
  this._params = prevParams;
1252
+ this.vars = prevVars;
1133
1253
  delete /** @type {any} */ (this)._currentGroup;
1134
1254
  }
1135
1255
  currentGroup = [];
@@ -1140,6 +1260,8 @@ class JSONPathTransformerContext {
1140
1260
  if (currentGroup.length > 0) {
1141
1261
  const prevContext = this._contextObj;
1142
1262
  const prevParams = this._params;
1263
+ const prevVars = this.vars;
1264
+ this.vars = {};
1143
1265
  try {
1144
1266
  this._contextObj = currentGroup;
1145
1267
  /** @type {any} */ (this)._currentGroup = currentGroup;
@@ -1147,6 +1269,7 @@ class JSONPathTransformerContext {
1147
1269
  } finally {
1148
1270
  this._contextObj = prevContext;
1149
1271
  this._params = prevParams;
1272
+ this.vars = prevVars;
1150
1273
  delete /** @type {any} */ (this)._currentGroup;
1151
1274
  }
1152
1275
  }
@@ -1645,12 +1768,18 @@ class JSONPathTransformerContext {
1645
1768
  }
1646
1769
 
1647
1770
  /**
1771
+ * Bind a variable, equivalent to `xsl:variable`. Accepts the same default/
1772
+ * value forms as `param()`/`withParam()`: a bare string (a JSONPath
1773
+ * expression), an explicit `{select}`, or a literal `{value}` — the last
1774
+ * for binding an already-computed value (e.g. `this.indexedDB(...)`'s
1775
+ * result) directly, with no selector round-trip.
1648
1776
  * @param {string} name - Variable name
1649
- * @param {string} select - JSONPath selector
1777
+ * @param {string|{select: string}|{value: unknown}} select - A JSONPath
1778
+ * expression string, an explicit `{select}`, or a literal `{value}`.
1650
1779
  * @returns {this}
1651
1780
  */
1652
1781
  variable (name, select) {
1653
- this.vars[name] = this.get(select, false);
1782
+ this.vars[name] = this._resolveParam(this._paramSpec(select));
1654
1783
  return this;
1655
1784
  }
1656
1785
 
@@ -1675,9 +1804,12 @@ class JSONPathTransformerContext {
1675
1804
  }
1676
1805
 
1677
1806
  /**
1678
- * Look up a parameter by name across the active with-param scope and the
1679
- * runtime `config.params`, so runtime-supplied params act like XSLT global
1680
- * parameters (visible to every template and expression).
1807
+ * Look up a parameter by name across the active with-param scope, any
1808
+ * `variable()`-set value, and the runtime `config.params`, so a bare
1809
+ * `$name` reference resolves the same way from `if()`/`choose()`/
1810
+ * comparisons/`valueOf()` regardless of which of those set it. Runtime
1811
+ * params act like XSLT global parameters (visible to every template and
1812
+ * expression); `vars` is more local, matching `xsl:variable` scoping.
1681
1813
  * @param {string} name
1682
1814
  * @returns {{has: boolean, value: any}}
1683
1815
  * @private
@@ -1686,6 +1818,9 @@ class JSONPathTransformerContext {
1686
1818
  if (this._params && Object.hasOwn(this._params, name)) {
1687
1819
  return {has: true, value: this._params[name]};
1688
1820
  }
1821
+ if (Object.hasOwn(this.vars, name)) {
1822
+ return {has: true, value: this.vars[name]};
1823
+ }
1689
1824
  if (Object.hasOwn(this._runtimeParams, name)) {
1690
1825
  return {has: true, value: this._runtimeParams[name]};
1691
1826
  }
@@ -2328,13 +2463,17 @@ class JSONPathTransformerContext {
2328
2463
  });
2329
2464
 
2330
2465
  // Evaluate the JSONPath expression with bound variables
2331
- // Temporarily set _params so expressions can access them
2466
+ // Temporarily set _params so expressions can access them; `vars` is
2467
+ // scoped fresh too, matching a called `xsl:function`'s own scope.
2332
2468
  const oldParams = this._params;
2469
+ const oldVars = this.vars;
2333
2470
  this._params = variables;
2471
+ this.vars = {};
2334
2472
  try {
2335
2473
  return this.get(sequence, false);
2336
2474
  } finally {
2337
2475
  this._params = oldParams;
2476
+ this.vars = oldVars;
2338
2477
  }
2339
2478
  }
2340
2479
  : body;
@@ -2370,15 +2509,24 @@ class JSONPathTransformerContext {
2370
2509
  * @param {any[]|
2371
2510
  * ((this: JSONPathTransformerContext<T>) => void)} [children] -
2372
2511
  * Child nodes or callback
2373
- * @param {(this: JSONPathTransformerContext<T>) => void} [cb] -
2374
- * Callback function
2512
+ * @param {(this: JSONPathTransformerContext<T>) => void
2513
+ * } [cb] - Callback function; may be async (e.g. to `await` a
2514
+ * `$indexedDB` fetch), in which case `element()` itself returns a
2515
+ * `Promise<this>` instead of `this` — check for `.then` (or `await`)
2516
+ * rather than assuming a synchronous return. (Typed as plain `void`,
2517
+ * not `void|Promise<void>` — see the note on `SimpleCallback` in
2518
+ * JSONJoiningTransformer.js.)
2375
2519
  * @param {string[]} [useAttributeSets] - Attribute set names to apply
2376
- * @returns {this}
2520
+ * @returns {this|Promise<this>}
2377
2521
  */
2378
2522
  element (name, atts, children, cb, useAttributeSets) {
2379
- /** @type {any} */ (this._getJoiningTransformer()).element(
2523
+ const ret = /** @type {any} */ (this._getJoiningTransformer()).element(
2380
2524
  name, atts, children, cb, useAttributeSets
2381
2525
  );
2526
+ if (ret && typeof ret.then === 'function') {
2527
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
2528
+ return ret.then(() => this);
2529
+ }
2382
2530
  return this;
2383
2531
  }
2384
2532
 
@@ -2569,13 +2717,23 @@ class JSONPathTransformerContext {
2569
2717
  *
2570
2718
  * @param {string} select - JSONPath selector expression
2571
2719
  * @param {(this: JSONPathTransformerContext<T>)
2572
- * => void} cb - Callback to invoke if condition is met
2573
- * @returns {this}
2720
+ * => void} cb - Callback to invoke if condition is met; may be async,
2721
+ * in which case `if()` itself returns a `Promise<this>` instead of
2722
+ * `this`. (Typed as plain `void`, not `void|Promise<void>` — see the
2723
+ * note on `SimpleCallback` in JSONJoiningTransformer.js.)
2724
+ * @returns {this|Promise<this>}
2574
2725
  */
2575
2726
  if (select, cb) {
2576
2727
  const passes = this._passesIf(select);
2577
2728
  if (passes && typeof cb === 'function') {
2578
- cb.call(this);
2729
+ // `cb`'s declared return type is plain `void` — cast here to
2730
+ // duck-type the real value; see the note on `SimpleCallback` in
2731
+ // JSONJoiningTransformer.js.
2732
+ const ret = /** @type {any} */ (cb.call(this));
2733
+ if (ret && typeof ret.then === 'function') {
2734
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
2735
+ return ret.then(() => this);
2736
+ }
2579
2737
  }
2580
2738
  return this;
2581
2739
  }
@@ -2749,19 +2907,32 @@ class JSONPathTransformerContext {
2749
2907
  * when the test does not pass (similar to xsl:choose/xsl:otherwise).
2750
2908
  * @param {string} select JSONPath selector
2751
2909
  * @param {(this: JSONPathTransformerContext<T>)
2752
- * => void} whenCb Callback when condition passes
2910
+ * => void} whenCb Callback when condition passes; may be async, in
2911
+ * which case `choose()` itself returns a `Promise<this>` instead of
2912
+ * `this`. (Typed as plain `void`, not `void|Promise<void>` — see the
2913
+ * note on `SimpleCallback` in JSONJoiningTransformer.js.)
2753
2914
  * @param {(this: JSONPathTransformerContext<T>)
2754
- * => void} [otherwiseCb] Callback when condition fails
2755
- * @returns {this}
2915
+ * => void} [otherwiseCb] Callback when condition fails; may likewise
2916
+ * be async.
2917
+ * @returns {this|Promise<this>}
2756
2918
  */
2757
2919
  choose (select, whenCb, otherwiseCb) {
2758
2920
  const passes = this._passesIf(select);
2921
+ // `whenCb`/`otherwiseCb`'s declared return type is plain `void` — cast
2922
+ // here to duck-type the real value; see the note on `SimpleCallback`
2923
+ // in JSONJoiningTransformer.js.
2924
+ /** @type {any} */
2925
+ let ret;
2759
2926
  if (passes) {
2760
2927
  if (typeof whenCb === 'function') {
2761
- whenCb.call(this);
2928
+ ret = whenCb.call(this);
2762
2929
  }
2763
2930
  } else if (typeof otherwiseCb === 'function') {
2764
- otherwiseCb.call(this);
2931
+ ret = otherwiseCb.call(this);
2932
+ }
2933
+ if (ret && typeof ret.then === 'function') {
2934
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
2935
+ return ret.then(() => this);
2765
2936
  }
2766
2937
  return this;
2767
2938
  }
@@ -391,6 +391,12 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
391
391
  if (_oldStrTemp !== undefined) {
392
392
  this._strTemp = (this._strTemp || '') + str;
393
393
  } else {
394
+ // Close any open parent start tag before appending content, matching
395
+ // text()'s behavior — string() is content too, not an attribute value.
396
+ if (this._openTagState) {
397
+ this.append('>');
398
+ this._openTagState = false;
399
+ }
394
400
  // Append to the output (or current container via append()).
395
401
  this.append(tmpStr + str);
396
402
  }
@@ -497,9 +503,14 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
497
503
  * @param {string|Element} elem - Element name or element object
498
504
  * @param {ElementAttributes} [atts] - Element attributes
499
505
  * @param {any[]} [childNodes] - Child nodes
500
- * @param {(this: StringJoiningTransformer) => void} [cb] - Callback function
506
+ * @param {(this: StringJoiningTransformer) => void} [cb] -
507
+ * Callback function; may be async (e.g. to `await` a `$indexedDB`
508
+ * fetch), in which case `element()` itself returns a `Promise` instead
509
+ * of `this` — check for `.then` (or `await`) rather than assuming a
510
+ * synchronous return. (Typed as plain `void`, not `void|Promise<void>`
511
+ * — see the note on `SimpleCallback` in JSONJoiningTransformer.js.)
501
512
  * @param {string[]} [useAttributeSets] - Attribute set names to apply
502
- * @returns {StringJoiningTransformer}
513
+ * @returns {StringJoiningTransformer|Promise<StringJoiningTransformer>}
503
514
  */
504
515
  element (elem, atts, childNodes, cb, useAttributeSets) {
505
516
  // If a parent element's start tag is still open, close it before
@@ -635,17 +646,32 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
635
646
  jml[method]({'#': childNodes})
636
647
  ));
637
648
  }
638
- cb.call(this._context || this);
649
+ // `cb`'s declared return type is plain `void` (see the parameter's
650
+ // JSDoc) so the leniency that lets any actual return value through
651
+ // still applies to callers — cast here to duck-type the real value.
652
+ const cbResult = /** @type {any} */ (cb.call(this._context || this));
639
653
 
640
654
  // Todo: Depending on an this._cfg.xmlElements option, allow for
641
655
  // XML self-closing when empty or as per the tag, HTML
642
656
  // self-closing tags (or polyglot-friendly self-closing)
643
- if (this._openTagState) {
644
- this.append('>');
645
- }
646
- this.append('</' + elName + '>');
647
- this._openTagState = oldTagState;
648
- return this;
657
+ const finish = () => {
658
+ if (this._openTagState) {
659
+ this.append('>');
660
+ }
661
+ this.append('</' + elName + '>');
662
+ this._openTagState = oldTagState;
663
+ return this;
664
+ };
665
+ // `cb` may be an async function (e.g. one that awaits
666
+ // `this.indexedDB(...)`/`this.renderDefault()` before appending
667
+ // content) — mirroring how a root/matched/named template's own return
668
+ // value is already handled elsewhere, closing the tag only after that
669
+ // settles keeps output correctly ordered.
670
+ if (cbResult && typeof cbResult.then === 'function') {
671
+ // eslint-disable-next-line promise/prefer-await-to-then -- Not async
672
+ return cbResult.then(() => finish());
673
+ }
674
+ return finish();
649
675
  }
650
676
 
651
677
  /**
@@ -619,12 +619,36 @@ class XPathTransformerContext {
619
619
  const prevTemplateParams = this._params;
620
620
  this._params = {0: node, ...appliedParams};
621
621
 
622
+ const {template: xpathTemplateFn} = templateObj;
623
+ /* c8 ignore start -- Todo: compile via compileJSONTemplate() once it
624
+ exists (see ~/idb-manager/JTLT-JSON-TEMPLATES-PROPOSAL.md) instead
625
+ of rejecting; no matched template can reach here with an Array
626
+ `template` until that lands (and the declarative subset is
627
+ JSONPath-only per that plan, so this may stay unreachable longer
628
+ than the JSONPath-side guards). */
629
+ if (Array.isArray(xpathTemplateFn)) {
630
+ throw new TypeError(
631
+ 'JSON (jamilih-shaped) Array templates are not yet supported by ' +
632
+ 'the XPath engine; compile with compileJSONTemplate() first.'
633
+ );
634
+ }
635
+ /* c8 ignore stop */
622
636
  /**
623
637
  * The template may return synchronously or return a Promise (e.g. from
624
638
  * `await this.indexedDB(...)`), which is awaited unless `config.sync`.
625
639
  * @type {any}
626
640
  */
627
- const ret = templateObj.template.call(this, node, {mode});
641
+ const ret = xpathTemplateFn.call(
642
+ // `this` carries runtime `extensions`; a consumer's
643
+ // `ContextExtensions` augmentation would otherwise reject it here
644
+ // (matching the cast already used at the other three call sites).
645
+ // `node` is cast too: `templateObj.template`'s static type is a
646
+ // union across a `dom`-typed `TemplateFunction` (expecting
647
+ // `DocumentFragment | Element`) and the mode-callback signature
648
+ // above (expecting plain `Node`) — the same node value satisfies
649
+ // whichever one actually runs.
650
+ /** @type {any} */ (this), /** @type {any} */ (node), {mode}
651
+ );
628
652
 
629
653
  // Restore previous parameter context
630
654
  this._params = prevTemplateParams;