jtlt 0.3.0 → 0.5.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 +18 -0
- package/README.md +46 -78
- package/demo/codemirror.esm.js +28242 -0
- package/demo/codemirror.js +94 -0
- package/demo/index.css +11 -0
- package/demo/index.html +11 -14
- package/demo/index.js +210 -26
- package/demo/vendor/fontoxpath/dist/fontoxpath.esm.js +600 -0
- package/demo/vendor/jamilih/dist/jml.mjs +2341 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +356 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.Stringifier.js +186 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +746 -0
- package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.html +33 -0
- package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.testing.js +25 -0
- package/demo/vendor/jhtml/src/jhtml-browser.js +5 -0
- package/demo/vendor/jhtml/src/jhtml-node.cts +3 -0
- package/demo/vendor/jhtml/src/jhtml-node.js +8 -0
- package/demo/vendor/jhtml/src/jhtml-node.mts +1 -0
- package/demo/vendor/jhtml/src/jhtml.cts +3 -0
- package/demo/vendor/jhtml/src/jhtml.js +602 -0
- package/demo/vendor/jhtml/src/jhtml.mts +1 -0
- package/demo/vendor/jsonpath-plus/dist/index-browser-esm.js +2158 -0
- package/demo/vendor/prsc/dist/prsc.esm.js +2 -0
- package/demo/vendor/simple-get-json/dist/index-es.js +151 -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 +27 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
- package/dist/DOMJoiningTransformer.d.ts +60 -8
- package/dist/DOMJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONJoiningTransformer.d.ts +8 -9
- package/dist/JSONJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +223 -4
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/StringJoiningTransformer.d.ts +7 -7
- package/dist/StringJoiningTransformer.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts +2 -2
- package/dist/XPathTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +161 -8
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/docs/API.expanded.md +5 -3
- package/docs/API.md +1 -1
- package/docs/TO-DO.md +62 -36
- package/eslint.config.js +3 -1
- package/package.json +28 -4
- package/rollup.config.js +13 -0
- package/src/AbstractJoiningTransformer.js +42 -0
- package/src/DOMJoiningTransformer.js +115 -23
- package/src/JSONJoiningTransformer.js +50 -26
- package/src/JSONPathTransformer.js +2 -0
- package/src/JSONPathTransformerContext.js +936 -5
- package/src/StringJoiningTransformer.js +25 -21
- package/src/XPathTransformer.js +3 -1
- package/src/XPathTransformerContext.js +884 -15
- package/src/index.js +16 -7
- package/tsconfig.json +5 -2
|
@@ -1,6 +1,35 @@
|
|
|
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
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {number|string|{
|
|
22
|
+
* value?: number|string,
|
|
23
|
+
* count?: string,
|
|
24
|
+
* format?: string,
|
|
25
|
+
* decimalFormat?: string,
|
|
26
|
+
* groupingSeparator?: string,
|
|
27
|
+
* groupingSize?: number,
|
|
28
|
+
* lang?: string,
|
|
29
|
+
* letterValue?: string
|
|
30
|
+
* }} NumberValue
|
|
31
|
+
*/
|
|
32
|
+
|
|
4
33
|
/**
|
|
5
34
|
* Sort spec types used by applyTemplates() and forEach().
|
|
6
35
|
* @typedef {{
|
|
@@ -46,6 +75,11 @@ import JSONPathTransformer from './JSONPathTransformer.js';
|
|
|
46
75
|
* @template [T = "json"]
|
|
47
76
|
*/
|
|
48
77
|
class JSONPathTransformerContext {
|
|
78
|
+
/**
|
|
79
|
+
* Holds the current iteration state (for position calculations).
|
|
80
|
+
* @type {{ index?: number } | undefined}
|
|
81
|
+
*/
|
|
82
|
+
iterationState;
|
|
49
83
|
/**
|
|
50
84
|
* @param {JSONPathTransformerContextConfig<T>} config
|
|
51
85
|
* @param {import('./index.js').JSONPathTemplateObject<T>[]} templates - Array
|
|
@@ -63,6 +97,8 @@ class JSONPathTransformerContext {
|
|
|
63
97
|
this.propertySets = {};
|
|
64
98
|
/** @type {Record<string, {match: string, use: string}>} */
|
|
65
99
|
this.keys = {};
|
|
100
|
+
/** @type {Record<string, DecimalFormatSymbols>} */
|
|
101
|
+
this.decimalFormats = {};
|
|
66
102
|
/** @type {boolean | undefined} */
|
|
67
103
|
this._initialized = undefined;
|
|
68
104
|
/** @type {string | undefined} */
|
|
@@ -241,6 +277,7 @@ class JSONPathTransformerContext {
|
|
|
241
277
|
if (type === 'number') {
|
|
242
278
|
const an = Number(aVal);
|
|
243
279
|
const bn = Number(bVal);
|
|
280
|
+
/* c8 ignore next -- both NaN tested; short-circuit branch tracking */
|
|
244
281
|
if (Number.isNaN(an) && Number.isNaN(bn)) {
|
|
245
282
|
return 0;
|
|
246
283
|
}
|
|
@@ -253,6 +290,7 @@ class JSONPathTransformerContext {
|
|
|
253
290
|
return (an - bn) * order;
|
|
254
291
|
}
|
|
255
292
|
// text
|
|
293
|
+
/* c8 ignore next 2 -- all null/undefined tested; OR short-circuit */
|
|
256
294
|
const aStr = aVal === null || aVal === undefined ? '' : String(aVal);
|
|
257
295
|
const bStr = bVal === null || bVal === undefined ? '' : String(bVal);
|
|
258
296
|
if (spec && spec.locale) {
|
|
@@ -260,6 +298,8 @@ class JSONPathTransformerContext {
|
|
|
260
298
|
bStr, spec.locale, spec.localeOptions
|
|
261
299
|
) * order;
|
|
262
300
|
}
|
|
301
|
+
/* c8 ignore next -- all comparison outcomes tested; ternary
|
|
302
|
+
* branch tracking */
|
|
263
303
|
return (aStr < bStr ? -1 : (aStr > bStr ? 1 : 0)) * order;
|
|
264
304
|
}
|
|
265
305
|
/**
|
|
@@ -390,12 +430,19 @@ class JSONPathTransformerContext {
|
|
|
390
430
|
that._parent = parent;
|
|
391
431
|
that._parentProperty = (parentProperty ?? that._parentProperty);
|
|
392
432
|
|
|
433
|
+
// Set up parameter context for valueOf() access in templates
|
|
434
|
+
const prevTemplateParams = that._params;
|
|
435
|
+
that._params = {0: value};
|
|
436
|
+
|
|
393
437
|
const ret =
|
|
394
438
|
/** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
|
|
395
439
|
templateObj
|
|
396
440
|
).template.call(
|
|
397
441
|
that, value, {mode, parent, parentProperty}
|
|
398
442
|
);
|
|
443
|
+
|
|
444
|
+
// Restore previous parameter context
|
|
445
|
+
that._params = prevTemplateParams;
|
|
399
446
|
if (typeof ret !== 'undefined') {
|
|
400
447
|
// After the undefined check, ret is ResultType<T>
|
|
401
448
|
that._getJoiningTransformer().append(
|
|
@@ -523,11 +570,13 @@ class JSONPathTransformerContext {
|
|
|
523
570
|
* @returns {number}
|
|
524
571
|
*/
|
|
525
572
|
function feCompareBySpec (aVal, bVal, spec) {
|
|
573
|
+
/* c8 ignore next 2 -- all spec combinations tested; && and || branches */
|
|
526
574
|
const order = (spec && spec.order === 'descending') ? -1 : 1;
|
|
527
575
|
const type = (spec && spec.type) || 'text';
|
|
528
576
|
if (type === 'number') {
|
|
529
577
|
const an = Number(aVal);
|
|
530
578
|
const bn = Number(bVal);
|
|
579
|
+
/* c8 ignore next -- both NaN tested; short-circuit branch tracking */
|
|
531
580
|
if (Number.isNaN(an) && Number.isNaN(bn)) {
|
|
532
581
|
return 0;
|
|
533
582
|
}
|
|
@@ -539,6 +588,7 @@ class JSONPathTransformerContext {
|
|
|
539
588
|
}
|
|
540
589
|
return (an - bn) * order;
|
|
541
590
|
}
|
|
591
|
+
/* c8 ignore next 2 -- all null/undefined tested; OR short-circuit */
|
|
542
592
|
const aStr = aVal === null || aVal === undefined ? '' : String(aVal);
|
|
543
593
|
const bStr = bVal === null || bVal === undefined ? '' : String(bVal);
|
|
544
594
|
if (spec && spec.locale) {
|
|
@@ -546,6 +596,8 @@ class JSONPathTransformerContext {
|
|
|
546
596
|
bStr, spec.locale, spec.localeOptions
|
|
547
597
|
) * order;
|
|
548
598
|
}
|
|
599
|
+
/* c8 ignore next -- all comparison outcomes tested; ternary
|
|
600
|
+
* branch tracking */
|
|
549
601
|
return (aStr < bStr ? -1 : (aStr > bStr ? 1 : 0)) * order;
|
|
550
602
|
}
|
|
551
603
|
/**
|
|
@@ -589,11 +641,364 @@ class JSONPathTransformerContext {
|
|
|
589
641
|
const comparator = feBuildComparator(sort);
|
|
590
642
|
const list = comparator ? [...matches].toSorted(comparator) : matches;
|
|
591
643
|
for (const m of list) {
|
|
592
|
-
|
|
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
|
+
}
|
|
593
894
|
}
|
|
895
|
+
|
|
594
896
|
return this;
|
|
595
897
|
}
|
|
596
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
|
+
|
|
597
1002
|
/**
|
|
598
1003
|
* @param {string|object} [select] - JSONPath selector
|
|
599
1004
|
* @returns {this}
|
|
@@ -612,6 +1017,48 @@ class JSONPathTransformerContext {
|
|
|
612
1017
|
? /** @type {{select?: string}} */ (select).select
|
|
613
1018
|
: select;
|
|
614
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
|
+
|
|
615
1062
|
// Check if this is a parameter reference (starts with $)
|
|
616
1063
|
if (selectStr && selectStr.startsWith('$')) {
|
|
617
1064
|
const paramName = selectStr.slice(1);
|
|
@@ -627,6 +1074,146 @@ class JSONPathTransformerContext {
|
|
|
627
1074
|
return this;
|
|
628
1075
|
}
|
|
629
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* Analyze a string with a regular expression, equivalent to
|
|
1079
|
+
* xsl:analyze-string. Processes matching and non-matching substrings
|
|
1080
|
+
* with separate callbacks.
|
|
1081
|
+
* @param {string} str - The string to analyze
|
|
1082
|
+
* @param {string|RegExp} regex - Regular expression to match against
|
|
1083
|
+
* @param {{
|
|
1084
|
+
* matchingSubstring?: (
|
|
1085
|
+
* this: JSONPathTransformerContext<T>,
|
|
1086
|
+
* substring: string,
|
|
1087
|
+
* groups: string[],
|
|
1088
|
+
* regexGroup: (n: number) => string
|
|
1089
|
+
* ) => void,
|
|
1090
|
+
* nonMatchingSubstring?: (
|
|
1091
|
+
* this: JSONPathTransformerContext<T>,
|
|
1092
|
+
* substring: string
|
|
1093
|
+
* ) => void,
|
|
1094
|
+
* flags?: string
|
|
1095
|
+
* }} options - Options object
|
|
1096
|
+
* @returns {this}
|
|
1097
|
+
*/
|
|
1098
|
+
analyzeString (str, regex, options = {}) {
|
|
1099
|
+
// Ensure we have a string
|
|
1100
|
+
const inputString = String(str || '');
|
|
1101
|
+
|
|
1102
|
+
// If empty string, do nothing
|
|
1103
|
+
if (inputString.length === 0) {
|
|
1104
|
+
return this;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const {
|
|
1108
|
+
matchingSubstring,
|
|
1109
|
+
nonMatchingSubstring,
|
|
1110
|
+
flags = ''
|
|
1111
|
+
} = options;
|
|
1112
|
+
|
|
1113
|
+
// Convert regex to RegExp if it's a string
|
|
1114
|
+
let regexObj;
|
|
1115
|
+
if (typeof regex === 'string') {
|
|
1116
|
+
// Ensure 'g' flag is present for global matching
|
|
1117
|
+
const actualFlags = flags.includes('g') ? flags : flags + 'g';
|
|
1118
|
+
regexObj = new RegExp(regex, actualFlags);
|
|
1119
|
+
} else {
|
|
1120
|
+
regexObj = regex;
|
|
1121
|
+
// Ensure global flag is set
|
|
1122
|
+
if (!regexObj.global) {
|
|
1123
|
+
regexObj = new RegExp(
|
|
1124
|
+
regexObj.source,
|
|
1125
|
+
regexObj.flags + 'g'
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Check for zero-length matches (error condition in XSLT)
|
|
1131
|
+
if (regexObj.test('')) {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
'Regular expression matches zero-length string'
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Store captured groups for access during callback
|
|
1138
|
+
/** @type {string[] | undefined} */
|
|
1139
|
+
let currentCapturedGroups;
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Get captured group by index.
|
|
1143
|
+
* @param {number} groupNumber - Group index
|
|
1144
|
+
* @returns {string} - Captured group or empty string
|
|
1145
|
+
*/
|
|
1146
|
+
const getRegexGroup = (groupNumber) => {
|
|
1147
|
+
if (!currentCapturedGroups ||
|
|
1148
|
+
groupNumber < 0 ||
|
|
1149
|
+
groupNumber >= currentCapturedGroups.length) {
|
|
1150
|
+
return '';
|
|
1151
|
+
}
|
|
1152
|
+
return currentCapturedGroups[groupNumber] || '';
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
// Save previous context to restore later
|
|
1156
|
+
const prevContext = this._contextObj;
|
|
1157
|
+
|
|
1158
|
+
let lastIndex = 0;
|
|
1159
|
+
let match;
|
|
1160
|
+
|
|
1161
|
+
// Bind callbacks to this context
|
|
1162
|
+
const boundMatchingSubstring = matchingSubstring
|
|
1163
|
+
? matchingSubstring.bind(this)
|
|
1164
|
+
: undefined;
|
|
1165
|
+
const boundNonMatchingSubstring = nonMatchingSubstring
|
|
1166
|
+
? nonMatchingSubstring.bind(this)
|
|
1167
|
+
: undefined;
|
|
1168
|
+
|
|
1169
|
+
// Find all matches
|
|
1170
|
+
while ((match = regexObj.exec(inputString)) !== null) {
|
|
1171
|
+
// Process non-matching substring before this match
|
|
1172
|
+
if (match.index > lastIndex) {
|
|
1173
|
+
const nonMatchingStr = inputString.slice(lastIndex, match.index);
|
|
1174
|
+
if (boundNonMatchingSubstring) {
|
|
1175
|
+
this._contextObj = nonMatchingStr;
|
|
1176
|
+
boundNonMatchingSubstring(nonMatchingStr);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Process matching substring
|
|
1181
|
+
if (boundMatchingSubstring) {
|
|
1182
|
+
const matchingStr = match[0];
|
|
1183
|
+
// Store captured groups: [full match, group1, group2, ...]
|
|
1184
|
+
currentCapturedGroups = [...match];
|
|
1185
|
+
this._contextObj = matchingStr;
|
|
1186
|
+
boundMatchingSubstring(
|
|
1187
|
+
matchingStr, currentCapturedGroups, getRegexGroup
|
|
1188
|
+
);
|
|
1189
|
+
currentCapturedGroups = undefined;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const {lastIndex: newLastIndex} = regexObj;
|
|
1193
|
+
lastIndex = newLastIndex;
|
|
1194
|
+
|
|
1195
|
+
// Prevent infinite loop on zero-length matches (shouldn't happen
|
|
1196
|
+
// due to earlier check, but defensive)
|
|
1197
|
+
if (match.index === regexObj.lastIndex) {
|
|
1198
|
+
regexObj.lastIndex++;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Process final non-matching substring
|
|
1203
|
+
if (lastIndex < inputString.length) {
|
|
1204
|
+
const nonMatchingStr = inputString.slice(lastIndex);
|
|
1205
|
+
if (boundNonMatchingSubstring) {
|
|
1206
|
+
this._contextObj = nonMatchingStr;
|
|
1207
|
+
boundNonMatchingSubstring(nonMatchingStr);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Restore previous context
|
|
1212
|
+
this._contextObj = prevContext;
|
|
1213
|
+
|
|
1214
|
+
return this;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
630
1217
|
/**
|
|
631
1218
|
* Deep copy selection or current context when omitted.
|
|
632
1219
|
* @param {string} [select] - JSONPath selector
|
|
@@ -735,16 +1322,314 @@ class JSONPathTransformerContext {
|
|
|
735
1322
|
}
|
|
736
1323
|
|
|
737
1324
|
/**
|
|
738
|
-
* Append a number to JSON output
|
|
739
|
-
*
|
|
740
|
-
*
|
|
1325
|
+
* Append a number to JSON output with xsl:number-like formatting.
|
|
1326
|
+
* @param {NumberValue} num - Number value, "position()" string, or
|
|
1327
|
+
* options object
|
|
741
1328
|
* @returns {this}
|
|
742
1329
|
*/
|
|
743
1330
|
number (num) {
|
|
744
|
-
|
|
1331
|
+
// Handle xsl:number-like functionality
|
|
1332
|
+
if (typeof num === 'object' && num !== null) {
|
|
1333
|
+
const opts = num;
|
|
1334
|
+
let {value} = opts;
|
|
1335
|
+
|
|
1336
|
+
// Handle position() and hierarchical numbering
|
|
1337
|
+
if (value === 'position()' || value === undefined) {
|
|
1338
|
+
const {count} = opts;
|
|
1339
|
+
// @ts-expect-error: dynamic property access
|
|
1340
|
+
const level = opts.level || 'single';
|
|
1341
|
+
|
|
1342
|
+
switch (level) {
|
|
1343
|
+
case 'single': {
|
|
1344
|
+
value = this.calculatePosition(count);
|
|
1345
|
+
// If count is set, simulate count by returning total items
|
|
1346
|
+
// in current context
|
|
1347
|
+
if (count) {
|
|
1348
|
+
const arr = Array.isArray(this.get(count, true))
|
|
1349
|
+
? this.get(count, true)
|
|
1350
|
+
/* c8 ignore next -- defensive: get(wrap) always returns array */
|
|
1351
|
+
: [];
|
|
1352
|
+
value = arr.length;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
case 'multiple': {
|
|
1358
|
+
// Hierarchical numbering: get position for each ancestor up to root
|
|
1359
|
+
const positions = [];
|
|
1360
|
+
let state = /** @type {any} */ (this._config).iterationState;
|
|
1361
|
+
while (state) {
|
|
1362
|
+
// If count is set, use count for each ancestor if possible
|
|
1363
|
+
if (count) {
|
|
1364
|
+
const arr = Array.isArray(this.get(count, true))
|
|
1365
|
+
? this.get(count, true)
|
|
1366
|
+
/* c8 ignore next -- defensive: get(wrap) returns array */
|
|
1367
|
+
: [];
|
|
1368
|
+
positions.unshift(arr.length);
|
|
1369
|
+
} else {
|
|
1370
|
+
positions.unshift(
|
|
1371
|
+
state.index !== undefined
|
|
1372
|
+
? state.index + 1
|
|
1373
|
+
/* c8 ignore next -- defensive: state always has index */
|
|
1374
|
+
: 1
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
state = state.parentState;
|
|
1378
|
+
}
|
|
1379
|
+
value = positions.join('.');
|
|
1380
|
+
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
case 'any': {
|
|
1384
|
+
// Count all matching items up to current
|
|
1385
|
+
value = this.calculatePosition(count);
|
|
1386
|
+
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
// No default
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Determine format string and locale
|
|
1394
|
+
let format = opts.format || '1';
|
|
1395
|
+
const locale = opts.lang || 'en';
|
|
1396
|
+
const {letterValue} = opts;
|
|
1397
|
+
|
|
1398
|
+
// If letterValue is 'alphabetic', force alphabetic format
|
|
1399
|
+
if (letterValue === 'alphabetic') {
|
|
1400
|
+
format = (opts.format && (/^[aA]$/v).test(opts.format)) ? opts.format : 'a';
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Ensure value is a number or string for formatting
|
|
1404
|
+
let numValue = value;
|
|
1405
|
+
// If value is undefined, fallback to opts.value
|
|
1406
|
+
if (typeof numValue === 'undefined') {
|
|
1407
|
+
numValue = opts.value;
|
|
1408
|
+
}
|
|
1409
|
+
if (typeof numValue === 'string') {
|
|
1410
|
+
numValue = Number(numValue);
|
|
1411
|
+
}
|
|
1412
|
+
if (typeof numValue !== 'number' || Number.isNaN(numValue)) {
|
|
1413
|
+
numValue = 1;
|
|
1414
|
+
}
|
|
1415
|
+
const formatted = this._formatNumber(
|
|
1416
|
+
numValue,
|
|
1417
|
+
format,
|
|
1418
|
+
opts.groupingSeparator,
|
|
1419
|
+
opts.groupingSize,
|
|
1420
|
+
locale,
|
|
1421
|
+
opts.decimalFormat
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
// Output as string if formatted, otherwise as number
|
|
1425
|
+
if (format && format !== '1') {
|
|
1426
|
+
this._getJoiningTransformer().plainText(formatted);
|
|
1427
|
+
} else {
|
|
1428
|
+
this._getJoiningTransformer().number(Number(formatted));
|
|
1429
|
+
}
|
|
1430
|
+
} else if (num === 'position()') {
|
|
1431
|
+
// Simple position() call
|
|
1432
|
+
const pos = this.calculatePosition();
|
|
1433
|
+
this._getJoiningTransformer().number(pos);
|
|
1434
|
+
} else {
|
|
1435
|
+
// Simple number
|
|
1436
|
+
this._getJoiningTransformer().number(
|
|
1437
|
+
typeof num === 'string' ? Number(num) : num
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
745
1440
|
return this;
|
|
746
1441
|
}
|
|
747
1442
|
|
|
1443
|
+
/**
|
|
1444
|
+
* Calculate position in current iteration context.
|
|
1445
|
+
* @param {string} [count] - JSONPath expression to match
|
|
1446
|
+
* @returns {number}
|
|
1447
|
+
*/
|
|
1448
|
+
calculatePosition (count) {
|
|
1449
|
+
// If count is provided, return the length of the matched array from
|
|
1450
|
+
// the root data
|
|
1451
|
+
if (count) {
|
|
1452
|
+
const result = jsonpath({
|
|
1453
|
+
path: count, json: this._origObj, resultType: 'value', wrap: true
|
|
1454
|
+
});
|
|
1455
|
+
if (Array.isArray(result)) {
|
|
1456
|
+
if (result.length === 0) {
|
|
1457
|
+
return 0;
|
|
1458
|
+
}
|
|
1459
|
+
// If the first item is an array, return its length
|
|
1460
|
+
if (Array.isArray(result[0])) {
|
|
1461
|
+
return result[0].length;
|
|
1462
|
+
}
|
|
1463
|
+
// Otherwise, return the number of matches
|
|
1464
|
+
return result.length;
|
|
1465
|
+
}
|
|
1466
|
+
/* c8 ignore next 3 -- defensive:
|
|
1467
|
+
jsonpath-plus with wrap:true always returns arrays */
|
|
1468
|
+
return 0;
|
|
1469
|
+
}
|
|
1470
|
+
// Get current index from iteration state
|
|
1471
|
+
const state = this.iterationState;
|
|
1472
|
+
if (state && typeof state.index === 'number') {
|
|
1473
|
+
return state.index + 1; // 1-indexed
|
|
1474
|
+
}
|
|
1475
|
+
return 1;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Format a number according to format string.
|
|
1480
|
+
* @param {number} num - Number to format
|
|
1481
|
+
* @param {string} format - Format string (1, a, A, i, I, 01, etc.)
|
|
1482
|
+
* @param {string} [groupingSeparator] - Separator for grouping
|
|
1483
|
+
* @param {number} [groupingSize] - Size of groups
|
|
1484
|
+
* @param {string} [decimalFormatName] - Name of decimal format to use
|
|
1485
|
+
* @param {string} [locale] - Locale for formatting
|
|
1486
|
+
* @returns {string}
|
|
1487
|
+
*/
|
|
1488
|
+
_formatNumber (
|
|
1489
|
+
num,
|
|
1490
|
+
format,
|
|
1491
|
+
groupingSeparator,
|
|
1492
|
+
groupingSize,
|
|
1493
|
+
decimalFormatName,
|
|
1494
|
+
locale = 'en'
|
|
1495
|
+
) {
|
|
1496
|
+
if (Number.isNaN(num)) {
|
|
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);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Get decimal format if specified
|
|
1505
|
+
const decimalFormat = decimalFormatName
|
|
1506
|
+
? this.decimalFormats[decimalFormatName]
|
|
1507
|
+
: this.decimalFormats[''];
|
|
1508
|
+
|
|
1509
|
+
let result;
|
|
1510
|
+
const formatChar = format.charAt(0);
|
|
1511
|
+
|
|
1512
|
+
switch (formatChar) {
|
|
1513
|
+
case 'i': {
|
|
1514
|
+
result = this._toRoman(num).toLowerCase();
|
|
1515
|
+
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
case 'I': {
|
|
1519
|
+
result = this._toRoman(num);
|
|
1520
|
+
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
case 'a': {
|
|
1524
|
+
result = this._toAlphabetic(num, false);
|
|
1525
|
+
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
case 'A': {
|
|
1529
|
+
result = this._toAlphabetic(num, true);
|
|
1530
|
+
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
case '0': {
|
|
1534
|
+
const width = format.length;
|
|
1535
|
+
const zeroDigit = decimalFormat?.zeroDigit || '0';
|
|
1536
|
+
result = String(num).padStart(width, zeroDigit);
|
|
1537
|
+
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
default: {
|
|
1541
|
+
// Use Intl.NumberFormat for decimal formatting if grouping/locale
|
|
1542
|
+
// options are provided
|
|
1543
|
+
let options = {};
|
|
1544
|
+
if (groupingSeparator || groupingSize) {
|
|
1545
|
+
options = {
|
|
1546
|
+
useGrouping: true
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
try {
|
|
1551
|
+
result = new Intl.NumberFormat(locale, options).format(num);
|
|
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) {
|
|
1571
|
+
result = result.replaceAll(',', groupingSeparator);
|
|
1572
|
+
}
|
|
1573
|
+
} catch (e) {
|
|
1574
|
+
result = String(num);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return result;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Convert number to Roman numerals.
|
|
1583
|
+
* @param {number} num - Number to convert (1-3999)
|
|
1584
|
+
* @returns {string}
|
|
1585
|
+
* @private
|
|
1586
|
+
*/
|
|
1587
|
+
// eslint-disable-next-line class-methods-use-this -- Avoid for now
|
|
1588
|
+
_toRoman (num) {
|
|
1589
|
+
if (num < 1 || num > 3999) {
|
|
1590
|
+
return String(num);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
|
|
1594
|
+
const syms = [
|
|
1595
|
+
'M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'
|
|
1596
|
+
];
|
|
1597
|
+
|
|
1598
|
+
let result = '';
|
|
1599
|
+
for (const [i, val] of vals.entries()) {
|
|
1600
|
+
while (num >= val) {
|
|
1601
|
+
result += syms[i];
|
|
1602
|
+
num -= val;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return result;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Convert number to alphabetic sequence.
|
|
1610
|
+
* @param {number} num - Number to convert
|
|
1611
|
+
* @param {boolean} uppercase - Use uppercase letters
|
|
1612
|
+
* @returns {string}
|
|
1613
|
+
* @private
|
|
1614
|
+
*/
|
|
1615
|
+
// eslint-disable-next-line class-methods-use-this -- Avoid for now
|
|
1616
|
+
_toAlphabetic (num, uppercase) {
|
|
1617
|
+
if (num < 1) {
|
|
1618
|
+
return String(num);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
let result = '';
|
|
1622
|
+
const base = uppercase ? 65 : 97; // 'A' or 'a'
|
|
1623
|
+
|
|
1624
|
+
while (num > 0) {
|
|
1625
|
+
num--; // Make 0-indexed
|
|
1626
|
+
result = String.fromCodePoint(base + (num % 26)) + result;
|
|
1627
|
+
num = Math.floor(num / 26);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
return result;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
748
1633
|
/**
|
|
749
1634
|
* Append plain text directly to the output without escaping or JSON
|
|
750
1635
|
* stringification. Mirrors the joining transformer API so templates can
|
|
@@ -802,6 +1687,17 @@ class JSONPathTransformerContext {
|
|
|
802
1687
|
return this;
|
|
803
1688
|
}
|
|
804
1689
|
|
|
1690
|
+
/**
|
|
1691
|
+
* @param {string} name
|
|
1692
|
+
* @param {import('./AbstractJoiningTransformer.js').
|
|
1693
|
+
* OutputCharacters} outputCharacters
|
|
1694
|
+
* @returns {this}
|
|
1695
|
+
*/
|
|
1696
|
+
characterMap (name, outputCharacters) {
|
|
1697
|
+
this._getJoiningTransformer().characterMap(name, outputCharacters);
|
|
1698
|
+
return this;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
805
1701
|
/**
|
|
806
1702
|
* Create an element. Mirrors the joining transformer API so templates can
|
|
807
1703
|
* call `this.element()`.
|
|
@@ -819,6 +1715,41 @@ class JSONPathTransformerContext {
|
|
|
819
1715
|
return this;
|
|
820
1716
|
}
|
|
821
1717
|
|
|
1718
|
+
/**
|
|
1719
|
+
* Adds a prefixed namespace declaration to the most recently opened
|
|
1720
|
+
* element. Mirrors the joining
|
|
1721
|
+
* transformer API so templates can call `this.attribute()`.
|
|
1722
|
+
* @param {string} prefix - Prefix
|
|
1723
|
+
* @param {string} namespaceURI - Namespace
|
|
1724
|
+
* @returns {this}
|
|
1725
|
+
*/
|
|
1726
|
+
namespace (prefix, namespaceURI) {
|
|
1727
|
+
/** @type {any} */ (this._getJoiningTransformer()).namespace(
|
|
1728
|
+
prefix, namespaceURI
|
|
1729
|
+
);
|
|
1730
|
+
return this;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Define a decimal format with custom symbols for number formatting.
|
|
1735
|
+
* Equivalent to xsl:decimal-format. If no name is provided, defines
|
|
1736
|
+
* the default format.
|
|
1737
|
+
* @param {string|DecimalFormatSymbols} nameOrSymbols - Format name or
|
|
1738
|
+
* symbols object if defining default
|
|
1739
|
+
* @param {DecimalFormatSymbols} [symbols] - Format symbols
|
|
1740
|
+
* @returns {this}
|
|
1741
|
+
*/
|
|
1742
|
+
decimalFormat (nameOrSymbols, symbols) {
|
|
1743
|
+
if (typeof nameOrSymbols === 'string') {
|
|
1744
|
+
// Named format
|
|
1745
|
+
this.decimalFormats[nameOrSymbols] = symbols || {};
|
|
1746
|
+
} else {
|
|
1747
|
+
// Default format (unnamed)
|
|
1748
|
+
this.decimalFormats[''] = nameOrSymbols;
|
|
1749
|
+
}
|
|
1750
|
+
return this;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
822
1753
|
/**
|
|
823
1754
|
* Add an attribute to the most recently opened element. Mirrors the joining
|
|
824
1755
|
* transformer API so templates can call `this.attribute()`.
|