jtlt 0.7.3 → 0.9.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.
@@ -359,6 +359,66 @@ class XPathTransformerContext {
359
359
  });
360
360
  let templateObj;
361
361
  if (!pathMatchedTemplates.length) { // default template rule branches
362
+ // Check mode config for no-match behavior
363
+ const joiner = this._getJoiningTransformer();
364
+ const modeConfig = joiner._modeConfig;
365
+ if (modeConfig) {
366
+ const onNoMatch = modeConfig.onNoMatch ?? 'text-only-copy';
367
+ if (modeConfig.warningOnNoMatch) {
368
+ // eslint-disable-next-line no-console -- Warning as specified
369
+ console.warn(
370
+ 'Warning: No template matches. ' +
371
+ 'Mode is configured with warningOnNoMatch=true.'
372
+ );
373
+ }
374
+ if (onNoMatch === 'fail') {
375
+ throw new Error(
376
+ 'No template matches. Mode is configured with onNoMatch="fail".'
377
+ );
378
+ }
379
+ if (onNoMatch === 'deep-skip') {
380
+ // Skip this node entirely
381
+ continue;
382
+ }
383
+ if (onNoMatch === 'shallow-copy') {
384
+ // Output the node without processing children
385
+ if (node.nodeType === 1 && node.ownerDocument?.defaultView) {
386
+ const serializer =
387
+ new node.ownerDocument.defaultView.XMLSerializer();
388
+ // Clone node without children for shallow copy
389
+ const clone = node.cloneNode(false);
390
+ joiner.rawAppend(serializer.serializeToString(clone));
391
+ } else if (node.nodeType === 3 && node.nodeValue) { // Text
392
+ joiner.text(node.nodeValue);
393
+ }
394
+ continue;
395
+ }
396
+ if (onNoMatch === 'deep-copy') {
397
+ // Output the node and all descendants
398
+ if (node.nodeType === 1 && node.ownerDocument?.defaultView) {
399
+ const serializer =
400
+ new node.ownerDocument.defaultView.XMLSerializer();
401
+ joiner.rawAppend(serializer.serializeToString(node));
402
+ } else if (node.nodeType === 3 && node.nodeValue) { // Text
403
+ joiner.text(node.nodeValue);
404
+ }
405
+ continue;
406
+ }
407
+ if (onNoMatch === 'text-only-copy') {
408
+ // Output only text content
409
+ if (node.nodeType === 3 && node.nodeValue) { // Text node
410
+ joiner.text(node.nodeValue);
411
+ } else if (node.nodeType === 1) { // Element - get text content
412
+ const {textContent} = node;
413
+ if (textContent) {
414
+ joiner.text(textContent);
415
+ }
416
+ }
417
+ continue;
418
+ }
419
+ // 'apply-templates', 'shallow-skip', or other:
420
+ // use default template rules
421
+ }
362
422
  // Default template rules (simplified compared to JSON version)
363
423
  const DTR = XPathTransformerContext.DefaultTemplateRules;
364
424
  // Treat Document (9) like Element (1) so the default root rule
@@ -391,6 +451,50 @@ class XPathTransformerContext {
391
451
  }
392
452
  return aPr > bPr ? -1 : 1;
393
453
  });
454
+
455
+ // Check for multiple matches with same priority when mode is configured
456
+ const joiner = this._getJoiningTransformer();
457
+ const modeConfig = joiner._modeConfig;
458
+ if (modeConfig && pathMatchedTemplates.length > 1) {
459
+ // Check if top two templates have equal priority
460
+ const topPriority =
461
+ typeof pathMatchedTemplates[0].priority === 'number'
462
+ ? pathMatchedTemplates[0].priority
463
+ : (this._config.specificityPriorityResolver &&
464
+ pathMatchedTemplates[0].path
465
+ ? this._config.specificityPriorityResolver(
466
+ pathMatchedTemplates[0].path
467
+ )
468
+ /* c8 ignore next 2 -- defensive, templates without paths
469
+ are already filtered at line 334 */
470
+ : 0);
471
+ const secondPriority =
472
+ typeof pathMatchedTemplates[1].priority === 'number'
473
+ ? pathMatchedTemplates[1].priority
474
+ : (this._config.specificityPriorityResolver &&
475
+ pathMatchedTemplates[1].path
476
+ ? this._config.specificityPriorityResolver(
477
+ pathMatchedTemplates[1].path
478
+ )
479
+ /* c8 ignore next 2 -- defensive, templates without paths
480
+ are already filtered at line 334 */
481
+ : 0);
482
+ if (topPriority === secondPriority) {
483
+ if (modeConfig.onMultipleMatch === 'fail') {
484
+ throw new Error(
485
+ 'Multiple templates match with equal priority. ' +
486
+ 'Mode is configured with onMultipleMatch="fail".'
487
+ );
488
+ } else if (modeConfig.warningOnMultipleMatch !== false) {
489
+ // eslint-disable-next-line no-console -- Warning as specified
490
+ console.warn(
491
+ 'Warning: Multiple templates match with equal priority. ' +
492
+ 'Mode is configured with warningOnMultipleMatch=true.'
493
+ );
494
+ }
495
+ }
496
+ }
497
+
394
498
  templateObj =
