@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
package/src/xslt/engine.js
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
* Based on W3C XSLT 1.0 Specification: http://www.w3.org/TR/1999/REC-xslt-19991116
|
|
4
4
|
*
|
|
5
5
|
* Processes XSLT stylesheets and transforms XML documents.
|
|
6
|
+
*
|
|
7
|
+
* XsltEngine is a facade: it owns the engine state and the helpers every
|
|
8
|
+
* part uses (XPath evaluation, XSLT element tests, warnings), while the
|
|
9
|
+
* methods live in modules under ./engine/ by concern and are installed on
|
|
10
|
+
* its prototype (see engine/methods.js):
|
|
11
|
+
*
|
|
12
|
+
* - stylesheetLoading.js: importStylesheet, xsl:import/xsl:include
|
|
13
|
+
* - topLevel.js: dispatch of top-level elements, simplified stylesheets
|
|
14
|
+
* - declarations.js, outputDeclaration.js: top-level declarations
|
|
15
|
+
* - templateRules.js: template registry and matching
|
|
16
|
+
* - templateInvocation.js: apply-templates, apply-imports, call-template
|
|
17
|
+
* - sequenceConstructor.js: instruction dispatch, fallback, extensions
|
|
18
|
+
* - workStack.js: the explicit stack instantiating templates and their
|
|
19
|
+
* content without deep JavaScript recursion, and the template depth limit
|
|
20
|
+
* - controlFlow.js: if, choose, for-each, sorting
|
|
21
|
+
* - bindings.js: variables and parameters
|
|
22
|
+
* - textInstructions.js, numbering.js: value-of, text, comment, PI,
|
|
23
|
+
* message, number
|
|
24
|
+
* - nodeConstruction.js, copyInstructions.js: literal result elements,
|
|
25
|
+
* element, attribute, attribute sets, copy, copy-of
|
|
26
|
+
* - transformation.js: transform entry points and the result tree
|
|
27
|
+
* - functionSupport.js: document(), generate-id() and key() services
|
|
6
28
|
*/
|
|
7
29
|
|
|
8
30
|
import { parse as parseXPath } from "../xpath/parser.js";
|
|
@@ -10,106 +32,94 @@ import { XPathEvaluator, XPathContext } from "../xpath/evaluator.js";
|
|
|
10
32
|
import { XSLT_NAMESPACE } from "./elements.js";
|
|
11
33
|
import { createXsltFunctions } from "./functions.js";
|
|
12
34
|
import { KeyIndexRegistry } from "./keys.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { WhitespaceFilter, stripWhitespaceNodes } from "./whitespace.js";
|
|
35
|
+
import { NamespaceAliasMap } from "./literalResult.js";
|
|
36
|
+
import { PatternMatcher } from "./patterns.js";
|
|
37
|
+
import { installMethods } from "./engine/methods.js";
|
|
17
38
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "./
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
39
|
+
createOutputSettings,
|
|
40
|
+
outputDeclarationMethods,
|
|
41
|
+
} from "./engine/outputDeclaration.js";
|
|
42
|
+
import { stylesheetLoadingMethods } from "./engine/stylesheetLoading.js";
|
|
43
|
+
import { topLevelMethods } from "./engine/topLevel.js";
|
|
44
|
+
import { declarationMethods } from "./engine/declarations.js";
|
|
45
|
+
import { templateRuleMethods } from "./engine/templateRules.js";
|
|
46
|
+
import { templateInvocationMethods } from "./engine/templateInvocation.js";
|
|
47
|
+
import { sequenceConstructorMethods } from "./engine/sequenceConstructor.js";
|
|
48
|
+
import { controlFlowMethods } from "./engine/controlFlow.js";
|
|
49
|
+
import { bindingMethods } from "./engine/bindings.js";
|
|
50
|
+
import { textInstructionMethods } from "./engine/textInstructions.js";
|
|
51
|
+
import { numberingMethods } from "./engine/numbering.js";
|
|
52
|
+
import { nodeConstructionMethods } from "./engine/nodeConstruction.js";
|
|
53
|
+
import { copyInstructionMethods } from "./engine/copyInstructions.js";
|
|
54
|
+
import { transformationMethods } from "./engine/transformation.js";
|
|
55
|
+
import { functionSupportMethods } from "./engine/functionSupport.js";
|
|
56
|
+
import {
|
|
57
|
+
XSLT_MAX_TEMPLATE_DEPTH,
|
|
58
|
+
workStackMethods,
|
|
59
|
+
} from "./engine/workStack.js";
|
|
25
60
|
|
|
26
|
-
|
|
61
|
+
export { XsltContext } from "./engine/context.js";
|
|
62
|
+
export { XSLT_MAX_TEMPLATE_DEPTH };
|
|
27
63
|
|
|
28
64
|
/**
|
|
29
|
-
*
|
|
65
|
+
* Largest node-set a single XPath step may produce inside a transformation.
|
|
66
|
+
*
|
|
67
|
+
* The standalone XPath API keeps its low default (XPathLimits.MAX_RESULT_SIZE)
|
|
68
|
+
* as a guard against untrusted expressions. A stylesheet is trusted program
|
|
69
|
+
* code and commonly walks documents with tens of thousands of nodes, which the
|
|
70
|
+
* native XSLTProcessor handles without any limit, so the engine uses a much
|
|
71
|
+
* higher bound. Override it with the `maxResultSize` engine option.
|
|
30
72
|
*/
|
|
31
|
-
export
|
|
32
|
-
constructor(options = {}) {
|
|
33
|
-
this.currentNode = options.currentNode;
|
|
34
|
-
this.currentNodeList = options.currentNodeList || [];
|
|
35
|
-
this.position = options.position || 1;
|
|
36
|
-
this.variables = { ...options.variables };
|
|
37
|
-
this.parameters = { ...options.parameters };
|
|
38
|
-
this.outputDocument = options.outputDocument;
|
|
39
|
-
this.stylesheet = options.stylesheet;
|
|
40
|
-
this.namespaces = { ...options.namespaces };
|
|
41
|
-
this.templates = options.templates || [];
|
|
42
|
-
this.keys = options.keys || {};
|
|
43
|
-
this.decimalFormats = options.decimalFormats || {};
|
|
44
|
-
this.outputMethod = options.outputMethod || "xml";
|
|
45
|
-
this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
|
|
46
|
-
this.currentTemplate = options.currentTemplate || null;
|
|
47
|
-
this.currentMode = options.currentMode ?? null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
clone(overrides = {}) {
|
|
51
|
-
return new XsltContext({
|
|
52
|
-
currentNode: overrides.currentNode ?? this.currentNode,
|
|
53
|
-
currentNodeList: overrides.currentNodeList ?? this.currentNodeList,
|
|
54
|
-
position: overrides.position ?? this.position,
|
|
55
|
-
variables: overrides.variables
|
|
56
|
-
? { ...this.variables, ...overrides.variables }
|
|
57
|
-
: { ...this.variables },
|
|
58
|
-
parameters: overrides.parameters
|
|
59
|
-
? { ...this.parameters, ...overrides.parameters }
|
|
60
|
-
: { ...this.parameters },
|
|
61
|
-
outputDocument: this.outputDocument,
|
|
62
|
-
stylesheet: this.stylesheet,
|
|
63
|
-
namespaces: overrides.namespaces
|
|
64
|
-
? { ...this.namespaces, ...overrides.namespaces }
|
|
65
|
-
: { ...this.namespaces },
|
|
66
|
-
templates: this.templates,
|
|
67
|
-
keys: this.keys,
|
|
68
|
-
decimalFormats: this.decimalFormats,
|
|
69
|
-
outputMethod: this.outputMethod,
|
|
70
|
-
xpathEvaluator: this.xpathEvaluator,
|
|
71
|
-
currentTemplate: overrides.currentTemplate ?? this.currentTemplate,
|
|
72
|
-
currentMode: overrides.currentMode ?? this.currentMode,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
getVariable(name) {
|
|
77
|
-
if (name in this.variables) {
|
|
78
|
-
return this.variables[name];
|
|
79
|
-
}
|
|
80
|
-
if (name in this.parameters) {
|
|
81
|
-
return this.parameters[name];
|
|
82
|
-
}
|
|
83
|
-
throw new Error(`Undefined variable: $${name}`);
|
|
84
|
-
}
|
|
73
|
+
export const XSLT_MAX_RESULT_SIZE = 5000000;
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Deepest XPath expression nesting allowed inside a transformation.
|
|
77
|
+
*
|
|
78
|
+
* Like XSLT_MAX_RESULT_SIZE, this is higher than the standalone XPath default
|
|
79
|
+
* (XPathLimits.MAX_RECURSION_DEPTH = 100) because stylesheets are trusted and
|
|
80
|
+
* generated ones often contain long chains such as `a + b + c + ...`.
|
|
81
|
+
* Override it with the `maxRecursionDepth` engine option.
|
|
82
|
+
*/
|
|
83
|
+
export const XSLT_MAX_EXPRESSION_DEPTH = 1000;
|
|
90
84
|
|
|
91
85
|
/**
|
|
92
86
|
* XSLT Engine
|
|
93
87
|
*/
|
|
94
88
|
export class XsltEngine {
|
|
89
|
+
/**
|
|
90
|
+
* @param {object} [options] - Engine options
|
|
91
|
+
* @param {number} [options.maxResultSize] - Largest node-set of one XPath step
|
|
92
|
+
* @param {number} [options.maxRecursionDepth] - Deepest XPath expression nesting
|
|
93
|
+
* @param {number} [options.maxTemplateDepth] - Deepest nesting of template
|
|
94
|
+
* instantiations (default XSLT_MAX_TEMPLATE_DEPTH, 3000 as in libxslt)
|
|
95
|
+
* @param {boolean} [options.legacyNameTests] - Deprecated: unprefixed name
|
|
96
|
+
* tests also match nodes in a namespace, as before 1.2.0
|
|
97
|
+
* @param {Function} [options.stylesheetLoader] - Loader for xsl:import/include
|
|
98
|
+
* @param {Function} [options.documentLoader] - Loader for document()
|
|
99
|
+
* @param {string} [options.baseUri] - Base URI of the stylesheet
|
|
100
|
+
* @param {object} [options.domParser] - Parser for loaded XML strings
|
|
101
|
+
* @param {boolean} [options.enableDynamicEvaluate] - Allow EXSLT
|
|
102
|
+
* `dyn:evaluate()` (off by default: it evaluates XPath built from data)
|
|
103
|
+
* @param {() => Date} [options.clock] - Clock for EXSLT current-time functions
|
|
104
|
+
*/
|
|
95
105
|
constructor(options = {}) {
|
|
96
|
-
this.
|
|
106
|
+
this.enableDynamicEvaluate = options.enableDynamicEvaluate === true;
|
|
107
|
+
this.clock = options.clock ?? null;
|
|
108
|
+
this.xpathEvaluator = new XPathEvaluator({
|
|
109
|
+
maxResultSize: options.maxResultSize ?? XSLT_MAX_RESULT_SIZE,
|
|
110
|
+
maxRecursionDepth: options.maxRecursionDepth ?? XSLT_MAX_EXPRESSION_DEPTH,
|
|
111
|
+
legacyNameTests: options.legacyNameTests,
|
|
112
|
+
});
|
|
113
|
+
// Template instantiation (see engine/workStack.js)
|
|
114
|
+
this.maxTemplateDepth = options.maxTemplateDepth ?? XSLT_MAX_TEMPLATE_DEPTH;
|
|
115
|
+
this.frames = null;
|
|
116
|
+
this.templateDepth = 0;
|
|
117
|
+
|
|
97
118
|
this.templates = [];
|
|
98
119
|
this.keys = {};
|
|
99
120
|
this.globalVariables = {};
|
|
100
121
|
this.globalParameters = {};
|
|
101
|
-
this.outputSettings =
|
|
102
|
-
method: "xml",
|
|
103
|
-
version: "1.0",
|
|
104
|
-
encoding: "UTF-8",
|
|
105
|
-
standalone: null,
|
|
106
|
-
indent: "no",
|
|
107
|
-
omitXmlDeclaration: "no",
|
|
108
|
-
doctypePublic: null,
|
|
109
|
-
doctypeSystem: null,
|
|
110
|
-
mediaType: null,
|
|
111
|
-
cdataSectionElements: [],
|
|
112
|
-
};
|
|
122
|
+
this.outputSettings = createOutputSettings();
|
|
113
123
|
this.namespaces = {};
|
|
114
124
|
this.decimalFormats = {};
|
|
115
125
|
this.stylesheetDoc = null;
|
|
@@ -121,1982 +131,128 @@ export class XsltEngine {
|
|
|
121
131
|
// Import/Include support
|
|
122
132
|
this.stylesheetLoader = options.stylesheetLoader || null;
|
|
123
133
|
this.currentImportPrecedence = 0;
|
|
124
|
-
|
|
134
|
+
// URIs of the stylesheets being loaded, used to detect import cycles
|
|
135
|
+
this.stylesheetStack = [];
|
|
125
136
|
this.baseUri = options.baseUri || "";
|
|
126
137
|
|
|
127
138
|
// document() support
|
|
128
139
|
this.documentLoader = options.documentLoader || null;
|
|
129
140
|
this.loadedDocuments = new Map();
|
|
130
141
|
|
|
142
|
+
// Parser for XML strings returned by the loaders (see domParsing.js)
|
|
143
|
+
this.domParser = options.domParser ?? null;
|
|
144
|
+
|
|
131
145
|
// generate-id() support
|
|
132
146
|
this.generatedIds = new WeakMap();
|
|
133
147
|
this.generatedIdCount = 0;
|
|
134
148
|
|
|
149
|
+
// Compiled XSLT patterns (template match, xsl:key, xsl:number)
|
|
150
|
+
this.patternMatcher = new PatternMatcher(this.xpathEvaluator);
|
|
151
|
+
|
|
135
152
|
// key() support
|
|
136
153
|
this.rootContext = null;
|
|
137
154
|
this.keyRegistry = new KeyIndexRegistry({
|
|
138
155
|
keys: this.keys,
|
|
139
|
-
matchesPattern: (node, pattern) =>
|
|
140
|
-
this.matchesPattern(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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)];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Set the stylesheet loader function for xsl:import and xsl:include
|
|
241
|
-
* @param {Function} loader - Function(href, baseUri) => Document or string (XML)
|
|
242
|
-
*/
|
|
243
|
-
setStylesheetLoader(loader) {
|
|
244
|
-
this.stylesheetLoader = loader;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
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
|
|
253
|
-
*/
|
|
254
|
-
resolveUri(href, baseUri) {
|
|
255
|
-
return resolveUri(href, baseUri);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Load an external stylesheet document
|
|
260
|
-
*/
|
|
261
|
-
loadStylesheet(href, baseUri) {
|
|
262
|
-
if (!this.stylesheetLoader) {
|
|
263
|
-
throw new Error(
|
|
264
|
-
`Cannot load stylesheet "${href}": no stylesheetLoader configured. ` +
|
|
265
|
-
"Use engine.setStylesheetLoader(fn) to provide a loader function.",
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const resolvedUri = this.resolveUri(href, baseUri);
|
|
270
|
-
const result = this.stylesheetLoader(resolvedUri, baseUri);
|
|
271
|
-
|
|
272
|
-
// If result is a string, it needs to be parsed (caller should handle this)
|
|
273
|
-
return { document: result, uri: resolvedUri };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Parse XML string to document (helper for stylesheet loading)
|
|
278
|
-
*/
|
|
279
|
-
parseXmlString(xmlString) {
|
|
280
|
-
if (typeof DOMParser !== "undefined") {
|
|
281
|
-
const parser = new DOMParser();
|
|
282
|
-
const doc = parser.parseFromString(xmlString, "application/xml");
|
|
283
|
-
const parseError = doc.querySelector("parsererror");
|
|
284
|
-
if (parseError) {
|
|
285
|
-
throw new Error(`XML parse error: ${parseError.textContent}`);
|
|
286
|
-
}
|
|
287
|
-
return doc;
|
|
288
|
-
}
|
|
289
|
-
throw new Error("XML parsing not available in this environment");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Import and compile an XSLT stylesheet
|
|
294
|
-
* @param {Document|Element} stylesheetNode - The stylesheet document or root element
|
|
295
|
-
* @param {string} [stylesheetUri] - Optional URI of the stylesheet for resolving imports
|
|
296
|
-
*/
|
|
297
|
-
importStylesheet(stylesheetNode, stylesheetUri) {
|
|
298
|
-
const isMainStylesheet = this.stylesheetDoc === null;
|
|
299
|
-
|
|
300
|
-
if (isMainStylesheet) {
|
|
301
|
-
this.stylesheetDoc = stylesheetNode.ownerDocument || stylesheetNode;
|
|
302
|
-
if (stylesheetUri) {
|
|
303
|
-
this.baseUri = stylesheetUri;
|
|
304
|
-
this.processedStylesheets.add(stylesheetUri);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const root = stylesheetNode.documentElement || stylesheetNode;
|
|
309
|
-
|
|
310
|
-
// Validate stylesheet
|
|
311
|
-
if (
|
|
312
|
-
!this.isXsltElement(root, "stylesheet") &&
|
|
313
|
-
!this.isXsltElement(root, "transform")
|
|
314
|
-
) {
|
|
315
|
-
// Check for literal result element (simplified stylesheet)
|
|
316
|
-
if (root.getAttribute && root.getAttribute("xsl:version")) {
|
|
317
|
-
this.processLiteralResultStylesheet(root);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
throw new Error(
|
|
321
|
-
"Invalid XSLT stylesheet: root element must be xsl:stylesheet or xsl:transform",
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Collect namespaces from root
|
|
326
|
-
this.collectNamespaces(root);
|
|
327
|
-
|
|
328
|
-
// XSLT 1.0: xsl:import elements MUST come first and are processed with lower precedence
|
|
329
|
-
// Process imports first (they have lower precedence than the importing stylesheet)
|
|
330
|
-
const imports = [];
|
|
331
|
-
const otherElements = [];
|
|
332
|
-
|
|
333
|
-
for (const child of root.childNodes) {
|
|
334
|
-
if (child.nodeType !== 1) continue;
|
|
335
|
-
|
|
336
|
-
if (this.isXsltElement(child, "import")) {
|
|
337
|
-
imports.push(child);
|
|
338
|
-
} else {
|
|
339
|
-
otherElements.push(child);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Process imports (lower precedence - process before current stylesheet)
|
|
344
|
-
for (const importNode of imports) {
|
|
345
|
-
this.processImport(importNode, stylesheetUri || this.baseUri);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Process other top-level elements (including includes)
|
|
349
|
-
for (const child of otherElements) {
|
|
350
|
-
if (this.isXsltElement(child, "template")) {
|
|
351
|
-
this.registerTemplate(child);
|
|
352
|
-
} else if (this.isXsltElement(child, "output")) {
|
|
353
|
-
this.processOutput(child);
|
|
354
|
-
} else if (this.isXsltElement(child, "variable")) {
|
|
355
|
-
this.processGlobalVariable(child);
|
|
356
|
-
} else if (this.isXsltElement(child, "param")) {
|
|
357
|
-
this.processGlobalParam(child);
|
|
358
|
-
} else if (this.isXsltElement(child, "key")) {
|
|
359
|
-
this.processKey(child);
|
|
360
|
-
} else if (this.isXsltElement(child, "decimal-format")) {
|
|
361
|
-
this.processDecimalFormat(child);
|
|
362
|
-
} else if (this.isXsltElement(child, "namespace-alias")) {
|
|
363
|
-
this.processNamespaceAlias(child);
|
|
364
|
-
} else if (this.isXsltElement(child, "attribute-set")) {
|
|
365
|
-
this.processAttributeSet(child);
|
|
366
|
-
} else if (this.isXsltElement(child, "strip-space")) {
|
|
367
|
-
this.processStripSpace(child);
|
|
368
|
-
} else if (this.isXsltElement(child, "preserve-space")) {
|
|
369
|
-
this.processPreserveSpace(child);
|
|
370
|
-
} else if (this.isXsltElement(child, "include")) {
|
|
371
|
-
this.processInclude(child, stylesheetUri || this.baseUri);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Increment import precedence after processing this stylesheet
|
|
376
|
-
if (isMainStylesheet) {
|
|
377
|
-
this.currentImportPrecedence++;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Process xsl:include element
|
|
383
|
-
* Includes are merged at the same import precedence level
|
|
384
|
-
*/
|
|
385
|
-
processInclude(node, baseUri) {
|
|
386
|
-
const href = node.getAttribute("href");
|
|
387
|
-
if (!href) {
|
|
388
|
-
throw new Error("xsl:include requires an href attribute");
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const resolvedUri = this.resolveUri(href, baseUri);
|
|
392
|
-
|
|
393
|
-
// Check for circular includes
|
|
394
|
-
if (this.processedStylesheets.has(resolvedUri)) {
|
|
395
|
-
throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
this.processedStylesheets.add(resolvedUri);
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
|
|
402
|
-
|
|
403
|
-
// Parse if string
|
|
404
|
-
let doc = stylesheetDoc;
|
|
405
|
-
if (typeof stylesheetDoc === "string") {
|
|
406
|
-
doc = this.parseXmlString(stylesheetDoc);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Process the included stylesheet at the same import precedence
|
|
410
|
-
const savedPrecedence = this.currentImportPrecedence;
|
|
411
|
-
this.processIncludedStylesheet(doc, resolvedUri);
|
|
412
|
-
this.currentImportPrecedence = savedPrecedence;
|
|
413
|
-
} catch (error) {
|
|
414
|
-
throw new Error(
|
|
415
|
-
`Failed to include stylesheet "${href}": ${error.message}`,
|
|
416
|
-
{ cause: error },
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Process xsl:import element
|
|
423
|
-
* Imports have lower precedence than the importing stylesheet
|
|
424
|
-
*/
|
|
425
|
-
processImport(node, baseUri) {
|
|
426
|
-
const href = node.getAttribute("href");
|
|
427
|
-
if (!href) {
|
|
428
|
-
throw new Error("xsl:import requires an href attribute");
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const resolvedUri = this.resolveUri(href, baseUri);
|
|
432
|
-
|
|
433
|
-
// Check for circular imports
|
|
434
|
-
if (this.processedStylesheets.has(resolvedUri)) {
|
|
435
|
-
throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
this.processedStylesheets.add(resolvedUri);
|
|
439
|
-
|
|
440
|
-
try {
|
|
441
|
-
const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
|
|
442
|
-
|
|
443
|
-
// Parse if string
|
|
444
|
-
let doc = stylesheetDoc;
|
|
445
|
-
if (typeof stylesheetDoc === "string") {
|
|
446
|
-
doc = this.parseXmlString(stylesheetDoc);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Process the imported stylesheet (imports have lower precedence)
|
|
450
|
-
// Don't increment precedence yet - imported templates get current (lower) precedence
|
|
451
|
-
this.processIncludedStylesheet(doc, resolvedUri);
|
|
452
|
-
|
|
453
|
-
// After processing import, increment precedence for next imports and main stylesheet
|
|
454
|
-
this.currentImportPrecedence++;
|
|
455
|
-
} catch (error) {
|
|
456
|
-
throw new Error(
|
|
457
|
-
`Failed to import stylesheet "${href}": ${error.message}`,
|
|
458
|
-
{ cause: error },
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Process an included/imported stylesheet document
|
|
465
|
-
*/
|
|
466
|
-
processIncludedStylesheet(stylesheetDoc, stylesheetUri) {
|
|
467
|
-
const root = stylesheetDoc.documentElement || stylesheetDoc;
|
|
468
|
-
|
|
469
|
-
// Validate stylesheet
|
|
470
|
-
if (
|
|
471
|
-
!this.isXsltElement(root, "stylesheet") &&
|
|
472
|
-
!this.isXsltElement(root, "transform")
|
|
473
|
-
) {
|
|
474
|
-
throw new Error(
|
|
475
|
-
"Included/imported document is not a valid XSLT stylesheet",
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Collect namespaces
|
|
480
|
-
this.collectNamespaces(root);
|
|
481
|
-
|
|
482
|
-
// Process imports first (they have lower precedence)
|
|
483
|
-
const imports = [];
|
|
484
|
-
const otherElements = [];
|
|
485
|
-
|
|
486
|
-
for (const child of root.childNodes) {
|
|
487
|
-
if (child.nodeType !== 1) continue;
|
|
488
|
-
|
|
489
|
-
if (this.isXsltElement(child, "import")) {
|
|
490
|
-
imports.push(child);
|
|
491
|
-
} else {
|
|
492
|
-
otherElements.push(child);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Process nested imports
|
|
497
|
-
for (const importNode of imports) {
|
|
498
|
-
this.processImport(importNode, stylesheetUri);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Process other elements
|
|
502
|
-
for (const child of otherElements) {
|
|
503
|
-
if (this.isXsltElement(child, "template")) {
|
|
504
|
-
this.registerTemplate(child);
|
|
505
|
-
} else if (this.isXsltElement(child, "output")) {
|
|
506
|
-
this.processOutput(child);
|
|
507
|
-
} else if (this.isXsltElement(child, "variable")) {
|
|
508
|
-
this.processGlobalVariable(child);
|
|
509
|
-
} else if (this.isXsltElement(child, "param")) {
|
|
510
|
-
this.processGlobalParam(child);
|
|
511
|
-
} else if (this.isXsltElement(child, "key")) {
|
|
512
|
-
this.processKey(child);
|
|
513
|
-
} else if (this.isXsltElement(child, "decimal-format")) {
|
|
514
|
-
this.processDecimalFormat(child);
|
|
515
|
-
} else if (this.isXsltElement(child, "namespace-alias")) {
|
|
516
|
-
this.processNamespaceAlias(child);
|
|
517
|
-
} else if (this.isXsltElement(child, "attribute-set")) {
|
|
518
|
-
this.processAttributeSet(child);
|
|
519
|
-
} else if (this.isXsltElement(child, "strip-space")) {
|
|
520
|
-
this.processStripSpace(child);
|
|
521
|
-
} else if (this.isXsltElement(child, "preserve-space")) {
|
|
522
|
-
this.processPreserveSpace(child);
|
|
523
|
-
} else if (this.isXsltElement(child, "include")) {
|
|
524
|
-
this.processInclude(child, stylesheetUri);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
processLiteralResultStylesheet(root) {
|
|
530
|
-
// Simplified stylesheet - entire document is one template matching /
|
|
531
|
-
this.templates.push({
|
|
532
|
-
match: "/",
|
|
533
|
-
name: null,
|
|
534
|
-
mode: null,
|
|
535
|
-
priority: 0.5,
|
|
536
|
-
node: root,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
collectNamespaces(node) {
|
|
541
|
-
if (!node.attributes) return;
|
|
542
|
-
|
|
543
|
-
for (const attr of node.attributes) {
|
|
544
|
-
if (attr.name.startsWith("xmlns:")) {
|
|
545
|
-
const prefix = attr.name.substring(6);
|
|
546
|
-
if (attr.value !== XSLT_NS) {
|
|
547
|
-
this.namespaces[prefix] = attr.value;
|
|
548
|
-
}
|
|
549
|
-
} else if (attr.name === "xmlns" && attr.value !== XSLT_NS) {
|
|
550
|
-
this.namespaces[""] = attr.value;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
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
|
-
*/
|
|
564
|
-
registerTemplate(node) {
|
|
565
|
-
const match = node.getAttribute("match");
|
|
566
|
-
const name = node.getAttribute("name");
|
|
567
|
-
const mode = node.getAttribute("mode") || null;
|
|
568
|
-
const priorityAttr = node.getAttribute("priority");
|
|
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
|
-
}
|
|
585
|
-
}
|
|
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
|
-
*/
|
|
593
|
-
calculatePriority(matchPattern) {
|
|
594
|
-
return calculatePriority(matchPattern ? matchPattern.trim() : matchPattern);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
processOutput(node) {
|
|
598
|
-
const method = node.getAttribute("method");
|
|
599
|
-
if (method) this.outputSettings.method = method;
|
|
600
|
-
|
|
601
|
-
const version = node.getAttribute("version");
|
|
602
|
-
if (version) this.outputSettings.version = version;
|
|
603
|
-
|
|
604
|
-
const encoding = node.getAttribute("encoding");
|
|
605
|
-
if (encoding) this.outputSettings.encoding = encoding;
|
|
606
|
-
|
|
607
|
-
const standalone = node.getAttribute("standalone");
|
|
608
|
-
if (standalone) this.outputSettings.standalone = standalone;
|
|
609
|
-
|
|
610
|
-
const indent = node.getAttribute("indent");
|
|
611
|
-
if (indent) this.outputSettings.indent = indent;
|
|
612
|
-
|
|
613
|
-
const omit = node.getAttribute("omit-xml-declaration");
|
|
614
|
-
if (omit) this.outputSettings.omitXmlDeclaration = omit;
|
|
615
|
-
|
|
616
|
-
const doctypePublic = node.getAttribute("doctype-public");
|
|
617
|
-
if (doctypePublic) this.outputSettings.doctypePublic = doctypePublic;
|
|
618
|
-
|
|
619
|
-
const doctypeSystem = node.getAttribute("doctype-system");
|
|
620
|
-
if (doctypeSystem) this.outputSettings.doctypeSystem = doctypeSystem;
|
|
621
|
-
|
|
622
|
-
const mediaType = node.getAttribute("media-type");
|
|
623
|
-
if (mediaType) this.outputSettings.mediaType = mediaType;
|
|
624
|
-
|
|
625
|
-
const cdataElements = node.getAttribute("cdata-section-elements");
|
|
626
|
-
if (cdataElements) {
|
|
627
|
-
this.outputSettings.cdataSectionElements = cdataElements
|
|
628
|
-
.split(/\s+/)
|
|
629
|
-
.filter(Boolean);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
processGlobalVariable(node) {
|
|
634
|
-
const name = node.getAttribute("name");
|
|
635
|
-
const select = node.getAttribute("select");
|
|
636
|
-
|
|
637
|
-
this.globalVariables[name] = { node, select };
|
|
638
|
-
}
|
|
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
|
-
*/
|
|
649
|
-
processGlobalParam(node) {
|
|
650
|
-
const name = node.getAttribute("name");
|
|
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];
|
|
677
|
-
|
|
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
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
processKey(node) {
|
|
717
|
-
const name = node.getAttribute("name");
|
|
718
|
-
const match = node.getAttribute("match");
|
|
719
|
-
const use = node.getAttribute("use");
|
|
720
|
-
|
|
721
|
-
this.keys[name] = { match, use };
|
|
722
|
-
this.keyRegistry.clear();
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
processDecimalFormat(node) {
|
|
726
|
-
const name = node.getAttribute("name") || "";
|
|
727
|
-
|
|
728
|
-
this.decimalFormats[name] = {
|
|
729
|
-
decimalSeparator: node.getAttribute("decimal-separator") || ".",
|
|
730
|
-
groupingSeparator: node.getAttribute("grouping-separator") || ",",
|
|
731
|
-
percent: node.getAttribute("percent") || "%",
|
|
732
|
-
perMille: node.getAttribute("per-mille") || "\u2030",
|
|
733
|
-
zeroDigit: node.getAttribute("zero-digit") || "0",
|
|
734
|
-
digit: node.getAttribute("digit") || "#",
|
|
735
|
-
patternSeparator: node.getAttribute("pattern-separator") || ";",
|
|
736
|
-
infinity: node.getAttribute("infinity") || "Infinity",
|
|
737
|
-
nan: node.getAttribute("NaN") || "NaN",
|
|
738
|
-
minusSign: node.getAttribute("minus-sign") || "-",
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
processNamespaceAlias(node) {
|
|
743
|
-
this.namespaceAliases.add(node);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
processAttributeSet(node) {
|
|
747
|
-
const name = node.getAttribute("name");
|
|
748
|
-
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
749
|
-
|
|
750
|
-
this.attributeSets[name] = {
|
|
751
|
-
node,
|
|
752
|
-
useAttributeSets: useAttributeSets
|
|
753
|
-
? useAttributeSets.split(/\s+/).filter(Boolean)
|
|
754
|
-
: [],
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
processStripSpace(node) {
|
|
759
|
-
const elements = node.getAttribute("elements");
|
|
760
|
-
if (elements) {
|
|
761
|
-
this.stripSpace.push(...elements.split(/\s+/).filter(Boolean));
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
processPreserveSpace(node) {
|
|
766
|
-
const elements = node.getAttribute("elements");
|
|
767
|
-
if (elements) {
|
|
768
|
-
this.preserveSpace.push(...elements.split(/\s+/).filter(Boolean));
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Transform a source document
|
|
774
|
-
*/
|
|
775
|
-
transform(sourceNode, ownerDocument) {
|
|
776
|
-
const doc =
|
|
777
|
-
ownerDocument || (typeof document !== "undefined" ? document : null);
|
|
778
|
-
|
|
779
|
-
if (!doc) {
|
|
780
|
-
throw new Error("No output document available");
|
|
781
|
-
}
|
|
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
|
-
|
|
789
|
-
// Create context - use document node as initial context for "/" template matching
|
|
790
|
-
// XPath paths like "RootElement/child" expect to start from document node
|
|
791
|
-
const context = new XsltContext({
|
|
792
|
-
currentNode: source,
|
|
793
|
-
currentNodeList: [source],
|
|
794
|
-
position: 1,
|
|
795
|
-
outputDocument: resultDocument,
|
|
796
|
-
stylesheet: this.stylesheetDoc,
|
|
797
|
-
namespaces: { ...this.namespaces },
|
|
798
|
-
templates: this.templates,
|
|
799
|
-
keys: this.keys,
|
|
800
|
-
decimalFormats: this.decimalFormats,
|
|
801
|
-
outputMethod: this.outputSettings.method,
|
|
802
|
-
xpathEvaluator: this.xpathEvaluator,
|
|
156
|
+
matchesPattern: (node, pattern, definition) =>
|
|
157
|
+
this.matchesPattern(
|
|
158
|
+
node,
|
|
159
|
+
pattern,
|
|
160
|
+
this.rootContext,
|
|
161
|
+
definition?.namespaces,
|
|
162
|
+
),
|
|
163
|
+
evaluateUse: (node, expression, definition) =>
|
|
164
|
+
this.evaluateKeyValues(node, expression, definition?.namespaces),
|
|
803
165
|
});
|
|
804
166
|
|
|
805
|
-
|
|
167
|
+
// Recoverable errors already reported, so each is reported once
|
|
168
|
+
this.reportedWarnings = new Set();
|
|
806
169
|
|
|
807
|
-
//
|
|
808
|
-
|
|
809
|
-
if (!(name in context.parameters)) {
|
|
810
|
-
context.parameters[name] = this.evaluateVariable(def, context);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
170
|
+
// Extension element implementations by expanded name `{uri}local`
|
|
171
|
+
this.extensionElements = new Map();
|
|
813
172
|
|
|
814
|
-
|
|
815
|
-
context.variables[name] = this.evaluateVariable(def, context);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Create result document fragment
|
|
819
|
-
const fragment = resultDocument.createDocumentFragment();
|
|
820
|
-
|
|
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);
|
|
825
|
-
|
|
826
|
-
return importResultFragment(fragment, doc);
|
|
173
|
+
this.xpathEvaluator.registerFunctions(createXsltFunctions(this));
|
|
827
174
|
}
|
|
828
175
|
|
|
829
176
|
/**
|
|
830
|
-
*
|
|
177
|
+
* Evaluate an XPath expression in an XSLT context.
|
|
831
178
|
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
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
|
|
179
|
+
* @param {string} expr - The expression
|
|
180
|
+
* @param {XsltContext} context - The current context
|
|
181
|
+
* @returns {*} The value
|
|
838
182
|
*/
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
183
|
+
evaluateXPath(expr, context) {
|
|
184
|
+
const ast = parseXPath(expr);
|
|
185
|
+
const xpathContext = new XPathContext(
|
|
186
|
+
context.currentNode,
|
|
187
|
+
context.position,
|
|
188
|
+
context.currentNodeList.length,
|
|
189
|
+
context.xpathVariables,
|
|
190
|
+
context.namespaces,
|
|
191
|
+
context,
|
|
847
192
|
);
|
|
193
|
+
return this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
848
194
|
}
|
|
849
195
|
|
|
850
196
|
/**
|
|
851
|
-
*
|
|
852
|
-
*/
|
|
853
|
-
transformToDocument(sourceNode) {
|
|
854
|
-
// For Node.js environments, we need a document implementation
|
|
855
|
-
const doc = this.createDocument(sourceNode);
|
|
856
|
-
const fragment = this.transform(sourceNode, doc);
|
|
857
|
-
|
|
858
|
-
// Move fragment contents to document
|
|
859
|
-
while (fragment.firstChild) {
|
|
860
|
-
doc.appendChild(fragment.firstChild);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
return doc;
|
|
864
|
-
}
|
|
865
|
-
|
|
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).
|
|
197
|
+
* Whether a node is in the XSLT namespace (or uses the xsl prefix).
|
|
871
198
|
*
|
|
872
|
-
* @param {Node}
|
|
873
|
-
* @returns {
|
|
199
|
+
* @param {Node} node - A stylesheet node
|
|
200
|
+
* @returns {boolean} True for an XSLT element
|
|
874
201
|
*/
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
202
|
+
isXsltNamespace(node) {
|
|
203
|
+
return (
|
|
204
|
+
node.namespaceURI === XSLT_NAMESPACE ||
|
|
205
|
+
(node.nodeName && node.nodeName.startsWith("xsl:"))
|
|
879
206
|
);
|
|
880
|
-
return serializeResult(fragment, this.outputSettings);
|
|
881
207
|
}
|
|
882
208
|
|
|
883
209
|
/**
|
|
884
|
-
*
|
|
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).
|
|
210
|
+
* Whether a node is the XSLT element of a local name.
|
|
889
211
|
*
|
|
890
|
-
* @param {Node}
|
|
891
|
-
* @
|
|
892
|
-
* @
|
|
212
|
+
* @param {Node} node - A stylesheet node
|
|
213
|
+
* @param {string} localName - The XSLT element name, e.g. "template"
|
|
214
|
+
* @returns {boolean} True for that element
|
|
893
215
|
*/
|
|
894
|
-
|
|
895
|
-
if (
|
|
896
|
-
return document.implementation.createDocument(null, null, null);
|
|
897
|
-
}
|
|
898
|
-
|
|
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
|
-
}
|
|
216
|
+
isXsltElement(node, localName) {
|
|
217
|
+
if (node.nodeType !== 1) return false;
|
|
907
218
|
|
|
908
|
-
|
|
219
|
+
const nodeName = node.localName || node.nodeName;
|
|
220
|
+
return (
|
|
221
|
+
(node.namespaceURI === XSLT_NAMESPACE && nodeName === localName) ||
|
|
222
|
+
node.nodeName === `xsl:${localName}`
|
|
223
|
+
);
|
|
909
224
|
}
|
|
910
225
|
|
|
911
226
|
/**
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
* A value supplied from outside (`setParameter`) wins over the `select`
|
|
915
|
-
* expression and over the instantiated content of the declaration.
|
|
227
|
+
* Report a recoverable stylesheet error with console.warn, once per engine
|
|
228
|
+
* and message (libxslt reports these errors and goes on).
|
|
916
229
|
*
|
|
917
|
-
* @param {
|
|
918
|
-
* @
|
|
919
|
-
* @returns {*} The variable value
|
|
230
|
+
* @param {string} message - The error description
|
|
231
|
+
* @returns {void}
|
|
920
232
|
*/
|
|
921
|
-
|
|
922
|
-
if (
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if (def.select) {
|
|
927
|
-
return this.evaluateXPath(def.select, context);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// If no select, evaluate content as result tree fragment
|
|
931
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
932
|
-
this.processChildren(def.node, context, fragment);
|
|
933
|
-
return fragment;
|
|
233
|
+
warnOnce(message) {
|
|
234
|
+
if (this.reportedWarnings.has(message)) return;
|
|
235
|
+
this.reportedWarnings.add(message);
|
|
236
|
+
console.warn(`XSLT: ${message}`);
|
|
934
237
|
}
|
|
238
|
+
}
|
|
935
239
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
this.processTemplate(template.node, newContext, output);
|
|
956
|
-
} else {
|
|
957
|
-
// Built-in templates
|
|
958
|
-
this.applyBuiltinTemplate(node, mode, context, output);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
/**
|
|
964
|
-
* Find the best matching template for a node
|
|
965
|
-
*/
|
|
966
|
-
findMatchingTemplate(node, mode, context, maxImportPrecedence = Infinity) {
|
|
967
|
-
let bestMatch = null;
|
|
968
|
-
let bestPriority = -Infinity;
|
|
969
|
-
let bestImportPrecedence = -Infinity;
|
|
970
|
-
|
|
971
|
-
for (const template of this.templates) {
|
|
972
|
-
if (template.mode !== mode) continue;
|
|
973
|
-
if (!template.match) continue;
|
|
974
|
-
if ((template.importPrecedence || 0) >= maxImportPrecedence) continue;
|
|
975
|
-
|
|
976
|
-
if (this.matchesPattern(node, template.match, context)) {
|
|
977
|
-
const priority = template.priority;
|
|
978
|
-
const importPrecedence = template.importPrecedence || 0;
|
|
979
|
-
|
|
980
|
-
if (
|
|
981
|
-
importPrecedence > bestImportPrecedence ||
|
|
982
|
-
(importPrecedence === bestImportPrecedence && priority > bestPriority)
|
|
983
|
-
) {
|
|
984
|
-
bestMatch = template;
|
|
985
|
-
bestPriority = priority;
|
|
986
|
-
bestImportPrecedence = importPrecedence;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
return bestMatch;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* Check if a node matches an XSLT pattern
|
|
996
|
-
*/
|
|
997
|
-
matchesPattern(node, pattern, context) {
|
|
998
|
-
// Split union patterns
|
|
999
|
-
const patterns = this.splitUnionPattern(pattern);
|
|
1000
|
-
|
|
1001
|
-
for (const p of patterns) {
|
|
1002
|
-
if (this.matchesSinglePattern(node, p.trim(), context)) {
|
|
1003
|
-
return true;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
return false;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
splitUnionPattern(pattern) {
|
|
1011
|
-
// Simple split on | not inside predicates or strings
|
|
1012
|
-
const parts = [];
|
|
1013
|
-
let current = "";
|
|
1014
|
-
let depth = 0;
|
|
1015
|
-
let inString = false;
|
|
1016
|
-
let stringChar = "";
|
|
1017
|
-
|
|
1018
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
1019
|
-
const char = pattern[i];
|
|
1020
|
-
|
|
1021
|
-
if (inString) {
|
|
1022
|
-
current += char;
|
|
1023
|
-
if (char === stringChar) {
|
|
1024
|
-
inString = false;
|
|
1025
|
-
}
|
|
1026
|
-
} else if (char === '"' || char === "'") {
|
|
1027
|
-
inString = true;
|
|
1028
|
-
stringChar = char;
|
|
1029
|
-
current += char;
|
|
1030
|
-
} else if (char === "[") {
|
|
1031
|
-
depth++;
|
|
1032
|
-
current += char;
|
|
1033
|
-
} else if (char === "]") {
|
|
1034
|
-
depth--;
|
|
1035
|
-
current += char;
|
|
1036
|
-
} else if (char === "|" && depth === 0) {
|
|
1037
|
-
parts.push(current);
|
|
1038
|
-
current = "";
|
|
1039
|
-
} else {
|
|
1040
|
-
current += char;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (current) {
|
|
1045
|
-
parts.push(current);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
return parts;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
matchesSinglePattern(node, pattern, context) {
|
|
1052
|
-
try {
|
|
1053
|
-
// Handle root pattern
|
|
1054
|
-
if (pattern === "/") {
|
|
1055
|
-
return (
|
|
1056
|
-
node.nodeType === 9 || node === node.ownerDocument?.documentElement
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Handle patterns like "item" (child match)
|
|
1061
|
-
// Need to check if node would be selected by pattern from parent
|
|
1062
|
-
const ast = parseXPath(pattern);
|
|
1063
|
-
|
|
1064
|
-
// For patterns starting with /, evaluate from root
|
|
1065
|
-
if (pattern.startsWith("/")) {
|
|
1066
|
-
const doc = node.ownerDocument || node;
|
|
1067
|
-
const xpathContext = new XPathContext(
|
|
1068
|
-
doc,
|
|
1069
|
-
1,
|
|
1070
|
-
1,
|
|
1071
|
-
{ ...context.variables, ...context.parameters },
|
|
1072
|
-
context.namespaces,
|
|
1073
|
-
context,
|
|
1074
|
-
);
|
|
1075
|
-
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
1076
|
-
const nodes = Array.isArray(result) ? result : [result];
|
|
1077
|
-
return nodes.includes(node);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
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) {
|
|
1084
|
-
const xpathContext = new XPathContext(
|
|
1085
|
-
parent,
|
|
1086
|
-
1,
|
|
1087
|
-
1,
|
|
1088
|
-
{ ...context.variables, ...context.parameters },
|
|
1089
|
-
context.namespaces,
|
|
1090
|
-
context,
|
|
1091
|
-
);
|
|
1092
|
-
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
1093
|
-
const nodes = Array.isArray(result) ? result : [result];
|
|
1094
|
-
return nodes.includes(node);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// For document node without parent
|
|
1098
|
-
const xpathContext = new XPathContext(
|
|
1099
|
-
node,
|
|
1100
|
-
1,
|
|
1101
|
-
1,
|
|
1102
|
-
{ ...context.variables, ...context.parameters },
|
|
1103
|
-
context.namespaces,
|
|
1104
|
-
context,
|
|
1105
|
-
);
|
|
1106
|
-
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
1107
|
-
const nodes = Array.isArray(result) ? result : [result];
|
|
1108
|
-
return nodes.includes(node);
|
|
1109
|
-
} catch {
|
|
1110
|
-
return false;
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* Apply built-in template rules
|
|
1116
|
-
*/
|
|
1117
|
-
applyBuiltinTemplate(node, mode, context, output) {
|
|
1118
|
-
switch (node.nodeType) {
|
|
1119
|
-
case 1: // Element
|
|
1120
|
-
case 9: // Document
|
|
1121
|
-
case 11: // Document Fragment
|
|
1122
|
-
// Process children
|
|
1123
|
-
this.applyTemplates(Array.from(node.childNodes), mode, context, output);
|
|
1124
|
-
break;
|
|
1125
|
-
|
|
1126
|
-
case 3: // Text
|
|
1127
|
-
case 4: {
|
|
1128
|
-
// CDATA
|
|
1129
|
-
// Copy text value
|
|
1130
|
-
const text = context.outputDocument.createTextNode(
|
|
1131
|
-
node.nodeValue || "",
|
|
1132
|
-
);
|
|
1133
|
-
output.appendChild(text);
|
|
1134
|
-
break;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
case 2: {
|
|
1138
|
-
// Attribute
|
|
1139
|
-
// Copy attribute value as text
|
|
1140
|
-
const attrText = context.outputDocument.createTextNode(
|
|
1141
|
-
node.nodeValue || "",
|
|
1142
|
-
);
|
|
1143
|
-
output.appendChild(attrText);
|
|
1144
|
-
break;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// Comments and PIs have no built-in template
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* Process template content
|
|
1153
|
-
*/
|
|
1154
|
-
processTemplate(templateNode, context, output) {
|
|
1155
|
-
// Process template parameters first
|
|
1156
|
-
const localContext = context.clone();
|
|
1157
|
-
|
|
1158
|
-
for (const child of templateNode.childNodes) {
|
|
1159
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "param")) {
|
|
1160
|
-
const name = child.getAttribute("name");
|
|
1161
|
-
if (!(name in localContext.parameters)) {
|
|
1162
|
-
localContext.parameters[name] = this.evaluateVariable(
|
|
1163
|
-
{ node: child, select: child.getAttribute("select") },
|
|
1164
|
-
localContext,
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
this.processChildren(templateNode, localContext, output);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
/**
|
|
1174
|
-
* Process child nodes of an XSLT element
|
|
1175
|
-
*/
|
|
1176
|
-
processChildren(node, context, output) {
|
|
1177
|
-
for (const child of node.childNodes) {
|
|
1178
|
-
this.processNode(child, context, output);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
/**
|
|
1183
|
-
* Process a single node in the stylesheet
|
|
1184
|
-
*/
|
|
1185
|
-
processNode(node, context, output) {
|
|
1186
|
-
switch (node.nodeType) {
|
|
1187
|
-
case 1: // Element
|
|
1188
|
-
this.processElement(node, context, output);
|
|
1189
|
-
break;
|
|
1190
|
-
|
|
1191
|
-
case 3: // Text
|
|
1192
|
-
case 4: {
|
|
1193
|
-
// CDATA
|
|
1194
|
-
// Output text if not whitespace only (or if preserving space)
|
|
1195
|
-
const text = node.nodeValue;
|
|
1196
|
-
if (text && (text.trim() || this.shouldPreserveSpace(node))) {
|
|
1197
|
-
const textNode = context.outputDocument.createTextNode(text);
|
|
1198
|
-
output.appendChild(textNode);
|
|
1199
|
-
}
|
|
1200
|
-
break;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
shouldPreserveSpace(node) {
|
|
1206
|
-
// Check xml:space attribute on ancestors
|
|
1207
|
-
let current = node.parentNode;
|
|
1208
|
-
while (current && current.nodeType === 1) {
|
|
1209
|
-
const space = current.getAttribute("xml:space");
|
|
1210
|
-
if (space === "preserve") return true;
|
|
1211
|
-
if (space === "default") return false;
|
|
1212
|
-
current = current.parentNode;
|
|
1213
|
-
}
|
|
1214
|
-
return false;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
/**
|
|
1218
|
-
* Process an element in the stylesheet
|
|
1219
|
-
*/
|
|
1220
|
-
processElement(node, context, output) {
|
|
1221
|
-
// Check if XSLT element
|
|
1222
|
-
if (this.isXsltNamespace(node)) {
|
|
1223
|
-
this.processXsltElement(node, context, output);
|
|
1224
|
-
} else {
|
|
1225
|
-
// Literal result element
|
|
1226
|
-
this.processLiteralResultElement(node, context, output);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
/**
|
|
1231
|
-
* Process an XSLT instruction element
|
|
1232
|
-
*/
|
|
1233
|
-
processXsltElement(node, context, output) {
|
|
1234
|
-
const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
|
|
1235
|
-
|
|
1236
|
-
switch (localName) {
|
|
1237
|
-
case "apply-templates":
|
|
1238
|
-
this.xslApplyTemplates(node, context, output);
|
|
1239
|
-
break;
|
|
1240
|
-
|
|
1241
|
-
case "apply-imports":
|
|
1242
|
-
this.xslApplyImports(node, context, output);
|
|
1243
|
-
break;
|
|
1244
|
-
|
|
1245
|
-
case "call-template":
|
|
1246
|
-
this.xslCallTemplate(node, context, output);
|
|
1247
|
-
break;
|
|
1248
|
-
|
|
1249
|
-
case "value-of":
|
|
1250
|
-
this.xslValueOf(node, context, output);
|
|
1251
|
-
break;
|
|
1252
|
-
|
|
1253
|
-
case "text":
|
|
1254
|
-
this.xslText(node, context, output);
|
|
1255
|
-
break;
|
|
1256
|
-
|
|
1257
|
-
case "element":
|
|
1258
|
-
this.xslElement(node, context, output);
|
|
1259
|
-
break;
|
|
1260
|
-
|
|
1261
|
-
case "attribute":
|
|
1262
|
-
this.xslAttribute(node, context, output);
|
|
1263
|
-
break;
|
|
1264
|
-
|
|
1265
|
-
case "if":
|
|
1266
|
-
this.xslIf(node, context, output);
|
|
1267
|
-
break;
|
|
1268
|
-
|
|
1269
|
-
case "choose":
|
|
1270
|
-
this.xslChoose(node, context, output);
|
|
1271
|
-
break;
|
|
1272
|
-
|
|
1273
|
-
case "for-each":
|
|
1274
|
-
this.xslForEach(node, context, output);
|
|
1275
|
-
break;
|
|
1276
|
-
|
|
1277
|
-
case "copy":
|
|
1278
|
-
this.xslCopy(node, context, output);
|
|
1279
|
-
break;
|
|
1280
|
-
|
|
1281
|
-
case "copy-of":
|
|
1282
|
-
this.xslCopyOf(node, context, output);
|
|
1283
|
-
break;
|
|
1284
|
-
|
|
1285
|
-
case "variable":
|
|
1286
|
-
this.xslVariable(node, context, output);
|
|
1287
|
-
break;
|
|
1288
|
-
|
|
1289
|
-
case "param":
|
|
1290
|
-
// Params are processed at template start
|
|
1291
|
-
break;
|
|
1292
|
-
|
|
1293
|
-
case "comment":
|
|
1294
|
-
this.xslComment(node, context, output);
|
|
1295
|
-
break;
|
|
1296
|
-
|
|
1297
|
-
case "processing-instruction":
|
|
1298
|
-
this.xslProcessingInstruction(node, context, output);
|
|
1299
|
-
break;
|
|
1300
|
-
|
|
1301
|
-
case "number":
|
|
1302
|
-
this.xslNumber(node, context, output);
|
|
1303
|
-
break;
|
|
1304
|
-
|
|
1305
|
-
case "sort":
|
|
1306
|
-
// Handled by apply-templates and for-each
|
|
1307
|
-
break;
|
|
1308
|
-
|
|
1309
|
-
case "with-param":
|
|
1310
|
-
// Handled by call-template and apply-templates
|
|
1311
|
-
break;
|
|
1312
|
-
|
|
1313
|
-
case "message":
|
|
1314
|
-
this.xslMessage(node, context, output);
|
|
1315
|
-
break;
|
|
1316
|
-
|
|
1317
|
-
case "fallback":
|
|
1318
|
-
// Used for forward compatibility
|
|
1319
|
-
break;
|
|
1320
|
-
|
|
1321
|
-
default:
|
|
1322
|
-
console.warn(`Unknown XSLT element: ${localName}`);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
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}
|
|
1337
|
-
*/
|
|
1338
|
-
processLiteralResultElement(node, context, output) {
|
|
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);
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
if (node.attributes) {
|
|
1360
|
-
for (const attr of node.attributes) {
|
|
1361
|
-
if (!shouldCopyAttribute(attr, XSLT_NS)) continue;
|
|
1362
|
-
|
|
1363
|
-
const value = this.processAttributeValueTemplate(attr.value, context);
|
|
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
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Process children
|
|
1382
|
-
this.processChildren(node, context, outputElement);
|
|
1383
|
-
|
|
1384
|
-
output.appendChild(outputElement);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
/**
|
|
1388
|
-
* Process attribute value templates (expressions in curly braces)
|
|
1389
|
-
*/
|
|
1390
|
-
processAttributeValueTemplate(value, context) {
|
|
1391
|
-
if (!value.includes("{")) return value;
|
|
1392
|
-
|
|
1393
|
-
let result = "";
|
|
1394
|
-
let i = 0;
|
|
1395
|
-
|
|
1396
|
-
while (i < value.length) {
|
|
1397
|
-
if (value[i] === "{") {
|
|
1398
|
-
if (value[i + 1] === "{") {
|
|
1399
|
-
// Escaped brace
|
|
1400
|
-
result += "{";
|
|
1401
|
-
i += 2;
|
|
1402
|
-
} else {
|
|
1403
|
-
// Find closing brace
|
|
1404
|
-
let depth = 1;
|
|
1405
|
-
let j = i + 1;
|
|
1406
|
-
while (j < value.length && depth > 0) {
|
|
1407
|
-
if (value[j] === "{") depth++;
|
|
1408
|
-
else if (value[j] === "}") depth--;
|
|
1409
|
-
j++;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
const expr = value.substring(i + 1, j - 1);
|
|
1413
|
-
const evalResult = this.evaluateXPath(expr, context);
|
|
1414
|
-
result += this.xpathEvaluator.toString(evalResult);
|
|
1415
|
-
i = j;
|
|
1416
|
-
}
|
|
1417
|
-
} else if (value[i] === "}") {
|
|
1418
|
-
if (value[i + 1] === "}") {
|
|
1419
|
-
// Escaped brace
|
|
1420
|
-
result += "}";
|
|
1421
|
-
i += 2;
|
|
1422
|
-
} else {
|
|
1423
|
-
throw new Error("Unmatched } in attribute value template");
|
|
1424
|
-
}
|
|
1425
|
-
} else {
|
|
1426
|
-
result += value[i];
|
|
1427
|
-
i++;
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
return result;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// XSLT Instructions
|
|
1435
|
-
xslApplyTemplates(node, context, output) {
|
|
1436
|
-
const select = node.getAttribute("select") || "node()";
|
|
1437
|
-
const mode = node.getAttribute("mode") || null;
|
|
1438
|
-
|
|
1439
|
-
// Evaluate select expression
|
|
1440
|
-
let nodes = this.evaluateXPath(select, context);
|
|
1441
|
-
if (!Array.isArray(nodes)) {
|
|
1442
|
-
nodes = nodes ? [nodes] : [];
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// Collect sort specifications
|
|
1446
|
-
const sortSpecs = [];
|
|
1447
|
-
for (const child of node.childNodes) {
|
|
1448
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
1449
|
-
sortSpecs.push({
|
|
1450
|
-
select: child.getAttribute("select") || ".",
|
|
1451
|
-
order: child.getAttribute("order") || "ascending",
|
|
1452
|
-
dataType: child.getAttribute("data-type") || "text",
|
|
1453
|
-
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
1454
|
-
lang: child.getAttribute("lang"),
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
// Apply sorting
|
|
1460
|
-
if (sortSpecs.length > 0) {
|
|
1461
|
-
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// Collect with-param values
|
|
1465
|
-
const params = {};
|
|
1466
|
-
for (const child of node.childNodes) {
|
|
1467
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
1468
|
-
const name = child.getAttribute("name");
|
|
1469
|
-
const selectAttr = child.getAttribute("select");
|
|
1470
|
-
if (selectAttr) {
|
|
1471
|
-
params[name] = this.evaluateXPath(selectAttr, context);
|
|
1472
|
-
} else {
|
|
1473
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1474
|
-
this.processChildren(child, context, fragment);
|
|
1475
|
-
params[name] = fragment;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
// Apply templates with new context including params
|
|
1481
|
-
const newContext = context.clone({
|
|
1482
|
-
parameters: { ...context.parameters, ...params },
|
|
1483
|
-
});
|
|
1484
|
-
this.applyTemplates(nodes, mode, newContext, output);
|
|
1485
|
-
}
|
|
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
|
-
|
|
1525
|
-
xslCallTemplate(node, context, output) {
|
|
1526
|
-
const name = node.getAttribute("name");
|
|
1527
|
-
|
|
1528
|
-
// Find named template
|
|
1529
|
-
const template = this.templates.find((t) => t.name === name);
|
|
1530
|
-
if (!template) {
|
|
1531
|
-
throw new Error(`Template not found: ${name}`);
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// Collect with-param values
|
|
1535
|
-
const params = {};
|
|
1536
|
-
for (const child of node.childNodes) {
|
|
1537
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
1538
|
-
const paramName = child.getAttribute("name");
|
|
1539
|
-
const selectAttr = child.getAttribute("select");
|
|
1540
|
-
if (selectAttr) {
|
|
1541
|
-
params[paramName] = this.evaluateXPath(selectAttr, context);
|
|
1542
|
-
} else {
|
|
1543
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1544
|
-
this.processChildren(child, context, fragment);
|
|
1545
|
-
params[paramName] = fragment;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
// Call template with params
|
|
1551
|
-
const newContext = context.clone({
|
|
1552
|
-
parameters: { ...context.parameters, ...params },
|
|
1553
|
-
});
|
|
1554
|
-
this.processTemplate(template.node, newContext, output);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
xslValueOf(node, context, output) {
|
|
1558
|
-
const select = node.getAttribute("select");
|
|
1559
|
-
const disableOutputEscaping =
|
|
1560
|
-
node.getAttribute("disable-output-escaping") === "yes";
|
|
1561
|
-
|
|
1562
|
-
const result = this.evaluateXPath(select, context);
|
|
1563
|
-
const text = this.xpathEvaluator.toString(result);
|
|
1564
|
-
|
|
1565
|
-
if (text) {
|
|
1566
|
-
const textNode = context.outputDocument.createTextNode(text);
|
|
1567
|
-
if (disableOutputEscaping) {
|
|
1568
|
-
textNode._disableOutputEscaping = true;
|
|
1569
|
-
}
|
|
1570
|
-
output.appendChild(textNode);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
xslText(node, context, output) {
|
|
1575
|
-
const disableOutputEscaping =
|
|
1576
|
-
node.getAttribute("disable-output-escaping") === "yes";
|
|
1577
|
-
let text = "";
|
|
1578
|
-
|
|
1579
|
-
for (const child of node.childNodes) {
|
|
1580
|
-
if (child.nodeType === 3 || child.nodeType === 4) {
|
|
1581
|
-
text += child.nodeValue || "";
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
if (text) {
|
|
1586
|
-
const textNode = context.outputDocument.createTextNode(text);
|
|
1587
|
-
if (disableOutputEscaping) {
|
|
1588
|
-
textNode._disableOutputEscaping = true;
|
|
1589
|
-
}
|
|
1590
|
-
output.appendChild(textNode);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
xslElement(node, context, output) {
|
|
1595
|
-
const name = this.processAttributeValueTemplate(
|
|
1596
|
-
node.getAttribute("name"),
|
|
1597
|
-
context,
|
|
1598
|
-
);
|
|
1599
|
-
const namespace = node.getAttribute("namespace");
|
|
1600
|
-
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
1601
|
-
|
|
1602
|
-
let element;
|
|
1603
|
-
if (namespace) {
|
|
1604
|
-
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
1605
|
-
element = context.outputDocument.createElementNS(ns, name);
|
|
1606
|
-
} else {
|
|
1607
|
-
element = context.outputDocument.createElement(name);
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
// Apply attribute sets
|
|
1611
|
-
if (useAttributeSets) {
|
|
1612
|
-
this.applyAttributeSets(useAttributeSets, context, element);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
this.processChildren(node, context, element);
|
|
1616
|
-
output.appendChild(element);
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
xslAttribute(node, context, output) {
|
|
1620
|
-
const name = this.processAttributeValueTemplate(
|
|
1621
|
-
node.getAttribute("name"),
|
|
1622
|
-
context,
|
|
1623
|
-
);
|
|
1624
|
-
const namespace = node.getAttribute("namespace");
|
|
1625
|
-
|
|
1626
|
-
// Collect content
|
|
1627
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1628
|
-
this.processChildren(node, context, fragment);
|
|
1629
|
-
|
|
1630
|
-
// Get text content
|
|
1631
|
-
let value = "";
|
|
1632
|
-
const getText = (n) => {
|
|
1633
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1634
|
-
value += n.nodeValue || "";
|
|
1635
|
-
} else if (n.childNodes) {
|
|
1636
|
-
for (const child of n.childNodes) {
|
|
1637
|
-
getText(child);
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
};
|
|
1641
|
-
getText(fragment);
|
|
1642
|
-
|
|
1643
|
-
// Add attribute to parent element
|
|
1644
|
-
if (output.nodeType === 1) {
|
|
1645
|
-
if (namespace) {
|
|
1646
|
-
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
1647
|
-
output.setAttributeNS(ns, name, value);
|
|
1648
|
-
} else {
|
|
1649
|
-
output.setAttribute(name, value);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
xslIf(node, context, output) {
|
|
1655
|
-
const test = node.getAttribute("test");
|
|
1656
|
-
const result = this.evaluateXPath(test, context);
|
|
1657
|
-
|
|
1658
|
-
if (this.xpathEvaluator.toBoolean(result)) {
|
|
1659
|
-
this.processChildren(node, context, output);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
xslChoose(node, context, output) {
|
|
1664
|
-
for (const child of node.childNodes) {
|
|
1665
|
-
if (child.nodeType !== 1) continue;
|
|
1666
|
-
|
|
1667
|
-
if (this.isXsltElement(child, "when")) {
|
|
1668
|
-
const test = child.getAttribute("test");
|
|
1669
|
-
const result = this.evaluateXPath(test, context);
|
|
1670
|
-
|
|
1671
|
-
if (this.xpathEvaluator.toBoolean(result)) {
|
|
1672
|
-
this.processChildren(child, context, output);
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
} else if (this.isXsltElement(child, "otherwise")) {
|
|
1676
|
-
this.processChildren(child, context, output);
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
xslForEach(node, context, output) {
|
|
1683
|
-
const select = node.getAttribute("select");
|
|
1684
|
-
|
|
1685
|
-
let nodes = this.evaluateXPath(select, context);
|
|
1686
|
-
if (!Array.isArray(nodes)) {
|
|
1687
|
-
nodes = nodes ? [nodes] : [];
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
// Collect sort specifications
|
|
1691
|
-
const sortSpecs = [];
|
|
1692
|
-
for (const child of node.childNodes) {
|
|
1693
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
1694
|
-
sortSpecs.push({
|
|
1695
|
-
select: child.getAttribute("select") || ".",
|
|
1696
|
-
order: child.getAttribute("order") || "ascending",
|
|
1697
|
-
dataType: child.getAttribute("data-type") || "text",
|
|
1698
|
-
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
1699
|
-
lang: child.getAttribute("lang"),
|
|
1700
|
-
});
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// Apply sorting
|
|
1705
|
-
if (sortSpecs.length > 0) {
|
|
1706
|
-
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
// Process each node
|
|
1710
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1711
|
-
const newContext = context.clone({
|
|
1712
|
-
currentNode: nodes[i],
|
|
1713
|
-
currentNodeList: nodes,
|
|
1714
|
-
position: i + 1,
|
|
1715
|
-
});
|
|
1716
|
-
|
|
1717
|
-
this.processChildren(node, newContext, output);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
xslCopy(node, context, output) {
|
|
1722
|
-
const currentNode = context.currentNode;
|
|
1723
|
-
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
1724
|
-
|
|
1725
|
-
switch (currentNode.nodeType) {
|
|
1726
|
-
case 1: {
|
|
1727
|
-
// Element
|
|
1728
|
-
let copy;
|
|
1729
|
-
if (currentNode.namespaceURI) {
|
|
1730
|
-
copy = context.outputDocument.createElementNS(
|
|
1731
|
-
currentNode.namespaceURI,
|
|
1732
|
-
currentNode.nodeName,
|
|
1733
|
-
);
|
|
1734
|
-
} else {
|
|
1735
|
-
copy = context.outputDocument.createElement(currentNode.nodeName);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
if (useAttributeSets) {
|
|
1739
|
-
this.applyAttributeSets(useAttributeSets, context, copy);
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
this.processChildren(node, context, copy);
|
|
1743
|
-
output.appendChild(copy);
|
|
1744
|
-
break;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
case 2: // Attribute
|
|
1748
|
-
if (output.nodeType === 1) {
|
|
1749
|
-
output.setAttribute(currentNode.name, currentNode.value);
|
|
1750
|
-
}
|
|
1751
|
-
break;
|
|
1752
|
-
|
|
1753
|
-
case 3: // Text
|
|
1754
|
-
case 4: {
|
|
1755
|
-
// CDATA
|
|
1756
|
-
const textCopy = context.outputDocument.createTextNode(
|
|
1757
|
-
currentNode.nodeValue || "",
|
|
1758
|
-
);
|
|
1759
|
-
output.appendChild(textCopy);
|
|
1760
|
-
break;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
case 7: {
|
|
1764
|
-
// Processing Instruction
|
|
1765
|
-
const piCopy = context.outputDocument.createProcessingInstruction(
|
|
1766
|
-
currentNode.target,
|
|
1767
|
-
currentNode.data,
|
|
1768
|
-
);
|
|
1769
|
-
output.appendChild(piCopy);
|
|
1770
|
-
break;
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
case 8: {
|
|
1774
|
-
// Comment
|
|
1775
|
-
const commentCopy = context.outputDocument.createComment(
|
|
1776
|
-
currentNode.nodeValue || "",
|
|
1777
|
-
);
|
|
1778
|
-
output.appendChild(commentCopy);
|
|
1779
|
-
break;
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
case 9: // Document
|
|
1783
|
-
case 11: // Document Fragment
|
|
1784
|
-
this.processChildren(node, context, output);
|
|
1785
|
-
break;
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
xslCopyOf(node, context, output) {
|
|
1790
|
-
const select = node.getAttribute("select");
|
|
1791
|
-
const result = this.evaluateXPath(select, context);
|
|
1792
|
-
|
|
1793
|
-
this.copyToOutput(result, context, output);
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
copyToOutput(value, context, output) {
|
|
1797
|
-
if (Array.isArray(value)) {
|
|
1798
|
-
for (const item of value) {
|
|
1799
|
-
this.copyToOutput(item, context, output);
|
|
1800
|
-
}
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
if (value && value.nodeType) {
|
|
1805
|
-
// Deep copy node
|
|
1806
|
-
const clone = this.deepCloneNode(value, context.outputDocument);
|
|
1807
|
-
output.appendChild(clone);
|
|
1808
|
-
} else if (
|
|
1809
|
-
typeof value === "string" ||
|
|
1810
|
-
typeof value === "number" ||
|
|
1811
|
-
typeof value === "boolean"
|
|
1812
|
-
) {
|
|
1813
|
-
const text = context.outputDocument.createTextNode(String(value));
|
|
1814
|
-
output.appendChild(text);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
deepCloneNode(node, targetDoc) {
|
|
1819
|
-
switch (node.nodeType) {
|
|
1820
|
-
case 1: {
|
|
1821
|
-
// Element
|
|
1822
|
-
let clone;
|
|
1823
|
-
if (node.namespaceURI && targetDoc.createElementNS) {
|
|
1824
|
-
clone = targetDoc.createElementNS(node.namespaceURI, node.nodeName);
|
|
1825
|
-
} else {
|
|
1826
|
-
clone = targetDoc.createElement(node.nodeName);
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
if (node.attributes) {
|
|
1830
|
-
for (const attr of node.attributes) {
|
|
1831
|
-
clone.setAttribute(attr.name, attr.value);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
for (const child of node.childNodes) {
|
|
1836
|
-
clone.appendChild(this.deepCloneNode(child, targetDoc));
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
return clone;
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
case 3: // Text
|
|
1843
|
-
case 4: // CDATA
|
|
1844
|
-
return targetDoc.createTextNode(node.nodeValue || "");
|
|
1845
|
-
|
|
1846
|
-
case 7: // Processing Instruction
|
|
1847
|
-
return targetDoc.createProcessingInstruction(node.target, node.data);
|
|
1848
|
-
|
|
1849
|
-
case 8: // Comment
|
|
1850
|
-
return targetDoc.createComment(node.nodeValue || "");
|
|
1851
|
-
|
|
1852
|
-
case 11: {
|
|
1853
|
-
// Document Fragment
|
|
1854
|
-
const frag = targetDoc.createDocumentFragment();
|
|
1855
|
-
for (const child of node.childNodes) {
|
|
1856
|
-
frag.appendChild(this.deepCloneNode(child, targetDoc));
|
|
1857
|
-
}
|
|
1858
|
-
return frag;
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
default:
|
|
1862
|
-
return targetDoc.createTextNode("");
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
xslVariable(node, context, _output) {
|
|
1867
|
-
const name = node.getAttribute("name");
|
|
1868
|
-
const select = node.getAttribute("select");
|
|
1869
|
-
|
|
1870
|
-
let value;
|
|
1871
|
-
if (select) {
|
|
1872
|
-
value = this.evaluateXPath(select, context);
|
|
1873
|
-
} else {
|
|
1874
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1875
|
-
this.processChildren(node, context, fragment);
|
|
1876
|
-
value = fragment;
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
context.setVariable(name, value);
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
xslComment(node, context, output) {
|
|
1883
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1884
|
-
this.processChildren(node, context, fragment);
|
|
1885
|
-
|
|
1886
|
-
let text = "";
|
|
1887
|
-
const getText = (n) => {
|
|
1888
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1889
|
-
text += n.nodeValue || "";
|
|
1890
|
-
} else if (n.childNodes) {
|
|
1891
|
-
for (const child of n.childNodes) {
|
|
1892
|
-
getText(child);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
};
|
|
1896
|
-
getText(fragment);
|
|
1897
|
-
|
|
1898
|
-
const comment = context.outputDocument.createComment(text);
|
|
1899
|
-
output.appendChild(comment);
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
xslProcessingInstruction(node, context, output) {
|
|
1903
|
-
const name = this.processAttributeValueTemplate(
|
|
1904
|
-
node.getAttribute("name"),
|
|
1905
|
-
context,
|
|
1906
|
-
);
|
|
1907
|
-
|
|
1908
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1909
|
-
this.processChildren(node, context, fragment);
|
|
1910
|
-
|
|
1911
|
-
let data = "";
|
|
1912
|
-
const getText = (n) => {
|
|
1913
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1914
|
-
data += n.nodeValue || "";
|
|
1915
|
-
} else if (n.childNodes) {
|
|
1916
|
-
for (const child of n.childNodes) {
|
|
1917
|
-
getText(child);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
};
|
|
1921
|
-
getText(fragment);
|
|
1922
|
-
|
|
1923
|
-
const pi = context.outputDocument.createProcessingInstruction(name, data);
|
|
1924
|
-
output.appendChild(pi);
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
xslNumber(node, context, output) {
|
|
1928
|
-
const value = node.getAttribute("value");
|
|
1929
|
-
const format = node.getAttribute("format") || "1";
|
|
1930
|
-
|
|
1931
|
-
let numbers;
|
|
1932
|
-
if (value) {
|
|
1933
|
-
numbers = [
|
|
1934
|
-
Math.round(
|
|
1935
|
-
this.xpathEvaluator.toNumber(this.evaluateXPath(value, context)),
|
|
1936
|
-
),
|
|
1937
|
-
];
|
|
1938
|
-
} else {
|
|
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
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
const text = context.outputDocument.createTextNode(
|
|
1952
|
-
formatXsltNumber(numbers, format),
|
|
1953
|
-
);
|
|
1954
|
-
output.appendChild(text);
|
|
1955
|
-
}
|
|
1956
|
-
|
|
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
|
-
*/
|
|
1964
|
-
formatNumber(number, format) {
|
|
1965
|
-
return formatXsltNumber([number], format);
|
|
1966
|
-
}
|
|
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
|
-
*/
|
|
1974
|
-
toRoman(num) {
|
|
1975
|
-
return toRoman(num);
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
xslMessage(node, context, _output) {
|
|
1979
|
-
const terminate = node.getAttribute("terminate") === "yes";
|
|
1980
|
-
|
|
1981
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1982
|
-
this.processChildren(node, context, fragment);
|
|
1983
|
-
|
|
1984
|
-
let text = "";
|
|
1985
|
-
const getText = (n) => {
|
|
1986
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1987
|
-
text += n.nodeValue || "";
|
|
1988
|
-
} else if (n.childNodes) {
|
|
1989
|
-
for (const child of n.childNodes) {
|
|
1990
|
-
getText(child);
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
};
|
|
1994
|
-
getText(fragment);
|
|
1995
|
-
|
|
1996
|
-
console.log("XSLT Message:", text);
|
|
1997
|
-
|
|
1998
|
-
if (terminate) {
|
|
1999
|
-
throw new Error(`XSLT terminated: ${text}`);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
applyAttributeSets(names, context, element) {
|
|
2004
|
-
const setNames = names.split(/\s+/).filter(Boolean);
|
|
2005
|
-
|
|
2006
|
-
for (const name of setNames) {
|
|
2007
|
-
const attrSet = this.attributeSets[name];
|
|
2008
|
-
if (attrSet) {
|
|
2009
|
-
// Apply inherited sets first
|
|
2010
|
-
if (attrSet.useAttributeSets.length > 0) {
|
|
2011
|
-
this.applyAttributeSets(
|
|
2012
|
-
attrSet.useAttributeSets.join(" "),
|
|
2013
|
-
context,
|
|
2014
|
-
element,
|
|
2015
|
-
);
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// Apply attributes from this set
|
|
2019
|
-
for (const child of attrSet.node.childNodes) {
|
|
2020
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "attribute")) {
|
|
2021
|
-
this.xslAttribute(child, context, element);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
sortNodes(nodes, sortSpecs, context) {
|
|
2029
|
-
return [...nodes].sort((a, b) => {
|
|
2030
|
-
for (const spec of sortSpecs) {
|
|
2031
|
-
const contextA = context.clone({ currentNode: a });
|
|
2032
|
-
const contextB = context.clone({ currentNode: b });
|
|
2033
|
-
|
|
2034
|
-
let valueA = this.evaluateXPath(spec.select, contextA);
|
|
2035
|
-
let valueB = this.evaluateXPath(spec.select, contextB);
|
|
2036
|
-
|
|
2037
|
-
// Convert to string for comparison (ensure non-null values)
|
|
2038
|
-
valueA = this.xpathEvaluator.toString(valueA) || "";
|
|
2039
|
-
valueB = this.xpathEvaluator.toString(valueB) || "";
|
|
2040
|
-
|
|
2041
|
-
if (spec.dataType === "number") {
|
|
2042
|
-
valueA = parseFloat(valueA) || 0;
|
|
2043
|
-
valueB = parseFloat(valueB) || 0;
|
|
2044
|
-
} else {
|
|
2045
|
-
// Text comparison
|
|
2046
|
-
if (spec.caseOrder === "lower-first") {
|
|
2047
|
-
valueA = valueA.toLowerCase();
|
|
2048
|
-
valueB = valueB.toLowerCase();
|
|
2049
|
-
} else {
|
|
2050
|
-
valueA = valueA.toUpperCase();
|
|
2051
|
-
valueB = valueB.toUpperCase();
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
let cmp;
|
|
2056
|
-
if (typeof valueA === "number") {
|
|
2057
|
-
cmp = valueA - valueB;
|
|
2058
|
-
} else {
|
|
2059
|
-
cmp = valueA.localeCompare(valueB, spec.lang || undefined);
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
if (spec.order === "descending") {
|
|
2063
|
-
cmp = -cmp;
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
if (cmp !== 0) return cmp;
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
return 0;
|
|
2070
|
-
});
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
evaluateXPath(expr, context) {
|
|
2074
|
-
const ast = parseXPath(expr);
|
|
2075
|
-
const xpathContext = new XPathContext(
|
|
2076
|
-
context.currentNode,
|
|
2077
|
-
context.position,
|
|
2078
|
-
context.currentNodeList.length,
|
|
2079
|
-
{ ...context.variables, ...context.parameters },
|
|
2080
|
-
context.namespaces,
|
|
2081
|
-
context,
|
|
2082
|
-
);
|
|
2083
|
-
return this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
isXsltNamespace(node) {
|
|
2087
|
-
return (
|
|
2088
|
-
node.namespaceURI === XSLT_NS ||
|
|
2089
|
-
(node.nodeName && node.nodeName.startsWith("xsl:"))
|
|
2090
|
-
);
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
isXsltElement(node, localName) {
|
|
2094
|
-
if (node.nodeType !== 1) return false;
|
|
2095
|
-
|
|
2096
|
-
const nodeName = node.localName || node.nodeName;
|
|
2097
|
-
return (
|
|
2098
|
-
(node.namespaceURI === XSLT_NS && nodeName === localName) ||
|
|
2099
|
-
node.nodeName === `xsl:${localName}`
|
|
2100
|
-
);
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
240
|
+
installMethods(
|
|
241
|
+
XsltEngine.prototype,
|
|
242
|
+
stylesheetLoadingMethods,
|
|
243
|
+
topLevelMethods,
|
|
244
|
+
declarationMethods,
|
|
245
|
+
outputDeclarationMethods,
|
|
246
|
+
templateRuleMethods,
|
|
247
|
+
templateInvocationMethods,
|
|
248
|
+
sequenceConstructorMethods,
|
|
249
|
+
workStackMethods,
|
|
250
|
+
controlFlowMethods,
|
|
251
|
+
bindingMethods,
|
|
252
|
+
textInstructionMethods,
|
|
253
|
+
numberingMethods,
|
|
254
|
+
nodeConstructionMethods,
|
|
255
|
+
copyInstructionMethods,
|
|
256
|
+
transformationMethods,
|
|
257
|
+
functionSupportMethods,
|
|
258
|
+
);
|