@tradik/xslt-processor 1.1.1 → 1.3.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.
Files changed (122) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +102 -757
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +17 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +3 -3
  9. package/bin/lib/transform.js +124 -33
  10. package/bin/xslt.js +26 -27
  11. package/dist/xslt-processor.browser.js +8784 -2720
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -6
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +8789 -2723
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +380 -21
  18. package/dist/xslt-processor.d.ts +380 -21
  19. package/dist/xslt-processor.js +8770 -2722
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +51 -11
  22. package/src/XSLTProcessor.js +343 -66
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +16 -4
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +475 -355
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +1 -1
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +176 -2020
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +22 -9
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +94 -15
  89. package/src/xslt/index.js +7 -1
  90. package/src/xslt/keys.js +51 -28
  91. package/src/xslt/literalResult.js +63 -7
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +171 -78
  94. package/src/xslt/numberFormat.js +124 -26
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +143 -6
  102. package/src/xslt/serializer/baseWriter.js +173 -66
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +14 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +49 -12
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +123 -25
  111. package/src/xslt/serializer/settings.js +89 -13
  112. package/src/xslt/serializer/textSerializer.js +58 -10
  113. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  114. package/src/xslt/serializer/xmlSerializer.js +113 -13
  115. package/src/xslt/serializer.js +50 -17
  116. package/src/xslt/sort.js +151 -0
  117. package/src/xslt/spaceNameTests.js +115 -0
  118. package/src/xslt/stylesheetChecks.js +206 -0
  119. package/src/xslt/stylesheetNamespaces.js +266 -0
  120. package/src/xslt/variables.js +152 -0
  121. package/src/xslt/whitespace.js +43 -27
  122. package/LICENSE +0 -29
@@ -6,9 +6,16 @@
6
6
  * the html serializer, which overrides the dialect hooks defined here.
7
7
  */
8
8
 
9
- import { TEXT_MODE, VOID_ELEMENTS } from "./constants.js";
9
+ import { TEXT_MODE, VOID_ELEMENTS, XHTML_NAMESPACE } from "./constants.js";
10
10
  import { escapeXmlAttribute, escapeXmlText } from "./escape.js";
11
+ import { characterReference } from "./encoding.js";
11
12
  import { BaseWriter } from "./baseWriter.js";
13
+ import { expandedNameKey } from "./settings.js";
14
+ import {
15
+ isXhtml1Doctype,
16
+ xhtmlHeadMeta,
17
+ xhtmlRootNamespace,
18
+ } from "./xhtmlDocument.js";
12
19
 
