xslt-processor 4.0.0 → 4.1.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/README.md CHANGED
@@ -219,6 +219,54 @@ HTML per se is not strict XML. Because of that, starting on version 2.0.0, this
219
219
  - Tags like `<hr>`, `<link>` and `<meta>` don't need to be closed. The output for these tags doesn't close them (adding a `/` before the tag closes, or a corresponding close tag);
220
220
  - This rule doesn't apply for XHTML, which is strict XML.
221
221
 
222
+ ### Whitespace Handling
223
+
224
+ This library supports `xsl:strip-space` and `xsl:preserve-space` for controlling whitespace in the input document.
225
+
226
+ #### `xsl:strip-space`
227
+
228
+ Use `<xsl:strip-space>` to remove whitespace-only text nodes from specified elements in the input document:
229
+
230
+ ```xml
231
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
232
+ <!-- Strip whitespace from all elements -->
233
+ <xsl:strip-space elements="*"/>
234
+
235
+ <!-- Or strip from specific elements -->
236
+ <xsl:strip-space elements="book chapter section"/>
237
+
238
+ <!-- ... templates ... -->
239
+ </xsl:stylesheet>
240
+ ```
241
+
242
+ The `elements` attribute accepts:
243
+ - `*` - matches all elements
244
+ - `name` - matches elements with the specified local name
245
+ - `prefix:*` - matches all elements in a namespace
246
+ - `prefix:name` - matches a specific element in a namespace
247
+ - Multiple patterns separated by whitespace (e.g., `"book chapter section"`)
248
+
249
+ #### `xsl:preserve-space`
250
+
251
+ Use `<xsl:preserve-space>` to preserve whitespace in specific elements, overriding `xsl:strip-space`:
252
+
253
+ ```xml
254
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
255
+ <xsl:strip-space elements="*"/>
256
+ <!-- Preserve whitespace in pre and code elements -->
257
+ <xsl:preserve-space elements="pre code"/>
258
+
259
+ <!-- ... templates ... -->
260
+ </xsl:stylesheet>
261
+ ```
262
+
263
+ #### Precedence Rules
264
+
265
+ 1. `xml:space="preserve"` attribute on an element takes highest precedence
266
+ 2. `xsl:preserve-space` overrides `xsl:strip-space` for matching elements
267
+ 3. `xsl:strip-space` applies to remaining matches
268
+ 4. By default (no declarations), whitespace is preserved
269
+
222
270
  ## References
223
271
 
224
272
  - XPath Specification: http://www.w3.org/TR/1999/REC-xpath-19991116
