jtlt 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc.json +1 -0
- package/CHANGES.md +18 -0
- package/README.md +99 -85
- package/demo/calltemplate-params-demo.js +13 -22
- package/demo/codemirror.esm.js +50 -15
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +17 -17
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +60 -61
- package/demo/vendor/jhtml/src/jhtml-browser.js +1 -0
- package/demo/vendor/jhtml/src/jhtml-node.js +1 -0
- package/demo/vendor/jhtml/src/jhtml.js +18 -15
- package/dist/AbstractJoiningTransformer.d.ts +5 -5
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts +13 -12
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +60 -20
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts +9 -2
- package/dist/XPathTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +56 -9
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/context-extensions.d.ts +24 -0
- package/dist/extendContext.d.ts +10 -0
- package/dist/extendContext.d.ts.map +1 -0
- package/dist/index.d.ts +121 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/indexedDB.d.ts +107 -0
- package/dist/indexedDB.d.ts.map +1 -0
- package/dist/maybeAsync.d.ts +15 -0
- package/dist/maybeAsync.d.ts.map +1 -0
- package/docs/API.expanded.md +70 -20
- package/docs/API.md +34 -4
- package/docs/TO-DO.md +17 -3
- package/eslint.config.js +11 -3
- package/package.json +22 -15
- package/pnpm-workspace.yaml +9 -0
- package/src/AbstractJoiningTransformer.js +3 -3
- package/src/JSONPathTransformer.js +48 -8
- package/src/JSONPathTransformerContext.js +175 -25
- package/src/XPathTransformer.js +33 -1
- package/src/XPathTransformerContext.js +145 -16
- package/src/context-extensions.d.ts +24 -0
- package/src/extendContext.js +21 -0
- package/src/index.js +102 -11
- package/src/indexedDB.js +530 -0
- package/src/maybeAsync.js +44 -0
- package/tsconfig.json +2 -2
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import {JSONPath as jsonpath} from 'jsonpath-plus';
|
|
2
2
|
import JSONPathTransformer from './JSONPathTransformer.js';
|
|
3
|
+
import {maybeAsyncLoop} from './maybeAsync.js';
|
|
4
|
+
import applyExtensions from './extendContext.js';
|
|
5
|
+
import {
|
|
6
|
+
parseIndexedDBExpression, queryIndexedDB, resolveIndexedDBQuery
|
|
7
|
+
} from './indexedDB.js';
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* @param {string} string
|
|
@@ -52,7 +57,7 @@ const escapeRegexReplacement = (string) => {
|
|
|
52
57
|
* ctx: JSONPathTransformerContext
|
|
53
58
|
* ) => number} SortComparator
|
|
54
59
|
* @typedef {string | SortObject | SortComparator |
|
|
55
|
-
* Array<string|SortObject>} SortSpec
|
|
60
|
+
* Array<string|SortObject> | null} SortSpec
|
|
56
61
|
*/
|
|
57
62
|
|
|
58
63
|
/**
|
|
@@ -88,10 +93,17 @@ const escapeRegexReplacement = (string) => {
|
|
|
88
93
|
* @property {JoiningTransformerMap[T]} joiningTransformer - Joining transformer
|
|
89
94
|
* @property {boolean} [preventEval] - Whether to prevent eval in
|
|
90
95
|
* JSONPath
|
|
96
|
+
* @property {boolean} [sync] - When true, throw if a template returns a
|
|
97
|
+
* Promise instead of awaiting it (disables `indexedDB()`)
|
|
91
98
|
* @property {(path: string) => number} [specificityPriorityResolver]
|
|
92
99
|
* Priority resolver function
|
|
93
100
|
* @property {import('./index.js').JSONPathTemplateObject<T>[]|
|
|
94
101
|
* import('./index.js').JSONPathTemplateArray<T>[]} [templates]
|
|
102
|
+
* @property {Record<string, unknown> & ThisType<
|
|
103
|
+
* import('./JSONPathTransformerContext.js').default<T> &
|
|
104
|
+
* import('./context-extensions.js').ContextExtensions
|
|
105
|
+
* >} [extensions] Extra methods/values
|
|
106
|
+
* merged onto this context so templates can call `this.myHelper()`
|
|
95
107
|
*/
|
|
96
108
|
|
|
97
109
|
/**
|
|
@@ -143,6 +155,9 @@ class JSONPathTransformerContext {
|
|
|
143
155
|
this._preserveSpaceElements = [];
|
|
144
156
|
/** @type {string[]} */
|
|
145
157
|
this._stripSpaceElements = [];
|
|
158
|
+
if (config.extensions) {
|
|
159
|
+
applyExtensions(this, config.extensions);
|
|
160
|
+
}
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
/**
|
|
@@ -195,7 +210,7 @@ class JSONPathTransformerContext {
|
|
|
195
210
|
|
|
196
211
|
/**
|
|
197
212
|
* Gets the current output.
|
|
198
|
-
* @returns {
|
|
213
|
+
* @returns {unknown} The output from the joining transformer
|
|
199
214
|
*/
|
|
200
215
|
getOutput () {
|
|
201
216
|
return this._getJoiningTransformer().get();
|
|
@@ -221,7 +236,7 @@ class JSONPathTransformerContext {
|
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
/**
|
|
224
|
-
* @param {
|
|
239
|
+
* @param {unknown} v - Value to set
|
|
225
240
|
* @returns {this}
|
|
226
241
|
*/
|
|
227
242
|
set (v) {
|
|
@@ -403,7 +418,7 @@ class JSONPathTransformerContext {
|
|
|
403
418
|
const prevCurrPath = that._currPath;
|
|
404
419
|
|
|
405
420
|
// Process in (sorted) order
|
|
406
|
-
|
|
421
|
+
const loopResult = maybeAsyncLoop(matches, (o) => {
|
|
407
422
|
const {value, parent, parentProperty, path} = o;
|
|
408
423
|
const _oldPath = that._currPath;
|
|
409
424
|
that._currPath += path.replace(/^\$/v, '');
|
|
@@ -454,17 +469,19 @@ class JSONPathTransformerContext {
|
|
|
454
469
|
}
|
|
455
470
|
if (onNoMatch === 'deep-skip') {
|
|
456
471
|
// Skip this node and its descendants entirely
|
|
457
|
-
|
|
472
|
+
return;
|
|
458
473
|
}
|
|
459
474
|
if (onNoMatch === 'shallow-copy') {
|
|
460
475
|
// Output the value as-is without processing children
|
|
461
476
|
joiner.append(value);
|
|
462
|
-
|
|
477
|
+
that._currPath = _oldPath;
|
|
478
|
+
return;
|
|
463
479
|
}
|
|
464
480
|
if (onNoMatch === 'deep-copy') {
|
|
465
481
|
// Output the value and all descendants as-is
|
|
466
482
|
joiner.append(JSON.stringify(value));
|
|
467
|
-
|
|
483
|
+
that._currPath = _oldPath;
|
|
484
|
+
return;
|
|
468
485
|
}
|
|
469
486
|
if (onNoMatch === 'text-only-copy') {
|
|
470
487
|
// Output only text content (primitives)
|
|
@@ -472,7 +489,8 @@ class JSONPathTransformerContext {
|
|
|
472
489
|
typeof value === 'boolean') {
|
|
473
490
|
joiner.append(String(value));
|
|
474
491
|
}
|
|
475
|
-
|
|
492
|
+
that._currPath = _oldPath;
|
|
493
|
+
return;
|
|
476
494
|
}
|
|
477
495
|
// 'apply-templates', 'shallow-skip', or other:
|
|
478
496
|
// use default template rules
|
|
@@ -573,15 +591,58 @@ class JSONPathTransformerContext {
|
|
|
573
591
|
const prevTemplateParams = that._params;
|
|
574
592
|
that._params = {0: value};
|
|
575
593
|
|
|
594
|
+
/**
|
|
595
|
+
* The template may return synchronously or return a Promise (e.g. from
|
|
596
|
+
* `await this.indexedDB(...)`), which is awaited unless `config.sync`.
|
|
597
|
+
* @type {any}
|
|
598
|
+
*/
|
|
576
599
|
const ret =
|
|
577
600
|
/** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
|
|
578
601
|
templateObj
|
|
579
602
|
).template.call(
|
|
580
|
-
|
|
603
|
+
// `this` carries runtime `extensions`; a consumer's
|
|
604
|
+
// `ContextExtensions` augmentation would otherwise reject `that`.
|
|
605
|
+
/** @type {any} */ (that), value, {mode, parent, parentProperty}
|
|
581
606
|
);
|
|
582
607
|
|
|
583
608
|
// Restore previous parameter context
|
|
584
609
|
that._params = prevTemplateParams;
|
|
610
|
+
if (ret !== null && typeof ret !== 'undefined' &&
|
|
611
|
+
typeof ret.then === 'function') {
|
|
612
|
+
if (that._config.sync) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
'A template returned a Promise but JTLT is configured with ' +
|
|
615
|
+
'`sync: true`.'
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
// The loop body deliberately mixes value and no-value returns so
|
|
619
|
+
// `maybeAsyncLoop` can stay synchronous unless a template awaits.
|
|
620
|
+
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
621
|
+
// eslint-disable-next-line promise/prefer-await-to-then, consistent-return -- intentional dynamic sync/async
|
|
622
|
+
return ret.then((/** @type {any} */ resolvedRet) => {
|
|
623
|
+
that._params = prevTemplateParams;
|
|
624
|
+
if (typeof resolvedRet !== 'undefined') {
|
|
625
|
+
const joiner = that._getJoiningTransformer();
|
|
626
|
+
/* c8 ignore start -- _openTagState only on
|
|
627
|
+
StringJoiningTransformer; string-output defensive check */
|
|
628
|
+
// @ts-expect-error -- _openTagState: StringJoiningTransformer only
|
|
629
|
+
if (joiner._openTagState) {
|
|
630
|
+
joiner.append('>');
|
|
631
|
+
// @ts-expect-error -- _openTagState: StringJoiningTransformer
|
|
632
|
+
joiner._openTagState = false;
|
|
633
|
+
}
|
|
634
|
+
/* c8 ignore stop */
|
|
635
|
+
joiner.append(resolvedRet);
|
|
636
|
+
}
|
|
637
|
+
that._parent = parent;
|
|
638
|
+
// Matches reached here always carry a parentProperty; the sync
|
|
639
|
+
// path covers the root-node fallback.
|
|
640
|
+
/* c8 ignore next */
|
|
641
|
+
that._parentProperty = (parentProperty ?? that._parentProperty);
|
|
642
|
+
that._currPath = _oldPath;
|
|
643
|
+
return undefined;
|
|
644
|
+
});
|
|
645
|
+
}
|
|
585
646
|
if (typeof ret !== 'undefined') {
|
|
586
647
|
// After the undefined check, ret is ResultType<T>
|
|
587
648
|
const joiner = that._getJoiningTransformer();
|
|
@@ -604,12 +665,27 @@ class JSONPathTransformerContext {
|
|
|
604
665
|
that._parent = parent;
|
|
605
666
|
that._parentProperty = (parentProperty ?? that._parentProperty);
|
|
606
667
|
that._currPath = _oldPath;
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (typeof loopResult?.then === 'function') {
|
|
671
|
+
// Returns a Promise<this> when a nested template ran asynchronously.
|
|
672
|
+
return /** @type {any} */ (
|
|
673
|
+
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
674
|
+
// eslint-disable-next-line promise/prefer-await-to-then -- intentional dynamic sync/async
|
|
675
|
+
loopResult.then(() => {
|
|
676
|
+
this._contextObj = prevContext;
|
|
677
|
+
this._parent = prevParent;
|
|
678
|
+
this._parentProperty = prevParentProp;
|
|
679
|
+
this._currPath = prevCurrPath;
|
|
680
|
+
return this;
|
|
681
|
+
})
|
|
682
|
+
);
|
|
607
683
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
684
|
+
|
|
685
|
+
this._contextObj = prevContext;
|
|
686
|
+
this._parent = prevParent;
|
|
687
|
+
this._parentProperty = prevParentProp;
|
|
688
|
+
this._currPath = prevCurrPath;
|
|
613
689
|
return this;
|
|
614
690
|
}
|
|
615
691
|
|
|
@@ -657,7 +733,11 @@ class JSONPathTransformerContext {
|
|
|
657
733
|
);
|
|
658
734
|
}
|
|
659
735
|
|
|
660
|
-
const result = templateObj.template.call(
|
|
736
|
+
const result = templateObj.template.call(
|
|
737
|
+
// `this` carries runtime `extensions`; a consumer's `ContextExtensions`
|
|
738
|
+
// augmentation would otherwise reject the bare context.
|
|
739
|
+
/** @type {any} */ (this), this._contextObj, {}
|
|
740
|
+
);
|
|
661
741
|
if (typeof result !== 'undefined') {
|
|
662
742
|
/** @type {any} */ (results).append(result);
|
|
663
743
|
}
|
|
@@ -819,9 +899,12 @@ class JSONPathTransformerContext {
|
|
|
819
899
|
* expression matches
|
|
820
900
|
* @param {string} [options.groupEndingWith] - Ends group when expression
|
|
821
901
|
* matches
|
|
822
|
-
* @param {
|
|
902
|
+
* @param {SortSpec<V>} [options.sort] - Sort specification (same as forEach)
|
|
823
903
|
* @param {(
|
|
824
|
-
* this: JSONPathTransformerContext<T>,
|
|
904
|
+
* this: JSONPathTransformerContext<T>,
|
|
905
|
+
* key: unknown,
|
|
906
|
+
* items: unknown[],
|
|
907
|
+
* ctx: JSONPathTransformerContext<T>
|
|
825
908
|
* ) => void} cb - Callback receives (groupingKey, groupItems, context)
|
|
826
909
|
* @returns {this}
|
|
827
910
|
*/
|
|
@@ -1087,7 +1170,7 @@ class JSONPathTransformerContext {
|
|
|
1087
1170
|
return 0;
|
|
1088
1171
|
}
|
|
1089
1172
|
if (Number.isNaN(an)) {
|
|
1090
|
-
return
|
|
1173
|
+
return order;
|
|
1091
1174
|
}
|
|
1092
1175
|
if (Number.isNaN(bn)) {
|
|
1093
1176
|
return -order;
|
|
@@ -1135,7 +1218,7 @@ class JSONPathTransformerContext {
|
|
|
1135
1218
|
|
|
1136
1219
|
/**
|
|
1137
1220
|
* Returns the current group (for use within forEachGroup callback).
|
|
1138
|
-
* @returns {
|
|
1221
|
+
* @returns {unknown[]|undefined}
|
|
1139
1222
|
*/
|
|
1140
1223
|
currentGroup () {
|
|
1141
1224
|
return /** @type {any} */ (this)._currentGroup;
|
|
@@ -1143,12 +1226,48 @@ class JSONPathTransformerContext {
|
|
|
1143
1226
|
|
|
1144
1227
|
/**
|
|
1145
1228
|
* Returns the current grouping key (for use within forEachGroup callback).
|
|
1146
|
-
* @returns {
|
|
1229
|
+
* @returns {unknown}
|
|
1147
1230
|
*/
|
|
1148
1231
|
currentGroupingKey () {
|
|
1149
1232
|
return /** @type {any} */ (this)._currentGroupingKey;
|
|
1150
1233
|
}
|
|
1151
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* Directly query IndexedDB from within a template, e.g.
|
|
1237
|
+
* `await this.indexedDB('myDB', 'myStore', {index: 'byAge'})`.
|
|
1238
|
+
*
|
|
1239
|
+
* Since IndexedDB access is asynchronous, this is unavailable when JTLT is
|
|
1240
|
+
* configured with `sync: true`.
|
|
1241
|
+
* @param {string} dbName - Database name
|
|
1242
|
+
* @param {string} storeName - Object store name
|
|
1243
|
+
* @param {import('./indexedDB.js').QueryOptions} [options] - Query options
|
|
1244
|
+
* @returns {Promise<unknown[]>} The matching records
|
|
1245
|
+
*/
|
|
1246
|
+
indexedDB (dbName, storeName, options) {
|
|
1247
|
+
if (this._config.sync) {
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
'The `indexedDB()` API is unavailable when JTLT is configured with ' +
|
|
1250
|
+
'`sync: true`.'
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
return queryIndexedDB(dbName, storeName, options);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Await a parsed `indexedDB(...)` expression and append its (stringified)
|
|
1258
|
+
* value to the output. Used by {@link valueOf}. Callers reject `config.sync`.
|
|
1259
|
+
* @param {import('./indexedDB.js').ParsedIndexedDBExpression} parsed
|
|
1260
|
+
* @param {any} results - The joining transformer
|
|
1261
|
+
* @returns {Promise<this>}
|
|
1262
|
+
*/
|
|
1263
|
+
async _appendIndexedDBValue (parsed, results) {
|
|
1264
|
+
const value = await resolveIndexedDBQuery(parsed, {
|
|
1265
|
+
preventEval: this._config.preventEval
|
|
1266
|
+
});
|
|
1267
|
+
results.text(String(value));
|
|
1268
|
+
return this;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1152
1271
|
/**
|
|
1153
1272
|
* @param {string|object} [select] - JSONPath selector
|
|
1154
1273
|
* @returns {this}
|
|
@@ -1159,6 +1278,26 @@ class JSONPathTransformerContext {
|
|
|
1159
1278
|
const results = this._getJoiningTransformer();
|
|
1160
1279
|
let result;
|
|
1161
1280
|
|
|
1281
|
+
const selectString = select && typeof select === 'object'
|
|
1282
|
+
? /** @type {{select?: string}} */ (select).select
|
|
1283
|
+
: select;
|
|
1284
|
+
|
|
1285
|
+
// Intercept `indexedDB('db', 'store')<trailing JSONPath>` expressions and
|
|
1286
|
+
// resolve them asynchronously before appending. Returns a Promise so
|
|
1287
|
+
// templates can `await this.valueOf(...)`.
|
|
1288
|
+
if (typeof selectString === 'string') {
|
|
1289
|
+
const parsed = parseIndexedDBExpression(selectString);
|
|
1290
|
+
if (parsed) {
|
|
1291
|
+
if (this._config.sync) {
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
'The `indexedDB()` function is unavailable when JTLT is ' +
|
|
1294
|
+
'configured with `sync: true`.'
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
return /** @type {any} */ (this._appendIndexedDBValue(parsed, results));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1162
1301
|
if (select && typeof select === 'object' &&
|
|
1163
1302
|
/** @type {{select?: string}} */ (select).select === '.') {
|
|
1164
1303
|
result = this._contextObj;
|
|
@@ -2079,8 +2218,8 @@ class JSONPathTransformerContext {
|
|
|
2079
2218
|
/**
|
|
2080
2219
|
* Invoke a registered stylesheet function with positional arguments.
|
|
2081
2220
|
* @param {string} name - Function name (with namespace)
|
|
2082
|
-
* @param {
|
|
2083
|
-
* @returns {
|
|
2221
|
+
* @param {unknown[]} args - Positional arguments
|
|
2222
|
+
* @returns {unknown} Function return value
|
|
2084
2223
|
*/
|
|
2085
2224
|
invokeFunctionByArity (name, args = []) {
|
|
2086
2225
|
return this._getJoiningTransformer().invokeFunctionByArity(name, args);
|
|
@@ -2233,12 +2372,23 @@ class JSONPathTransformerContext {
|
|
|
2233
2372
|
|
|
2234
2373
|
/**
|
|
2235
2374
|
* @param {string} name - Key name
|
|
2236
|
-
* @param {
|
|
2237
|
-
* @returns {
|
|
2375
|
+
* @param {unknown} value - Value to match
|
|
2376
|
+
* @returns {unknown}
|
|
2238
2377
|
*/
|
|
2239
2378
|
getKey (name, value) {
|
|
2240
2379
|
const key = this.keys[name];
|
|
2241
|
-
|
|
2380
|
+
// Keys are document-global (like `xsl:key`): resolve `match` against the
|
|
2381
|
+
// root, not the current context (which may be a `forEach`/`applyTemplates`
|
|
2382
|
+
// item).
|
|
2383
|
+
const matches = /** @type {any[]} */ (
|
|
2384
|
+
(/** @type {any} */ (jsonpath))({
|
|
2385
|
+
path: JSONPathTransformer.makeJSONPathAbsolute(key.match),
|
|
2386
|
+
json: this._origObj,
|
|
2387
|
+
preventEval: this._config.preventEval,
|
|
2388
|
+
wrap: true,
|
|
2389
|
+
returnType: 'value'
|
|
2390
|
+
})
|
|
2391
|
+
);
|
|
2242
2392
|
for (const match of matches) { // For objects or arrays
|
|
2243
2393
|
if (match && typeof match === 'object' &&
|
|
2244
2394
|
match[key.use] === value) {
|
package/src/XPathTransformer.js
CHANGED
|
@@ -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 {
|
|
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
|
}
|
|
@@ -2,6 +2,13 @@ import xpath2 from 'xpath2.js'; // Runtime JS import; ambient types declared
|
|
|
2
2
|
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
3
3
|
// xpathVersion: 1 => browser/native XPathEvaluator API; 2 => xpath2.js, 3 => fontoxpath
|
|
4
4
|
import fontoxpath from 'fontoxpath';
|
|
5
|
+
import {maybeAsyncLoop} from './maybeAsync.js';
|
|
6
|
+
import applyExtensions from './extendContext.js';
|
|
7
|
+
import {
|
|
8
|
+
queryIndexedDB,
|
|
9
|
+
xpathExpressionUsesIndexedDB,
|
|
10
|
+
evaluateXPathWithIndexedDB
|
|
11
|
+
} from './indexedDB.js';
|
|
5
12
|
// import xsdValidator from 'xsd-validator';
|
|
6
13
|
|
|
7
14
|
/**
|
|
@@ -19,7 +26,16 @@ const escapeRegexReplacement = (string) => {
|
|
|
19
26
|
* @property {import('./index.js').
|
|
20
27
|
* JoiningTransformer} joiningTransformer Joiner
|
|
21
28
|
* @property {boolean} [errorOnEqualPriority]
|
|
29
|
+
* @property {boolean} [sync] - When true, throw if a template returns a
|
|
30
|
+
* Promise instead of awaiting it (disables `indexedDB()`)
|
|
31
|
+
* @property {boolean} [preventEval] - Whether to prevent eval in the JSONPath
|
|
32
|
+
* trailing segment of an `indexedDB(...)` expression
|
|
22
33
|
* @property {(path: string) => number} [specificityPriorityResolver]
|
|
34
|
+
* @property {Record<string, unknown> & ThisType<
|
|
35
|
+
* import('./XPathTransformerContext.js').default &
|
|
36
|
+
* import('./context-extensions.js').ContextExtensions
|
|
37
|
+
* >} [extensions] Extra methods/values
|
|
38
|
+
* merged onto this context so templates can call `this.myHelper()`
|
|
23
39
|
*/
|
|
24
40
|
|
|
25
41
|
/**
|
|
@@ -124,11 +140,19 @@ class XPathTransformerContext {
|
|
|
124
140
|
this._preserveSpaceElements = [];
|
|
125
141
|
/** @type {string[]} */
|
|
126
142
|
this._stripSpaceElements = [];
|
|
143
|
+
if (config.extensions) {
|
|
144
|
+
applyExtensions(this, config.extensions);
|
|
145
|
+
}
|
|
127
146
|
}
|
|
128
147
|
|
|
129
|
-
/** @returns {import('./index.js').
|
|
148
|
+
/** @returns {import('./index.js').BuiltinJoiningTransformer} */
|
|
130
149
|
_getJoiningTransformer () {
|
|
131
|
-
|
|
150
|
+
// Engine internals only ever run against a built-in joiner (they touch
|
|
151
|
+
// concrete members such as `_modeConfig`); a custom stub is the caller's
|
|
152
|
+
// responsibility.
|
|
153
|
+
return /** @type {import('./index.js').BuiltinJoiningTransformer} */ (
|
|
154
|
+
this._config.joiningTransformer
|
|
155
|
+
);
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
/**
|
|
@@ -398,9 +422,13 @@ class XPathTransformerContext {
|
|
|
398
422
|
});
|
|
399
423
|
|
|
400
424
|
const preApplyContext = this._contextNode;
|
|
425
|
+
const isSync = Boolean(/** @type {any} */ (this._config).sync);
|
|
401
426
|
|
|
402
|
-
// Process each node
|
|
403
|
-
|
|
427
|
+
// Process each node. `maybeAsyncLoop` keeps this a plain synchronous
|
|
428
|
+
// loop unless a template returns a Promise (e.g. via
|
|
429
|
+
// `await this.indexedDB(...)`), in which case it chains and the whole
|
|
430
|
+
// call becomes asynchronous.
|
|
431
|
+
const loopResult = maybeAsyncLoop(nodes, (node) => {
|
|
404
432
|
// Path resolution simplified (could track full XPath if needed)
|
|
405
433
|
const pathMatchedTemplates = modeMatched.filter((t) => {
|
|
406
434
|
// Basic matching: template.path is XPath tested for existence
|
|
@@ -438,7 +466,7 @@ class XPathTransformerContext {
|
|
|
438
466
|
}
|
|
439
467
|
if (onNoMatch === 'deep-skip') {
|
|
440
468
|
// Skip this node entirely
|
|
441
|
-
|
|
469
|
+
return undefined;
|
|
442
470
|
}
|
|
443
471
|
if (onNoMatch === 'shallow-copy') {
|
|
444
472
|
// Output the node without processing children
|
|
@@ -451,7 +479,7 @@ class XPathTransformerContext {
|
|
|
451
479
|
} else if (node.nodeType === 3 && node.nodeValue) { // Text
|
|
452
480
|
joiner.text(node.nodeValue);
|
|
453
481
|
}
|
|
454
|
-
|
|
482
|
+
return undefined;
|
|
455
483
|
}
|
|
456
484
|
if (onNoMatch === 'deep-copy') {
|
|
457
485
|
// Output the node and all descendants
|
|
@@ -462,7 +490,7 @@ class XPathTransformerContext {
|
|
|
462
490
|
} else if (node.nodeType === 3 && node.nodeValue) { // Text
|
|
463
491
|
joiner.text(node.nodeValue);
|
|
464
492
|
}
|
|
465
|
-
|
|
493
|
+
return undefined;
|
|
466
494
|
}
|
|
467
495
|
if (onNoMatch === 'text-only-copy') {
|
|
468
496
|
// Output only text content
|
|
@@ -474,7 +502,7 @@ class XPathTransformerContext {
|
|
|
474
502
|
joiner.text(textContent);
|
|
475
503
|
}
|
|
476
504
|
}
|
|
477
|
-
|
|
505
|
+
return undefined;
|
|
478
506
|
}
|
|
479
507
|
// 'apply-templates', 'shallow-skip', or other:
|
|
480
508
|
// use default template rules
|
|
@@ -569,11 +597,45 @@ class XPathTransformerContext {
|
|
|
569
597
|
const prevTemplateParams = this._params;
|
|
570
598
|
this._params = {0: node};
|
|
571
599
|
|
|
600
|
+
/**
|
|
601
|
+
* The template may return synchronously or return a Promise (e.g. from
|
|
602
|
+
* `await this.indexedDB(...)`), which is awaited unless `config.sync`.
|
|
603
|
+
* @type {any}
|
|
604
|
+
*/
|
|
572
605
|
const ret = templateObj.template.call(this, node, {mode});
|
|
573
606
|
|
|
574
607
|
// Restore previous parameter context
|
|
575
608
|
this._params = prevTemplateParams;
|
|
576
609
|
|
|
610
|
+
if (ret !== null && typeof ret !== 'undefined' &&
|
|
611
|
+
typeof ret.then === 'function') {
|
|
612
|
+
if (isSync) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
'A template returned a Promise but JTLT is configured with ' +
|
|
615
|
+
'`sync: true`.'
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
619
|
+
// eslint-disable-next-line promise/prefer-await-to-then -- intentional dynamic sync/async
|
|
620
|
+
return ret.then((/** @type {any} */ resolvedRet) => {
|
|
621
|
+
if (typeof resolvedRet !== 'undefined') {
|
|
622
|
+
const joiner = this._getJoiningTransformer();
|
|
623
|
+
/* c8 ignore start -- _openTagState only on
|
|
624
|
+
StringJoiningTransformer; string-output defensive check */
|
|
625
|
+
// @ts-expect-error -- _openTagState: StringJoiningTransformer only
|
|
626
|
+
if (joiner._openTagState) {
|
|
627
|
+
joiner.append('>');
|
|
628
|
+
// @ts-expect-error -- _openTagState: StringJoiningTransformer
|
|
629
|
+
joiner._openTagState = false;
|
|
630
|
+
}
|
|
631
|
+
/* c8 ignore stop */
|
|
632
|
+
joiner.append(resolvedRet);
|
|
633
|
+
}
|
|
634
|
+
this._contextNode = node;
|
|
635
|
+
return undefined;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
577
639
|
if (typeof ret !== 'undefined') {
|
|
578
640
|
const joiner = this._getJoiningTransformer();
|
|
579
641
|
// Close any open tag before appending template return value
|
|
@@ -586,6 +648,18 @@ class XPathTransformerContext {
|
|
|
586
648
|
joiner.append(ret);
|
|
587
649
|
}
|
|
588
650
|
this._contextNode = node; // Restore (placeholder for more complex state)
|
|
651
|
+
return undefined;
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
if (typeof loopResult?.then === 'function') {
|
|
655
|
+
return /** @type {any} */ (
|
|
656
|
+
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
657
|
+
// eslint-disable-next-line promise/prefer-await-to-then -- intentional dynamic sync/async
|
|
658
|
+
loopResult.then(() => {
|
|
659
|
+
this._contextNode = preApplyContext;
|
|
660
|
+
return this;
|
|
661
|
+
})
|
|
662
|
+
);
|
|
589
663
|
}
|
|
590
664
|
|
|
591
665
|
this._contextNode = preApplyContext;
|
|
@@ -691,7 +765,10 @@ class XPathTransformerContext {
|
|
|
691
765
|
* @param {string} [options.groupEndingWith] - Ends group when expression
|
|
692
766
|
* matches
|
|
693
767
|
* @param {(
|
|
694
|
-
* this: XPathTransformerContext,
|
|
768
|
+
* this: XPathTransformerContext,
|
|
769
|
+
* key: unknown,
|
|
770
|
+
* items: Node[],
|
|
771
|
+
* ctx: XPathTransformerContext
|
|
695
772
|
* ) => void} cb - Callback receives (groupingKey, groupItems, context)
|
|
696
773
|
* @returns {this}
|
|
697
774
|
*/
|
|
@@ -919,25 +996,77 @@ class XPathTransformerContext {
|
|
|
919
996
|
|
|
920
997
|
/**
|
|
921
998
|
* Returns the current grouping key (for use within forEachGroup callback).
|
|
922
|
-
* @returns {
|
|
999
|
+
* @returns {unknown}
|
|
923
1000
|
*/
|
|
924
1001
|
currentGroupingKey () {
|
|
925
1002
|
return /** @type {any} */ (this)._currentGroupingKey;
|
|
926
1003
|
}
|
|
927
1004
|
|
|
1005
|
+
/**
|
|
1006
|
+
* Directly query IndexedDB from within a template, e.g.
|
|
1007
|
+
* `await this.indexedDB('myDB', 'myStore', {index: 'byAge'})`.
|
|
1008
|
+
*
|
|
1009
|
+
* Since IndexedDB access is asynchronous, this is unavailable when JTLT is
|
|
1010
|
+
* configured with `sync: true`.
|
|
1011
|
+
* @param {string} dbName - Database name
|
|
1012
|
+
* @param {string} storeName - Object store name
|
|
1013
|
+
* @param {import('./indexedDB.js').QueryOptions} [options] - Query options
|
|
1014
|
+
* @returns {Promise<unknown[]>} The matching records
|
|
1015
|
+
*/
|
|
1016
|
+
indexedDB (dbName, storeName, options) {
|
|
1017
|
+
if (/** @type {any} */ (this._config).sync) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
'The `indexedDB()` API is unavailable when JTLT is configured with ' +
|
|
1020
|
+
'`sync: true`.'
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
return queryIndexedDB(dbName, storeName, options);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Evaluate an XPath selector that calls the `jtlt:indexedDB(...)` function,
|
|
1028
|
+
* awaiting the underlying IndexedDB reads, then append the string result.
|
|
1029
|
+
* Used by {@link valueOf}. Callers reject `config.sync`.
|
|
1030
|
+
* @param {string} selectStr
|
|
1031
|
+
* @returns {Promise<XPathTransformerContext>}
|
|
1032
|
+
*/
|
|
1033
|
+
async _appendIndexedDBValue (selectStr) {
|
|
1034
|
+
const value = await evaluateXPathWithIndexedDB(
|
|
1035
|
+
selectStr, this._contextNode
|
|
1036
|
+
);
|
|
1037
|
+
this._getJoiningTransformer().text(value);
|
|
1038
|
+
return this;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
928
1041
|
/**
|
|
929
1042
|
* Append the value from an XPath expression or the context node text.
|
|
930
1043
|
* @param {string|object} [select]
|
|
931
1044
|
* @returns {XPathTransformerContext}
|
|
932
1045
|
*/
|
|
933
1046
|
valueOf (select) {
|
|
934
|
-
const jt = this._getJoiningTransformer();
|
|
935
|
-
let val;
|
|
936
|
-
|
|
937
1047
|
const selectStr = typeof select === 'object'
|
|
938
1048
|
? /** @type {{select?: string}} */ (select).select
|
|
939
1049
|
: select;
|
|
940
1050
|
|
|
1051
|
+
// `jtlt:indexedDB(...)` is a registered XPath function backed by async
|
|
1052
|
+
// IndexedDB reads. When it appears in the selector, evaluate the whole
|
|
1053
|
+
// expression asynchronously (fontoxpath itself stays synchronous) and
|
|
1054
|
+
// return a Promise so templates can `await this.valueOf(...)`.
|
|
1055
|
+
if (xpathExpressionUsesIndexedDB(selectStr)) {
|
|
1056
|
+
if (/** @type {any} */ (this._config).sync) {
|
|
1057
|
+
throw new Error(
|
|
1058
|
+
'The `indexedDB()` XPath function is unavailable when JTLT is ' +
|
|
1059
|
+
'configured with `sync: true`.'
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
return /** @type {any} */ (
|
|
1063
|
+
this._appendIndexedDBValue(/** @type {string} */ (selectStr))
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const jt = this._getJoiningTransformer();
|
|
1068
|
+
let val;
|
|
1069
|
+
|
|
941
1070
|
// Check if select is a custom function call: prefix:name(...)
|
|
942
1071
|
// Try to parse and invoke registered function
|
|
943
1072
|
if (selectStr && (/^[^:]+:[^\(]+\(/v).test(selectStr)) {
|
|
@@ -1039,7 +1168,7 @@ class XPathTransformerContext {
|
|
|
1039
1168
|
const resultToAppend = Array.isArray(result)
|
|
1040
1169
|
? result.join(' ')
|
|
1041
1170
|
: result;
|
|
1042
|
-
jt.append(resultToAppend);
|
|
1171
|
+
jt.append(/** @type {string | Node} */ (resultToAppend));
|
|
1043
1172
|
return this;
|
|
1044
1173
|
}
|
|
1045
1174
|
/* c8 ignore start -- Error handler for malformed function calls */
|
|
@@ -2002,8 +2131,8 @@ class XPathTransformerContext {
|
|
|
2002
2131
|
/**
|
|
2003
2132
|
* Invoke a registered stylesheet function with positional arguments.
|
|
2004
2133
|
* @param {string} name - Function name (with namespace)
|
|
2005
|
-
* @param {
|
|
2006
|
-
* @returns {
|
|
2134
|
+
* @param {unknown[]} args - Positional arguments
|
|
2135
|
+
* @returns {unknown} Function return value
|
|
2007
2136
|
*/
|
|
2008
2137
|
invokeFunctionByArity (name, args = []) {
|
|
2009
2138
|
return this._getJoiningTransformer().invokeFunctionByArity(name, args);
|