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
|
@@ -48,6 +48,11 @@ class XPathTransformerContext {
|
|
|
48
48
|
this.propertySets = {};
|
|
49
49
|
/** @type {Record<string, {match: string, use: string}>} */
|
|
50
50
|
this.keys = {};
|
|
51
|
+
/**
|
|
52
|
+
* @type {Record<string,
|
|
53
|
+
* import('./JSONPathTransformerContext.js').DecimalFormatSymbols>}
|
|
54
|
+
*/
|
|
55
|
+
this.decimalFormats = {};
|
|
51
56
|
/** @type {boolean|undefined} */
|
|
52
57
|
this._initialized = undefined;
|
|
53
58
|
/** @type {string|undefined} */
|
|
@@ -293,7 +298,16 @@ class XPathTransformerContext {
|
|
|
293
298
|
);
|
|
294
299
|
}
|
|
295
300
|
this._contextNode = node;
|
|
301
|
+
|
|
302
|
+
// Set up parameter context for valueOf() access in templates
|
|
303
|
+
const prevTemplateParams = this._params;
|
|
304
|
+
this._params = {0: node};
|
|
305
|
+
|
|
296
306
|
const ret = templateObj.template.call(this, node, {mode});
|
|
307
|
+
|
|
308
|
+
// Restore previous parameter context
|
|
309
|
+
this._params = prevTemplateParams;
|
|
310
|
+
|
|
297
311
|
if (typeof ret !== 'undefined') {
|
|
298
312
|
this._getJoiningTransformer().append(ret);
|
|
299
313
|
}
|
|
@@ -371,11 +385,269 @@ class XPathTransformerContext {
|
|
|
371
385
|
const nodesResult = this._evalXPath(select, true);
|
|
372
386
|
const nodes = /** @type {Node[]} */ (nodesResult);
|
|
373
387
|
for (const n of nodes) {
|
|
374
|
-
|
|
388
|
+
// Set up parameter context for valueOf() access
|
|
389
|
+
const prevParams = this._params;
|
|
390
|
+
const prevContext = this._contextNode;
|
|
391
|
+
this._params = {0: n};
|
|
392
|
+
this._contextNode = n;
|
|
393
|
+
try {
|
|
394
|
+
cb.call(this, n);
|
|
395
|
+
} finally {
|
|
396
|
+
// Restore previous parameter context
|
|
397
|
+
this._params = prevParams;
|
|
398
|
+
this._contextNode = prevContext;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Groups items and executes callback for each group.
|
|
406
|
+
* Equivalent to XSLT's xsl:for-each-group.
|
|
407
|
+
* @param {string} select - XPath selector for items to group
|
|
408
|
+
* @param {object} options - Grouping options
|
|
409
|
+
* @param {string} [options.groupBy] - XPath expression to group by value
|
|
410
|
+
* @param {string} [options.groupAdjacent] - Groups adjacent items with
|
|
411
|
+
* same value
|
|
412
|
+
* @param {string} [options.groupStartingWith] - Starts new group when
|
|
413
|
+
* expression matches
|
|
414
|
+
* @param {string} [options.groupEndingWith] - Ends group when expression
|
|
415
|
+
* matches
|
|
416
|
+
* @param {(
|
|
417
|
+
* this: XPathTransformerContext, key: any, items: Node[], ctx: any
|
|
418
|
+
* ) => void} cb - Callback receives (groupingKey, groupItems, context)
|
|
419
|
+
* @returns {this}
|
|
420
|
+
*/
|
|
421
|
+
forEachGroup (select, options, cb) {
|
|
422
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
423
|
+
const that = this;
|
|
424
|
+
const {groupBy, groupAdjacent, groupStartingWith, groupEndingWith} =
|
|
425
|
+
options;
|
|
426
|
+
|
|
427
|
+
const nodesResult = this._evalXPath(select, true);
|
|
428
|
+
const nodes = /** @type {Node[]} */ (nodesResult);
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @param {string} expr
|
|
432
|
+
* @param {Node} node
|
|
433
|
+
* @returns {any}
|
|
434
|
+
*/
|
|
435
|
+
function evalInContext (expr, node) {
|
|
436
|
+
if (expr === '.' || expr === '@') {
|
|
437
|
+
return node.textContent;
|
|
438
|
+
}
|
|
439
|
+
// Temporarily set context to evaluate expression
|
|
440
|
+
const prevNode = that._contextNode;
|
|
441
|
+
that._contextNode = node;
|
|
442
|
+
try {
|
|
443
|
+
const result = that._evalXPath(expr, false);
|
|
444
|
+
// XPath v2/v3 always return arrays; handle NodeList/Array results
|
|
445
|
+
// by taking first item or its text content
|
|
446
|
+
const resultWithLength = /** @type {{length: number}} */ (result);
|
|
447
|
+
if (resultWithLength.length > 0) {
|
|
448
|
+
const firstItem = /** @type {any} */ (result)[0];
|
|
449
|
+
return firstItem?.textContent ?? firstItem;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
} finally {
|
|
453
|
+
that._contextNode = prevNode;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** @type {Map<string, Node[]>} */
|
|
458
|
+
const groups = new Map();
|
|
459
|
+
|
|
460
|
+
if (groupBy) {
|
|
461
|
+
// Group by computed value
|
|
462
|
+
for (const node of nodes) {
|
|
463
|
+
const key = evalInContext(groupBy, node);
|
|
464
|
+
// Handle undefined by converting to null for JSON serialization
|
|
465
|
+
const keyStr = JSON.stringify(key === undefined ? null : key);
|
|
466
|
+
if (!groups.has(keyStr)) {
|
|
467
|
+
groups.set(keyStr, []);
|
|
468
|
+
}
|
|
469
|
+
/** @type {Node[]} */ (groups.get(keyStr)).push(node);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
for (const [keyStr, items] of groups) {
|
|
473
|
+
const key = JSON.parse(keyStr);
|
|
474
|
+
// Convert null back to undefined if that was the original value
|
|
475
|
+
const actualKey = key === null && keyStr === 'null' ? undefined : key;
|
|
476
|
+
const prevParams = this._params;
|
|
477
|
+
const prevContext = this._contextNode;
|
|
478
|
+
try {
|
|
479
|
+
this._contextNode = items[0];
|
|
480
|
+
/** @type {any} */ (this)._currentGroup = items;
|
|
481
|
+
/** @type {any} */ (this)._currentGroupingKey = actualKey;
|
|
482
|
+
cb.call(this, actualKey, items, this);
|
|
483
|
+
} finally {
|
|
484
|
+
this._params = prevParams;
|
|
485
|
+
this._contextNode = prevContext;
|
|
486
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
487
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} else if (groupAdjacent) {
|
|
491
|
+
// Group adjacent items with same value
|
|
492
|
+
/** @type {string|null} */
|
|
493
|
+
let currentKey = null;
|
|
494
|
+
/** @type {Node[]} */
|
|
495
|
+
let currentGroup = [];
|
|
496
|
+
|
|
497
|
+
for (const node of nodes) {
|
|
498
|
+
const key = evalInContext(groupAdjacent, node);
|
|
499
|
+
const keyStr = JSON.stringify(key);
|
|
500
|
+
|
|
501
|
+
if (currentKey === null || currentKey !== keyStr) {
|
|
502
|
+
if (currentGroup.length > 0) {
|
|
503
|
+
const prevParams = this._params;
|
|
504
|
+
const prevContext = this._contextNode;
|
|
505
|
+
try {
|
|
506
|
+
this._contextNode = currentGroup[0];
|
|
507
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
508
|
+
/** @type {any} */ (this)._currentGroupingKey =
|
|
509
|
+
JSON.parse(/** @type {string} */ (currentKey));
|
|
510
|
+
cb.call(
|
|
511
|
+
this,
|
|
512
|
+
JSON.parse(/** @type {string} */ (currentKey)),
|
|
513
|
+
currentGroup,
|
|
514
|
+
this
|
|
515
|
+
);
|
|
516
|
+
} finally {
|
|
517
|
+
this._params = prevParams;
|
|
518
|
+
this._contextNode = prevContext;
|
|
519
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
520
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
currentKey = keyStr;
|
|
524
|
+
currentGroup = [node];
|
|
525
|
+
} else {
|
|
526
|
+
currentGroup.push(node);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Process last group
|
|
531
|
+
if (currentGroup.length > 0) {
|
|
532
|
+
const prevParams = this._params;
|
|
533
|
+
const prevContext = this._contextNode;
|
|
534
|
+
try {
|
|
535
|
+
this._contextNode = currentGroup[0];
|
|
536
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
537
|
+
/** @type {any} */ (this)._currentGroupingKey =
|
|
538
|
+
JSON.parse(/** @type {string} */ (currentKey));
|
|
539
|
+
cb.call(
|
|
540
|
+
this,
|
|
541
|
+
JSON.parse(/** @type {string} */ (currentKey)),
|
|
542
|
+
currentGroup,
|
|
543
|
+
this
|
|
544
|
+
);
|
|
545
|
+
} finally {
|
|
546
|
+
this._params = prevParams;
|
|
547
|
+
this._contextNode = prevContext;
|
|
548
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
549
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
} else if (groupStartingWith) {
|
|
553
|
+
// Start new group when expression matches
|
|
554
|
+
/** @type {Node[]} */
|
|
555
|
+
let currentGroup = [];
|
|
556
|
+
|
|
557
|
+
for (const node of nodes) {
|
|
558
|
+
const startMatch = evalInContext(groupStartingWith, node);
|
|
559
|
+
|
|
560
|
+
if (startMatch && currentGroup.length > 0) {
|
|
561
|
+
const prevParams = this._params;
|
|
562
|
+
const prevContext = this._contextNode;
|
|
563
|
+
try {
|
|
564
|
+
this._contextNode = currentGroup[0];
|
|
565
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
566
|
+
cb.call(this, null, currentGroup, this);
|
|
567
|
+
} finally {
|
|
568
|
+
this._params = prevParams;
|
|
569
|
+
this._contextNode = prevContext;
|
|
570
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
571
|
+
}
|
|
572
|
+
currentGroup = [];
|
|
573
|
+
}
|
|
574
|
+
currentGroup.push(node);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Process last group
|
|
578
|
+
if (currentGroup.length > 0) {
|
|
579
|
+
const prevParams = this._params;
|
|
580
|
+
const prevContext = this._contextNode;
|
|
581
|
+
try {
|
|
582
|
+
this._contextNode = currentGroup[0];
|
|
583
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
584
|
+
cb.call(this, null, currentGroup, this);
|
|
585
|
+
} finally {
|
|
586
|
+
this._params = prevParams;
|
|
587
|
+
this._contextNode = prevContext;
|
|
588
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} else if (groupEndingWith) {
|
|
592
|
+
// End group when expression matches
|
|
593
|
+
/** @type {Node[]} */
|
|
594
|
+
let currentGroup = [];
|
|
595
|
+
|
|
596
|
+
for (const node of nodes) {
|
|
597
|
+
currentGroup.push(node);
|
|
598
|
+
const endMatch = evalInContext(groupEndingWith, node);
|
|
599
|
+
|
|
600
|
+
if (endMatch) {
|
|
601
|
+
const prevParams = this._params;
|
|
602
|
+
const prevContext = this._contextNode;
|
|
603
|
+
try {
|
|
604
|
+
this._contextNode = currentGroup[0];
|
|
605
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
606
|
+
cb.call(this, null, currentGroup, this);
|
|
607
|
+
} finally {
|
|
608
|
+
this._params = prevParams;
|
|
609
|
+
this._contextNode = prevContext;
|
|
610
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
611
|
+
}
|
|
612
|
+
currentGroup = [];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Process last group if not ended
|
|
617
|
+
if (currentGroup.length > 0) {
|
|
618
|
+
const prevParams = this._params;
|
|
619
|
+
const prevContext = this._contextNode;
|
|
620
|
+
try {
|
|
621
|
+
this._contextNode = currentGroup[0];
|
|
622
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
623
|
+
cb.call(this, null, currentGroup, this);
|
|
624
|
+
} finally {
|
|
625
|
+
this._params = prevParams;
|
|
626
|
+
this._contextNode = prevContext;
|
|
627
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
375
630
|
}
|
|
631
|
+
|
|
376
632
|
return this;
|
|
377
633
|
}
|
|
378
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Returns the current group (for use within forEachGroup callback).
|
|
637
|
+
* @returns {Node[]|undefined}
|
|
638
|
+
*/
|
|
639
|
+
currentGroup () {
|
|
640
|
+
return /** @type {any} */ (this)._currentGroup;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Returns the current grouping key (for use within forEachGroup callback).
|
|
645
|
+
* @returns {any}
|
|
646
|
+
*/
|
|
647
|
+
currentGroupingKey () {
|
|
648
|
+
return /** @type {any} */ (this)._currentGroupingKey;
|
|
649
|
+
}
|
|
650
|
+
|
|
379
651
|
/**
|
|
380
652
|
* Append the value from an XPath expression or the context node text.
|
|
381
653
|
* @param {string|object} [select]
|
|
@@ -389,11 +661,66 @@ class XPathTransformerContext {
|
|
|
389
661
|
? /** @type {{select?: string}} */ (select).select
|
|
390
662
|
: select;
|
|
391
663
|
|
|
664
|
+
// Check for format-number() function call
|
|
665
|
+
if (selectStr && selectStr.includes('format-number(')) {
|
|
666
|
+
const match = (/format-number\((?<value>[^,\)]+)(?:,\s*["'](?<format>[^"']+)["'])?(?:,\s*["'](?<decimalFormat>[^"']*)["'])?\)/v).exec(selectStr);
|
|
667
|
+
if (match && match.groups) {
|
|
668
|
+
const {
|
|
669
|
+
value: valueExpr,
|
|
670
|
+
format: formatStr,
|
|
671
|
+
decimalFormat: decimalFormatName
|
|
672
|
+
} = match.groups;
|
|
673
|
+
// Evaluate the value expression
|
|
674
|
+
let numValue;
|
|
675
|
+
if (valueExpr.trim().startsWith('$')) {
|
|
676
|
+
// Parameter reference
|
|
677
|
+
const paramName = valueExpr.trim().slice(1);
|
|
678
|
+
numValue = this._params && paramName in this._params
|
|
679
|
+
? this._params[paramName]
|
|
680
|
+
: 0;
|
|
681
|
+
} else {
|
|
682
|
+
// Try to parse as number or evaluate as XPath
|
|
683
|
+
const trimmed = valueExpr.trim();
|
|
684
|
+
numValue = Number.isNaN(Number(trimmed))
|
|
685
|
+
? this._evalXPath(trimmed, false)
|
|
686
|
+
: Number(trimmed);
|
|
687
|
+
}
|
|
688
|
+
const num = typeof numValue === 'string' ? Number(numValue) : numValue;
|
|
689
|
+
const format = formatStr || '1';
|
|
690
|
+
const formatted = this._formatNumber(
|
|
691
|
+
num,
|
|
692
|
+
format,
|
|
693
|
+
undefined,
|
|
694
|
+
undefined,
|
|
695
|
+
decimalFormatName || '',
|
|
696
|
+
'en'
|
|
697
|
+
);
|
|
698
|
+
jt.append(formatted);
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
392
703
|
// Check if this is a parameter reference (starts with $)
|
|
393
704
|
if (selectStr && selectStr.startsWith('$')) {
|
|
394
705
|
const paramName = selectStr.slice(1);
|
|
395
706
|
if (this._params && paramName in this._params) {
|
|
396
707
|
val = this._params[paramName];
|
|
708
|
+
// If val is a Node, extract its text content
|
|
709
|
+
if (val && typeof val === 'object' && 'nodeType' in val) {
|
|
710
|
+
if (val.nodeType === 3) {
|
|
711
|
+
// Text node: use nodeValue
|
|
712
|
+
val = val.nodeValue;
|
|
713
|
+
} else if (val.nodeType === 9) {
|
|
714
|
+
// Document node: use documentElement.textContent
|
|
715
|
+
const doc = /** @type {Document} */ (val);
|
|
716
|
+
val = doc.documentElement
|
|
717
|
+
? doc.documentElement.textContent
|
|
718
|
+
: null;
|
|
719
|
+
} else {
|
|
720
|
+
// Other nodes: use textContent
|
|
721
|
+
val = val.textContent;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
397
724
|
} else {
|
|
398
725
|
// Fall back to normal XPath evaluation
|
|
399
726
|
const resResult = this._evalXPath(selectStr, true);
|
|
@@ -406,9 +733,19 @@ class XPathTransformerContext {
|
|
|
406
733
|
/* c8 ignore stop */
|
|
407
734
|
}
|
|
408
735
|
} else if (!selectStr || selectStr === '.') {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
736
|
+
if (this._contextNode.nodeType === 3) {
|
|
737
|
+
// Text node: use nodeValue
|
|
738
|
+
val = this._contextNode.nodeValue;
|
|
739
|
+
} else if (this._contextNode.nodeType === 9) {
|
|
740
|
+
// Document node: use documentElement.textContent
|
|
741
|
+
const doc = /** @type {Document} */ (this._contextNode);
|
|
742
|
+
val = doc.documentElement
|
|
743
|
+
? doc.documentElement.textContent
|
|
744
|
+
: null;
|
|
745
|
+
} else {
|
|
746
|
+
// Other nodes: use textContent
|
|
747
|
+
val = this._contextNode.textContent;
|
|
748
|
+
}
|
|
412
749
|
} else {
|
|
413
750
|
const resResult = this._evalXPath(selectStr, true);
|
|
414
751
|
const res = /** @type {Node[]} */ (resResult);
|
|
@@ -549,6 +886,7 @@ class XPathTransformerContext {
|
|
|
549
886
|
* level?: 'single'|'multiple'|'any',
|
|
550
887
|
* from?: string,
|
|
551
888
|
* format?: string,
|
|
889
|
+
* decimalFormat?: string,
|
|
552
890
|
* groupingSeparator?: string,
|
|
553
891
|
* groupingSize?: number
|
|
554
892
|
* }} num - Number value, "position()" string, or options object
|
|
@@ -733,15 +1071,27 @@ class XPathTransformerContext {
|
|
|
733
1071
|
* @param {string} format - Format string (1, a, A, i, I, 01, etc.)
|
|
734
1072
|
* @param {string} [groupingSeparator] - Separator for grouping (e.g., ',')
|
|
735
1073
|
* @param {number} [groupingSize] - Size of groups (e.g., 3 for 1,000)
|
|
1074
|
+
* @param {string} [decimalFormatName] - Named decimal format to use
|
|
736
1075
|
* @param {string} [locale]
|
|
737
1076
|
* @returns {string}
|
|
738
1077
|
* @private
|
|
739
1078
|
*/
|
|
740
|
-
_formatNumber (
|
|
1079
|
+
_formatNumber (
|
|
1080
|
+
num, format, groupingSeparator, groupingSize, decimalFormatName, locale
|
|
1081
|
+
) {
|
|
741
1082
|
if (Number.isNaN(num)) {
|
|
742
1083
|
return String(num);
|
|
743
1084
|
}
|
|
744
1085
|
|
|
1086
|
+
// Get decimal format symbols if specified
|
|
1087
|
+
const symbols = decimalFormatName !== undefined &&
|
|
1088
|
+
decimalFormatName in this.decimalFormats
|
|
1089
|
+
? this.decimalFormats[decimalFormatName]
|
|
1090
|
+
: undefined;
|
|
1091
|
+
|
|
1092
|
+
// Default locale
|
|
1093
|
+
const loc = locale || 'en';
|
|
1094
|
+
|
|
745
1095
|
let result;
|
|
746
1096
|
const formatChar = format.charAt(0);
|
|
747
1097
|
|
|
@@ -768,7 +1118,8 @@ class XPathTransformerContext {
|
|
|
768
1118
|
}
|
|
769
1119
|
case '0': {
|
|
770
1120
|
const width = format.length;
|
|
771
|
-
|
|
1121
|
+
const zeroDigit = symbols?.zeroDigit || '0';
|
|
1122
|
+
result = String(num).padStart(width, zeroDigit);
|
|
772
1123
|
|
|
773
1124
|
break;
|
|
774
1125
|
}
|
|
@@ -782,8 +1133,26 @@ class XPathTransformerContext {
|
|
|
782
1133
|
};
|
|
783
1134
|
}
|
|
784
1135
|
try {
|
|
785
|
-
result = new Intl.NumberFormat(
|
|
786
|
-
|
|
1136
|
+
result = new Intl.NumberFormat(loc, options).format(num);
|
|
1137
|
+
|
|
1138
|
+
// Apply decimal format symbols if specified
|
|
1139
|
+
if (symbols) {
|
|
1140
|
+
// Use placeholders to avoid conflicts during replacement
|
|
1141
|
+
const TEMP_GROUP = '\u0000GROUPSEP\u0000';
|
|
1142
|
+
const TEMP_DECIMAL = '\u0000DECIMALSEP\u0000';
|
|
1143
|
+
|
|
1144
|
+
// Replace with temporary placeholders first
|
|
1145
|
+
result = result.replaceAll(',', TEMP_GROUP);
|
|
1146
|
+
result = result.replaceAll('.', TEMP_DECIMAL);
|
|
1147
|
+
|
|
1148
|
+
// Now replace with actual symbols
|
|
1149
|
+
const effectiveGroupingSep = groupingSeparator ||
|
|
1150
|
+
symbols.groupingSeparator || ',';
|
|
1151
|
+
const effectiveDecimalSep = symbols.decimalSeparator || '.';
|
|
1152
|
+
|
|
1153
|
+
result = result.replaceAll(TEMP_GROUP, effectiveGroupingSep);
|
|
1154
|
+
result = result.replaceAll(TEMP_DECIMAL, effectiveDecimalSep);
|
|
1155
|
+
} else if (groupingSeparator) {
|
|
787
1156
|
result = result.replaceAll(',', groupingSeparator);
|
|
788
1157
|
}
|
|
789
1158
|
} catch (e) {
|
|
@@ -864,6 +1233,15 @@ class XPathTransformerContext {
|
|
|
864
1233
|
this._getJoiningTransformer().propValue(prop, val);
|
|
865
1234
|
return this;
|
|
866
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Alias for propValue(). Append a key-value pair to the current map/object.
|
|
1238
|
+
* @param {string} prop Property name
|
|
1239
|
+
* @param {any} val Value
|
|
1240
|
+
* @returns {XPathTransformerContext}
|
|
1241
|
+
*/
|
|
1242
|
+
mapEntry (prop, val) {
|
|
1243
|
+
return this.propValue(prop, val);
|
|
1244
|
+
}
|
|
867
1245
|
/**
|
|
868
1246
|
* Append object.
|
|
869
1247
|
* @param {Record<string, unknown>|
|
|
@@ -884,6 +1262,23 @@ class XPathTransformerContext {
|
|
|
884
1262
|
propSets);
|
|
885
1263
|
return this;
|
|
886
1264
|
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Alias for object(). Append an object/map.
|
|
1267
|
+
* @param {Record<string, unknown>|
|
|
1268
|
+
* ((this: XPathTransformerContext) => void)} objOrCb Object or callback
|
|
1269
|
+
* @param {((this: XPathTransformerContext) => void)|
|
|
1270
|
+
* any[]} [cbOrUsePropertySets] Callback or property sets
|
|
1271
|
+
* @param {any[]|
|
|
1272
|
+
* Record<string, unknown>} [usePropertySetsOrPropSets]
|
|
1273
|
+
* Property sets or props
|
|
1274
|
+
* @param {Record<string, unknown>} [propSets] Additional property sets
|
|
1275
|
+
* @returns {XPathTransformerContext}
|
|
1276
|
+
*/
|
|
1277
|
+
map (objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets, propSets) {
|
|
1278
|
+
return this.object(
|
|
1279
|
+
objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets, propSets
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
887
1282
|
/**
|
|
888
1283
|
* Append array.
|
|
889
1284
|
* @param {any[]|
|
|
@@ -910,6 +1305,27 @@ class XPathTransformerContext {
|
|
|
910
1305
|
return this;
|
|
911
1306
|
}
|
|
912
1307
|
|
|
1308
|
+
/**
|
|
1309
|
+
* @param {string} name
|
|
1310
|
+
* @param {import('./AbstractJoiningTransformer.js').
|
|
1311
|
+
* OutputCharacters} outputCharacters
|
|
1312
|
+
* @returns {this}
|
|
1313
|
+
*/
|
|
1314
|
+
characterMap (name, outputCharacters) {
|
|
1315
|
+
this._getJoiningTransformer().characterMap(name, outputCharacters);
|
|
1316
|
+
return this;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* @param {string} name
|
|
1321
|
+
* @param {Record<string, string>} attributes
|
|
1322
|
+
* @returns {this}
|
|
1323
|
+
*/
|
|
1324
|
+
attributeSet (name, attributes) {
|
|
1325
|
+
this._getJoiningTransformer().attributeSet(name, attributes);
|
|
1326
|
+
return this;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
913
1329
|
/**
|
|
914
1330
|
* Append element.
|
|
915
1331
|
* @param {string} name Tag name
|
|
@@ -918,13 +1334,53 @@ class XPathTransformerContext {
|
|
|
918
1334
|
* @param {any[]|((this: XPathTransformerContext)=>void)} [children]
|
|
919
1335
|
* Children
|
|
920
1336
|
* @param {(this: XPathTransformerContext)=>void} [cb] Callback
|
|
1337
|
+
* @param {string[]} [useAttributeSets] - Attribute set names to apply
|
|
921
1338
|
* @returns {XPathTransformerContext}
|
|
922
1339
|
*/
|
|
923
|
-
element (name, atts, children, cb) {
|
|
924
|
-
|
|
925
|
-
|
|
1340
|
+
element (name, atts, children, cb, useAttributeSets) {
|
|
1341
|
+
/** @type {any} */ (this._getJoiningTransformer()).element(
|
|
1342
|
+
name, atts, children, cb, useAttributeSets
|
|
1343
|
+
);
|
|
926
1344
|
return this;
|
|
927
1345
|
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Adds a prefixed namespace declaration to the most recently opened
|
|
1349
|
+
* element. Mirrors the joining
|
|
1350
|
+
* transformer API so templates can call `this.attribute()`.
|
|
1351
|
+
* @param {string} prefix - Prefix
|
|
1352
|
+
* @param {string} namespaceURI - Namespace
|
|
1353
|
+
* @returns {this}
|
|
1354
|
+
*/
|
|
1355
|
+
namespace (prefix, namespaceURI) {
|
|
1356
|
+
/** @type {any} */ (this._getJoiningTransformer()).namespace(
|
|
1357
|
+
prefix, namespaceURI
|
|
1358
|
+
);
|
|
1359
|
+
return this;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Define a decimal format with custom symbols for number formatting.
|
|
1364
|
+
* Equivalent to xsl:decimal-format. If no name is provided, defines
|
|
1365
|
+
* the default format.
|
|
1366
|
+
* @param {string|import('./JSONPathTransformerContext.js').
|
|
1367
|
+
* DecimalFormatSymbols} nameOrSymbols - Format name or symbols object
|
|
1368
|
+
* if defining default
|
|
1369
|
+
* @param {import('./JSONPathTransformerContext.js').
|
|
1370
|
+
* DecimalFormatSymbols} [symbols] - Format symbols
|
|
1371
|
+
* @returns {this}
|
|
1372
|
+
*/
|
|
1373
|
+
decimalFormat (nameOrSymbols, symbols) {
|
|
1374
|
+
if (typeof nameOrSymbols === 'string') {
|
|
1375
|
+
// Named format
|
|
1376
|
+
this.decimalFormats[nameOrSymbols] = symbols || {};
|
|
1377
|
+
} else {
|
|
1378
|
+
// Default format (unnamed)
|
|
1379
|
+
this.decimalFormats[''] = nameOrSymbols;
|
|
1380
|
+
}
|
|
1381
|
+
return this;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
928
1384
|
/**
|
|
929
1385
|
* Append attribute.
|
|
930
1386
|
* @param {string} name Attribute name
|
|
@@ -1138,6 +1594,26 @@ class XPathTransformerContext {
|
|
|
1138
1594
|
return this;
|
|
1139
1595
|
}
|
|
1140
1596
|
|
|
1597
|
+
/**
|
|
1598
|
+
* Assert that a test condition is true, throwing an error if it fails.
|
|
1599
|
+
* Equivalent to xsl:assert. Evaluates an XPath expression using the
|
|
1600
|
+
* same truthiness rules as if() and choose().
|
|
1601
|
+
* @param {string} test - XPath expression to test
|
|
1602
|
+
* @param {string} [message] - Optional error message to include
|
|
1603
|
+
* @returns {XPathTransformerContext}
|
|
1604
|
+
* @throws {Error} When the test expression evaluates to false
|
|
1605
|
+
*/
|
|
1606
|
+
assert (test, message) {
|
|
1607
|
+
const passes = this._passesIf(test);
|
|
1608
|
+
if (!passes) {
|
|
1609
|
+
const errorMsg = message
|
|
1610
|
+
? `Assertion failed: ${message}`
|
|
1611
|
+
: `Assertion failed: ${test}`;
|
|
1612
|
+
throw new Error(errorMsg);
|
|
1613
|
+
}
|
|
1614
|
+
return this;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1141
1617
|
/**
|
|
1142
1618
|
* Analyze a string with a regular expression, equivalent to
|
|
1143
1619
|
* xsl:analyze-string. Processes matching and non-matching substrings
|
package/src/index.js
CHANGED
|
@@ -380,7 +380,13 @@ class JTLT {
|
|
|
380
380
|
);
|
|
381
381
|
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
382
382
|
// eslint-disable-next-line unicorn/no-array-method-this-argument -- Not array
|
|
383
|
-
this.forEach(path,
|
|
383
|
+
this.forEach(path, (arg) => {
|
|
384
|
+
const ret = fn.call(this, arg);
|
|
385
|
+
if (typeof ret !== 'undefined') {
|
|
386
|
+
// @ts-expect-error Ok?
|
|
387
|
+
this._getJoiningTransformer().append(ret);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
384
390
|
}
|
|
385
391
|
: cfg.query || (
|
|
386
392
|
typeof cfg.templates === 'function'
|