jtlt 0.8.0 → 0.10.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 +11 -1
- package/README.md +2 -4
- package/dist/AbstractJoiningTransformer.d.ts +92 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
- package/dist/DOMJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +39 -0
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/StringJoiningTransformer.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +39 -0
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/docs/API.expanded.md +33 -0
- package/docs/API.md +22 -0
- package/docs/TO-DO.md +1 -1
- package/eslint.config.js +2 -1
- package/package.json +1 -1
- package/src/AbstractJoiningTransformer.js +113 -0
- package/src/DOMJoiningTransformer.js +39 -1
- package/src/JSONJoiningTransformer.js +53 -2
- package/src/JSONPathTransformer.js +4 -0
- package/src/JSONPathTransformerContext.js +138 -1
- package/src/StringJoiningTransformer.js +38 -1
- package/src/XPathTransformer.js +4 -0
- package/src/XPathTransformerContext.js +156 -3
- package/src/index.js +1 -0
|
@@ -77,6 +77,39 @@ class AbstractJoiningTransformer {
|
|
|
77
77
|
|
|
78
78
|
/** @type {Map<string, string>} */
|
|
79
79
|
this._namespaceAliases = new Map();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @type {{
|
|
83
|
+
* onMultipleMatch?: "use-last"|"fail",
|
|
84
|
+
* warningOnMultipleMatch?: boolean,
|
|
85
|
+
* onNoMatch?: "shallow-copy"|"deep-copy"|"fail"|"apply-templates"|
|
|
86
|
+
* "shallow-skip"|"deep-skip"|"text-only-copy",
|
|
87
|
+
* warningOnNoMatch?: boolean
|
|
88
|
+
* }|undefined}
|
|
89
|
+
*/
|
|
90
|
+
this._modeConfig = undefined;
|
|
91
|
+
|
|
92
|
+
/** @type {Set<string>} */
|
|
93
|
+
this._excludeResultPrefixes = new Set();
|
|
94
|
+
|
|
95
|
+
/** @type {Set<string>} */
|
|
96
|
+
this._usedNamespacePrefixes = new Set();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pending namespace declarations that may be excluded.
|
|
100
|
+
* @type {Array<{
|
|
101
|
+
* prefix: string,
|
|
102
|
+
* namespaceURI: string,
|
|
103
|
+
* callback: () => void
|
|
104
|
+
* }>}
|
|
105
|
+
*/
|
|
106
|
+
this._pendingNamespaces = [];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Track which prefixes have pending namespace declarations.
|
|
110
|
+
* @type {Map<string, {prefix: string, namespaceURI: string}>}
|
|
111
|
+
*/
|
|
112
|
+
this._pendingNamespaceMap = new Map();
|
|
80
113
|
}
|
|
81
114
|
|
|
82
115
|
/**
|
|
@@ -101,6 +134,52 @@ class AbstractJoiningTransformer {
|
|
|
101
134
|
return this;
|
|
102
135
|
}
|
|
103
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Configure mode behavior (similar to xsl:mode).
|
|
139
|
+
* @param {{
|
|
140
|
+
* onMultipleMatch?: "use-last"|"fail",
|
|
141
|
+
* warningOnMultipleMatch?: boolean,
|
|
142
|
+
* onNoMatch?: "shallow-copy"|"deep-copy"|"fail"|"apply-templates"|
|
|
143
|
+
* "shallow-skip"|"deep-skip"|"text-only-copy",
|
|
144
|
+
* warningOnNoMatch?: boolean
|
|
145
|
+
* }} cfg - Mode configuration
|
|
146
|
+
* @returns {this}
|
|
147
|
+
*/
|
|
148
|
+
mode (cfg) {
|
|
149
|
+
this._modeConfig = cfg;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Configure stylesheet behavior (similar to xsl:stylesheet).
|
|
155
|
+
* Unlike xsl:stylesheet, this is a directive method and does not contain
|
|
156
|
+
* nested content.
|
|
157
|
+
* @param {{
|
|
158
|
+
* excludeResultPrefixes?: string[]
|
|
159
|
+
* }} cfg - Stylesheet configuration
|
|
160
|
+
* @returns {this}
|
|
161
|
+
*/
|
|
162
|
+
stylesheet (cfg) {
|
|
163
|
+
if (cfg.excludeResultPrefixes) {
|
|
164
|
+
for (const prefix of cfg.excludeResultPrefixes) {
|
|
165
|
+
// Normalize empty string to internal #default representation
|
|
166
|
+
this._excludeResultPrefixes.add(prefix === '' ? '#default' : prefix);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Alias for stylesheet() method (XSLT compatibility).
|
|
174
|
+
* @param {{
|
|
175
|
+
* excludeResultPrefixes?: string[]
|
|
176
|
+
* }} cfg - Stylesheet configuration
|
|
177
|
+
* @returns {this}
|
|
178
|
+
*/
|
|
179
|
+
transform (cfg) {
|
|
180
|
+
return this.stylesheet(cfg);
|
|
181
|
+
}
|
|
182
|
+
|
|
104
183
|
/**
|
|
105
184
|
* @param {string} name
|
|
106
185
|
* @param {OutputCharacters} outputCharacters
|
|
@@ -171,6 +250,12 @@ class AbstractJoiningTransformer {
|
|
|
171
250
|
const prefix = colonIdx === -1 ? '#default' : elemName.slice(0, colonIdx);
|
|
172
251
|
const alias = this._getNamespaceAlias(prefix);
|
|
173
252
|
|
|
253
|
+
// Track that this prefix (after aliasing) is actually used
|
|
254
|
+
this._usedNamespacePrefixes.add(alias);
|
|
255
|
+
|
|
256
|
+
// If this prefix was buffered, output it now
|
|
257
|
+
this._flushPendingNamespace(alias);
|
|
258
|
+
|
|
174
259
|
if (colonIdx === -1) {
|
|
175
260
|
if (alias === '#default') {
|
|
176
261
|
return elemName;
|
|
@@ -184,6 +269,34 @@ class AbstractJoiningTransformer {
|
|
|
184
269
|
) + elemName.slice(colonIdx + 1);
|
|
185
270
|
}
|
|
186
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Output a pending namespace declaration if it exists.
|
|
274
|
+
* This method should be overridden by subclasses.
|
|
275
|
+
* @param {string} _prefix
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
// eslint-disable-next-line class-methods-use-this -- Abstract
|
|
279
|
+
_flushPendingNamespace (_prefix) {
|
|
280
|
+
// Default implementation does nothing - subclasses override
|
|
281
|
+
// Using _prefix to indicate unused parameter
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Track attribute name prefix usage.
|
|
286
|
+
* @param {string} attrName
|
|
287
|
+
* @returns {void}
|
|
288
|
+
*/
|
|
289
|
+
_trackAttributePrefix (attrName) {
|
|
290
|
+
const colonIdx = attrName.indexOf(':');
|
|
291
|
+
if (colonIdx !== -1 && !attrName.startsWith('xmlns')) {
|
|
292
|
+
const prefix = attrName.slice(0, colonIdx);
|
|
293
|
+
this._usedNamespacePrefixes.add(prefix);
|
|
294
|
+
|
|
295
|
+
// If this prefix was buffered, output it now
|
|
296
|
+
this._flushPendingNamespace(prefix);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
187
300
|
/**
|
|
188
301
|
* @param {string} str
|
|
189
302
|
* @returns {string}
|
|
@@ -479,7 +479,24 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
479
479
|
* @returns {DOMJoiningTransformer}
|
|
480
480
|
*/
|
|
481
481
|
namespace (prefix, namespaceURI) {
|
|
482
|
-
|
|
482
|
+
let alias = this._getNamespaceAlias(prefix);
|
|
483
|
+
|
|
484
|
+
// Normalize empty string or undefined/null to #default
|
|
485
|
+
if (!alias) {
|
|
486
|
+
alias = '#default';
|
|
487
|
+
}
|
|
488
|
+
const normalizedPrefix = alias;
|
|
489
|
+
|
|
490
|
+
// If this prefix is excluded, buffer it instead of outputting immediately
|
|
491
|
+
if (this._excludeResultPrefixes.has(normalizedPrefix)) {
|
|
492
|
+
this._pendingNamespaceMap.set(normalizedPrefix, {
|
|
493
|
+
prefix: alias,
|
|
494
|
+
namespaceURI
|
|
495
|
+
});
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Not excluded, output immediately
|
|
483
500
|
/** @type {Element} */
|
|
484
501
|
(this._dom).setAttributeNS(
|
|
485
502
|
'http://www.w3.org/2000/xmlns/',
|
|
@@ -489,6 +506,23 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
489
506
|
return this;
|
|
490
507
|
}
|
|
491
508
|
|
|
509
|
+
/**
|
|
510
|
+
* @param {string} prefix
|
|
511
|
+
* @returns {void}
|
|
512
|
+
* @override
|
|
513
|
+
*/
|
|
514
|
+
_flushPendingNamespace (prefix) {
|
|
515
|
+
const pending = this._pendingNamespaceMap.get(prefix);
|
|
516
|
+
if (pending) {
|
|
517
|
+
/** @type {Element} */
|
|
518
|
+
(this._dom).setAttributeNS(
|
|
519
|
+
'http://www.w3.org/2000/xmlns/',
|
|
520
|
+
pending.prefix === '#default' ? 'xmlns' : 'xmlns:' + pending.prefix,
|
|
521
|
+
this._replaceCharacterMaps(pending.namespaceURI)
|
|
522
|
+
);
|
|
523
|
+
this._pendingNamespaceMap.delete(prefix);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
492
526
|
/**
|
|
493
527
|
* @param {string} name
|
|
494
528
|
* @param {string} val
|
|
@@ -499,6 +533,10 @@ class DOMJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
499
533
|
this._dom.nodeType !== 1) {
|
|
500
534
|
throw new Error('You may only set an attribute on an element');
|
|
501
535
|
}
|
|
536
|
+
|
|
537
|
+
// Track attribute prefix usage for namespace exclusion
|
|
538
|
+
this._trackAttributePrefix(name);
|
|
539
|
+
|
|
502
540
|
(/** @type {Element} */ (this._dom)).setAttribute(
|
|
503
541
|
this._replaceNamespaceAliasInNamespaceDeclaration(name),
|
|
504
542
|
this._replaceCharacterMaps(val)
|
|
@@ -551,6 +551,25 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
551
551
|
// No-op outside an element() callback (JSON joiner semantics)
|
|
552
552
|
return this;
|
|
553
553
|
}
|
|
554
|
+
|
|
555
|
+
let alias = this._getNamespaceAlias(prefix);
|
|
556
|
+
|
|
557
|
+
// Normalize empty string or undefined/null to #default
|
|
558
|
+
if (!alias) {
|
|
559
|
+
alias = '#default';
|
|
560
|
+
}
|
|
561
|
+
const normalizedPrefix = alias;
|
|
562
|
+
|
|
563
|
+
// If this prefix is excluded, buffer it instead of outputting immediately
|
|
564
|
+
if (this._excludeResultPrefixes.has(normalizedPrefix)) {
|
|
565
|
+
this._pendingNamespaceMap.set(normalizedPrefix, {
|
|
566
|
+
prefix: alias,
|
|
567
|
+
namespaceURI
|
|
568
|
+
});
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Not excluded, output immediately
|
|
554
573
|
const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
|
|
555
574
|
const {attsObj} = top;
|
|
556
575
|
|
|
@@ -558,7 +577,6 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
558
577
|
attsObj.xmlns = {};
|
|
559
578
|
}
|
|
560
579
|
|
|
561
|
-
const alias = this._getNamespaceAlias(prefix);
|
|
562
580
|
/** @type {Record<string, string>} */
|
|
563
581
|
(attsObj.xmlns)[alias === '#default' ? '' : alias] =
|
|
564
582
|
this._replaceCharacterMaps(namespaceURI);
|
|
@@ -566,6 +584,32 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
566
584
|
return this;
|
|
567
585
|
}
|
|
568
586
|
|
|
587
|
+
/**
|
|
588
|
+
* @param {string} prefix
|
|
589
|
+
* @returns {void}
|
|
590
|
+
* @override
|
|
591
|
+
*/
|
|
592
|
+
_flushPendingNamespace (prefix) {
|
|
593
|
+
if (!this._elementStack.length) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const pending = this._pendingNamespaceMap.get(prefix);
|
|
598
|
+
if (pending) {
|
|
599
|
+
const top = /** @type {ElementInfo} */ (this._elementStack.at(-1));
|
|
600
|
+
const {attsObj} = top;
|
|
601
|
+
|
|
602
|
+
if (!attsObj.xmlns) {
|
|
603
|
+
attsObj.xmlns = {};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** @type {Record<string, string>} */
|
|
607
|
+
(attsObj.xmlns)[pending.prefix === '#default' ? '' : pending.prefix] =
|
|
608
|
+
this._replaceCharacterMaps(pending.namespaceURI);
|
|
609
|
+
|
|
610
|
+
this._pendingNamespaceMap.delete(prefix);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
569
613
|
/**
|
|
570
614
|
* Adds/updates an attribute for the most recently open element built via
|
|
571
615
|
* a callback-driven element(). When not in an element callback context,
|
|
@@ -599,11 +643,18 @@ class JSONJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
599
643
|
if (name === '$a' && Array.isArray(val)) {
|
|
600
644
|
val.forEach((pair) => {
|
|
601
645
|
if (Array.isArray(pair) && pair.length > 1) {
|
|
602
|
-
|
|
646
|
+
const attrName = String(pair[0]);
|
|
647
|
+
// Track each ordered attribute
|
|
648
|
+
this._trackAttributePrefix(attrName);
|
|
649
|
+
attsObj[attrName] = this._replaceCharacterMaps(pair[1]);
|
|
603
650
|
}
|
|
604
651
|
});
|
|
605
652
|
return this;
|
|
606
653
|
}
|
|
654
|
+
|
|
655
|
+
// Track attribute prefix usage for namespace exclusion
|
|
656
|
+
this._trackAttributePrefix(name);
|
|
657
|
+
|
|
607
658
|
attsObj[this._replaceNamespaceAliasInNamespaceDeclaration(name)] =
|
|
608
659
|
this._replaceCharacterMaps(/** @type {string} */ (val));
|
|
609
660
|
return this;
|
|
@@ -30,6 +30,10 @@ class JSONPathTransformer {
|
|
|
30
30
|
{path: template[0], template: template[1]}
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
|
+
// Normalize 'match' to 'path' for XSLT compatibility
|
|
34
|
+
if (template.match && !template.path) {
|
|
35
|
+
return {...template, path: template.match};
|
|
36
|
+
}
|
|
33
37
|
return template;
|
|
34
38
|
});
|
|
35
39
|
this.templates.forEach((template, i, templates) => {
|
|
@@ -408,6 +408,48 @@ class JSONPathTransformerContext {
|
|
|
408
408
|
|
|
409
409
|
let templateObj;
|
|
410
410
|
if (!pathMatchedTemplates.length) {
|
|
411
|
+
// Check mode config for no-match behavior
|
|
412
|
+
const joiner = that._getJoiningTransformer();
|
|
413
|
+
const modeConfig = joiner._modeConfig;
|
|
414
|
+
if (modeConfig) {
|
|
415
|
+
const onNoMatch = modeConfig.onNoMatch ?? 'text-only-copy';
|
|
416
|
+
if (modeConfig.warningOnNoMatch) {
|
|
417
|
+
// eslint-disable-next-line no-console -- Warning as specified
|
|
418
|
+
console.warn(
|
|
419
|
+
'Warning: No template matches. ' +
|
|
420
|
+
'Mode is configured with warningOnNoMatch=true.'
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (onNoMatch === 'fail') {
|
|
424
|
+
throw new Error(
|
|
425
|
+
'No template matches. Mode is configured with onNoMatch="fail".'
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (onNoMatch === 'deep-skip') {
|
|
429
|
+
// Skip this node and its descendants entirely
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (onNoMatch === 'shallow-copy') {
|
|
433
|
+
// Output the value as-is without processing children
|
|
434
|
+
joiner.append(value);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (onNoMatch === 'deep-copy') {
|
|
438
|
+
// Output the value and all descendants as-is
|
|
439
|
+
joiner.append(JSON.stringify(value));
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (onNoMatch === 'text-only-copy') {
|
|
443
|
+
// Output only text content (primitives)
|
|
444
|
+
if (typeof value === 'string' || typeof value === 'number' ||
|
|
445
|
+
typeof value === 'boolean') {
|
|
446
|
+
joiner.append(String(value));
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// 'apply-templates', 'shallow-skip', or other:
|
|
451
|
+
// use default template rules
|
|
452
|
+
}
|
|
411
453
|
const dtr = JSONPathTransformer.DefaultTemplateRules;
|
|
412
454
|
if (propertyNamesMode) {
|
|
413
455
|
templateObj = dtr.transformPropertyNames;
|
|
@@ -446,6 +488,49 @@ class JSONPathTransformerContext {
|
|
|
446
488
|
return (aPriority > bPriority) ? -1 : 1;
|
|
447
489
|
});
|
|
448
490
|
|
|
491
|
+
// Check for multiple matches with same priority when mode is configured
|
|
492
|
+
const joiner = that._getJoiningTransformer();
|
|
493
|
+
const modeConfig = joiner._modeConfig;
|
|
494
|
+
if (modeConfig && pathMatchedTemplates.length > 1) {
|
|
495
|
+
// Check if top two templates have equal priority
|
|
496
|
+
const topPriority =
|
|
497
|
+
typeof pathMatchedTemplates[0].priority === 'number'
|
|
498
|
+
? pathMatchedTemplates[0].priority
|
|
499
|
+
: (that._config.specificityPriorityResolver &&
|
|
500
|
+
pathMatchedTemplates[0].path
|
|
501
|
+
? that._config.specificityPriorityResolver(
|
|
502
|
+
pathMatchedTemplates[0].path
|
|
503
|
+
)
|
|
504
|
+
/* c8 ignore next 2 -- defensive, templates without paths
|
|
505
|
+
are already filtered at line 388 */
|
|
506
|
+
: 0);
|
|
507
|
+
const secondPriority =
|
|
508
|
+
typeof pathMatchedTemplates[1].priority === 'number'
|
|
509
|
+
? pathMatchedTemplates[1].priority
|
|
510
|
+
: (that._config.specificityPriorityResolver &&
|
|
511
|
+
pathMatchedTemplates[1].path
|
|
512
|
+
? that._config.specificityPriorityResolver(
|
|
513
|
+
pathMatchedTemplates[1].path
|
|
514
|
+
)
|
|
515
|
+
/* c8 ignore next 2 -- defensive, templates without paths
|
|
516
|
+
are already filtered at line 388 */
|
|
517
|
+
: 0);
|
|
518
|
+
if (topPriority === secondPriority) {
|
|
519
|
+
if (modeConfig.onMultipleMatch === 'fail') {
|
|
520
|
+
throw new Error(
|
|
521
|
+
'Multiple templates match with equal priority. ' +
|
|
522
|
+
'Mode is configured with onMultipleMatch="fail".'
|
|
523
|
+
);
|
|
524
|
+
} else if (modeConfig.warningOnMultipleMatch !== false) {
|
|
525
|
+
// eslint-disable-next-line no-console -- Warning as specified
|
|
526
|
+
console.warn(
|
|
527
|
+
'Warning: Multiple templates match with equal priority. ' +
|
|
528
|
+
'Mode is configured with warningOnMultipleMatch=true.'
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
449
534
|
templateObj =
|
|
450
535
|
/** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
|
|
451
536
|
pathMatchedTemplates.shift()
|
|
@@ -471,7 +556,18 @@ class JSONPathTransformerContext {
|
|
|
471
556
|
that._params = prevTemplateParams;
|
|
472
557
|
if (typeof ret !== 'undefined') {
|
|
473
558
|
// After the undefined check, ret is ResultType<T>
|
|
474
|
-
that._getJoiningTransformer()
|
|
559
|
+
const joiner = that._getJoiningTransformer();
|
|
560
|
+
// Close any open tag before appending template return value
|
|
561
|
+
/* c8 ignore start -- _openTagState only on StringJoiningTransformer,
|
|
562
|
+
defensive check for string output mode */
|
|
563
|
+
// @ts-expect-error -- _openTagState only on StringJoiningTransformer
|
|
564
|
+
if (joiner._openTagState) {
|
|
565
|
+
joiner.append('>');
|
|
566
|
+
// @ts-expect-error -- _openTagState only on StringJoiningTransformer
|
|
567
|
+
joiner._openTagState = false;
|
|
568
|
+
}
|
|
569
|
+
/* c8 ignore stop */
|
|
570
|
+
joiner.append(
|
|
475
571
|
/** @type {string|Node|*} */ (ret)
|
|
476
572
|
);
|
|
477
573
|
}
|
|
@@ -1757,6 +1853,22 @@ class JSONPathTransformerContext {
|
|
|
1757
1853
|
return this;
|
|
1758
1854
|
}
|
|
1759
1855
|
|
|
1856
|
+
/**
|
|
1857
|
+
* Configure mode behavior (similar to xsl:mode).
|
|
1858
|
+
* @param {{
|
|
1859
|
+
* onMultipleMatch?: "use-last"|"fail",
|
|
1860
|
+
* warningOnMultipleMatch?: boolean,
|
|
1861
|
+
* onNoMatch?: "shallow-copy"|"deep-copy"|"fail"|"apply-templates"|
|
|
1862
|
+
* "shallow-skip"|"deep-skip"|"text-only-copy",
|
|
1863
|
+
* warningOnNoMatch?: boolean
|
|
1864
|
+
* }} cfg - Mode configuration
|
|
1865
|
+
* @returns {this}
|
|
1866
|
+
*/
|
|
1867
|
+
mode (cfg) {
|
|
1868
|
+
this._getJoiningTransformer().mode(cfg);
|
|
1869
|
+
return this;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1760
1872
|
/**
|
|
1761
1873
|
* @param {string} name
|
|
1762
1874
|
* @param {import('./AbstractJoiningTransformer.js').
|
|
@@ -1790,6 +1902,31 @@ class JSONPathTransformerContext {
|
|
|
1790
1902
|
return this;
|
|
1791
1903
|
}
|
|
1792
1904
|
|
|
1905
|
+
/**
|
|
1906
|
+
* Configure stylesheet behavior (similar to xsl:stylesheet).
|
|
1907
|
+
* Unlike xsl:stylesheet, this is a directive method and does not contain
|
|
1908
|
+
* nested content.
|
|
1909
|
+
* @param {{
|
|
1910
|
+
* excludeResultPrefixes?: string[]
|
|
1911
|
+
* }} cfg - Stylesheet configuration
|
|
1912
|
+
* @returns {this}
|
|
1913
|
+
*/
|
|
1914
|
+
stylesheet (cfg) {
|
|
1915
|
+
this._getJoiningTransformer().stylesheet(cfg);
|
|
1916
|
+
return this;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Alias for stylesheet() method (XSLT compatibility).
|
|
1921
|
+
* @param {{
|
|
1922
|
+
* excludeResultPrefixes?: string[]
|
|
1923
|
+
* }} cfg - Stylesheet configuration
|
|
1924
|
+
* @returns {this}
|
|
1925
|
+
*/
|
|
1926
|
+
transform (cfg) {
|
|
1927
|
+
return this.stylesheet(cfg);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1793
1930
|
/**
|
|
1794
1931
|
* Create an element. Mirrors the joining transformer API so templates can
|
|
1795
1932
|
* call `this.element()`.
|
|
@@ -633,7 +633,24 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
633
633
|
* @returns {StringJoiningTransformer}
|
|
634
634
|
*/
|
|
635
635
|
namespace (prefix, namespaceURI) {
|
|
636
|
-
|
|
636
|
+
let alias = this._getNamespaceAlias(prefix);
|
|
637
|
+
|
|
638
|
+
// Normalize empty string or undefined/null to #default
|
|
639
|
+
if (!alias) {
|
|
640
|
+
alias = '#default';
|
|
641
|
+
}
|
|
642
|
+
const normalizedPrefix = alias;
|
|
643
|
+
|
|
644
|
+
// If this prefix is excluded, buffer it instead of outputting immediately
|
|
645
|
+
if (this._excludeResultPrefixes.has(normalizedPrefix)) {
|
|
646
|
+
this._pendingNamespaceMap.set(normalizedPrefix, {
|
|
647
|
+
prefix: alias,
|
|
648
|
+
namespaceURI
|
|
649
|
+
});
|
|
650
|
+
return this;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Not excluded, output immediately
|
|
637
654
|
this.append(' ' + (
|
|
638
655
|
alias === '#default' ? 'xmlns' : 'xmlns:' + alias
|
|
639
656
|
) + '="' +
|
|
@@ -641,6 +658,22 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
641
658
|
return this;
|
|
642
659
|
}
|
|
643
660
|
|
|
661
|
+
/**
|
|
662
|
+
* @param {string} prefix
|
|
663
|
+
* @returns {void}
|
|
664
|
+
* @override
|
|
665
|
+
*/
|
|
666
|
+
_flushPendingNamespace (prefix) {
|
|
667
|
+
const pending = this._pendingNamespaceMap.get(prefix);
|
|
668
|
+
if (pending) {
|
|
669
|
+
this.append(' ' + (
|
|
670
|
+
pending.prefix === '#default' ? 'xmlns' : 'xmlns:' + pending.prefix
|
|
671
|
+
) + '="' +
|
|
672
|
+
this._replaceCharacterMaps(pending.namespaceURI) + '"');
|
|
673
|
+
this._pendingNamespaceMap.delete(prefix);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
644
677
|
/**
|
|
645
678
|
* @param {string} name - Attribute name
|
|
646
679
|
* @param {string|Record<string, unknown>|
|
|
@@ -703,6 +736,10 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
703
736
|
val = (this._cfg.preEscapedAttributes || avoidAttEscape)
|
|
704
737
|
? valStr
|
|
705
738
|
: valStr.replaceAll('&', '&').replaceAll('"', '"');
|
|
739
|
+
|
|
740
|
+
// Track attribute prefix usage for namespace exclusion
|
|
741
|
+
this._trackAttributePrefix(name);
|
|
742
|
+
|
|
706
743
|
this.append(
|
|
707
744
|
' ' + this._replaceNamespaceAliasInNamespaceDeclaration(name) + '="' +
|
|
708
745
|
this._replaceCharacterMaps(val) + '"'
|
package/src/XPathTransformer.js
CHANGED
|
@@ -34,6 +34,10 @@ class XPathTransformer {
|
|
|
34
34
|
if (Array.isArray(template)) {
|
|
35
35
|
return {path: template[0], template: template[1]};
|
|
36
36
|
}
|
|
37
|
+
// Normalize 'match' to 'path' for XSLT compatibility
|
|
38
|
+
if (template.match && !template.path) {
|
|
39
|
+
return {...template, path: template.match};
|
|
40
|
+
}
|
|
37
41
|
return template;
|
|
38
42
|
});
|
|
39
43
|
this.templates.forEach((template) => {
|