13
20
  export class XmlWriter extends BaseWriter {
14
21
  /**
@@ -20,11 +27,26 @@ export class XmlWriter extends BaseWriter {
20
27
  }
21
28
 
22
29
  /**
23
- * Whether namespace declarations have to be written.
24
- * @returns {boolean} Always true for XML output
30
+ * Whether the output is an XHTML 1.0 document (see xhtmlDocument.js).
31
+ * @returns {boolean} True when an XHTML 1.0 doctype is declared
25
32
  */
26
- get emitsNamespaces() {
27
- return true;
33
+ get isXhtmlDocument() {
34
+ return isXhtml1Doctype(this.settings);
35
+ }
36
+
37
+ /**
38
+ * Namespace declarations of an element, plus the XHTML namespace libxml2
39
+ * adds to an `html` element in no namespace of an XHTML 1.0 document.
40
+ *
41
+ * @param {Array<{prefix: string, uri: string}>} declarations - Declarations
42
+ * @param {Element} element - The element they are written on
43
+ * @returns {string} Attribute markup, starting with a space when non-empty
44
+ */
45
+ namespaceMarkup(declarations, element) {
46
+ const markup = super.namespaceMarkup(declarations);
47
+ return this.isXhtmlDocument
48
+ ? markup + xhtmlRootNamespace(element, declarations)
49
+ : markup;
28
50
  }
29
51
 
30
52
  /**
@@ -43,6 +65,16 @@ export class XmlWriter extends BaseWriter {
43
65
  return TEXT_MODE.CDATA;
44
66
  }
45
67
 
68
+ /**
69
+ * Whether a line break follows a top-level comment that another node
70
+ * follows, as libxslt (xsltSaveResultTo) writes unless `indent="no"` is
71
+ * declared.
72
+ * @returns {boolean} True for xml output without `indent="no"`
73
+ */
74
+ get topLevelLineBreaks() {
75
+ return this.settings.topLevelLineBreaks === true;
76
+ }
77
+
46
78
  /**
47
79
  * Build the document type declaration for the xml output method.
48
80
  *
@@ -62,18 +94,55 @@ export class XmlWriter extends BaseWriter {
62
94
  }
63
95
 
64
96
  /**
65
- * Determine how the character data children of an element are written.
97
+ * Determine how the character data children of an element are written:
98
+ * as CDATA sections when the expanded name of the element is listed in
99
+ * `cdata-section-elements` (XSLT 1.0 section 16.1).
66
100
  *
67
101
  * @param {Element} element - Parent element
68
102
  * @returns {string} A {@link TEXT_MODE} value
69
103
  */
70
104
  childTextMode(element) {
71
- const names = this.settings.cdataSectionElements;
72
- return names.has(element.nodeName) || names.has(element.localName)
105
+ return this.isCdataSectionElement(element)
73
106
  ? TEXT_MODE.CDATA
74
107
  : TEXT_MODE.ESCAPE;
75
108
  }
76
109
 
110
+ /**
111
+ * Whether an element is listed in `cdata-section-elements`. Prefixed names
112
+ * the engine passed unresolved are resolved with the element's in-scope
113
+ * namespaces.
114
+ *
115
+ * @param {Element} element - Element to test
116
+ * @returns {boolean} True when its text is written as CDATA sections
117
+ */
118
+ isCdataSectionElement(element) {
119
+ const { cdataSectionElements, cdataSectionQNames = [] } = this.settings;
120
+ const namespaceUri = element.namespaceURI || null;
121
+ const { localName } = element;
122
+ if (cdataSectionElements.has(expandedNameKey(namespaceUri, localName))) {
123
+ return true;
124
+ }
125
+ return cdataSectionQNames.some(
126
+ (qname) =>
127
+ qname.localName === localName &&
128
+ namespaceUri !== null &&
129
+ element.lookupNamespaceURI(qname.prefix) === namespaceUri,
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Markup the serializer itself adds as the first child of an element: the
135
+ * Content-Type meta of an XHTML 1.0 document head, as libxml2 writes it.
136
+ *
137
+ * @param {Element} element - Element being written
138
+ * @returns {string} The meta element, or an empty string
139
+ */
140
+ leadingChildMarkup(element) {
141
+ return this.isXhtmlDocument
142
+ ? xhtmlHeadMeta(element, this.settings.encoding)
143
+ : "";
144
+ }
145
+
77
146
  /**
78
147
  * Whether the content of an element may be re-indented.
79
148
  *
@@ -87,12 +156,33 @@ export class XmlWriter extends BaseWriter {
87
156
  /**
88
157
  * Build the markup closing an element that has no children.
89
158
  *
159
+ * XHTML elements follow the XHTML compatibility guidelines, as libxml2
160
+ * (Chrome) and the DOM serializer (Firefox) do: void elements become
161
+ * `<br />` and every other empty element gets an explicit end tag, because
162
+ * `<script/>` or `<div/>` break when XHTML reaches an HTML parser. Other
163
+ * elements use the XML empty-element tag.
164
+ *
90
165
  * @param {Element} element - Empty element
91
- * @param {string} _name - Element name as written
166
+ * @param {string} name - Element name as written
92
167
  * @returns {string} Markup terminating the start tag
93
168
  */
94
- emptyElementMarkup(element, _name) {
95
- return this.xhtml && this.isVoidElement(element) ? " />" : "/>";
169
+ emptyElementMarkup(element, name) {
170
+ if (!this.followsXhtmlConventions(element)) return "/>";
171
+ return this.isVoidElement(element) ? " />" : `></${name}>`;
172
+ }
173
+
174
+ /**
175
+ * Whether an element is written with the XHTML empty-element conventions:
176
+ * elements in the XHTML namespace, and namespace-less elements when the
177
+ * output method is `xhtml` or the document type is XHTML 1.0.
178
+ *
179
+ * @param {Element} element - Element to test
180
+ * @returns {boolean} True for XHTML elements
181
+ */
182
+ followsXhtmlConventions(element) {
183
+ const namespace = element.namespaceURI || null;
184
+ if (namespace === XHTML_NAMESPACE) return true;
185
+ return namespace === null && (this.xhtml || this.isXhtmlDocument);
96
186
  }
97
187
 
98
188
  /**
@@ -112,7 +202,7 @@ export class XmlWriter extends BaseWriter {
112
202
  * @returns {string} Escaped text
113
203
  */
114
204
  escapeText(value) {
115
- return escapeXmlText(value);
205
+ return this.encodeReferences(escapeXmlText(value));
116
206
  }
117
207
 
118
208
  /**
@@ -122,6 +212,16 @@ export class XmlWriter extends BaseWriter {
122
212
  * @returns {string} Escaped value
123
213
  */
124
214
  escapeAttribute(value) {
125
- return escapeXmlAttribute(value);
215
+ return this.encodeReferences(escapeXmlAttribute(value));
216
+ }
217
+
218
+ /**
219
+ * Reference to a character the output encoding cannot represent.
220
+ *
221
+ * @param {number} codePoint - The code point
222
+ * @returns {string} A numeric character reference
223
+ */
224
+ characterReference(codePoint) {
225
+ return characterReference(codePoint);
126
226
  }
127
227
  }
@@ -16,7 +16,8 @@
16
16
  import { resolveOutputSettings } from "./serializer/settings.js";
17
17
  import { XmlWriter } from "./serializer/xmlSerializer.js";
18
18
  import { HtmlWriter } from "./serializer/htmlSerializer.js";
19
- import { serializeText } from "./serializer/textSerializer.js";
19
+ import { textChunks } from "./serializer/textSerializer.js";
20
+ import { toChunkSize } from "./serializer/chunks.js";
20
21
 
21
22
  export { markRawText, isRawText, rawTextNodes } from "./serializer/rawText.js";
22
23
  export {
@@ -26,7 +27,50 @@ export {
26
27
  } from "./serializer/settings.js";
27
28
  export { XmlWriter } from "./serializer/xmlSerializer.js";
28
29
  export { HtmlWriter } from "./serializer/htmlSerializer.js";
29
- export { serializeText } from "./serializer/textSerializer.js";
30
+ export { serializeText, textChunks } from "./serializer/textSerializer.js";
31
+ export {
32
+ ChunkBuffer,
33
+ DEFAULT_CHUNK_SIZE,
34
+ toChunkSize,
35
+ } from "./serializer/chunks.js";
36
+
37
+ /**
38
+ * Serialize a transformation result incrementally, honoring the same
39
+ * `xsl:output` settings as {@link serializeResult}. The result tree must be
40
+ * complete (XSLT 1.0 builds it in memory); what is bounded is the output
41
+ * text: chunks are yielded as soon as they are full, so a consumer can write
42
+ * them out while the rest is being serialized.
43
+ *
44
+ * @param {Node|null} node - Result document, fragment or element
45
+ * @param {object} [outputSettings] - `xsl:output` settings (see serializeResult)
46
+ * @param {{chunkSize?: number}} [options] - `chunkSize` in UTF-16 code units,
47
+ * {@link DEFAULT_CHUNK_SIZE} (16 KiB) by default, Infinity for one chunk
48
+ * @yields {string} Non-empty chunks of at most `chunkSize` code units (one
49
+ * more when a surrogate pair straddles the boundary)
50
+ * @returns {Iterator<string>} The chunks; joined, they equal
51
+ * `serializeResult(node, outputSettings)`
52
+ * @throws {RangeError} When `chunkSize` is not a positive integer or Infinity
53
+ *
54
+ * @example
55
+ * for (const chunk of serializeChunks(resultDocument, { method: "xml" })) {
56
+ * stream.write(chunk);
57
+ * }
58
+ */
59
+ export function serializeChunks(node, outputSettings = {}, options = {}) {
60
+ const chunkSize = toChunkSize(options.chunkSize);
61
+ if (!node) return [][Symbol.iterator]();
62
+
63
+ const settings = resolveOutputSettings(outputSettings, node);
64
+
65
+ if (settings.method === "text") {
66
+ return textChunks(node, chunkSize);
67
+ }
68
+ const writer =
69
+ settings.method === "html"
70
+ ? new HtmlWriter(settings)
71
+ : new XmlWriter(settings, { xhtml: settings.method === "xhtml" });
72
+ return writer.chunks(node, chunkSize);
73
+ }
30
74
 
31
75
  /**
32
76
  * Serialize a transformation result to a string.
@@ -39,19 +83,8 @@ export { serializeText } from "./serializer/textSerializer.js";
39
83
  * @returns {string} The serialized result, or an empty string for a null node
40
84
  */
41
85
  export function serializeResult(node, outputSettings = {}) {
42
- if (!node) {
43
- return "";
44
- }
45
-
46
- const settings = resolveOutputSettings(outputSettings, node);
47
-
48
- if (settings.method === "text") {
49
- return serializeText(node);
50
- }
51
- if (settings.method === "html") {
52
- return new HtmlWriter(settings).serialize(node);
53
- }
54
- return new XmlWriter(settings, {
55
- xhtml: settings.method === "xhtml",
56
- }).serialize(node);
86
+ const chunks = serializeChunks(node, outputSettings, { chunkSize: Infinity });
87
+ let output = "";
88
+ for (const chunk of chunks) output += chunk;
89
+ return output;
57
90
  }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * xsl:sort (XSLT 1.0 section 10).
3
+ *
4
+ * Sort keys are computed once per node (decorate, sort, undecorate) instead of
5
+ * once per comparison, and the attributes `order`, `lang`, `data-type` and
6
+ * `case-order` are evaluated as attribute value templates.
7
+ *
8
+ * Text keys are compared by Unicode code point unless `lang` or `case-order`
9
+ * is given. That matches libxslt, the engine behind the native XSLTProcessor
10
+ * in Chromium browsers, so a stylesheet sorts the same way with this library
11
+ * as it did natively. With `lang` or `case-order` an `Intl.Collator` is used.
12
+ *
13
+ * @module xslt/sort
14
+ */
15
+
16
+ /**
17
+ * Compare two strings by Unicode code point (not by UTF-16 code unit).
18
+ *
19
+ * @param {string} a - First string
20
+ * @param {string} b - Second string
21
+ * @returns {number} Negative, zero or positive
22
+ */
23
+ export function compareCodePoints(a, b) {
24
+ const left = a[Symbol.iterator]();
25
+ const right = b[Symbol.iterator]();
26
+
27
+ for (;;) {
28
+ const x = left.next();
29
+ const y = right.next();
30
+ if (x.done || y.done) return Number(y.done) - Number(x.done) || 0;
31
+ const diff = x.value.codePointAt(0) - y.value.codePointAt(0);
32
+ if (diff !== 0) return diff;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Compare two numeric sort keys; NaN precedes every number (section 10).
38
+ *
39
+ * @param {number} a - First key
40
+ * @param {number} b - Second key
41
+ * @returns {number} Negative, zero or positive
42
+ */
43
+ export function compareNumbers(a, b) {
44
+ const aNaN = Number.isNaN(a);
45
+ const bNaN = Number.isNaN(b);
46
+ if (aNaN || bNaN) return Number(bNaN) - Number(aNaN) || 0;
47
+ if (a === b) return 0;
48
+ return a < b ? -1 : 1;
49
+ }
50
+
51
+ /**
52
+ * Build the text comparator for one sort key.
53
+ *
54
+ * @param {string} lang - Evaluated `lang` attribute (may be empty)
55
+ * @param {string} caseOrder - Evaluated `case-order` attribute (may be empty)
56
+ * @returns {(a: string, b: string) => number} The comparator
57
+ */
58
+ function textComparator(lang, caseOrder) {
59
+ if (!lang && !caseOrder) return compareCodePoints;
60
+
61
+ let collator;
62
+ try {
63
+ collator = new Intl.Collator(lang || undefined, {
64
+ caseFirst: caseOrder === "lower-first" ? "lower" : "upper",
65
+ sensitivity: "variant",
66
+ });
67
+ } catch {
68
+ collator = new Intl.Collator(undefined, {
69
+ caseFirst: caseOrder === "lower-first" ? "lower" : "upper",
70
+ sensitivity: "variant",
71
+ });
72
+ }
73
+ return collator.compare;
74
+ }
75
+
76
+ /**
77
+ * @typedef {object} SortHost
78
+ * @property {(expr: string, context: object) => *} evaluate - Evaluate an XPath expression
79
+ * @property {(value: string, context: object) => string} avt - Evaluate an attribute value template
80
+ * @property {(value: *) => string} toString - XPath string() conversion
81
+ * @property {(value: *) => number} toNumber - XPath number() conversion
82
+ */
83
+
84
+ /**
85
+ * Resolve one xsl:sort element into a key specification.
86
+ *
87
+ * @param {Element} element - The xsl:sort element
88
+ * @param {object} context - Context of the sorting instruction (for AVTs)
89
+ * @param {SortHost} host - Engine callbacks
90
+ * @returns {{select: string, descending: boolean, numeric: boolean, compare: Function}} Specification
91
+ */
92
+ export function resolveSortSpec(element, context, host) {
93
+ const attribute = (name) => {
94
+ const raw = element.getAttribute(name);
95
+ return raw ? host.avt(raw, context).trim() : "";
96
+ };
97
+
98
+ const dataType = attribute("data-type");
99
+ const numeric = dataType === "number";
100
+
101
+ return {
102
+ select: element.getAttribute("select") || ".",
103
+ descending: attribute("order") === "descending",
104
+ numeric,
105
+ compare: numeric
106
+ ? compareNumbers
107
+ : textComparator(attribute("lang"), attribute("case-order")),
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Sort a node list by the given xsl:sort elements.
113
+ *
114
+ * Each key is evaluated with the node as current node, the unsorted list as
115
+ * current node list and the node's position in that list, as the
116
+ * specification requires. Ties keep the original order.
117
+ *
118
+ * @param {Node[]} nodes - Nodes in their original (document) order
119
+ * @param {Element[]} sortElements - The xsl:sort children, in order
120
+ * @param {object} context - XSLT context of the sorting instruction
121
+ * @param {SortHost} host - Engine callbacks
122
+ * @returns {Node[]} A new, sorted array
123
+ */
124
+ export function sortNodes(nodes, sortElements, context, host) {
125
+ if (sortElements.length === 0 || nodes.length < 2) return [...nodes];
126
+
127
+ const specs = sortElements.map((el) => resolveSortSpec(el, context, host));
128
+
129
+ const rows = nodes.map((node, index) => {
130
+ const nodeContext = context.clone({
131
+ currentNode: node,
132
+ currentNodeList: nodes,
133
+ position: index + 1,
134
+ });
135
+ const keys = specs.map((spec) => {
136
+ const text = host.toString(host.evaluate(spec.select, nodeContext));
137
+ return spec.numeric ? host.toNumber(text) : text;
138
+ });
139
+ return { node, index, keys };
140
+ });
141
+
142
+ rows.sort((a, b) => {
143
+ for (let k = 0; k < specs.length; k++) {
144
+ const cmp = specs[k].compare(a.keys[k], b.keys[k]);
145
+ if (cmp !== 0) return specs[k].descending ? -cmp : cmp;
146
+ }
147
+ return a.index - b.index;
148
+ });
149
+
150
+ return rows.map((row) => row.node);
151
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Name tests of `xsl:strip-space` and `xsl:preserve-space` (XSLT 1.0
3
+ * section 3.4).
4
+ *
5
+ * The `elements` attribute lists XPath name tests: `*`, `prefix:*` or a
6
+ * QName. The prefixes are expanded with the namespace declarations in scope
7
+ * on the declaring element, and an unprefixed name only matches elements in
8
+ * no namespace (XPath 1.0 section 2.3), so `p:*` matches by namespace URI
9
+ * whatever prefix the source document uses. Name tests given as plain
10
+ * strings (the {@link WhitespaceFilter} API before 1.2.0) are still matched
11
+ * by their lexical name.
12
+ *
13
+ * @module xslt/spaceNameTests
14
+ */
15
+
16
+ "use strict";
17
+
18
+ import { isQName } from "./qname.js";
19
+ import { splitQName } from "./resultNamespaces.js";
20
+ import { inScopeNamespaces, resolvePrefix } from "./stylesheetNamespaces.js";
21
+
22
+ /**
23
+ * @typedef {Object} ElementNameTest
24
+ * @property {string|null|undefined} namespaceUri - The namespace to match,
25
+ * null for no namespace, undefined for any (the `*` test)
26
+ * @property {string} localName - The local name, `*` for any
27
+ */
28
+
29
+ /**
30
+ * Expand the name tests of an `elements` attribute.
31
+ *
32
+ * @param {string|null} value - Whitespace separated name tests
33
+ * @param {Element} element - The xsl:strip-space or xsl:preserve-space element
34
+ * @param {(message: string) => void} warn - Reports a skipped name test
35
+ * @returns {ElementNameTest[]} The expanded name tests
36
+ *
37
+ * @example
38
+ * // <xsl:strip-space xmlns:m="urn:m" elements="m:* p *"/>
39
+ * compileSpaceNameTests("m:* p *", element, console.warn);
40
+ * // [{ namespaceUri: "urn:m", localName: "*" },
41
+ * // { namespaceUri: null, localName: "p" },
42
+ * // { namespaceUri: undefined, localName: "*" }]
43
+ */
44
+ export function compileSpaceNameTests(value, element, warn) {
45
+ const tests = [];
46
+ for (const token of (value ?? "").split(/[ \t\r\n]+/).filter(Boolean)) {
47
+ if (token === "*") {
48
+ tests.push({ namespaceUri: undefined, localName: "*" });
49
+ continue;
50
+ }
51
+ const { prefix, localName } = splitQName(token);
52
+ const qname = localName === "*" ? `${prefix}:x` : token;
53
+ const namespaceUri = prefix
54
+ ? resolvePrefix(inScopeNamespaces(element), prefix)
55
+ : null;
56
+ if (!isQName(qname) || (prefix && !namespaceUri)) {
57
+ warn(
58
+ `xsl:${element.localName} elements: "${token}" is not a name test with a declared prefix and is ignored`,
59
+ );
60
+ continue;
61
+ }
62
+ tests.push({ namespaceUri, localName });
63
+ }
64
+ return tests;
65
+ }
66
+
67
+ /**
68
+ * The XSLT default priority of an element name test (section 5.5).
69
+ *
70
+ * @param {string|ElementNameTest} nameTest - A name test
71
+ * @returns {number} -0.5 for `*`, -0.25 for `prefix:*`, 0 for a name
72
+ */
73
+ export function nameTestPriority(nameTest) {
74
+ if (typeof nameTest === "string") {
75
+ if (nameTest === "*") return -0.5;
76
+ return nameTest.endsWith(":*") ? -0.25 : 0;
77
+ }
78
+ if (nameTest.namespaceUri === undefined) return -0.5;
79
+ return nameTest.localName === "*" ? -0.25 : 0;
80
+ }
81
+
82
+ /**
83
+ * Whether an element matches a lexical name test (the string API).
84
+ *
85
+ * @param {Element} element - The element to test
86
+ * @param {string} nameTest - `*`, `prefix:*` or a name
87
+ * @returns {boolean} True when the element matches
88
+ */
89
+ function matchesLexicalTest(element, nameTest) {
90
+ if (nameTest === "*") return true;
91
+ if (nameTest.endsWith(":*")) {
92
+ return element.nodeName.startsWith(`${nameTest.slice(0, -2)}:`);
93
+ }
94
+ return element.nodeName === nameTest || element.localName === nameTest;
95
+ }
96
+
97
+ /**
98
+ * Whether an element matches a name test.
99
+ *
100
+ * @param {Element} element - The element to test
101
+ * @param {string|ElementNameTest} nameTest - A name test
102
+ * @returns {boolean} True when the element matches
103
+ *
104
+ * @example
105
+ * matchesNameTest(pElement, { namespaceUri: null, localName: "p" }); // true
106
+ */
107
+ export function matchesNameTest(element, nameTest) {
108
+ if (typeof nameTest === "string") {
109
+ return matchesLexicalTest(element, nameTest);
110
+ }
111
+ const { namespaceUri, localName } = nameTest;
112
+ if (namespaceUri === undefined) return true;
113
+ if ((element.namespaceURI || null) !== namespaceUri) return false;
114
+ return localName === "*" || element.localName === localName;
115
+ }