jtlt 0.14.0 → 0.16.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 +16 -0
- package/README.md +5 -4
- package/demo/codemirror.esm.js +50 -15
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +102 -0
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +104 -0
- 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 +30 -3
- package/dist/index.d.ts.map +1 -1
- package/docs/TO-DO.md +15 -1
- package/package.json +19 -14
- package/pnpm-workspace.yaml +4 -4
- package/src/JSONPathTransformer.js +3 -1
- package/src/JSONPathTransformerContext.js +162 -12
- package/src/XPathTransformerContext.js +162 -9
- package/src/context-extensions.d.ts +24 -0
- package/src/extendContext.js +21 -0
- package/src/index.js +22 -3
- package/tsconfig.json +2 -2
|
@@ -3,6 +3,7 @@ import xpath2 from 'xpath2.js'; // Runtime JS import; ambient types declared
|
|
|
3
3
|
// xpathVersion: 1 => browser/native XPathEvaluator API; 2 => xpath2.js, 3 => fontoxpath
|
|
4
4
|
import fontoxpath from 'fontoxpath';
|
|
5
5
|
import {maybeAsyncLoop} from './maybeAsync.js';
|
|
6
|
+
import applyExtensions from './extendContext.js';
|
|
6
7
|
import {
|
|
7
8
|
queryIndexedDB,
|
|
8
9
|
xpathExpressionUsesIndexedDB,
|
|
@@ -30,6 +31,14 @@ const escapeRegexReplacement = (string) => {
|
|
|
30
31
|
* @property {boolean} [preventEval] - Whether to prevent eval in the JSONPath
|
|
31
32
|
* trailing segment of an `indexedDB(...)` expression
|
|
32
33
|
* @property {(path: string) => number} [specificityPriorityResolver]
|
|
34
|
+
* @property {Record<string, unknown>} [params] Runtime parameter values
|
|
35
|
+
* (like an XSLT processor's stylesheet parameters); a `param()` with a
|
|
36
|
+
* matching name uses this value instead of its declared default
|
|
37
|
+
* @property {Record<string, unknown> & ThisType<
|
|
38
|
+
* import('./XPathTransformerContext.js').default &
|
|
39
|
+
* import('./context-extensions.js').ContextExtensions
|
|
40
|
+
* >} [extensions] Extra methods/values
|
|
41
|
+
* merged onto this context so templates can call `this.myHelper()`
|
|
33
42
|
*/
|
|
34
43
|
|
|
35
44
|
/**
|
|
@@ -130,10 +139,26 @@ class XPathTransformerContext {
|
|
|
130
139
|
this._currPath = undefined; // XPath string of current context
|
|
131
140
|
/** @type {Record<string, any> | undefined} */
|
|
132
141
|
this._params = undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Parameter values supplied at runtime via `config.params`, mirroring the
|
|
144
|
+
* stylesheet parameters an XSLT processor is handed. A `param()` whose
|
|
145
|
+
* name appears here takes this value instead of its declared default.
|
|
146
|
+
* @type {Record<string, unknown>}
|
|
147
|
+
*/
|
|
148
|
+
this._runtimeParams = /** @type {any} */ (config).params || {};
|
|
149
|
+
/**
|
|
150
|
+
* Parameters staged by `withParam()` and consumed (then cleared) by the
|
|
151
|
+
* next `callTemplate()` or `applyTemplates()` call.
|
|
152
|
+
* @type {{name: string, select?: string, value?: unknown}[] | undefined}
|
|
153
|
+
*/
|
|
154
|
+
this._pendingParams = undefined;
|
|
133
155
|
/** @type {string[]} */
|
|
134
156
|
this._preserveSpaceElements = [];
|
|
135
157
|
/** @type {string[]} */
|
|
136
158
|
this._stripSpaceElements = [];
|
|
159
|
+
if (config.extensions) {
|
|
160
|
+
applyExtensions(this, config.extensions);
|
|
161
|
+
}
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
/** @returns {import('./index.js').BuiltinJoiningTransformer} */
|
|
@@ -402,6 +427,12 @@ class XPathTransformerContext {
|
|
|
402
427
|
} else {
|
|
403
428
|
select ||= '*';
|
|
404
429
|
}
|
|
430
|
+
// Resolve params staged via `this.withParam()` in the calling context,
|
|
431
|
+
// once, before iterating; each matched template receives them
|
|
432
|
+
// (`xsl:apply-templates` / `xsl:with-param`).
|
|
433
|
+
/** @type {Record<string, unknown>} */
|
|
434
|
+
const appliedParams = {};
|
|
435
|
+
this._drainPendingParams(appliedParams);
|
|
405
436
|
const nodesResult = this._evalXPath(select, true);
|
|
406
437
|
const nodes = /** @type {Node[]} */ (nodesResult);
|
|
407
438
|
const modeMatched = this._templates.filter((t) => {
|
|
@@ -586,7 +617,7 @@ class XPathTransformerContext {
|
|
|
586
617
|
|
|
587
618
|
// Set up parameter context for valueOf() access in templates
|
|
588
619
|
const prevTemplateParams = this._params;
|
|
589
|
-
this._params = {0: node};
|
|
620
|
+
this._params = {0: node, ...appliedParams};
|
|
590
621
|
|
|
591
622
|
/**
|
|
592
623
|
* The template may return synchronously or return a Promise (e.g. from
|
|
@@ -679,6 +710,10 @@ class XPathTransformerContext {
|
|
|
679
710
|
const params = {};
|
|
680
711
|
this._params = params;
|
|
681
712
|
|
|
713
|
+
// Params staged via `this.withParam()` seed the set; explicit `withParam`
|
|
714
|
+
// entries below override any of the same name (`xsl:with-param`).
|
|
715
|
+
this._drainPendingParams(params);
|
|
716
|
+
|
|
682
717
|
withParams.forEach((withParam, index) => {
|
|
683
718
|
const value = withParam.value !== undefined
|
|
684
719
|
? withParam.value
|
|
@@ -1143,9 +1178,8 @@ class XPathTransformerContext {
|
|
|
1143
1178
|
// Parameter reference
|
|
1144
1179
|
if (arg.startsWith('$')) {
|
|
1145
1180
|
const paramName = arg.slice(1);
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
: undefined;
|
|
1181
|
+
const {has, value} = this._lookupParam(paramName);
|
|
1182
|
+
return has ? value : undefined;
|
|
1149
1183
|
}
|
|
1150
1184
|
// XPath expression - evaluate it
|
|
1151
1185
|
return this._evalXPath(arg, false);
|
|
@@ -1184,9 +1218,8 @@ class XPathTransformerContext {
|
|
|
1184
1218
|
if (valueExpr.trimStart().startsWith('$')) {
|
|
1185
1219
|
// Parameter reference
|
|
1186
1220
|
const paramName = valueExpr.trim().slice(1);
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
: 0;
|
|
1221
|
+
const {has, value} = this._lookupParam(paramName);
|
|
1222
|
+
numValue = has ? value : 0;
|
|
1190
1223
|
} else {
|
|
1191
1224
|
// Try to parse as number or evaluate as XPath
|
|
1192
1225
|
const trimmed = valueExpr.trim();
|
|
@@ -1212,8 +1245,9 @@ class XPathTransformerContext {
|
|
|
1212
1245
|
// Check if this is a parameter reference (starts with $)
|
|
1213
1246
|
if (selectStr && selectStr.startsWith('$')) {
|
|
1214
1247
|
const paramName = selectStr.slice(1);
|
|
1215
|
-
|
|
1216
|
-
|
|
1248
|
+
const paramLookup = this._lookupParam(paramName);
|
|
1249
|
+
if (paramLookup.has) {
|
|
1250
|
+
val = paramLookup.value;
|
|
1217
1251
|
// If val is a Node, extract its text content
|
|
1218
1252
|
if (val && typeof val === 'object' && 'nodeType' in val) {
|
|
1219
1253
|
if (val.nodeType === 3) {
|
|
@@ -1372,6 +1406,125 @@ class XPathTransformerContext {
|
|
|
1372
1406
|
this.vars[name] = this.get(select, true);
|
|
1373
1407
|
return this;
|
|
1374
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Normalize a `param()`/`withParam()` default/value argument to `{select}`
|
|
1412
|
+
* or `{value}`: a bare string is an XPath expression, `{value}` is a
|
|
1413
|
+
* literal, and `{select}` (or an omitted argument) is an expression.
|
|
1414
|
+
* @param {string|{select?: string, value?: unknown}|undefined} arg
|
|
1415
|
+
* @returns {{select?: string, value?: unknown}}
|
|
1416
|
+
* @private
|
|
1417
|
+
*/
|
|
1418
|
+
// eslint-disable-next-line class-methods-use-this -- pure helper
|
|
1419
|
+
_paramSpec (arg) {
|
|
1420
|
+
if (typeof arg === 'string') {
|
|
1421
|
+
return {select: arg};
|
|
1422
|
+
}
|
|
1423
|
+
if (arg && 'value' in arg) {
|
|
1424
|
+
return {value: arg.value};
|
|
1425
|
+
}
|
|
1426
|
+
// `{select}` object, or omitted (a default of `undefined`).
|
|
1427
|
+
return {select: arg && arg.select};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* Look up a parameter by name across the active with-param scope and the
|
|
1432
|
+
* runtime `config.params`, so runtime-supplied params act like XSLT global
|
|
1433
|
+
* parameters (visible to every template and expression).
|
|
1434
|
+
* @param {string} name
|
|
1435
|
+
* @returns {{has: boolean, value: any}}
|
|
1436
|
+
* @private
|
|
1437
|
+
*/
|
|
1438
|
+
_lookupParam (name) {
|
|
1439
|
+
if (this._params && Object.hasOwn(this._params, name)) {
|
|
1440
|
+
return {has: true, value: this._params[name]};
|
|
1441
|
+
}
|
|
1442
|
+
if (Object.hasOwn(this._runtimeParams, name)) {
|
|
1443
|
+
return {has: true, value: this._runtimeParams[name]};
|
|
1444
|
+
}
|
|
1445
|
+
return {has: false, value: undefined};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Resolve a `{select}` or `{value}` parameter spec to its value in the
|
|
1450
|
+
* current context. An XPath `select` is resolved to a first/atomic value
|
|
1451
|
+
* (as `callTemplate`'s `withParam` already does), so `$name` references
|
|
1452
|
+
* render as a string rather than a node object.
|
|
1453
|
+
* @param {{select?: string, value?: unknown}} spec
|
|
1454
|
+
* @returns {any}
|
|
1455
|
+
* @private
|
|
1456
|
+
*/
|
|
1457
|
+
_resolveParam (spec) {
|
|
1458
|
+
if ('value' in spec) {
|
|
1459
|
+
return spec.value;
|
|
1460
|
+
}
|
|
1461
|
+
if (spec.select === undefined) {
|
|
1462
|
+
return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
const resolved = this.get(spec.select, false);
|
|
1465
|
+
// A node-set `select` yields an array; bind the first node so a `$name`
|
|
1466
|
+
// reference renders as its string-value (XSLT behavior).
|
|
1467
|
+
return Array.isArray(resolved) ? resolved[0] : resolved;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Resolve staged `withParam()` entries into `target` (in the calling
|
|
1472
|
+
* context) and clear the staged set. Shared by `callTemplate()` and
|
|
1473
|
+
* `applyTemplates()`.
|
|
1474
|
+
* @param {Record<string, unknown>} target
|
|
1475
|
+
* @returns {void}
|
|
1476
|
+
* @private
|
|
1477
|
+
*/
|
|
1478
|
+
_drainPendingParams (target) {
|
|
1479
|
+
const pending = this._pendingParams;
|
|
1480
|
+
this._pendingParams = undefined;
|
|
1481
|
+
if (!pending) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
for (const entry of pending) {
|
|
1485
|
+
target[entry.name] = this._resolveParam(entry);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Declare a template parameter, equivalent to `xsl:param`. Binds `name` to
|
|
1491
|
+
* the given default, unless a value was supplied by the caller (via
|
|
1492
|
+
* `this.withParam()` or `callTemplate`'s `withParam`) or at runtime (via
|
|
1493
|
+
* `config.params`), in which case the supplied value wins.
|
|
1494
|
+
* @param {string} name - Parameter name
|
|
1495
|
+
* @param {string|{select: string}|{value: unknown}} [select] - The default:
|
|
1496
|
+
* an XPath expression string, an explicit `{select}`, or a literal
|
|
1497
|
+
* `{value}`. Omitted means a default of `undefined`.
|
|
1498
|
+
* @returns {XPathTransformerContext}
|
|
1499
|
+
*/
|
|
1500
|
+
param (name, select) {
|
|
1501
|
+
this._params ||= {};
|
|
1502
|
+
const supplied = this._lookupParam(name);
|
|
1503
|
+
// A caller's with-param or a `config.params` value overrides the default.
|
|
1504
|
+
const resolved = supplied.has
|
|
1505
|
+
? supplied.value
|
|
1506
|
+
: this._resolveParam(this._paramSpec(select));
|
|
1507
|
+
this._params[name] = resolved;
|
|
1508
|
+
this.vars[name] = resolved;
|
|
1509
|
+
return this;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* Stage a parameter for the next `callTemplate()` or `applyTemplates()`
|
|
1514
|
+
* call, equivalent to `xsl:with-param`. The staged set is consumed and
|
|
1515
|
+
* cleared by that call; entries are evaluated in the current (calling)
|
|
1516
|
+
* context.
|
|
1517
|
+
* @param {string} name - Parameter name
|
|
1518
|
+
* @param {string|{select: string}|{value: unknown}} [select] - An XPath
|
|
1519
|
+
* expression string, an explicit `{select}`, or a literal `{value}`.
|
|
1520
|
+
* @returns {XPathTransformerContext}
|
|
1521
|
+
*/
|
|
1522
|
+
withParam (name, select) {
|
|
1523
|
+
this._pendingParams ||= [];
|
|
1524
|
+
this._pendingParams.push({name, ...this._paramSpec(select)});
|
|
1525
|
+
return this;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1375
1528
|
/**
|
|
1376
1529
|
* Log a message (for debugging).
|
|
1377
1530
|
* @param {unknown} json Any value
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-augmentable registry of the helper methods/values added to a
|
|
3
|
+
* template context through the `extensions` option.
|
|
4
|
+
*
|
|
5
|
+
* `extensions` are merged onto the `this` seen inside templates at runtime,
|
|
6
|
+
* but TypeScript cannot see them unless they are declared here. Augment this
|
|
7
|
+
* interface with declaration merging so calls like `this.myHelper()` inside a
|
|
8
|
+
* template type-check without suppressions:
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* declare module 'jtlt/context-extensions' {
|
|
13
|
+
* interface ContextExtensions {
|
|
14
|
+
* myHelper (): void;
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Leaving it un-augmented keeps templates strict: an undeclared `this.foo()`
|
|
20
|
+
* is still a type error.
|
|
21
|
+
*
|
|
22
|
+
* Intentionally empty; consumers populate it via declaration merging.
|
|
23
|
+
*/
|
|
24
|
+
export interface ContextExtensions {}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merges `extensions` onto a template context instance so templates can
|
|
3
|
+
* invoke them as `this.myHelper()`. Shared by JSONPathTransformerContext
|
|
4
|
+
* and XPathTransformerContext.
|
|
5
|
+
* @param {object} context - The context instance to extend
|
|
6
|
+
* @param {Record<string, unknown>} extensions - Extra methods/values
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
9
|
+
export default function applyExtensions (context, extensions) {
|
|
10
|
+
for (const key of Object.keys(extensions)) {
|
|
11
|
+
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
12
|
+
// eslint-disable-next-line unicorn/no-computed-property-existence-check -- Needed
|
|
13
|
+
if (key in context) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Extension property "${key}" conflicts with an existing ` +
|
|
16
|
+
'context property.'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
Object.assign(context, extensions);
|
|
21
|
+
}
|
package/src/index.js
CHANGED
|
@@ -44,11 +44,15 @@ export const setWindow = (win) => {
|
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* A callable template function with an engine-specific `this`.
|
|
47
|
+
* A callable template function with an engine-specific `this`. The `this`
|
|
48
|
+
* type is intersected with {@link ContextExtensions} so helpers registered
|
|
49
|
+
* through the `extensions` option are visible once a consumer augments that
|
|
50
|
+
* interface.
|
|
48
51
|
* @template T
|
|
49
52
|
* @template U
|
|
50
53
|
* @template TCtx
|
|
51
|
-
* @typedef {(this: TCtx
|
|
54
|
+
* @typedef {(this: TCtx &
|
|
55
|
+
* import('./context-extensions.js').ContextExtensions,
|
|
52
56
|
* value: ResultType<U>,
|
|
53
57
|
* cfg?: {mode?: string}
|
|
54
58
|
* ) => ResultType<T>|void|Promise<ResultType<T>|void>} TemplateFunction
|
|
@@ -214,6 +218,11 @@ export const setWindow = (win) => {
|
|
|
214
218
|
* transformer.
|
|
215
219
|
* @property {object} [parent] Parent object for context
|
|
216
220
|
* @property {string} [parentProperty] Parent property name for context
|
|
221
|
+
* @property {Record<string, unknown>} [params] Parameter values supplied at
|
|
222
|
+
* runtime, mirroring the stylesheet parameters an XSLT processor is handed.
|
|
223
|
+
* A `this.param(name, default)` declaration whose name appears here resolves
|
|
224
|
+
* to this value instead of its default, and `$name` references such a
|
|
225
|
+
* parameter from any template.
|
|
217
226
|
*/
|
|
218
227
|
|
|
219
228
|
/**
|
|
@@ -233,6 +242,10 @@ export const setWindow = (win) => {
|
|
|
233
242
|
* forQuery?: [string, TemplateFunction<T, "json",
|
|
234
243
|
* import('./XPathTransformerContext.js').default
|
|
235
244
|
* >],
|
|
245
|
+
* extensions?: Record<string, unknown> & ThisType<
|
|
246
|
+
* import('./JSONPathTransformerContext.js').default<T> &
|
|
247
|
+
* import('./context-extensions.js').ContextExtensions
|
|
248
|
+
* >,
|
|
236
249
|
* engineType?: 'jsonpath',
|
|
237
250
|
* outputType?: T
|
|
238
251
|
* }} JSONPathJTLTOptions
|
|
@@ -255,6 +268,10 @@ export const setWindow = (win) => {
|
|
|
255
268
|
* forQuery?: [string, TemplateFunction<T, "dom",
|
|
256
269
|
* import('./JSONPathTransformerContext.js').default
|
|
257
270
|
* >],
|
|
271
|
+
* extensions?: Record<string, unknown> & ThisType<
|
|
272
|
+
* import('./XPathTransformerContext.js').default &
|
|
273
|
+
* import('./context-extensions.js').ContextExtensions
|
|
274
|
+
* >,
|
|
258
275
|
* engineType: 'xpath',
|
|
259
276
|
* xpathVersion?: 1|2|3.1,
|
|
260
277
|
* outputType?: T
|
|
@@ -476,7 +493,9 @@ class JTLT {
|
|
|
476
493
|
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
477
494
|
// eslint-disable-next-line unicorn/no-array-method-this-argument -- Not array
|
|
478
495
|
this.forEach(path, (arg) => {
|
|
479
|
-
|
|
496
|
+
// `this` carries runtime `extensions`; a consumer's
|
|
497
|
+
// `ContextExtensions` augmentation would otherwise reject it here.
|
|
498
|
+
const ret = fn.call(/** @type {any} */ (this), arg);
|
|
480
499
|
if (typeof ret !== 'undefined') {
|
|
481
500
|
// @ts-expect-error Ok?
|
|
482
501
|
this._getJoiningTransformer().append(ret);
|
package/tsconfig.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"types": ["mocha"]
|
|
12
12
|
},
|
|
13
13
|
"include": [
|
|
14
|
-
"*.js", "*.d.ts", "src/**/*.js", "
|
|
15
|
-
"typings/xpath2-js.d.ts"
|
|
14
|
+
"*.js", "*.d.ts", "src/**/*.js", "src/**/*.d.ts", "demo/**/*.js",
|
|
15
|
+
"test/**/*.js", "test/**/*.d.ts", "typings/xpath2-js.d.ts"
|
|
16
16
|
],
|
|
17
17
|
"exclude": ["node_modules", "test.js", "./dist/**/*.js", "demo/codemirror.esm.js", "demo/vendor"]
|
|
18
18
|
}
|