jtlt 0.17.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.
- package/.gitignore +3 -0
- package/.npmignore +3 -0
- package/CHANGES.md +14 -0
- package/README.md +76 -0
- package/demo/vendor/jamilih/dist/jml.mjs +898 -25
- package/dist/DOMJoiningTransformer.d.ts +73 -25
- package/dist/DOMJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONJoiningTransformer.d.ts +5 -3
- package/dist/JSONJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +106 -19
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/StringJoiningTransformer.d.ts +8 -3
- package/dist/StringJoiningTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +44 -0
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index.d.ts +104 -261
- package/dist/index.d.ts.map +1 -1
- package/dist/jsonTemplate.d.ts +37 -0
- package/dist/jsonTemplate.d.ts.map +1 -0
- package/docs/API.expanded.md +75 -1
- package/docs/API.md +20 -0
- package/docs/TO-DO.md +10 -4
- package/package.json +6 -6
- package/pnpm-workspace.yaml +4 -1
- package/src/DOMJoiningTransformer.js +37 -11
- package/src/JSONJoiningTransformer.js +94 -67
- package/src/JSONPathTransformer.js +40 -10
- package/src/JSONPathTransformerContext.js +337 -47
- package/src/StringJoiningTransformer.js +35 -9
- package/src/XPathTransformerContext.js +140 -2
- package/src/index.js +115 -4
- package/src/jsonTemplate.js +410 -0
|
@@ -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
|
-
|
|
623
|
-
|
|
624
|
-
)
|
|
625
|
-
|
|
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
|
-
|
|
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 =
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
}
|
|
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
|
|
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.
|
|
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
|
|
1679
|
-
* runtime `config.params`, so
|
|
1680
|
-
*
|
|
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
|
|
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
|
|
|
@@ -2553,6 +2701,14 @@ class JSONPathTransformerContext {
|
|
|
2553
2701
|
* with `param()`, supplied via `withParam()`, or provided at runtime as
|
|
2554
2702
|
* `config.params` — rather than a JSONPath expression.
|
|
2555
2703
|
*
|
|
2704
|
+
* The test may also be a simple binary comparison of a reference against a
|
|
2705
|
+
* literal, evaluated without `eval`, e.g. `this.if('$name === "x"')` or
|
|
2706
|
+
* `this.if('$.count < 50')`. The left side is a bare `$name` parameter or a
|
|
2707
|
+
* plain dotted/indexed `$...` path; the operator is one of `===`, `!==`,
|
|
2708
|
+
* `==`, `!=`, `<`, `<=`, `>`, `>=`; the right side is a string, number,
|
|
2709
|
+
* boolean, `null`, or `undefined` literal. Anything more complex (filter
|
|
2710
|
+
* expressions, function calls) is left to the JSONPath engine.
|
|
2711
|
+
*
|
|
2556
2712
|
* Truthiness rules:
|
|
2557
2713
|
* - If the selection (with wrap) yields an array with length > 0, the
|
|
2558
2714
|
* condition passes.
|
|
@@ -2561,13 +2717,23 @@ class JSONPathTransformerContext {
|
|
|
2561
2717
|
*
|
|
2562
2718
|
* @param {string} select - JSONPath selector expression
|
|
2563
2719
|
* @param {(this: JSONPathTransformerContext<T>)
|
|
2564
|
-
* => void} cb - Callback to invoke if condition is met
|
|
2565
|
-
*
|
|
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>}
|
|
2566
2725
|
*/
|
|
2567
2726
|
if (select, cb) {
|
|
2568
2727
|
const passes = this._passesIf(select);
|
|
2569
2728
|
if (passes && typeof cb === 'function') {
|
|
2570
|
-
cb
|
|
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
|
+
}
|
|
2571
2737
|
}
|
|
2572
2738
|
return this;
|
|
2573
2739
|
}
|
|
@@ -2613,36 +2779,160 @@ class JSONPathTransformerContext {
|
|
|
2613
2779
|
*/
|
|
2614
2780
|
_passesIf (select) {
|
|
2615
2781
|
if (typeof select === 'string') {
|
|
2616
|
-
const
|
|
2782
|
+
const trimmed = select.trim();
|
|
2783
|
+
const paramRef = trimmed.match(/^\$(?<name>[\w\-]+)$/v);
|
|
2617
2784
|
if (paramRef && paramRef.groups) {
|
|
2618
2785
|
const param = this._lookupParam(paramRef.groups.name);
|
|
2619
2786
|
if (param.has) {
|
|
2620
2787
|
return this._isTruthyResult(param.value);
|
|
2621
2788
|
}
|
|
2622
2789
|
}
|
|
2790
|
+
const cmp = this._parseComparison(trimmed);
|
|
2791
|
+
if (cmp) {
|
|
2792
|
+
return this._compareValues(
|
|
2793
|
+
this._resolveComparand(cmp.left), cmp.op, cmp.right
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2623
2796
|
}
|
|
2624
2797
|
// Evaluate with wrapping to detect non-empty match sets
|
|
2625
2798
|
return this._isTruthyResult(this.get(select, true));
|
|
2626
2799
|
}
|
|
2627
2800
|
|
|
2801
|
+
/**
|
|
2802
|
+
* Parse a simple `<ref> <op> <literal>` comparison test (no `eval`), such
|
|
2803
|
+
* as `$name === "x"` or `$.a.b < 50`. The left side must be a bare `$name`
|
|
2804
|
+
* parameter reference or a plain dotted/indexed `$...` path — no filter
|
|
2805
|
+
* expressions or function calls; the right side a string, number, boolean,
|
|
2806
|
+
* `null`, or `undefined` literal. Returns `null` when the string is not
|
|
2807
|
+
* such a comparison, so richer JSONPath expressions fall through untouched.
|
|
2808
|
+
* @param {string} str
|
|
2809
|
+
* @returns {{left: string, op: string, right: unknown}|null}
|
|
2810
|
+
* @private
|
|
2811
|
+
*/
|
|
2812
|
+
_parseComparison (str) {
|
|
2813
|
+
const m = str.match(
|
|
2814
|
+
/^(?<left>\$[\w$\-]*(?:\.[\w$\-]+|\[\d+\]|\['[^']*'\]|\["[^"]*"\])*)\s*(?<op>===|!==|==|!=|<=|>=|<|>)\s*(?<right>.+)$/v
|
|
2815
|
+
);
|
|
2816
|
+
if (!m || !m.groups) {
|
|
2817
|
+
return null;
|
|
2818
|
+
}
|
|
2819
|
+
const right = this._parseLiteral(m.groups.right.trim());
|
|
2820
|
+
if (!right) {
|
|
2821
|
+
return null;
|
|
2822
|
+
}
|
|
2823
|
+
return {left: m.groups.left, op: m.groups.op, right: right.value};
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
/**
|
|
2827
|
+
* Parse a JSON-ish scalar literal: a double- or single-quoted string, a
|
|
2828
|
+
* number, or `true` / `false` / `null` / `undefined`. Returns `null` when
|
|
2829
|
+
* `str` is none of these.
|
|
2830
|
+
* @param {string} str
|
|
2831
|
+
* @returns {{value: unknown}|null}
|
|
2832
|
+
* @private
|
|
2833
|
+
*/
|
|
2834
|
+
// eslint-disable-next-line class-methods-use-this -- pure helper
|
|
2835
|
+
_parseLiteral (str) {
|
|
2836
|
+
if ((/^"(?:[^"\\]|\\.)*"$/v).test(str)) {
|
|
2837
|
+
return {value: JSON.parse(str)};
|
|
2838
|
+
}
|
|
2839
|
+
if ((/^'(?:[^'\\]|\\.)*'$/v).test(str)) {
|
|
2840
|
+
return {value: str.slice(1, -1).replaceAll(/\\(?=['\\])/gv, '')};
|
|
2841
|
+
}
|
|
2842
|
+
if ((/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+\-]?\d+)?$/v).test(str)) {
|
|
2843
|
+
return {value: Number(str)};
|
|
2844
|
+
}
|
|
2845
|
+
if (str === 'true') {
|
|
2846
|
+
return {value: true};
|
|
2847
|
+
}
|
|
2848
|
+
if (str === 'false') {
|
|
2849
|
+
return {value: false};
|
|
2850
|
+
}
|
|
2851
|
+
if (str === 'null') {
|
|
2852
|
+
return {value: null};
|
|
2853
|
+
}
|
|
2854
|
+
if (str === 'undefined') {
|
|
2855
|
+
return {value: undefined};
|
|
2856
|
+
}
|
|
2857
|
+
return null;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
/**
|
|
2861
|
+
* Resolve the left side of a simple comparison: a bare `$name` parameter
|
|
2862
|
+
* reference (local, with-param, then runtime `config.params`), otherwise a
|
|
2863
|
+
* plain `$...` path evaluated (unwrapped) in the current context.
|
|
2864
|
+
* @param {string} ref
|
|
2865
|
+
* @returns {unknown}
|
|
2866
|
+
* @private
|
|
2867
|
+
*/
|
|
2868
|
+
_resolveComparand (ref) {
|
|
2869
|
+
const paramRef = ref.match(/^\$(?<name>[\w\-]+)$/v);
|
|
2870
|
+
if (paramRef && paramRef.groups) {
|
|
2871
|
+
const param = this._lookupParam(paramRef.groups.name);
|
|
2872
|
+
if (param.has) {
|
|
2873
|
+
return param.value;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return this.get(ref, false);
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
/**
|
|
2880
|
+
* Apply a comparison operator to two already-resolved values.
|
|
2881
|
+
* @param {any} a - Left operand
|
|
2882
|
+
* @param {string} op - One of `===`, `!==`, `==`, `!=`, `<`, `<=`, `>`, `>=`
|
|
2883
|
+
* @param {any} b - Right operand
|
|
2884
|
+
* @returns {boolean}
|
|
2885
|
+
* @private
|
|
2886
|
+
*/
|
|
2887
|
+
// eslint-disable-next-line class-methods-use-this -- pure helper
|
|
2888
|
+
_compareValues (a, op, b) {
|
|
2889
|
+
/* eslint-disable eqeqeq -- `==`/`!=` are deliberately offered */
|
|
2890
|
+
switch (op) {
|
|
2891
|
+
case '===': return a === b;
|
|
2892
|
+
case '!==': return a !== b;
|
|
2893
|
+
case '==': return a == b;
|
|
2894
|
+
case '!=': return a != b;
|
|
2895
|
+
case '<': return a < b;
|
|
2896
|
+
case '<=': return a <= b;
|
|
2897
|
+
case '>': return a > b;
|
|
2898
|
+
case '>=': return a >= b;
|
|
2899
|
+
/* c8 ignore next -- the regex only yields the operators handled above */
|
|
2900
|
+
default: return false;
|
|
2901
|
+
}
|
|
2902
|
+
/* eslint-enable eqeqeq -- restore */
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2628
2905
|
/**
|
|
2629
2906
|
* Like `if()`, but also supports an optional fallback callback executed
|
|
2630
2907
|
* when the test does not pass (similar to xsl:choose/xsl:otherwise).
|
|
2631
2908
|
* @param {string} select JSONPath selector
|
|
2632
2909
|
* @param {(this: JSONPathTransformerContext<T>)
|
|
2633
|
-
* => 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.)
|
|
2634
2914
|
* @param {(this: JSONPathTransformerContext<T>)
|
|
2635
|
-
* => void} [otherwiseCb] Callback when condition fails
|
|
2636
|
-
*
|
|
2915
|
+
* => void} [otherwiseCb] Callback when condition fails; may likewise
|
|
2916
|
+
* be async.
|
|
2917
|
+
* @returns {this|Promise<this>}
|
|
2637
2918
|
*/
|
|
2638
2919
|
choose (select, whenCb, otherwiseCb) {
|
|
2639
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;
|
|
2640
2926
|
if (passes) {
|
|
2641
2927
|
if (typeof whenCb === 'function') {
|
|
2642
|
-
whenCb.call(this);
|
|
2928
|
+
ret = whenCb.call(this);
|
|
2643
2929
|
}
|
|
2644
2930
|
} else if (typeof otherwiseCb === 'function') {
|
|
2645
|
-
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);
|
|
2646
2936
|
}
|
|
2647
2937
|
return this;
|
|
2648
2938
|
}
|