package/index.d.mts CHANGED
@@ -570,6 +570,17 @@ declare class Xslt {
570
570
  outputOmitXmlDeclaration: string;
571
571
  version: string;
572
572
  firstTemplateRan: boolean;
573
+ /**
574
+ * List of element name patterns from xsl:strip-space declarations.
575
+ * Whitespace-only text nodes inside matching elements will be stripped.
576
+ */
577
+ stripSpacePatterns: string[];
578
+ /**
579
+ * List of element name patterns from xsl:preserve-space declarations.
580
+ * Whitespace-only text nodes inside matching elements will be preserved.
581
+ * preserve-space takes precedence over strip-space for conflicting patterns.
582
+ */
583
+ preserveSpacePatterns: string[];
573
584
  constructor(options?: Partial<XsltOptions>);
574
585
  /**
575
586
  * The exported entry point of the XSL-T processor.
@@ -704,6 +715,39 @@ declare class Xslt {
704
715
  * @todo case-order is not implemented.
705
716
  */
706
717
  protected xsltSort(context: ExprContext, template: XNode): void;
718
+ /**
719
+ * Implements `xsl:strip-space`.
720
+ * Collects element name patterns for which whitespace-only text nodes should be stripped.
721
+ * @param template The `<xsl:strip-space>` node.
722
+ */
723
+ protected xsltStripSpace(template: XNode): void;
724
+ /**
725
+ * Implements `xsl:preserve-space`.
726
+ * Collects element name patterns for which whitespace-only text nodes should be preserved.
727
+ * preserve-space takes precedence over strip-space for matching elements.
728
+ * @param template The `<xsl:preserve-space>` node.
729
+ */
730
+ protected xsltPreserveSpace(template: XNode): void;
731
+ /**
732
+ * Determines if a text node from the input document should be stripped.
733
+ * This applies xsl:strip-space and xsl:preserve-space rules to whitespace-only text nodes.
734
+ * @param textNode The text node to check.
735
+ * @returns True if the text node should be stripped (not included in output).
736
+ */
737
+ protected shouldStripWhitespaceNode(textNode: XNode): boolean;
738
+ /**
739
+ * Matches an element name against a strip-space/preserve-space pattern.
740
+ * Supports:
741
+ * - "*" matches any element
742
+ * - "prefix:*" matches any element in a namespace
743
+ * - "name" matches elements with that local name
744
+ * - "prefix:name" matches elements with that QName
745
+ * @param elementName The local name of the element.
746
+ * @param pattern The pattern to match against.
747
+ * @param element The element node (for namespace checking).
748
+ * @returns True if the element matches the pattern.
749
+ */
750
+ protected matchesNamePattern(elementName: string, pattern: string, element: XNode): boolean;
707
751
  /**
708
752
  * Implements `xsl:template`.
709
753
  * @param context The Expression Context.
package/index.d.ts CHANGED
@@ -570,6 +570,17 @@ declare class Xslt {
570
570
  outputOmitXmlDeclaration: string;
571
571
  version: string;
572
572
  firstTemplateRan: boolean;
573
+ /**
574
+ * List of element name patterns from xsl:strip-space declarations.
575
+ * Whitespace-only text nodes inside matching elements will be stripped.
576
+ */
577
+ stripSpacePatterns: string[];
578
+ /**
579
+ * List of element name patterns from xsl:preserve-space declarations.
580
+ * Whitespace-only text nodes inside matching elements will be preserved.
581
+ * preserve-space takes precedence over strip-space for conflicting patterns.
582
+ */
583
+ preserveSpacePatterns: string[];
573
584
  constructor(options?: Partial<XsltOptions>);
574
585
  /**
575
586
  * The exported entry point of the XSL-T processor.
@@ -704,6 +715,39 @@ declare class Xslt {
704
715
  * @todo case-order is not implemented.
705
716
  */
706
717
  protected xsltSort(context: ExprContext, template: XNode): void;
718
+ /**
719
+ * Implements `xsl:strip-space`.
720
+ * Collects element name patterns for which whitespace-only text nodes should be stripped.
721
+ * @param template The `<xsl:strip-space>` node.
722
+ */
723
+ protected xsltStripSpace(template: XNode): void;
724
+ /**
725
+ * Implements `xsl:preserve-space`.
726
+ * Collects element name patterns for which whitespace-only text nodes should be preserved.
727
+ * preserve-space takes precedence over strip-space for matching elements.
728
+ * @param template The `<xsl:preserve-space>` node.
729
+ */
730
+ protected xsltPreserveSpace(template: XNode): void;
731
+ /**
732
+ * Determines if a text node from the input document should be stripped.
733
+ * This applies xsl:strip-space and xsl:preserve-space rules to whitespace-only text nodes.
734
+ * @param textNode The text node to check.
735
+ * @returns True if the text node should be stripped (not included in output).
736
+ */
737
+ protected shouldStripWhitespaceNode(textNode: XNode): boolean;
738
+ /**
739
+ * Matches an element name against a strip-space/preserve-space pattern.
740
+ * Supports:
741
+ * - "*" matches any element
742
+ * - "prefix:*" matches any element in a namespace
743
+ * - "name" matches elements with that local name
744
+ * - "prefix:name" matches elements with that QName
745
+ * @param elementName The local name of the element.
746
+ * @param pattern The pattern to match against.
747
+ * @param element The element node (for namespace checking).
748
+ * @returns True if the element matches the pattern.
749
+ */
750
+ protected matchesNamePattern(elementName: string, pattern: string, element: XNode): boolean;
707
751
  /**
708
752
  * Implements `xsl:template`.
709
753
  * @param context The Expression Context.
package/index.js CHANGED
@@ -3548,6 +3548,8 @@ var Xslt = class {
3548
3548
  };
3549
3549
  this.outputMethod = "xml";
3550
3550
  this.outputOmitXmlDeclaration = "no";
3551
+ this.stripSpacePatterns = [];
3552
+ this.preserveSpacePatterns = [];
3551
3553
  this.decimalFormatSettings = {
3552
3554
  decimalSeparator: ".",
3553
3555
  groupingSeparator: ",",
@@ -3680,14 +3682,16 @@ var Xslt = class {
3680
3682
  yield this.xsltVariable(context, template, false);
3681
3683
  break;
3682
3684
  case "preserve-space":
3683
- throw new Error(`not implemented: ${template.localName}`);
3685
+ this.xsltPreserveSpace(template);
3686
+ break;
3684
3687
  case "processing-instruction":
3685
3688
  throw new Error(`not implemented: ${template.localName}`);
3686
3689
  case "sort":
3687
3690
  this.xsltSort(context, template);
3688
3691
  break;
3689
3692
  case "strip-space":
3690
- throw new Error(`not implemented: ${template.localName}`);
3693
+ this.xsltStripSpace(template);
3694
+ break;
3691
3695
  case "stylesheet":
3692
3696
  case "transform":
3693
3697
  yield this.xsltTransformOrStylesheet(context, template, output);
@@ -3847,6 +3851,9 @@ var Xslt = class {
3847
3851
  return node;
3848
3852
  }
3849
3853
  if (source.nodeType == DOM_TEXT_NODE) {
3854
+ if (this.shouldStripWhitespaceNode(source)) {
3855
+ return null;
3856
+ }
3850
3857
  let node = domCreateTextNode(this.outputDocument, source.nodeValue);
3851
3858
  node.siblingPosition = destination.childNodes.length;
3852
3859
  domAppendChild(destination, node);
@@ -4106,6 +4113,99 @@ var Xslt = class {
4106
4113
  }
4107
4114
  this.xPath.xPathSort(context, sort);
4108
4115
  }
4116
+ /**
4117
+ * Implements `xsl:strip-space`.
4118
+ * Collects element name patterns for which whitespace-only text nodes should be stripped.
4119
+ * @param template The `<xsl:strip-space>` node.
4120
+ */
4121
+ xsltStripSpace(template) {
4122
+ const elements = xmlGetAttribute(template, "elements");
4123
+ if (elements) {
4124
+ const patterns = elements.trim().split(/\s+/);
4125
+ this.stripSpacePatterns.push(...patterns);
4126
+ }
4127
+ }
4128
+ /**
4129
+ * Implements `xsl:preserve-space`.
4130
+ * Collects element name patterns for which whitespace-only text nodes should be preserved.
4131
+ * preserve-space takes precedence over strip-space for matching elements.
4132
+ * @param template The `<xsl:preserve-space>` node.
4133
+ */
4134
+ xsltPreserveSpace(template) {
4135
+ const elements = xmlGetAttribute(template, "elements");
4136
+ if (elements) {
4137
+ const patterns = elements.trim().split(/\s+/);
4138
+ this.preserveSpacePatterns.push(...patterns);
4139
+ }
4140
+ }
4141
+ /**
4142
+ * Determines if a text node from the input document should be stripped.
4143
+ * This applies xsl:strip-space and xsl:preserve-space rules to whitespace-only text nodes.
4144
+ * @param textNode The text node to check.
4145
+ * @returns True if the text node should be stripped (not included in output).
4146
+ */
4147
+ shouldStripWhitespaceNode(textNode) {
4148
+ if (!textNode.nodeValue || !textNode.nodeValue.match(/^\s*$/)) {
4149
+ return false;
4150
+ }
4151
+ if (this.stripSpacePatterns.length === 0) {
4152
+ return false;
4153
+ }
4154
+ const parentElement = textNode.parentNode;
4155
+ if (!parentElement || parentElement.nodeType !== DOM_ELEMENT_NODE) {
4156
+ return false;
4157
+ }
4158
+ let ancestor = parentElement;
4159
+ while (ancestor && ancestor.nodeType === DOM_ELEMENT_NODE) {
4160
+ const xmlspace = domGetAttributeValue(ancestor, "xml:space");
4161
+ if (xmlspace === "preserve") {
4162
+ return false;
4163
+ }
4164
+ if (xmlspace === "default") {
4165
+ break;
4166
+ }
4167
+ ancestor = ancestor.parentNode;
4168
+ }
4169
+ const parentName = parentElement.localName || parentElement.nodeName;
4170
+ for (const pattern of this.preserveSpacePatterns) {
4171
+ if (this.matchesNamePattern(parentName, pattern, parentElement)) {
4172
+ return false;
4173
+ }
4174
+ }
4175
+ for (const pattern of this.stripSpacePatterns) {
4176
+ if (this.matchesNamePattern(parentName, pattern, parentElement)) {
4177
+ return true;
4178
+ }
4179
+ }
4180
+ return false;
4181
+ }
4182
+ /**
4183
+ * Matches an element name against a strip-space/preserve-space pattern.
4184
+ * Supports:
4185
+ * - "*" matches any element
4186
+ * - "prefix:*" matches any element in a namespace
4187
+ * - "name" matches elements with that local name
4188
+ * - "prefix:name" matches elements with that QName
4189
+ * @param elementName The local name of the element.
4190
+ * @param pattern The pattern to match against.
4191
+ * @param element The element node (for namespace checking).
4192
+ * @returns True if the element matches the pattern.
4193
+ */
4194
+ matchesNamePattern(elementName, pattern, element) {
4195
+ if (pattern === "*") {
4196
+ return true;
4197
+ }
4198
+ if (pattern.includes(":")) {
4199
+ const [prefix, localPart] = pattern.split(":");
4200
+ const elementPrefix = element.prefix || "";
4201
+ if (localPart === "*") {
4202
+ return elementPrefix === prefix;
4203
+ } else {
4204
+ return elementPrefix === prefix && elementName === localPart;
4205
+ }
4206
+ }
4207
+ return elementName === pattern;
4208
+ }
4109
4209
  /**
4110
4210
  * Implements `xsl:template`.
4111
4211
  * @param context The Expression Context.
@@ -4310,6 +4410,9 @@ var Xslt = class {
4310
4410
  */
4311
4411
  commonLogicTextNode(context, template, output) {
4312
4412
  if (output) {
4413
+ if (this.shouldStripWhitespaceNode(template)) {
4414
+ return;
4415
+ }
4313
4416
  let node = domCreateTextNode(this.outputDocument, template.nodeValue);
4314
4417
  node.siblingPosition = output.childNodes.length;
4315
4418
  domAppendChild(output, node);