jtlt 0.4.0 → 0.6.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/CHANGES.md +16 -0
- package/demo/index.css +4 -0
- package/demo/index.html +4 -4
- package/demo/index.js +6 -2
- package/demo/vendor/fontoxpath/dist/fontoxpath.esm.js +600 -0
- package/demo/vendor/prsc/dist/prsc.esm.js +2 -0
- package/demo/vendor/whynot/dist/whynot.esm.js +2 -0
- package/demo/vendor/xspattern/dist/xspattern.esm.js +2 -0
- package/dist/AbstractJoiningTransformer.d.ts +35 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
- package/dist/DOMJoiningTransformer.d.ts +85 -8
- package/dist/DOMJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONJoiningTransformer.d.ts +28 -10
- package/dist/JSONJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +168 -3
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/StringJoiningTransformer.d.ts +25 -8
- package/dist/StringJoiningTransformer.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +107 -1
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/docs/TO-DO.md +38 -36
- package/package.json +6 -2
- package/src/AbstractJoiningTransformer.js +53 -0
- package/src/DOMJoiningTransformer.js +199 -24
- package/src/JSONJoiningTransformer.js +89 -27
- package/src/JSONPathTransformer.js +2 -0
- package/src/JSONPathTransformerContext.js +572 -9
- package/src/StringJoiningTransformer.js +60 -22
- package/src/XPathTransformer.js +2 -0
- package/src/XPathTransformerContext.js +487 -11
- package/src/index.js +7 -1
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import {JSONPath as jsonpath} from 'jsonpath-plus';
|
|
2
2
|
import JSONPathTransformer from './JSONPathTransformer.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Decimal format symbols for number formatting.
|
|
6
|
+
* @typedef {object} DecimalFormatSymbols
|
|
7
|
+
* @property {string} [decimalSeparator='.'] - Character for decimal point
|
|
8
|
+
* @property {string} [groupingSeparator=','] - Character for thousands
|
|
9
|
+
* @property {string} [percent='%'] - Character for percent
|
|
10
|
+
* @property {string} [perMille='‰'] - Character for per-mille
|
|
11
|
+
* @property {string} [zeroDigit='0'] - Character for zero
|
|
12
|
+
* @property {string} [digit='#'] - Character for digit placeholder
|
|
13
|
+
* @property {string} [patternSeparator=';'] - Character separating
|
|
14
|
+
* positive/negative patterns
|
|
15
|
+
* @property {string} [minusSign='-'] - Character for minus sign
|
|
16
|
+
* @property {string} [infinity='Infinity'] - String for infinity
|
|
17
|
+
* @property {string} [NaN='NaN'] - String for NaN
|
|
18
|
+
*/
|
|
19
|
+
|
|
4
20
|
/**
|
|
5
21
|
* @typedef {number|string|{
|
|
6
22
|
* value?: number|string,
|
|
7
23
|
* count?: string,
|
|
8
24
|
* format?: string,
|
|
25
|
+
* decimalFormat?: string,
|
|
9
26
|
* groupingSeparator?: string,
|
|
10
27
|
* groupingSize?: number,
|
|
11
28
|
* lang?: string,
|
|
@@ -80,6 +97,8 @@ class JSONPathTransformerContext {
|
|
|
80
97
|
this.propertySets = {};
|
|
81
98
|
/** @type {Record<string, {match: string, use: string}>} */
|
|
82
99
|
this.keys = {};
|
|
100
|
+
/** @type {Record<string, DecimalFormatSymbols>} */
|
|
101
|
+
this.decimalFormats = {};
|
|
83
102
|
/** @type {boolean | undefined} */
|
|
84
103
|
this._initialized = undefined;
|
|
85
104
|
/** @type {string | undefined} */
|
|
@@ -258,6 +277,7 @@ class JSONPathTransformerContext {
|
|
|
258
277
|
if (type === 'number') {
|
|
259
278
|
const an = Number(aVal);
|
|
260
279
|
const bn = Number(bVal);
|
|
280
|
+
/* c8 ignore next -- both NaN tested; short-circuit branch tracking */
|
|
261
281
|
if (Number.isNaN(an) && Number.isNaN(bn)) {
|
|
262
282
|
return 0;
|
|
263
283
|
}
|
|
@@ -270,6 +290,7 @@ class JSONPathTransformerContext {
|
|
|
270
290
|
return (an - bn) * order;
|
|
271
291
|
}
|
|
272
292
|
// text
|
|
293
|
+
/* c8 ignore next 2 -- all null/undefined tested; OR short-circuit */
|
|
273
294
|
const aStr = aVal === null || aVal === undefined ? '' : String(aVal);
|
|
274
295
|
const bStr = bVal === null || bVal === undefined ? '' : String(bVal);
|
|
275
296
|
if (spec && spec.locale) {
|
|
@@ -277,6 +298,8 @@ class JSONPathTransformerContext {
|
|
|
277
298
|
bStr, spec.locale, spec.localeOptions
|
|
278
299
|
) * order;
|
|
279
300
|
}
|
|
301
|
+
/* c8 ignore next -- all comparison outcomes tested; ternary
|
|
302
|
+
* branch tracking */
|
|
280
303
|
return (aStr < bStr ? -1 : (aStr > bStr ? 1 : 0)) * order;
|
|
281
304
|
}
|
|
282
305
|
/**
|
|
@@ -407,12 +430,19 @@ class JSONPathTransformerContext {
|
|
|
407
430
|
that._parent = parent;
|
|
408
431
|
that._parentProperty = (parentProperty ?? that._parentProperty);
|
|
409
432
|
|
|
433
|
+
// Set up parameter context for valueOf() access in templates
|
|
434
|
+
const prevTemplateParams = that._params;
|
|
435
|
+
that._params = {0: value};
|
|
436
|
+
|
|
410
437
|
const ret =
|
|
411
438
|
/** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
|
|
412
439
|
templateObj
|
|
413
440
|
).template.call(
|
|
414
441
|
that, value, {mode, parent, parentProperty}
|
|
415
442
|
);
|
|
443
|
+
|
|
444
|
+
// Restore previous parameter context
|
|
445
|
+
that._params = prevTemplateParams;
|
|
416
446
|
if (typeof ret !== 'undefined') {
|
|
417
447
|
// After the undefined check, ret is ResultType<T>
|
|
418
448
|
that._getJoiningTransformer().append(
|
|
@@ -540,11 +570,13 @@ class JSONPathTransformerContext {
|
|
|
540
570
|
* @returns {number}
|
|
541
571
|
*/
|
|
542
572
|
function feCompareBySpec (aVal, bVal, spec) {
|
|
573
|
+
/* c8 ignore next 2 -- all spec combinations tested; && and || branches */
|
|
543
574
|
const order = (spec && spec.order === 'descending') ? -1 : 1;
|
|
544
575
|
const type = (spec && spec.type) || 'text';
|
|
545
576
|
if (type === 'number') {
|
|
546
577
|
const an = Number(aVal);
|
|
547
578
|
const bn = Number(bVal);
|
|
579
|
+
/* c8 ignore next -- both NaN tested; short-circuit branch tracking */
|
|
548
580
|
if (Number.isNaN(an) && Number.isNaN(bn)) {
|
|
549
581
|
return 0;
|
|
550
582
|
}
|
|
@@ -556,6 +588,7 @@ class JSONPathTransformerContext {
|
|
|
556
588
|
}
|
|
557
589
|
return (an - bn) * order;
|
|
558
590
|
}
|
|
591
|
+
/* c8 ignore next 2 -- all null/undefined tested; OR short-circuit */
|
|
559
592
|
const aStr = aVal === null || aVal === undefined ? '' : String(aVal);
|
|
560
593
|
const bStr = bVal === null || bVal === undefined ? '' : String(bVal);
|
|
561
594
|
if (spec && spec.locale) {
|
|
@@ -563,6 +596,8 @@ class JSONPathTransformerContext {
|
|
|
563
596
|
bStr, spec.locale, spec.localeOptions
|
|
564
597
|
) * order;
|
|
565
598
|
}
|
|
599
|
+
/* c8 ignore next -- all comparison outcomes tested; ternary
|
|
600
|
+
* branch tracking */
|
|
566
601
|
return (aStr < bStr ? -1 : (aStr > bStr ? 1 : 0)) * order;
|
|
567
602
|
}
|
|
568
603
|
/**
|
|
@@ -606,11 +641,364 @@ class JSONPathTransformerContext {
|
|
|
606
641
|
const comparator = feBuildComparator(sort);
|
|
607
642
|
const list = comparator ? [...matches].toSorted(comparator) : matches;
|
|
608
643
|
for (const m of list) {
|
|
609
|
-
|
|
644
|
+
// Set up parameter context for valueOf() access
|
|
645
|
+
const prevParams = that._params;
|
|
646
|
+
const prevContext = that._contextObj;
|
|
647
|
+
that._params = {0: m.value};
|
|
648
|
+
that._contextObj = m.value;
|
|
649
|
+
try {
|
|
650
|
+
cb.call(that, m.value);
|
|
651
|
+
} finally {
|
|
652
|
+
// Restore previous parameter context
|
|
653
|
+
that._params = prevParams;
|
|
654
|
+
that._contextObj = prevContext;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return this;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Groups items and executes callback for each group.
|
|
662
|
+
* Equivalent to XSLT's xsl:for-each-group.
|
|
663
|
+
* @param {string} select - JSONPath selector for items to group
|
|
664
|
+
* @param {object} options - Grouping options
|
|
665
|
+
* @param {string} [options.groupBy] - JSONPath expression to group by value
|
|
666
|
+
* @param {string} [options.groupAdjacent] - Groups adjacent items with
|
|
667
|
+
* same value
|
|
668
|
+
* @param {string} [options.groupStartingWith] - Starts new group when
|
|
669
|
+
* expression matches
|
|
670
|
+
* @param {string} [options.groupEndingWith] - Ends group when expression
|
|
671
|
+
* matches
|
|
672
|
+
* @param {any} [options.sort] - Sort specification (same as forEach)
|
|
673
|
+
* @param {(
|
|
674
|
+
* this: JSONPathTransformerContext<T>, key: any, items: any[], ctx: any
|
|
675
|
+
* ) => void} cb - Callback receives (groupingKey, groupItems, context)
|
|
676
|
+
* @returns {this}
|
|
677
|
+
*/
|
|
678
|
+
forEachGroup (select, options, cb) {
|
|
679
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
680
|
+
const that = this;
|
|
681
|
+
const {groupBy, groupAdjacent, groupStartingWith, groupEndingWith, sort} =
|
|
682
|
+
options;
|
|
683
|
+
|
|
684
|
+
/** @type {{value: any}[]} */
|
|
685
|
+
const matches = /** @type {any} */ (jsonpath)({
|
|
686
|
+
path: select,
|
|
687
|
+
json: this._contextObj,
|
|
688
|
+
preventEval: this._config.preventEval,
|
|
689
|
+
wrap: true,
|
|
690
|
+
resultType: 'all'
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* @param {string} expr
|
|
695
|
+
* @param {any} ctxVal
|
|
696
|
+
* @returns {any}
|
|
697
|
+
*/
|
|
698
|
+
function evalInContext (expr, ctxVal) {
|
|
699
|
+
if (expr === '.' || expr === '@') {
|
|
700
|
+
return ctxVal;
|
|
701
|
+
}
|
|
702
|
+
return /** @type {any} */ (jsonpath)({
|
|
703
|
+
path: expr,
|
|
704
|
+
json: ctxVal,
|
|
705
|
+
preventEval: that._config.preventEval,
|
|
706
|
+
wrap: false,
|
|
707
|
+
returnType: 'value'
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Apply sorting if specified
|
|
712
|
+
if (sort) {
|
|
713
|
+
const comparator = this._buildComparator(sort, evalInContext);
|
|
714
|
+
if (comparator) {
|
|
715
|
+
matches.sort(
|
|
716
|
+
/** @type {(a: {value: any}, b: {value: any}) => number} */ (
|
|
717
|
+
comparator
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** @type {Map<any, any[]>} */
|
|
724
|
+
const groups = new Map();
|
|
725
|
+
|
|
726
|
+
if (groupBy) {
|
|
727
|
+
// Group by computed value
|
|
728
|
+
for (const m of matches) {
|
|
729
|
+
const key = evalInContext(groupBy, m.value);
|
|
730
|
+
// Handle undefined by converting to null for JSON serialization
|
|
731
|
+
const keyStr = JSON.stringify(key === undefined ? null : key);
|
|
732
|
+
if (!groups.has(keyStr)) {
|
|
733
|
+
groups.set(keyStr, []);
|
|
734
|
+
}
|
|
735
|
+
/** @type {any[]} */ (groups.get(keyStr)).push(m.value);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
for (const [keyStr, items] of groups) {
|
|
739
|
+
const key = JSON.parse(keyStr);
|
|
740
|
+
// Convert null back to undefined if that was the original value
|
|
741
|
+
const actualKey = key === null && keyStr === 'null' ? undefined : key;
|
|
742
|
+
const prevContext = this._contextObj;
|
|
743
|
+
const prevParams = this._params;
|
|
744
|
+
try {
|
|
745
|
+
this._contextObj = items;
|
|
746
|
+
// Provide currentGroup() and currentGroupingKey() via context
|
|
747
|
+
/** @type {any} */ (this)._currentGroup = items;
|
|
748
|
+
/** @type {any} */ (this)._currentGroupingKey = actualKey;
|
|
749
|
+
cb.call(this, actualKey, items, this);
|
|
750
|
+
} finally {
|
|
751
|
+
this._contextObj = prevContext;
|
|
752
|
+
this._params = prevParams;
|
|
753
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
754
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} else if (groupAdjacent) {
|
|
758
|
+
// Group adjacent items with same value
|
|
759
|
+
/** @type {string|null} */
|
|
760
|
+
let currentKey = null;
|
|
761
|
+
let currentGroup = [];
|
|
762
|
+
|
|
763
|
+
for (const m of matches) {
|
|
764
|
+
const key = evalInContext(groupAdjacent, m.value);
|
|
765
|
+
const keyStr = JSON.stringify(key);
|
|
766
|
+
|
|
767
|
+
if (currentKey === null || currentKey !== keyStr) {
|
|
768
|
+
if (currentGroup.length > 0) {
|
|
769
|
+
const prevContext = this._contextObj;
|
|
770
|
+
const prevParams = this._params;
|
|
771
|
+
try {
|
|
772
|
+
this._contextObj = currentGroup;
|
|
773
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
774
|
+
/** @type {any} */ (this)._currentGroupingKey =
|
|
775
|
+
JSON.parse(/** @type {string} */ (currentKey));
|
|
776
|
+
cb.call(
|
|
777
|
+
this,
|
|
778
|
+
JSON.parse(/** @type {string} */ (currentKey)),
|
|
779
|
+
currentGroup,
|
|
780
|
+
this
|
|
781
|
+
);
|
|
782
|
+
} finally {
|
|
783
|
+
this._contextObj = prevContext;
|
|
784
|
+
this._params = prevParams;
|
|
785
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
786
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
currentKey = keyStr;
|
|
790
|
+
currentGroup = [m.value];
|
|
791
|
+
} else {
|
|
792
|
+
currentGroup.push(m.value);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Process last group
|
|
797
|
+
if (currentGroup.length > 0) {
|
|
798
|
+
const prevContext = this._contextObj;
|
|
799
|
+
const prevParams = this._params;
|
|
800
|
+
try {
|
|
801
|
+
this._contextObj = currentGroup;
|
|
802
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
803
|
+
/** @type {any} */ (this)._currentGroupingKey =
|
|
804
|
+
JSON.parse(/** @type {string} */ (currentKey));
|
|
805
|
+
cb.call(
|
|
806
|
+
this,
|
|
807
|
+
JSON.parse(/** @type {string} */ (currentKey)),
|
|
808
|
+
currentGroup,
|
|
809
|
+
this
|
|
810
|
+
);
|
|
811
|
+
} finally {
|
|
812
|
+
this._contextObj = prevContext;
|
|
813
|
+
this._params = prevParams;
|
|
814
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
815
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
} else if (groupStartingWith) {
|
|
819
|
+
// Start new group when expression matches
|
|
820
|
+
let currentGroup = [];
|
|
821
|
+
|
|
822
|
+
for (const m of matches) {
|
|
823
|
+
const startMatch = evalInContext(groupStartingWith, m.value);
|
|
824
|
+
|
|
825
|
+
if (startMatch && currentGroup.length > 0) {
|
|
826
|
+
const prevContext = this._contextObj;
|
|
827
|
+
const prevParams = this._params;
|
|
828
|
+
try {
|
|
829
|
+
this._contextObj = currentGroup;
|
|
830
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
831
|
+
cb.call(this, null, currentGroup, this);
|
|
832
|
+
} finally {
|
|
833
|
+
this._contextObj = prevContext;
|
|
834
|
+
this._params = prevParams;
|
|
835
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
836
|
+
}
|
|
837
|
+
currentGroup = [];
|
|
838
|
+
}
|
|
839
|
+
currentGroup.push(m.value);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Process last group
|
|
843
|
+
if (currentGroup.length > 0) {
|
|
844
|
+
const prevContext = this._contextObj;
|
|
845
|
+
const prevParams = this._params;
|
|
846
|
+
try {
|
|
847
|
+
this._contextObj = currentGroup;
|
|
848
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
849
|
+
cb.call(this, null, currentGroup, this);
|
|
850
|
+
} finally {
|
|
851
|
+
this._contextObj = prevContext;
|
|
852
|
+
this._params = prevParams;
|
|
853
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} else if (groupEndingWith) {
|
|
857
|
+
// End group when expression matches
|
|
858
|
+
let currentGroup = [];
|
|
859
|
+
|
|
860
|
+
for (const m of matches) {
|
|
861
|
+
currentGroup.push(m.value);
|
|
862
|
+
const endMatch = evalInContext(groupEndingWith, m.value);
|
|
863
|
+
|
|
864
|
+
if (endMatch) {
|
|
865
|
+
const prevContext = this._contextObj;
|
|
866
|
+
const prevParams = this._params;
|
|
867
|
+
try {
|
|
868
|
+
this._contextObj = currentGroup;
|
|
869
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
870
|
+
cb.call(this, null, currentGroup, this);
|
|
871
|
+
} finally {
|
|
872
|
+
this._contextObj = prevContext;
|
|
873
|
+
this._params = prevParams;
|
|
874
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
875
|
+
}
|
|
876
|
+
currentGroup = [];
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Process last group if not ended
|
|
881
|
+
if (currentGroup.length > 0) {
|
|
882
|
+
const prevContext = this._contextObj;
|
|
883
|
+
const prevParams = this._params;
|
|
884
|
+
try {
|
|
885
|
+
this._contextObj = currentGroup;
|
|
886
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
887
|
+
cb.call(this, null, currentGroup, this);
|
|
888
|
+
} finally {
|
|
889
|
+
this._contextObj = prevContext;
|
|
890
|
+
this._params = prevParams;
|
|
891
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
610
894
|
}
|
|
895
|
+
|
|
611
896
|
return this;
|
|
612
897
|
}
|
|
613
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Helper to build comparator for sorting.
|
|
901
|
+
* @param {any} sortSpec
|
|
902
|
+
* @param {(expr: string, ctxVal: any) => any} evalFn
|
|
903
|
+
* @returns {((a: {value: any}, b: {value: any}) => number)|null}
|
|
904
|
+
* @private
|
|
905
|
+
*/
|
|
906
|
+
_buildComparator (sortSpec, evalFn) {
|
|
907
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
908
|
+
const that = this;
|
|
909
|
+
|
|
910
|
+
if (typeof sortSpec === 'function') {
|
|
911
|
+
return function (
|
|
912
|
+
/** @type {{value: any}} */ a,
|
|
913
|
+
/** @type {{value: any}} */ b
|
|
914
|
+
) {
|
|
915
|
+
return sortSpec(a.value, b.value, that);
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* @param {any} aVal
|
|
921
|
+
* @param {any} bVal
|
|
922
|
+
* @param {{
|
|
923
|
+
* order?: 'ascending'|'descending', type?: 'text'|'number',
|
|
924
|
+
* locale?: string, localeOptions?: any
|
|
925
|
+
* }|undefined} spec
|
|
926
|
+
* @returns {number}
|
|
927
|
+
*/
|
|
928
|
+
function compareBySpec (aVal, bVal, spec) {
|
|
929
|
+
/* c8 ignore next 2 -- all spec combinations tested; && and || branches */
|
|
930
|
+
const order = (spec && spec.order === 'descending') ? -1 : 1;
|
|
931
|
+
const type = (spec && spec.type) || 'text';
|
|
932
|
+
if (type === 'number') {
|
|
933
|
+
const an = Number(aVal);
|
|
934
|
+
const bn = Number(bVal);
|
|
935
|
+
/* c8 ignore next -- both NaN tested; short-circuit branch tracking */
|
|
936
|
+
if (Number.isNaN(an) && Number.isNaN(bn)) {
|
|
937
|
+
return 0;
|
|
938
|
+
}
|
|
939
|
+
if (Number.isNaN(an)) {
|
|
940
|
+
return Number(order);
|
|
941
|
+
}
|
|
942
|
+
if (Number.isNaN(bn)) {
|
|
943
|
+
return -1 * order;
|
|
944
|
+
}
|
|
945
|
+
return (an - bn) * order;
|
|
946
|
+
}
|
|
947
|
+
/* c8 ignore next 2 -- all null/undefined tested; OR short-circuit */
|
|
948
|
+
const aStr = aVal === null || aVal === undefined ? '' : String(aVal);
|
|
949
|
+
const bStr = bVal === null || bVal === undefined ? '' : String(bVal);
|
|
950
|
+
if (spec && spec.locale) {
|
|
951
|
+
return aStr.localeCompare(
|
|
952
|
+
bStr, spec.locale, spec.localeOptions
|
|
953
|
+
) * order;
|
|
954
|
+
}
|
|
955
|
+
/* c8 ignore next 2 -- all comparison outcomes tested; ternary
|
|
956
|
+
* branch tracking */
|
|
957
|
+
return (aStr < bStr ? -1 : (aStr > bStr ? 1 : 0)) * order;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const specs = Array.isArray(sortSpec) ? sortSpec : [sortSpec];
|
|
961
|
+
return function (
|
|
962
|
+
/** @type {{value: any}} */ a,
|
|
963
|
+
/** @type {{value: any}} */ b
|
|
964
|
+
) {
|
|
965
|
+
for (const s of specs) {
|
|
966
|
+
if (typeof s === 'string') {
|
|
967
|
+
const av = evalFn(s, a.value);
|
|
968
|
+
const bv = evalFn(s, b.value);
|
|
969
|
+
const c = compareBySpec(av, bv, {type: 'text', order: 'ascending'});
|
|
970
|
+
if (c !== 0) {
|
|
971
|
+
return c;
|
|
972
|
+
}
|
|
973
|
+
} else if (s && typeof s === 'object') {
|
|
974
|
+
const av = evalFn(s.select, a.value);
|
|
975
|
+
const bv = evalFn(s.select, b.value);
|
|
976
|
+
const c = compareBySpec(av, bv, s);
|
|
977
|
+
if (c !== 0) {
|
|
978
|
+
return c;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return 0;
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Returns the current group (for use within forEachGroup callback).
|
|
988
|
+
* @returns {any[]|undefined}
|
|
989
|
+
*/
|
|
990
|
+
currentGroup () {
|
|
991
|
+
return /** @type {any} */ (this)._currentGroup;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Returns the current grouping key (for use within forEachGroup callback).
|
|
996
|
+
* @returns {any}
|
|
997
|
+
*/
|
|
998
|
+
currentGroupingKey () {
|
|
999
|
+
return /** @type {any} */ (this)._currentGroupingKey;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
614
1002
|
/**
|
|
615
1003
|
* @param {string|object} [select] - JSONPath selector
|
|
616
1004
|
* @returns {this}
|
|
@@ -629,6 +1017,48 @@ class JSONPathTransformerContext {
|
|
|
629
1017
|
? /** @type {{select?: string}} */ (select).select
|
|
630
1018
|
: select;
|
|
631
1019
|
|
|
1020
|
+
// Check for format-number() function call
|
|
1021
|
+
if (selectStr && selectStr.includes('format-number(')) {
|
|
1022
|
+
const match = (/format-number\((?<value>[^,\)]+)(?:,\s*["'](?<format>[^"']+)["'])?(?:,\s*["'](?<decimalFormat>[^"']*)["'])?\)/v).exec(selectStr);
|
|
1023
|
+
if (match && match.groups) {
|
|
1024
|
+
const {
|
|
1025
|
+
value: valueExpr,
|
|
1026
|
+
format: formatStr,
|
|
1027
|
+
decimalFormat: decimalFormatName
|
|
1028
|
+
} = match.groups;
|
|
1029
|
+
// Evaluate the value expression
|
|
1030
|
+
let numValue;
|
|
1031
|
+
if (valueExpr.trim().startsWith('$') &&
|
|
1032
|
+
!valueExpr.includes('.') && !valueExpr.includes('[')) {
|
|
1033
|
+
// Parameter reference (no path components)
|
|
1034
|
+
const paramName = valueExpr.trim().slice(1);
|
|
1035
|
+
numValue = this._params && paramName in this._params
|
|
1036
|
+
? this._params[paramName]
|
|
1037
|
+
: 0;
|
|
1038
|
+
} else {
|
|
1039
|
+
// Try to parse as number or evaluate as JSONPath
|
|
1040
|
+
const trimmed = valueExpr.trim();
|
|
1041
|
+
numValue = Number.isNaN(Number(trimmed))
|
|
1042
|
+
? this.get(trimmed, false)
|
|
1043
|
+
: Number(trimmed);
|
|
1044
|
+
}
|
|
1045
|
+
const num = typeof numValue === 'string'
|
|
1046
|
+
? Number(numValue)
|
|
1047
|
+
: numValue;
|
|
1048
|
+
const format = formatStr || '1';
|
|
1049
|
+
const formatted = this._formatNumber(
|
|
1050
|
+
num,
|
|
1051
|
+
format,
|
|
1052
|
+
undefined,
|
|
1053
|
+
undefined,
|
|
1054
|
+
decimalFormatName || '',
|
|
1055
|
+
'en'
|
|
1056
|
+
);
|
|
1057
|
+
/** @type {any} */ (results).append(formatted);
|
|
1058
|
+
return this;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
632
1062
|
// Check if this is a parameter reference (starts with $)
|
|
633
1063
|
if (selectStr && selectStr.startsWith('$')) {
|
|
634
1064
|
const paramName = selectStr.slice(1);
|
|
@@ -987,7 +1417,8 @@ class JSONPathTransformerContext {
|
|
|
987
1417
|
format,
|
|
988
1418
|
opts.groupingSeparator,
|
|
989
1419
|
opts.groupingSize,
|
|
990
|
-
locale
|
|
1420
|
+
locale,
|
|
1421
|
+
opts.decimalFormat
|
|
991
1422
|
);
|
|
992
1423
|
|
|
993
1424
|
// Output as string if formatted, otherwise as number
|
|
@@ -1050,14 +1481,31 @@ class JSONPathTransformerContext {
|
|
|
1050
1481
|
* @param {string} format - Format string (1, a, A, i, I, 01, etc.)
|
|
1051
1482
|
* @param {string} [groupingSeparator] - Separator for grouping
|
|
1052
1483
|
* @param {number} [groupingSize] - Size of groups
|
|
1053
|
-
* @param {string} [
|
|
1484
|
+
* @param {string} [decimalFormatName] - Name of decimal format to use
|
|
1485
|
+
* @param {string} [locale] - Locale for formatting
|
|
1054
1486
|
* @returns {string}
|
|
1055
1487
|
*/
|
|
1056
|
-
_formatNumber (
|
|
1488
|
+
_formatNumber (
|
|
1489
|
+
num,
|
|
1490
|
+
format,
|
|
1491
|
+
groupingSeparator,
|
|
1492
|
+
groupingSize,
|
|
1493
|
+
decimalFormatName,
|
|
1494
|
+
locale = 'en'
|
|
1495
|
+
) {
|
|
1057
1496
|
if (Number.isNaN(num)) {
|
|
1058
|
-
|
|
1497
|
+
// Check for custom NaN string in decimal format
|
|
1498
|
+
const fmt = decimalFormatName
|
|
1499
|
+
? this.decimalFormats[decimalFormatName]
|
|
1500
|
+
: this.decimalFormats[''];
|
|
1501
|
+
return fmt?.NaN || String(num);
|
|
1059
1502
|
}
|
|
1060
1503
|
|
|
1504
|
+
// Get decimal format if specified
|
|
1505
|
+
const decimalFormat = decimalFormatName
|
|
1506
|
+
? this.decimalFormats[decimalFormatName]
|
|
1507
|
+
: this.decimalFormats[''];
|
|
1508
|
+
|
|
1061
1509
|
let result;
|
|
1062
1510
|
const formatChar = format.charAt(0);
|
|
1063
1511
|
|
|
@@ -1084,7 +1532,8 @@ class JSONPathTransformerContext {
|
|
|
1084
1532
|
}
|
|
1085
1533
|
case '0': {
|
|
1086
1534
|
const width = format.length;
|
|
1087
|
-
|
|
1535
|
+
const zeroDigit = decimalFormat?.zeroDigit || '0';
|
|
1536
|
+
result = String(num).padStart(width, zeroDigit);
|
|
1088
1537
|
|
|
1089
1538
|
break;
|
|
1090
1539
|
}
|
|
@@ -1100,7 +1549,25 @@ class JSONPathTransformerContext {
|
|
|
1100
1549
|
|
|
1101
1550
|
try {
|
|
1102
1551
|
result = new Intl.NumberFormat(locale, options).format(num);
|
|
1103
|
-
|
|
1552
|
+
|
|
1553
|
+
// Apply decimal format symbols if specified
|
|
1554
|
+
if (decimalFormat) {
|
|
1555
|
+
// Use placeholders to avoid conflicts during replacement
|
|
1556
|
+
const TEMP_GROUP = '\u0000GROUPSEP\u0000';
|
|
1557
|
+
const TEMP_DECIMAL = '\u0000DECIMALSEP\u0000';
|
|
1558
|
+
|
|
1559
|
+
// Replace with temporary placeholders first
|
|
1560
|
+
result = result.replaceAll(',', TEMP_GROUP);
|
|
1561
|
+
result = result.replaceAll('.', TEMP_DECIMAL);
|
|
1562
|
+
|
|
1563
|
+
// Now replace with actual symbols
|
|
1564
|
+
const effectiveGroupingSep = groupingSeparator ||
|
|
1565
|
+
decimalFormat.groupingSeparator || ',';
|
|
1566
|
+
const effectiveDecimalSep = decimalFormat.decimalSeparator || '.';
|
|
1567
|
+
|
|
1568
|
+
result = result.replaceAll(TEMP_GROUP, effectiveGroupingSep);
|
|
1569
|
+
result = result.replaceAll(TEMP_DECIMAL, effectiveDecimalSep);
|
|
1570
|
+
} else if (groupingSeparator) {
|
|
1104
1571
|
result = result.replaceAll(',', groupingSeparator);
|
|
1105
1572
|
}
|
|
1106
1573
|
} catch (e) {
|
|
@@ -1187,6 +1654,16 @@ class JSONPathTransformerContext {
|
|
|
1187
1654
|
return this;
|
|
1188
1655
|
}
|
|
1189
1656
|
|
|
1657
|
+
/**
|
|
1658
|
+
* Alias for propValue(). Set a key-value pair in the current map/object.
|
|
1659
|
+
* @param {string} prop - Property name
|
|
1660
|
+
* @param {any} val - Property value
|
|
1661
|
+
* @returns {this}
|
|
1662
|
+
*/
|
|
1663
|
+
mapEntry (prop, val) {
|
|
1664
|
+
return this.propValue(prop, val);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1190
1667
|
/**
|
|
1191
1668
|
* Build an object. Mirrors the joining transformer API. All joiners now
|
|
1192
1669
|
* support both signatures: (obj, cb, usePropertySets, propSets) with seed
|
|
@@ -1199,6 +1676,15 @@ class JSONPathTransformerContext {
|
|
|
1199
1676
|
return this;
|
|
1200
1677
|
}
|
|
1201
1678
|
|
|
1679
|
+
/**
|
|
1680
|
+
* Alias for object(). Build an object/map.
|
|
1681
|
+
* @param {...any} args - Arguments to pass to joiner
|
|
1682
|
+
* @returns {this}
|
|
1683
|
+
*/
|
|
1684
|
+
map (...args) {
|
|
1685
|
+
return this.object(...args);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1202
1688
|
/**
|
|
1203
1689
|
* Build an array. Mirrors the joining transformer API. All joiners now
|
|
1204
1690
|
* support both signatures: (arr, cb) with seed array or (cb) without.
|
|
@@ -1220,6 +1706,27 @@ class JSONPathTransformerContext {
|
|
|
1220
1706
|
return this;
|
|
1221
1707
|
}
|
|
1222
1708
|
|
|
1709
|
+
/**
|
|
1710
|
+
* @param {string} name
|
|
1711
|
+
* @param {import('./AbstractJoiningTransformer.js').
|
|
1712
|
+
* OutputCharacters} outputCharacters
|
|
1713
|
+
* @returns {this}
|
|
1714
|
+
*/
|
|
1715
|
+
characterMap (name, outputCharacters) {
|
|
1716
|
+
this._getJoiningTransformer().characterMap(name, outputCharacters);
|
|
1717
|
+
return this;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* @param {string} name
|
|
1722
|
+
* @param {Record<string, string>} attributes
|
|
1723
|
+
* @returns {this}
|
|
1724
|
+
*/
|
|
1725
|
+
attributeSet (name, attributes) {
|
|
1726
|
+
this._getJoiningTransformer().attributeSet(name, attributes);
|
|
1727
|
+
return this;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1223
1730
|
/**
|
|
1224
1731
|
* Create an element. Mirrors the joining transformer API so templates can
|
|
1225
1732
|
* call `this.element()`.
|
|
@@ -1228,15 +1735,51 @@ class JSONPathTransformerContext {
|
|
|
1228
1735
|
* @param {any[]} [children] - Child nodes
|
|
1229
1736
|
* @param {import('./JSONJoiningTransformer.js').
|
|
1230
1737
|
* SimpleCallback<T>} [cb] - Callback function
|
|
1738
|
+
* @param {string[]} [useAttributeSets] - Attribute set names to apply
|
|
1231
1739
|
* @returns {this}
|
|
1232
1740
|
*/
|
|
1233
|
-
element (name, atts, children, cb) {
|
|
1741
|
+
element (name, atts, children, cb, useAttributeSets) {
|
|
1234
1742
|
/** @type {any} */ (this._getJoiningTransformer()).element(
|
|
1235
|
-
name, atts, children, cb
|
|
1743
|
+
name, atts, children, cb, useAttributeSets
|
|
1744
|
+
);
|
|
1745
|
+
return this;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Adds a prefixed namespace declaration to the most recently opened
|
|
1750
|
+
* element. Mirrors the joining
|
|
1751
|
+
* transformer API so templates can call `this.attribute()`.
|
|
1752
|
+
* @param {string} prefix - Prefix
|
|
1753
|
+
* @param {string} namespaceURI - Namespace
|
|
1754
|
+
* @returns {this}
|
|
1755
|
+
*/
|
|
1756
|
+
namespace (prefix, namespaceURI) {
|
|
1757
|
+
/** @type {any} */ (this._getJoiningTransformer()).namespace(
|
|
1758
|
+
prefix, namespaceURI
|
|
1236
1759
|
);
|
|
1237
1760
|
return this;
|
|
1238
1761
|
}
|
|
1239
1762
|
|
|
1763
|
+
/**
|
|
1764
|
+
* Define a decimal format with custom symbols for number formatting.
|
|
1765
|
+
* Equivalent to xsl:decimal-format. If no name is provided, defines
|
|
1766
|
+
* the default format.
|
|
1767
|
+
* @param {string|DecimalFormatSymbols} nameOrSymbols - Format name or
|
|
1768
|
+
* symbols object if defining default
|
|
1769
|
+
* @param {DecimalFormatSymbols} [symbols] - Format symbols
|
|
1770
|
+
* @returns {this}
|
|
1771
|
+
*/
|
|
1772
|
+
decimalFormat (nameOrSymbols, symbols) {
|
|
1773
|
+
if (typeof nameOrSymbols === 'string') {
|
|
1774
|
+
// Named format
|
|
1775
|
+
this.decimalFormats[nameOrSymbols] = symbols || {};
|
|
1776
|
+
} else {
|
|
1777
|
+
// Default format (unnamed)
|
|
1778
|
+
this.decimalFormats[''] = nameOrSymbols;
|
|
1779
|
+
}
|
|
1780
|
+
return this;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1240
1783
|
/**
|
|
1241
1784
|
* Add an attribute to the most recently opened element. Mirrors the joining
|
|
1242
1785
|
* transformer API so templates can call `this.attribute()`.
|
|
@@ -1424,6 +1967,26 @@ class JSONPathTransformerContext {
|
|
|
1424
1967
|
}
|
|
1425
1968
|
return this;
|
|
1426
1969
|
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Assert that a test condition is true, throwing an error if it fails.
|
|
1973
|
+
* Equivalent to xsl:assert. Evaluates a JSONPath expression using the
|
|
1974
|
+
* same truthiness rules as if() and choose().
|
|
1975
|
+
* @param {string} test - JSONPath expression to test
|
|
1976
|
+
* @param {string} [message] - Optional error message to include
|
|
1977
|
+
* @returns {this}
|
|
1978
|
+
* @throws {Error} When the test expression evaluates to false
|
|
1979
|
+
*/
|
|
1980
|
+
assert (test, message) {
|
|
1981
|
+
const passes = this._passesIf(test);
|
|
1982
|
+
if (!passes) {
|
|
1983
|
+
const errorMsg = message
|
|
1984
|
+
? `Assertion failed: ${message}`
|
|
1985
|
+
: `Assertion failed: ${test}`;
|
|
1986
|
+
throw new Error(errorMsg);
|
|
1987
|
+
}
|
|
1988
|
+
return this;
|
|
1989
|
+
}
|
|
1427
1990
|
}
|
|
1428
1991
|
|
|
1429
1992
|
export default JSONPathTransformerContext;
|