@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.
- package/LICENSE.md +1 -1
- package/README.md +102 -757
- package/bin/lib/decode.js +15 -0
- package/bin/lib/dom.js +177 -0
- package/bin/lib/loaders.js +127 -0
- package/bin/lib/options.js +17 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +3 -3
- package/bin/lib/transform.js +124 -33
- package/bin/xslt.js +26 -27
- package/dist/xslt-processor.browser.js +8784 -2720
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -6
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +8789 -2723
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +380 -21
- package/dist/xslt-processor.d.ts +380 -21
- package/dist/xslt-processor.js +8770 -2722
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +51 -11
- package/src/XSLTProcessor.js +343 -66
- package/src/async/abort.js +63 -0
- package/src/async/documentUris.js +128 -0
- package/src/async/loaders.js +134 -0
- package/src/async/preload.js +159 -0
- package/src/async/processor.js +206 -0
- package/src/async/stream.js +125 -0
- package/src/bridge/engine.js +221 -0
- package/src/bridge/loader.js +78 -0
- package/src/bridge/results.js +75 -0
- package/src/bridge/version.js +63 -0
- package/src/index.js +16 -4
- package/src/io/decode.js +140 -0
- package/src/io/readSource.js +167 -0
- package/src/xpath/axes.js +562 -0
- package/src/xpath/documentOrder.js +270 -0
- package/src/xpath/evaluator.js +475 -355
- package/src/xpath/index.js +8 -2
- package/src/xpath/namespaceNodes.js +172 -0
- package/src/xpath/nodeSetFunctions.js +169 -0
- package/src/xpath/parser.js +30 -5
- package/src/xpath/strings.js +183 -0
- package/src/xpath/tokenizer.js +37 -23
- package/src/xslt/attributeSets.js +95 -0
- package/src/xslt/avt.js +103 -0
- package/src/xslt/computedNames.js +91 -0
- package/src/xslt/copying.js +212 -0
- package/src/xslt/declarationNames.js +80 -0
- package/src/xslt/domParsing.js +95 -0
- package/src/xslt/elements.js +1 -1
- package/src/xslt/engine/bindings.js +195 -0
- package/src/xslt/engine/context.js +105 -0
- package/src/xslt/engine/controlFlow.js +145 -0
- package/src/xslt/engine/copyInstructions.js +133 -0
- package/src/xslt/engine/declarations.js +233 -0
- package/src/xslt/engine/functionSupport.js +103 -0
- package/src/xslt/engine/methods.js +33 -0
- package/src/xslt/engine/nodeConstruction.js +187 -0
- package/src/xslt/engine/numbering.js +104 -0
- package/src/xslt/engine/outputDeclaration.js +77 -0
- package/src/xslt/engine/sequenceConstructor.js +228 -0
- package/src/xslt/engine/stylesheetLoading.js +208 -0
- package/src/xslt/engine/templateInvocation.js +253 -0
- package/src/xslt/engine/templateRules.js +243 -0
- package/src/xslt/engine/textInstructions.js +171 -0
- package/src/xslt/engine/topLevel.js +130 -0
- package/src/xslt/engine/transformation.js +263 -0
- package/src/xslt/engine/workStack.js +245 -0
- package/src/xslt/engine.js +176 -2020
- package/src/xslt/exslt/arguments.js +99 -0
- package/src/xslt/exslt/calendar.js +120 -0
- package/src/xslt/exslt/common.js +44 -0
- package/src/xslt/exslt/dateCalc.js +261 -0
- package/src/xslt/exslt/dateFormat.js +150 -0
- package/src/xslt/exslt/dateParse.js +265 -0
- package/src/xslt/exslt/dates.js +259 -0
- package/src/xslt/exslt/duration.js +207 -0
- package/src/xslt/exslt/dynamic.js +59 -0
- package/src/xslt/exslt/index.js +59 -0
- package/src/xslt/exslt/math.js +177 -0
- package/src/xslt/exslt/sets.js +96 -0
- package/src/xslt/exslt/stringOps.js +163 -0
- package/src/xslt/exslt/strings.js +147 -0
- package/src/xslt/exslt/uri.js +92 -0
- package/src/xslt/formatNumber.js +22 -9
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +94 -15
- package/src/xslt/index.js +7 -1
- package/src/xslt/keys.js +51 -28
- package/src/xslt/literalResult.js +63 -7
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +171 -78
- package/src/xslt/numberFormat.js +124 -26
- package/src/xslt/outputNames.js +58 -0
- package/src/xslt/patternCompiler.js +175 -0
- package/src/xslt/patterns.js +324 -0
- package/src/xslt/qname.js +90 -0
- package/src/xslt/resultDocument.js +98 -0
- package/src/xslt/resultNamespaces.js +219 -0
- package/src/xslt/resultTree.js +143 -6
- package/src/xslt/serializer/baseWriter.js +173 -66
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +14 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +49 -12
- package/src/xslt/serializer/frames.js +168 -0
- package/src/xslt/serializer/htmlDoctype.js +102 -0
- package/src/xslt/serializer/htmlEntities.js +77 -0
- package/src/xslt/serializer/htmlSerializer.js +123 -25
- package/src/xslt/serializer/settings.js +89 -13
- package/src/xslt/serializer/textSerializer.js +58 -10
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +113 -13
- package/src/xslt/serializer.js +50 -17
- package/src/xslt/sort.js +151 -0
- package/src/xslt/spaceNameTests.js +115 -0
- package/src/xslt/stylesheetChecks.js +206 -0
- package/src/xslt/stylesheetNamespaces.js +266 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +43 -27
- 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
|
|
24
|
-
* @returns {boolean}
|
|
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
|
|
27
|
-
return
|
|
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
|
-
|
|
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}
|
|
166
|
+
* @param {string} name - Element name as written
|
|
92
167
|
* @returns {string} Markup terminating the start tag
|
|
93
168
|
*/
|
|
94
|
-
emptyElementMarkup(element,
|
|
95
|
-
|
|
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
|
}
|
package/src/xslt/serializer.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
43
|
-
|
|
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
|
}
|
package/src/xslt/sort.js
ADDED
|
@@ -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
|
+
}
|