@tradik/xslt-processor 1.0.2 → 1.1.1
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 +292 -47
- package/bin/lib/options.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +115 -0
- package/bin/xslt.js +68 -162
- package/dist/xslt-processor.browser.js +2073 -163
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +6 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +2077 -162
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +299 -0
- package/dist/xslt-processor.d.ts +92 -4
- package/dist/xslt-processor.js +2072 -161
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +27 -16
- package/src/XSLTProcessor.js +177 -8
- package/src/index.js +11 -5
- package/src/xpath/evaluator.js +48 -7
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine.js +474 -185
- package/src/xslt/formatNumber.js +220 -0
- package/src/xslt/functions.js +191 -0
- package/src/xslt/index.js +31 -0
- package/src/xslt/keys.js +141 -0
- package/src/xslt/literalResult.js +167 -0
- package/src/xslt/number.js +178 -0
- package/src/xslt/numberFormat.js +155 -0
- package/src/xslt/resultTree.js +74 -0
- package/src/xslt/serializer/baseWriter.js +283 -0
- package/src/xslt/serializer/constants.js +78 -0
- package/src/xslt/serializer/escape.js +98 -0
- package/src/xslt/serializer/htmlSerializer.js +141 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +103 -0
- package/src/xslt/serializer/textSerializer.js +29 -0
- package/src/xslt/serializer/xmlSerializer.js +127 -0
- package/src/xslt/serializer.js +57 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/whitespace.js +184 -0
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- package/src/xslt/engine.test.js +0 -3130
package/src/xslt/engine.js
CHANGED
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
import { parse as parseXPath } from "../xpath/parser.js";
|
|
9
9
|
import { XPathEvaluator, XPathContext } from "../xpath/evaluator.js";
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { XSLT_NAMESPACE } from "./elements.js";
|
|
11
|
+
import { createXsltFunctions } from "./functions.js";
|
|
12
|
+
import { KeyIndexRegistry } from "./keys.js";
|
|
13
|
+
import { countXsltNumber } from "./number.js";
|
|
14
|
+
import { formatXsltNumber, toRoman } from "./numberFormat.js";
|
|
15
|
+
import { resolveUri, stripFragment } from "./uri.js";
|
|
16
|
+
import { WhitespaceFilter, stripWhitespaceNodes } from "./whitespace.js";
|
|
17
|
+
import {
|
|
18
|
+
NamespaceAliasMap,
|
|
19
|
+
getXsltAttribute,
|
|
20
|
+
shouldCopyAttribute,
|
|
21
|
+
} from "./literalResult.js";
|
|
22
|
+
import { createResultDocument, importResultFragment } from "./resultTree.js";
|
|
23
|
+
import { calculatePriority } from "./templatePriority.js";
|
|
24
|
+
import { serializeResult } from "./serializer.js";
|
|
25
|
+
|
|
26
|
+
const XSLT_NS = XSLT_NAMESPACE;
|
|
12
27
|
|
|
13
28
|
/**
|
|
14
29
|
* XSLT Processing Context
|
|
@@ -28,6 +43,8 @@ export class XsltContext {
|
|
|
28
43
|
this.decimalFormats = options.decimalFormats || {};
|
|
29
44
|
this.outputMethod = options.outputMethod || "xml";
|
|
30
45
|
this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
|
|
46
|
+
this.currentTemplate = options.currentTemplate || null;
|
|
47
|
+
this.currentMode = options.currentMode ?? null;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
clone(overrides = {}) {
|
|
@@ -51,6 +68,8 @@ export class XsltContext {
|
|
|
51
68
|
decimalFormats: this.decimalFormats,
|
|
52
69
|
outputMethod: this.outputMethod,
|
|
53
70
|
xpathEvaluator: this.xpathEvaluator,
|
|
71
|
+
currentTemplate: overrides.currentTemplate ?? this.currentTemplate,
|
|
72
|
+
currentMode: overrides.currentMode ?? this.currentMode,
|
|
54
73
|
});
|
|
55
74
|
}
|
|
56
75
|
|
|
@@ -81,7 +100,9 @@ export class XsltEngine {
|
|
|
81
100
|
this.globalParameters = {};
|
|
82
101
|
this.outputSettings = {
|
|
83
102
|
method: "xml",
|
|
103
|
+
version: "1.0",
|
|
84
104
|
encoding: "UTF-8",
|
|
105
|
+
standalone: null,
|
|
85
106
|
indent: "no",
|
|
86
107
|
omitXmlDeclaration: "no",
|
|
87
108
|
doctypePublic: null,
|
|
@@ -93,7 +114,7 @@ export class XsltEngine {
|
|
|
93
114
|
this.decimalFormats = {};
|
|
94
115
|
this.stylesheetDoc = null;
|
|
95
116
|
this.attributeSets = {};
|
|
96
|
-
this.namespaceAliases =
|
|
117
|
+
this.namespaceAliases = new NamespaceAliasMap();
|
|
97
118
|
this.stripSpace = [];
|
|
98
119
|
this.preserveSpace = [];
|
|
99
120
|
|
|
@@ -102,6 +123,117 @@ export class XsltEngine {
|
|
|
102
123
|
this.currentImportPrecedence = 0;
|
|
103
124
|
this.processedStylesheets = new Set();
|
|
104
125
|
this.baseUri = options.baseUri || "";
|
|
126
|
+
|
|
127
|
+
// document() support
|
|
128
|
+
this.documentLoader = options.documentLoader || null;
|
|
129
|
+
this.loadedDocuments = new Map();
|
|
130
|
+
|
|
131
|
+
// generate-id() support
|
|
132
|
+
this.generatedIds = new WeakMap();
|
|
133
|
+
this.generatedIdCount = 0;
|
|
134
|
+
|
|
135
|
+
// key() support
|
|
136
|
+
this.rootContext = null;
|
|
137
|
+
this.keyRegistry = new KeyIndexRegistry({
|
|
138
|
+
keys: this.keys,
|
|
139
|
+
matchesPattern: (node, pattern) =>
|
|
140
|
+
this.matchesPattern(node, pattern, this.rootContext),
|
|
141
|
+
evaluateUse: (node, expression) =>
|
|
142
|
+
this.evaluateKeyValues(node, expression),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.xpathEvaluator.registerFunctions(createXsltFunctions(this));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Set the loader used by the XSLT `document()` function.
|
|
150
|
+
*
|
|
151
|
+
* The loader is synchronous and must return a `Document`, an XML string or
|
|
152
|
+
* null. Returning null (or configuring no loader at all) makes `document()`
|
|
153
|
+
* evaluate to an empty node-set instead of failing the transformation.
|
|
154
|
+
*
|
|
155
|
+
* @param {((uri: string, baseUri?: string) => (Document|string|null))|null} loader - The loader, or null to remove it
|
|
156
|
+
* @returns {XsltEngine} This engine, to allow chaining
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* engine.setDocumentLoader((uri) => readFileSync(uri, 'utf8'));
|
|
160
|
+
*/
|
|
161
|
+
setDocumentLoader(loader) {
|
|
162
|
+
this.documentLoader = loader ?? null;
|
|
163
|
+
this.loadedDocuments.clear();
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Load an external document for the `document()` function.
|
|
169
|
+
*
|
|
170
|
+
* Results are cached per resolved URI for the life of the engine, so the same
|
|
171
|
+
* URI always yields the identical node-set.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} uri - The requested URI, fragment identifiers are ignored
|
|
174
|
+
* @param {string} [baseUri] - Base URI used to resolve relative references
|
|
175
|
+
* @returns {Document|null} The loaded document, or null when unavailable
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* engine.loadDocument('data.xml', '/styles/main.xsl');
|
|
179
|
+
*/
|
|
180
|
+
loadDocument(uri, baseUri) {
|
|
181
|
+
const target = stripFragment(uri);
|
|
182
|
+
|
|
183
|
+
if (target === "") return this.stylesheetDoc;
|
|
184
|
+
if (!this.documentLoader) return null;
|
|
185
|
+
|
|
186
|
+
const resolved = resolveUri(target, baseUri);
|
|
187
|
+
if (this.loadedDocuments.has(resolved)) {
|
|
188
|
+
return this.loadedDocuments.get(resolved);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const loaded = this.documentLoader(resolved, baseUri);
|
|
192
|
+
const doc =
|
|
193
|
+
typeof loaded === "string" ? this.parseXmlString(loaded) : loaded || null;
|
|
194
|
+
|
|
195
|
+
this.loadedDocuments.set(resolved, doc);
|
|
196
|
+
return doc;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Return the stable identifier of a node for `generate-id()`.
|
|
201
|
+
*
|
|
202
|
+
* @param {Node} node - The node to identify
|
|
203
|
+
* @returns {string} An identifier starting with a letter
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* engine.generateId(element); // 'N1'
|
|
207
|
+
*/
|
|
208
|
+
generateId(node) {
|
|
209
|
+
let id = this.generatedIds.get(node);
|
|
210
|
+
if (!id) {
|
|
211
|
+
this.generatedIdCount++;
|
|
212
|
+
id = `N${this.generatedIdCount}`;
|
|
213
|
+
this.generatedIds.set(node, id);
|
|
214
|
+
}
|
|
215
|
+
return id;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Evaluate the `use` expression of an `xsl:key` for one node.
|
|
220
|
+
*
|
|
221
|
+
* @param {Node} node - The node being indexed
|
|
222
|
+
* @param {string} expression - The `use` expression
|
|
223
|
+
* @returns {string[]} The key values contributed by the node
|
|
224
|
+
*/
|
|
225
|
+
evaluateKeyValues(node, expression) {
|
|
226
|
+
const context = this.rootContext.clone({
|
|
227
|
+
currentNode: node,
|
|
228
|
+
currentNodeList: [node],
|
|
229
|
+
position: 1,
|
|
230
|
+
});
|
|
231
|
+
const value = this.evaluateXPath(expression, context);
|
|
232
|
+
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
return value.map((item) => this.xpathEvaluator.getStringValue(item));
|
|
235
|
+
}
|
|
236
|
+
return [this.xpathEvaluator.toString(value)];
|
|
105
237
|
}
|
|
106
238
|
|
|
107
239
|
/**
|
|
@@ -114,22 +246,13 @@ export class XsltEngine {
|
|
|
114
246
|
|
|
115
247
|
/**
|
|
116
248
|
* Resolve a relative URI against a base URI
|
|
249
|
+
*
|
|
250
|
+
* @param {string} href - The URI to resolve
|
|
251
|
+
* @param {string} [baseUri] - The base URI
|
|
252
|
+
* @returns {string} The resolved URI
|
|
117
253
|
*/
|
|
118
254
|
resolveUri(href, baseUri) {
|
|
119
|
-
|
|
120
|
-
!baseUri ||
|
|
121
|
-
href.startsWith("http://") ||
|
|
122
|
-
href.startsWith("https://") ||
|
|
123
|
-
href.startsWith("/")
|
|
124
|
-
) {
|
|
125
|
-
return href;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Remove filename from baseUri to get directory
|
|
129
|
-
const lastSlash = baseUri.lastIndexOf("/");
|
|
130
|
-
const baseDir = lastSlash >= 0 ? baseUri.substring(0, lastSlash + 1) : "";
|
|
131
|
-
|
|
132
|
-
return baseDir + href;
|
|
255
|
+
return resolveUri(href, baseUri);
|
|
133
256
|
}
|
|
134
257
|
|
|
135
258
|
/**
|
|
@@ -290,6 +413,7 @@ export class XsltEngine {
|
|
|
290
413
|
} catch (error) {
|
|
291
414
|
throw new Error(
|
|
292
415
|
`Failed to include stylesheet "${href}": ${error.message}`,
|
|
416
|
+
{ cause: error },
|
|
293
417
|
);
|
|
294
418
|
}
|
|
295
419
|
}
|
|
@@ -331,6 +455,7 @@ export class XsltEngine {
|
|
|
331
455
|
} catch (error) {
|
|
332
456
|
throw new Error(
|
|
333
457
|
`Failed to import stylesheet "${href}": ${error.message}`,
|
|
458
|
+
{ cause: error },
|
|
334
459
|
);
|
|
335
460
|
}
|
|
336
461
|
}
|
|
@@ -427,62 +552,61 @@ export class XsltEngine {
|
|
|
427
552
|
}
|
|
428
553
|
}
|
|
429
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Register a template rule.
|
|
557
|
+
*
|
|
558
|
+
* A union match pattern is equivalent to a set of template rules, one per
|
|
559
|
+
* alternative (XSLT 1.0 section 5.5), so each alternative is registered
|
|
560
|
+
* separately with its own default priority.
|
|
561
|
+
*
|
|
562
|
+
* @param {Element} node - The xsl:template element
|
|
563
|
+
*/
|
|
430
564
|
registerTemplate(node) {
|
|
431
565
|
const match = node.getAttribute("match");
|
|
432
566
|
const name = node.getAttribute("name");
|
|
433
567
|
const mode = node.getAttribute("mode") || null;
|
|
434
568
|
const priorityAttr = node.getAttribute("priority");
|
|
435
|
-
const
|
|
436
|
-
?
|
|
437
|
-
:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
569
|
+
const alternatives = match
|
|
570
|
+
? this.splitUnionPattern(match).map((p) => p.trim())
|
|
571
|
+
: [null];
|
|
572
|
+
|
|
573
|
+
for (const alternative of alternatives) {
|
|
574
|
+
this.templates.push({
|
|
575
|
+
match: alternative,
|
|
576
|
+
name,
|
|
577
|
+
mode,
|
|
578
|
+
priority: priorityAttr
|
|
579
|
+
? parseFloat(priorityAttr)
|
|
580
|
+
: this.calculatePriority(alternative),
|
|
581
|
+
importPrecedence: this.currentImportPrecedence,
|
|
582
|
+
node,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
447
585
|
}
|
|
448
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Default priority of a single match pattern (see templatePriority.js).
|
|
589
|
+
*
|
|
590
|
+
* @param {string|null} matchPattern - The match pattern
|
|
591
|
+
* @returns {number} The default priority
|
|
592
|
+
*/
|
|
449
593
|
calculatePriority(matchPattern) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// Simplified priority calculation based on XPath 1.0 spec
|
|
453
|
-
// - NodeType or * have priority -0.5
|
|
454
|
-
// - NCName:* has priority -0.25
|
|
455
|
-
// - QName has priority 0
|
|
456
|
-
// - Other patterns have priority 0.5
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
matchPattern === "*" ||
|
|
460
|
-
matchPattern === "node()" ||
|
|
461
|
-
matchPattern === "text()" ||
|
|
462
|
-
matchPattern === "comment()" ||
|
|
463
|
-
matchPattern === "processing-instruction()"
|
|
464
|
-
) {
|
|
465
|
-
return -0.5;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (matchPattern.includes(":*")) {
|
|
469
|
-
return -0.25;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (/^[a-zA-Z_][\w.-]*$/.test(matchPattern)) {
|
|
473
|
-
return 0;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return 0.5;
|
|
594
|
+
return calculatePriority(matchPattern ? matchPattern.trim() : matchPattern);
|
|
477
595
|
}
|
|
478
596
|
|
|
479
597
|
processOutput(node) {
|
|
480
598
|
const method = node.getAttribute("method");
|
|
481
599
|
if (method) this.outputSettings.method = method;
|
|
482
600
|
|
|
601
|
+
const version = node.getAttribute("version");
|
|
602
|
+
if (version) this.outputSettings.version = version;
|
|
603
|
+
|
|
483
604
|
const encoding = node.getAttribute("encoding");
|
|
484
605
|
if (encoding) this.outputSettings.encoding = encoding;
|
|
485
606
|
|
|
607
|
+
const standalone = node.getAttribute("standalone");
|
|
608
|
+
if (standalone) this.outputSettings.standalone = standalone;
|
|
609
|
+
|
|
486
610
|
const indent = node.getAttribute("indent");
|
|
487
611
|
if (indent) this.outputSettings.indent = indent;
|
|
488
612
|
|
|
@@ -513,11 +637,80 @@ export class XsltEngine {
|
|
|
513
637
|
this.globalVariables[name] = { node, select };
|
|
514
638
|
}
|
|
515
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Register an `xsl:param` top level declaration.
|
|
642
|
+
*
|
|
643
|
+
* A value supplied from outside (through `setParameter`) has precedence over
|
|
644
|
+
* the declared default, so it survives compilation of the stylesheet.
|
|
645
|
+
*
|
|
646
|
+
* @param {Element} node - The `xsl:param` element
|
|
647
|
+
* @returns {void}
|
|
648
|
+
*/
|
|
516
649
|
processGlobalParam(node) {
|
|
517
650
|
const name = node.getAttribute("name");
|
|
518
651
|
const select = node.getAttribute("select");
|
|
652
|
+
const existing = this.globalParameters[name];
|
|
653
|
+
const definition = { node, select };
|
|
654
|
+
|
|
655
|
+
if (existing && "value" in existing) {
|
|
656
|
+
definition.value = existing.value;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
this.globalParameters[name] = definition;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Supply the value of a global parameter from outside the stylesheet.
|
|
664
|
+
*
|
|
665
|
+
* The value is merged into the `xsl:param` declaration when there is one, so
|
|
666
|
+
* removing the value later restores the declared default.
|
|
667
|
+
*
|
|
668
|
+
* @param {string} name - The parameter name, `{uri}local` when namespaced
|
|
669
|
+
* @param {*} value - The value to use
|
|
670
|
+
* @returns {void}
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* engine.setParameterValue('sortOrder', 'ascending');
|
|
674
|
+
*/
|
|
675
|
+
setParameterValue(name, value) {
|
|
676
|
+
const definition = this.globalParameters[name];
|
|
519
677
|
|
|
520
|
-
|
|
678
|
+
if (definition) definition.value = value;
|
|
679
|
+
else this.globalParameters[name] = { value };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Remove an externally supplied parameter value.
|
|
684
|
+
*
|
|
685
|
+
* The `xsl:param` declaration of the stylesheet is kept, so the parameter
|
|
686
|
+
* falls back to its declared default instead of becoming undefined.
|
|
687
|
+
*
|
|
688
|
+
* @param {string} name - The parameter name, `{uri}local` when namespaced
|
|
689
|
+
* @returns {void}
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* engine.clearParameterValue('sortOrder');
|
|
693
|
+
*/
|
|
694
|
+
clearParameterValue(name) {
|
|
695
|
+
const definition = this.globalParameters[name];
|
|
696
|
+
if (!definition) return;
|
|
697
|
+
|
|
698
|
+
if (definition.node) delete definition.value;
|
|
699
|
+
else delete this.globalParameters[name];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Remove every externally supplied parameter value.
|
|
704
|
+
*
|
|
705
|
+
* @returns {void}
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* engine.clearParameterValues();
|
|
709
|
+
*/
|
|
710
|
+
clearParameterValues() {
|
|
711
|
+
for (const name of Object.keys(this.globalParameters)) {
|
|
712
|
+
this.clearParameterValue(name);
|
|
713
|
+
}
|
|
521
714
|
}
|
|
522
715
|
|
|
523
716
|
processKey(node) {
|
|
@@ -526,6 +719,7 @@ export class XsltEngine {
|
|
|
526
719
|
const use = node.getAttribute("use");
|
|
527
720
|
|
|
528
721
|
this.keys[name] = { match, use };
|
|
722
|
+
this.keyRegistry.clear();
|
|
529
723
|
}
|
|
530
724
|
|
|
531
725
|
processDecimalFormat(node) {
|
|
@@ -546,9 +740,7 @@ export class XsltEngine {
|
|
|
546
740
|
}
|
|
547
741
|
|
|
548
742
|
processNamespaceAlias(node) {
|
|
549
|
-
|
|
550
|
-
const result = node.getAttribute("result-prefix");
|
|
551
|
-
this.namespaceAliases[stylesheet] = result;
|
|
743
|
+
this.namespaceAliases.add(node);
|
|
552
744
|
}
|
|
553
745
|
|
|
554
746
|
processAttributeSet(node) {
|
|
@@ -588,13 +780,19 @@ export class XsltEngine {
|
|
|
588
780
|
throw new Error("No output document available");
|
|
589
781
|
}
|
|
590
782
|
|
|
783
|
+
// Build the result tree in a neutral XML document: creating nodes directly
|
|
784
|
+
// in an HTML owner document would lower case names and force the XHTML
|
|
785
|
+
// namespace on every element.
|
|
786
|
+
const resultDocument = createResultDocument(doc);
|
|
787
|
+
const source = this.prepareSource(sourceNode, doc);
|
|
788
|
+
|
|
591
789
|
// Create context - use document node as initial context for "/" template matching
|
|
592
790
|
// XPath paths like "RootElement/child" expect to start from document node
|
|
593
791
|
const context = new XsltContext({
|
|
594
|
-
currentNode:
|
|
595
|
-
currentNodeList: [
|
|
792
|
+
currentNode: source,
|
|
793
|
+
currentNodeList: [source],
|
|
596
794
|
position: 1,
|
|
597
|
-
outputDocument:
|
|
795
|
+
outputDocument: resultDocument,
|
|
598
796
|
stylesheet: this.stylesheetDoc,
|
|
599
797
|
namespaces: { ...this.namespaces },
|
|
600
798
|
templates: this.templates,
|
|
@@ -604,6 +802,8 @@ export class XsltEngine {
|
|
|
604
802
|
xpathEvaluator: this.xpathEvaluator,
|
|
605
803
|
});
|
|
606
804
|
|
|
805
|
+
this.rootContext = context;
|
|
806
|
+
|
|
607
807
|
// Evaluate global variables
|
|
608
808
|
for (const [name, def] of Object.entries(this.globalParameters)) {
|
|
609
809
|
if (!(name in context.parameters)) {
|
|
@@ -616,17 +816,35 @@ export class XsltEngine {
|
|
|
616
816
|
}
|
|
617
817
|
|
|
618
818
|
// Create result document fragment
|
|
619
|
-
const fragment =
|
|
819
|
+
const fragment = resultDocument.createDocumentFragment();
|
|
620
820
|
|
|
621
|
-
// Apply templates to
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
context,
|
|
626
|
-
fragment,
|
|
627
|
-
);
|
|
821
|
+
// Apply templates to document node (not documentElement)
|
|
822
|
+
// This ensures "/" template has document as context, so paths like
|
|
823
|
+
// "RootElement/child" work correctly
|
|
824
|
+
this.applyTemplates([source], null, context, fragment);
|
|
628
825
|
|
|
629
|
-
return fragment;
|
|
826
|
+
return importResultFragment(fragment, doc);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Apply `xsl:strip-space` to the source tree.
|
|
831
|
+
*
|
|
832
|
+
* Stripping produces a copy so the caller's document is never modified; when
|
|
833
|
+
* no `xsl:strip-space` is declared the original node is used unchanged.
|
|
834
|
+
*
|
|
835
|
+
* @param {Node} sourceNode - The source document or element
|
|
836
|
+
* @param {Document} ownerDocument - Document providing the DOM implementation
|
|
837
|
+
* @returns {Node} The source to transform
|
|
838
|
+
*/
|
|
839
|
+
prepareSource(sourceNode, ownerDocument) {
|
|
840
|
+
const filter = new WhitespaceFilter(this.stripSpace, this.preserveSpace);
|
|
841
|
+
if (!filter.isActive()) return sourceNode;
|
|
842
|
+
|
|
843
|
+
return stripWhitespaceNodes(
|
|
844
|
+
sourceNode,
|
|
845
|
+
filter,
|
|
846
|
+
createResultDocument(ownerDocument),
|
|
847
|
+
);
|
|
630
848
|
}
|
|
631
849
|
|
|
632
850
|
/**
|
|
@@ -634,7 +852,7 @@ export class XsltEngine {
|
|
|
634
852
|
*/
|
|
635
853
|
transformToDocument(sourceNode) {
|
|
636
854
|
// For Node.js environments, we need a document implementation
|
|
637
|
-
const doc = this.createDocument();
|
|
855
|
+
const doc = this.createDocument(sourceNode);
|
|
638
856
|
const fragment = this.transform(sourceNode, doc);
|
|
639
857
|
|
|
640
858
|
// Move fragment contents to document
|
|
@@ -645,16 +863,66 @@ export class XsltEngine {
|
|
|
645
863
|
return doc;
|
|
646
864
|
}
|
|
647
865
|
|
|
648
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Transform a source document and serialize the result to a string.
|
|
868
|
+
*
|
|
869
|
+
* Non-W3C convenience method: the result tree is serialized honoring the
|
|
870
|
+
* `xsl:output` settings of the stylesheet (XSLT 1.0 section 16).
|
|
871
|
+
*
|
|
872
|
+
* @param {Node} sourceNode - Source document or element to transform
|
|
873
|
+
* @returns {string} The serialized transformation result
|
|
874
|
+
*/
|
|
875
|
+
transformToString(sourceNode) {
|
|
876
|
+
const fragment = this.transform(
|
|
877
|
+
sourceNode,
|
|
878
|
+
this.createDocument(sourceNode),
|
|
879
|
+
);
|
|
880
|
+
return serializeResult(fragment, this.outputSettings);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Create an empty XML document to hold a transformation result.
|
|
885
|
+
*
|
|
886
|
+
* Uses the global `document` when running in a browser and otherwise falls
|
|
887
|
+
* back to the DOM implementation owning `referenceNode` (e.g. a jsdom or
|
|
888
|
+
* xmldom document in Node.js).
|
|
889
|
+
*
|
|
890
|
+
* @param {Node} [referenceNode] - Any node whose DOM implementation can be reused
|
|
891
|
+
* @returns {Document} A new empty document
|
|
892
|
+
* @throws {Error} When no DOM implementation is available
|
|
893
|
+
*/
|
|
894
|
+
createDocument(referenceNode) {
|
|
649
895
|
if (typeof document !== "undefined") {
|
|
650
896
|
return document.implementation.createDocument(null, null, null);
|
|
651
897
|
}
|
|
652
898
|
|
|
653
|
-
|
|
899
|
+
const ownerDocument =
|
|
900
|
+
referenceNode &&
|
|
901
|
+
(referenceNode.nodeType === 9
|
|
902
|
+
? referenceNode
|
|
903
|
+
: referenceNode.ownerDocument);
|
|
904
|
+
if (ownerDocument?.implementation) {
|
|
905
|
+
return ownerDocument.implementation.createDocument(null, null, null);
|
|
906
|
+
}
|
|
907
|
+
|
|
654
908
|
throw new Error("Document creation not available in this environment");
|
|
655
909
|
}
|
|
656
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Compute the value of a variable or parameter definition.
|
|
913
|
+
*
|
|
914
|
+
* A value supplied from outside (`setParameter`) wins over the `select`
|
|
915
|
+
* expression and over the instantiated content of the declaration.
|
|
916
|
+
*
|
|
917
|
+
* @param {{value?: *, select?: string, node?: Element}} def - The definition
|
|
918
|
+
* @param {XsltContext} context - The context used for evaluation
|
|
919
|
+
* @returns {*} The variable value
|
|
920
|
+
*/
|
|
657
921
|
evaluateVariable(def, context) {
|
|
922
|
+
if ("value" in def) {
|
|
923
|
+
return def.value;
|
|
924
|
+
}
|
|
925
|
+
|
|
658
926
|
if (def.select) {
|
|
659
927
|
return this.evaluateXPath(def.select, context);
|
|
660
928
|
}
|
|
@@ -680,6 +948,8 @@ export class XsltEngine {
|
|
|
680
948
|
currentNode: node,
|
|
681
949
|
currentNodeList: nodeList,
|
|
682
950
|
position: i + 1,
|
|
951
|
+
currentTemplate: template,
|
|
952
|
+
currentMode: mode,
|
|
683
953
|
});
|
|
684
954
|
|
|
685
955
|
this.processTemplate(template.node, newContext, output);
|
|
@@ -693,7 +963,7 @@ export class XsltEngine {
|
|
|
693
963
|
/**
|
|
694
964
|
* Find the best matching template for a node
|
|
695
965
|
*/
|
|
696
|
-
findMatchingTemplate(node, mode, context) {
|
|
966
|
+
findMatchingTemplate(node, mode, context, maxImportPrecedence = Infinity) {
|
|
697
967
|
let bestMatch = null;
|
|
698
968
|
let bestPriority = -Infinity;
|
|
699
969
|
let bestImportPrecedence = -Infinity;
|
|
@@ -701,6 +971,7 @@ export class XsltEngine {
|
|
|
701
971
|
for (const template of this.templates) {
|
|
702
972
|
if (template.mode !== mode) continue;
|
|
703
973
|
if (!template.match) continue;
|
|
974
|
+
if ((template.importPrecedence || 0) >= maxImportPrecedence) continue;
|
|
704
975
|
|
|
705
976
|
if (this.matchesPattern(node, template.match, context)) {
|
|
706
977
|
const priority = template.priority;
|
|
@@ -799,20 +1070,24 @@ export class XsltEngine {
|
|
|
799
1070
|
1,
|
|
800
1071
|
{ ...context.variables, ...context.parameters },
|
|
801
1072
|
context.namespaces,
|
|
1073
|
+
context,
|
|
802
1074
|
);
|
|
803
1075
|
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
804
1076
|
const nodes = Array.isArray(result) ? result : [result];
|
|
805
1077
|
return nodes.includes(node);
|
|
806
1078
|
}
|
|
807
1079
|
|
|
808
|
-
// For relative patterns, check if this node matches when evaluated from
|
|
809
|
-
|
|
1080
|
+
// For relative patterns, check if this node matches when evaluated from
|
|
1081
|
+
// its parent; attribute nodes are reached through their owner element
|
|
1082
|
+
const parent = node.nodeType === 2 ? node.ownerElement : node.parentNode;
|
|
1083
|
+
if (parent) {
|
|
810
1084
|
const xpathContext = new XPathContext(
|
|
811
|
-
|
|
1085
|
+
parent,
|
|
812
1086
|
1,
|
|
813
1087
|
1,
|
|
814
1088
|
{ ...context.variables, ...context.parameters },
|
|
815
1089
|
context.namespaces,
|
|
1090
|
+
context,
|
|
816
1091
|
);
|
|
817
1092
|
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
818
1093
|
const nodes = Array.isArray(result) ? result : [result];
|
|
@@ -826,6 +1101,7 @@ export class XsltEngine {
|
|
|
826
1101
|
1,
|
|
827
1102
|
{ ...context.variables, ...context.parameters },
|
|
828
1103
|
context.namespaces,
|
|
1104
|
+
context,
|
|
829
1105
|
);
|
|
830
1106
|
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
831
1107
|
const nodes = Array.isArray(result) ? result : [result];
|
|
@@ -962,6 +1238,10 @@ export class XsltEngine {
|
|
|
962
1238
|
this.xslApplyTemplates(node, context, output);
|
|
963
1239
|
break;
|
|
964
1240
|
|
|
1241
|
+
case "apply-imports":
|
|
1242
|
+
this.xslApplyImports(node, context, output);
|
|
1243
|
+
break;
|
|
1244
|
+
|
|
965
1245
|
case "call-template":
|
|
966
1246
|
this.xslCallTemplate(node, context, output);
|
|
967
1247
|
break;
|
|
@@ -1045,42 +1325,56 @@ export class XsltEngine {
|
|
|
1045
1325
|
|
|
1046
1326
|
/**
|
|
1047
1327
|
* Process a literal result element (non-XSLT)
|
|
1328
|
+
*
|
|
1329
|
+
* Applies `xsl:namespace-alias` to the element and its attributes, honours
|
|
1330
|
+
* `xsl:use-attribute-sets` and keeps XSLT-only attributes and namespace
|
|
1331
|
+
* declarations out of the result tree.
|
|
1332
|
+
*
|
|
1333
|
+
* @param {Element} node - The literal result element in the stylesheet
|
|
1334
|
+
* @param {XsltContext} context - The current XSLT context
|
|
1335
|
+
* @param {Node} output - The result tree node receiving the element
|
|
1336
|
+
* @returns {void}
|
|
1048
1337
|
*/
|
|
1049
1338
|
processLiteralResultElement(node, context, output) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
if (
|
|
1067
|
-
|
|
1068
|
-
resolvedNS,
|
|
1069
|
-
nodeName,
|
|
1070
|
-
);
|
|
1071
|
-
} else {
|
|
1072
|
-
outputElement = context.outputDocument.createElement(nodeName);
|
|
1339
|
+
const localName = node.localName || node.nodeName;
|
|
1340
|
+
const alias = this.namespaceAliases.resolve(node.namespaceURI, localName);
|
|
1341
|
+
const namespaceUri = alias ? alias.namespaceUri : node.namespaceURI;
|
|
1342
|
+
const qname = alias ? alias.qname : node.nodeName;
|
|
1343
|
+
|
|
1344
|
+
const outputElement =
|
|
1345
|
+
namespaceUri && context.outputDocument.createElementNS
|
|
1346
|
+
? context.outputDocument.createElementNS(namespaceUri, qname)
|
|
1347
|
+
: context.outputDocument.createElement(qname);
|
|
1348
|
+
|
|
1349
|
+
// Attribute sets come first so literal attributes take precedence
|
|
1350
|
+
const useAttributeSets = getXsltAttribute(
|
|
1351
|
+
node,
|
|
1352
|
+
"use-attribute-sets",
|
|
1353
|
+
XSLT_NS,
|
|
1354
|
+
);
|
|
1355
|
+
if (useAttributeSets) {
|
|
1356
|
+
this.applyAttributeSets(useAttributeSets, context, outputElement);
|
|
1073
1357
|
}
|
|
1074
1358
|
|
|
1075
|
-
// Copy attributes (except XSLT namespace)
|
|
1076
1359
|
if (node.attributes) {
|
|
1077
1360
|
for (const attr of node.attributes) {
|
|
1078
|
-
if (attr
|
|
1079
|
-
if (attr.name.startsWith("xmlns")) continue;
|
|
1361
|
+
if (!shouldCopyAttribute(attr, XSLT_NS)) continue;
|
|
1080
1362
|
|
|
1081
|
-
// Process attribute value templates
|
|
1082
1363
|
const value = this.processAttributeValueTemplate(attr.value, context);
|
|
1083
|
-
|
|
1364
|
+
const attrAlias = this.namespaceAliases.resolve(
|
|
1365
|
+
attr.namespaceURI,
|
|
1366
|
+
attr.localName || attr.name,
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
if (attrAlias) {
|
|
1370
|
+
outputElement.setAttributeNS(
|
|
1371
|
+
attrAlias.namespaceUri,
|
|
1372
|
+
attrAlias.qname,
|
|
1373
|
+
value,
|
|
1374
|
+
);
|
|
1375
|
+
} else {
|
|
1376
|
+
outputElement.setAttribute(attr.name, value);
|
|
1377
|
+
}
|
|
1084
1378
|
}
|
|
1085
1379
|
}
|
|
1086
1380
|
|
|
@@ -1190,6 +1484,44 @@ export class XsltEngine {
|
|
|
1190
1484
|
this.applyTemplates(nodes, mode, newContext, output);
|
|
1191
1485
|
}
|
|
1192
1486
|
|
|
1487
|
+
/**
|
|
1488
|
+
* Instantiate `xsl:apply-imports`.
|
|
1489
|
+
*
|
|
1490
|
+
* Only templates with a lower import precedence than the template being
|
|
1491
|
+
* instantiated are considered; when none matches, the built-in template rules
|
|
1492
|
+
* apply, exactly as for `xsl:apply-templates`.
|
|
1493
|
+
*
|
|
1494
|
+
* @param {Element} node - The `xsl:apply-imports` element
|
|
1495
|
+
* @param {XsltContext} context - The current XSLT context
|
|
1496
|
+
* @param {Node} output - The result tree node receiving the output
|
|
1497
|
+
* @returns {void}
|
|
1498
|
+
*/
|
|
1499
|
+
xslApplyImports(node, context, output) {
|
|
1500
|
+
const currentNode = context.currentNode;
|
|
1501
|
+
const mode = context.currentMode ?? null;
|
|
1502
|
+
const precedence = context.currentTemplate
|
|
1503
|
+
? context.currentTemplate.importPrecedence || 0
|
|
1504
|
+
: 0;
|
|
1505
|
+
|
|
1506
|
+
const template = this.findMatchingTemplate(
|
|
1507
|
+
currentNode,
|
|
1508
|
+
mode,
|
|
1509
|
+
context,
|
|
1510
|
+
precedence,
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1513
|
+
if (!template) {
|
|
1514
|
+
this.applyBuiltinTemplate(currentNode, mode, context, output);
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
this.processTemplate(
|
|
1519
|
+
template.node,
|
|
1520
|
+
context.clone({ currentTemplate: template }),
|
|
1521
|
+
output,
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1193
1525
|
xslCallTemplate(node, context, output) {
|
|
1194
1526
|
const name = node.getAttribute("name");
|
|
1195
1527
|
|
|
@@ -1595,96 +1927,52 @@ export class XsltEngine {
|
|
|
1595
1927
|
xslNumber(node, context, output) {
|
|
1596
1928
|
const value = node.getAttribute("value");
|
|
1597
1929
|
const format = node.getAttribute("format") || "1";
|
|
1598
|
-
const level = node.getAttribute("level") || "single";
|
|
1599
1930
|
|
|
1600
|
-
let
|
|
1931
|
+
let numbers;
|
|
1601
1932
|
if (value) {
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1933
|
+
numbers = [
|
|
1934
|
+
Math.round(
|
|
1935
|
+
this.xpathEvaluator.toNumber(this.evaluateXPath(value, context)),
|
|
1936
|
+
),
|
|
1937
|
+
];
|
|
1605
1938
|
} else {
|
|
1606
|
-
|
|
1607
|
-
|
|
1939
|
+
numbers = countXsltNumber(
|
|
1940
|
+
context.currentNode,
|
|
1941
|
+
{
|
|
1942
|
+
level: node.getAttribute("level") || "single",
|
|
1943
|
+
count: node.getAttribute("count"),
|
|
1944
|
+
from: node.getAttribute("from"),
|
|
1945
|
+
},
|
|
1946
|
+
(candidate, pattern) =>
|
|
1947
|
+
this.matchesPattern(candidate, pattern, context),
|
|
1948
|
+
);
|
|
1608
1949
|
}
|
|
1609
1950
|
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1951
|
+
const text = context.outputDocument.createTextNode(
|
|
1952
|
+
formatXsltNumber(numbers, format),
|
|
1953
|
+
);
|
|
1612
1954
|
output.appendChild(text);
|
|
1613
1955
|
}
|
|
1614
1956
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
let n = 1;
|
|
1623
|
-
let sibling = node.previousSibling;
|
|
1624
|
-
while (sibling) {
|
|
1625
|
-
if (sibling.nodeType === 1) {
|
|
1626
|
-
if (!count || this.matchesPattern(sibling, count, context)) {
|
|
1627
|
-
n++;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
sibling = sibling.previousSibling;
|
|
1631
|
-
}
|
|
1632
|
-
return n;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
return 1;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1957
|
+
/**
|
|
1958
|
+
* Format a single number with an `xsl:number` format token.
|
|
1959
|
+
*
|
|
1960
|
+
* @param {number} number - The number to format
|
|
1961
|
+
* @param {string} format - The format token, e.g. `1`, `01`, `a`, `I`
|
|
1962
|
+
* @returns {string} The formatted number
|
|
1963
|
+
*/
|
|
1638
1964
|
formatNumber(number, format) {
|
|
1639
|
-
|
|
1640
|
-
if (/^[0-9]+$/.test(format)) {
|
|
1641
|
-
return String(number).padStart(format.length, "0");
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
if (format === "a") {
|
|
1645
|
-
return String.fromCharCode(96 + ((number - 1) % 26) + 1);
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
if (format === "A") {
|
|
1649
|
-
return String.fromCharCode(64 + ((number - 1) % 26) + 1);
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
if (format === "i") {
|
|
1653
|
-
return this.toRoman(number).toLowerCase();
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
if (format === "I") {
|
|
1657
|
-
return this.toRoman(number);
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
return String(number);
|
|
1965
|
+
return formatXsltNumber([number], format);
|
|
1661
1966
|
}
|
|
1662
1967
|
|
|
1968
|
+
/**
|
|
1969
|
+
* Convert a number to an upper case Roman numeral.
|
|
1970
|
+
*
|
|
1971
|
+
* @param {number} num - The number to convert
|
|
1972
|
+
* @returns {string} The Roman numeral
|
|
1973
|
+
*/
|
|
1663
1974
|
toRoman(num) {
|
|
1664
|
-
|
|
1665
|
-
["M", 1000],
|
|
1666
|
-
["CM", 900],
|
|
1667
|
-
["D", 500],
|
|
1668
|
-
["CD", 400],
|
|
1669
|
-
["C", 100],
|
|
1670
|
-
["XC", 90],
|
|
1671
|
-
["L", 50],
|
|
1672
|
-
["XL", 40],
|
|
1673
|
-
["X", 10],
|
|
1674
|
-
["IX", 9],
|
|
1675
|
-
["V", 5],
|
|
1676
|
-
["IV", 4],
|
|
1677
|
-
["I", 1],
|
|
1678
|
-
];
|
|
1679
|
-
|
|
1680
|
-
let result = "";
|
|
1681
|
-
for (const [numeral, value] of romanNumerals) {
|
|
1682
|
-
while (num >= value) {
|
|
1683
|
-
result += numeral;
|
|
1684
|
-
num -= value;
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
return result;
|
|
1975
|
+
return toRoman(num);
|
|
1688
1976
|
}
|
|
1689
1977
|
|
|
1690
1978
|
xslMessage(node, context, _output) {
|
|
@@ -1790,6 +2078,7 @@ export class XsltEngine {
|
|
|
1790
2078
|
context.currentNodeList.length,
|
|
1791
2079
|
{ ...context.variables, ...context.parameters },
|
|
1792
2080
|
context.namespaces,
|
|
2081
|
+
context,
|
|
1793
2082
|
);
|
|
1794
2083
|
return this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
1795
2084
|
}
|