395
499
  /**
396
500
  * @type {import('./index.js').XPathTemplateObject<any>}
@@ -410,7 +514,15 @@ class XPathTransformerContext {
410
514
  this._params = prevTemplateParams;
411
515
 
412
516
  if (typeof ret !== 'undefined') {
413
- this._getJoiningTransformer().append(ret);
517
+ const joiner = this._getJoiningTransformer();
518
+ // Close any open tag before appending template return value
519
+ // @ts-expect-error -- _openTagState only on StringJoiningTransformer
520
+ if (joiner._openTagState) {
521
+ joiner.append('>');
522
+ // @ts-expect-error -- _openTagState only on StringJoiningTransformer
523
+ joiner._openTagState = false;
524
+ }
525
+ joiner.append(ret);
414
526
  }
415
527
  this._contextNode = node; // Restore (placeholder for more complex state)
416
528
  }
@@ -1438,6 +1550,22 @@ class XPathTransformerContext {
1438
1550
  return this;
1439
1551
  }
1440
1552
 
1553
+ /**
1554
+ * Configure mode behavior (similar to xsl:mode).
1555
+ * @param {{
1556
+ * onMultipleMatch?: "use-last"|"fail",
1557
+ * warningOnMultipleMatch?: boolean,
1558
+ * onNoMatch?: "shallow-copy"|"deep-copy"|"fail"|"apply-templates"|
1559
+ * "shallow-skip"|"deep-skip"|"text-only-copy",
1560
+ * warningOnNoMatch?: boolean
1561
+ * }} cfg - Mode configuration
1562
+ * @returns {this}
1563
+ */
1564
+ mode (cfg) {
1565
+ this._getJoiningTransformer().mode(cfg);
1566
+ return this;
1567
+ }
1568
+
1441
1569
  /**
1442
1570
  * @param {string} name
1443
1571
  * @param {import('./AbstractJoiningTransformer.js').
@@ -1459,13 +1587,27 @@ class XPathTransformerContext {
1459
1587
  return this;
1460
1588
  }
1461
1589
 
1590
+ /**
1591
+ * @param {string} stylesheetPrefix
1592
+ * @param {string} resultPrefix
1593
+ * @returns {this}
1594
+ */
1595
+ namespaceAlias (stylesheetPrefix, resultPrefix) {
1596
+ this._getJoiningTransformer().namespaceAlias(
1597
+ stylesheetPrefix, resultPrefix
1598
+ );
1599
+ return this;
1600
+ }
1601
+
1462
1602
  /**
1463
1603
  * Append element.
1464
1604
  * @param {string} name Tag name
1465
1605
  * @param {Record<string, string>|any[]|
1466
- * ((this: XPathTransformerContext)=>void)} [atts] Attributes
1467
- * @param {any[]|((this: XPathTransformerContext)=>void)} [children]
1468
- * Children
1606
+ * ((this: XPathTransformerContext)=>void)} [atts] Attributes,
1607
+ * children, or callback
1608
+ * @param {any[]|
1609
+ * ((this: XPathTransformerContext)=>void)} [children]
1610
+ * Children or callback
1469
1611
  * @param {(this: XPathTransformerContext)=>void} [cb] Callback
1470
1612
  * @param {string[]} [useAttributeSets] - Attribute set names to apply
1471
1613
  * @returns {XPathTransformerContext}
@@ -1895,7 +2037,7 @@ class XPathTransformerContext {
1895
2037
  * @returns {void}
1896
2038
  */
1897
2039
  template (node, cfg) {
1898
- this.applyTemplates('.', cfg.mode);
2040
+ this.applyTemplates('node()', cfg.mode);
1899
2041
  }
1900
2042
  },
1901
2043
  transformElements: {
@@ -1906,7 +2048,7 @@ class XPathTransformerContext {
1906
2048
  * @returns {void}
1907
2049
  */
1908
2050
  template (node, cfg) {
1909
- this.applyTemplates('*', cfg.mode);
2051
+ this.applyTemplates('node()', cfg.mode);
1910
2052
  }
1911
2053
  },
1912
2054
  transformTextNodes: {
package/src/index.js CHANGED
@@ -34,6 +34,7 @@ export const setWindow = (win) => {
34
34
  * @template TCtx
35
35
  * @typedef {object} TemplateObject
36
36
  * @property {string} [path] - JSONPath or XPath selector for matching nodes
37
+ * @property {string} [match] - Alias for 'path' (XSLT compatibility)
37
38
  * @property {string} [name] - Optional name for calling via callTemplate
38
39
  * @property {string} [mode] - Optional mode for template matching
39
40
  * @property {number} [priority] - Priority for template selection