@tradik/xslt-processor 1.0.3 → 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 +110 -520
- 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 +131 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +206 -0
- package/bin/xslt.js +73 -168
- package/dist/xslt-processor.browser.js +9564 -1585
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +9572 -1586
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +658 -0
- package/dist/xslt-processor.d.ts +459 -12
- package/dist/xslt-processor.js +9546 -1582
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +71 -20
- package/src/XSLTProcessor.js +494 -48
- 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 +26 -8
- 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 +518 -357
- 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 +57 -0
- 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 +184 -1736
- 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 +233 -0
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +270 -0
- package/src/xslt/index.js +38 -1
- package/src/xslt/keys.js +164 -0
- package/src/xslt/literalResult.js +223 -0
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +271 -0
- package/src/xslt/numberFormat.js +253 -0
- 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 +211 -0
- package/src/xslt/serializer/baseWriter.js +390 -0
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +92 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +135 -0
- 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 +239 -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 +179 -0
- package/src/xslt/serializer/textSerializer.js +77 -0
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +227 -0
- package/src/xslt/serializer.js +90 -0
- 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/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +200 -0
- package/LICENSE +0 -29
- 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
|
@@ -3,1808 +3,256 @@
|
|
|
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";
|
|
9
31
|
import { XPathEvaluator, XPathContext } from "../xpath/evaluator.js";
|
|
10
|
-
|
|
11
|
-
|
|
32
|
+
import { XSLT_NAMESPACE } from "./elements.js";
|
|
33
|
+
import { createXsltFunctions } from "./functions.js";
|
|
34
|
+
import { KeyIndexRegistry } from "./keys.js";
|
|
35
|
+
import { NamespaceAliasMap } from "./literalResult.js";
|
|
36
|
+
import { PatternMatcher } from "./patterns.js";
|
|
37
|
+
import { installMethods } from "./engine/methods.js";
|
|
38
|
+
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";
|
|
60
|
+
|
|
61
|
+
export { XsltContext } from "./engine/context.js";
|
|
62
|
+
export { XSLT_MAX_TEMPLATE_DEPTH };
|
|
12
63
|
|
|
13
64
|
/**
|
|
14
|
-
*
|
|
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.
|
|
15
72
|
*/
|
|
16
|
-
export
|
|
17
|
-
constructor(options = {}) {
|
|
18
|
-
this.currentNode = options.currentNode;
|
|
19
|
-
this.currentNodeList = options.currentNodeList || [];
|
|
20
|
-
this.position = options.position || 1;
|
|
21
|
-
this.variables = { ...options.variables };
|
|
22
|
-
this.parameters = { ...options.parameters };
|
|
23
|
-
this.outputDocument = options.outputDocument;
|
|
24
|
-
this.stylesheet = options.stylesheet;
|
|
25
|
-
this.namespaces = { ...options.namespaces };
|
|
26
|
-
this.templates = options.templates || [];
|
|
27
|
-
this.keys = options.keys || {};
|
|
28
|
-
this.decimalFormats = options.decimalFormats || {};
|
|
29
|
-
this.outputMethod = options.outputMethod || "xml";
|
|
30
|
-
this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
|
|
31
|
-
}
|
|
73
|
+
export const XSLT_MAX_RESULT_SIZE = 5000000;
|
|
32
74
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? { ...this.parameters, ...overrides.parameters }
|
|
43
|
-
: { ...this.parameters },
|
|
44
|
-
outputDocument: this.outputDocument,
|
|
45
|
-
stylesheet: this.stylesheet,
|
|
46
|
-
namespaces: overrides.namespaces
|
|
47
|
-
? { ...this.namespaces, ...overrides.namespaces }
|
|
48
|
-
: { ...this.namespaces },
|
|
49
|
-
templates: this.templates,
|
|
50
|
-
keys: this.keys,
|
|
51
|
-
decimalFormats: this.decimalFormats,
|
|
52
|
-
outputMethod: this.outputMethod,
|
|
53
|
-
xpathEvaluator: this.xpathEvaluator,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
getVariable(name) {
|
|
58
|
-
if (name in this.variables) {
|
|
59
|
-
return this.variables[name];
|
|
60
|
-
}
|
|
61
|
-
if (name in this.parameters) {
|
|
62
|
-
return this.parameters[name];
|
|
63
|
-
}
|
|
64
|
-
throw new Error(`Undefined variable: $${name}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
setVariable(name, value) {
|
|
68
|
-
this.variables[name] = value;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
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;
|
|
71
84
|
|
|
72
85
|
/**
|
|
73
86
|
* XSLT Engine
|
|
74
87
|
*/
|
|
75
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
|
+
*/
|
|
76
105
|
constructor(options = {}) {
|
|
77
|
-
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
|
+
|
|
78
118
|
this.templates = [];
|
|
79
119
|
this.keys = {};
|
|
80
120
|
this.globalVariables = {};
|
|
81
121
|
this.globalParameters = {};
|
|
82
|
-
this.outputSettings =
|
|
83
|
-
method: "xml",
|
|
84
|
-
encoding: "UTF-8",
|
|
85
|
-
indent: "no",
|
|
86
|
-
omitXmlDeclaration: "no",
|
|
87
|
-
doctypePublic: null,
|
|
88
|
-
doctypeSystem: null,
|
|
89
|
-
mediaType: null,
|
|
90
|
-
cdataSectionElements: [],
|
|
91
|
-
};
|
|
122
|
+
this.outputSettings = createOutputSettings();
|
|
92
123
|
this.namespaces = {};
|
|
93
124
|
this.decimalFormats = {};
|
|
94
125
|
this.stylesheetDoc = null;
|
|
95
126
|
this.attributeSets = {};
|
|
96
|
-
this.namespaceAliases =
|
|
127
|
+
this.namespaceAliases = new NamespaceAliasMap();
|
|
97
128
|
this.stripSpace = [];
|
|
98
129
|
this.preserveSpace = [];
|
|
99
130
|
|
|
100
131
|
// Import/Include support
|
|
101
132
|
this.stylesheetLoader = options.stylesheetLoader || null;
|
|
102
133
|
this.currentImportPrecedence = 0;
|
|
103
|
-
|
|
134
|
+
// URIs of the stylesheets being loaded, used to detect import cycles
|
|
135
|
+
this.stylesheetStack = [];
|
|
104
136
|
this.baseUri = options.baseUri || "";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Set the stylesheet loader function for xsl:import and xsl:include
|
|
109
|
-
* @param {Function} loader - Function(href, baseUri) => Document or string (XML)
|
|
110
|
-
*/
|
|
111
|
-
setStylesheetLoader(loader) {
|
|
112
|
-
this.stylesheetLoader = loader;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Resolve a relative URI against a base URI
|
|
117
|
-
*/
|
|
118
|
-
resolveUri(href, baseUri) {
|
|
119
|
-
if (
|
|
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;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Load an external stylesheet document
|
|
137
|
-
*/
|
|
138
|
-
loadStylesheet(href, baseUri) {
|
|
139
|
-
if (!this.stylesheetLoader) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`Cannot load stylesheet "${href}": no stylesheetLoader configured. ` +
|
|
142
|
-
"Use engine.setStylesheetLoader(fn) to provide a loader function.",
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const resolvedUri = this.resolveUri(href, baseUri);
|
|
147
|
-
const result = this.stylesheetLoader(resolvedUri, baseUri);
|
|
148
|
-
|
|
149
|
-
// If result is a string, it needs to be parsed (caller should handle this)
|
|
150
|
-
return { document: result, uri: resolvedUri };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Parse XML string to document (helper for stylesheet loading)
|
|
155
|
-
*/
|
|
156
|
-
parseXmlString(xmlString) {
|
|
157
|
-
if (typeof DOMParser !== "undefined") {
|
|
158
|
-
const parser = new DOMParser();
|
|
159
|
-
const doc = parser.parseFromString(xmlString, "application/xml");
|
|
160
|
-
const parseError = doc.querySelector("parsererror");
|
|
161
|
-
if (parseError) {
|
|
162
|
-
throw new Error(`XML parse error: ${parseError.textContent}`);
|
|
163
|
-
}
|
|
164
|
-
return doc;
|
|
165
|
-
}
|
|
166
|
-
throw new Error("XML parsing not available in this environment");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Import and compile an XSLT stylesheet
|
|
171
|
-
* @param {Document|Element} stylesheetNode - The stylesheet document or root element
|
|
172
|
-
* @param {string} [stylesheetUri] - Optional URI of the stylesheet for resolving imports
|
|
173
|
-
*/
|
|
174
|
-
importStylesheet(stylesheetNode, stylesheetUri) {
|
|
175
|
-
const isMainStylesheet = this.stylesheetDoc === null;
|
|
176
|
-
|
|
177
|
-
if (isMainStylesheet) {
|
|
178
|
-
this.stylesheetDoc = stylesheetNode.ownerDocument || stylesheetNode;
|
|
179
|
-
if (stylesheetUri) {
|
|
180
|
-
this.baseUri = stylesheetUri;
|
|
181
|
-
this.processedStylesheets.add(stylesheetUri);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const root = stylesheetNode.documentElement || stylesheetNode;
|
|
186
|
-
|
|
187
|
-
// Validate stylesheet
|
|
188
|
-
if (
|
|
189
|
-
!this.isXsltElement(root, "stylesheet") &&
|
|
190
|
-
!this.isXsltElement(root, "transform")
|
|
191
|
-
) {
|
|
192
|
-
// Check for literal result element (simplified stylesheet)
|
|
193
|
-
if (root.getAttribute && root.getAttribute("xsl:version")) {
|
|
194
|
-
this.processLiteralResultStylesheet(root);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
throw new Error(
|
|
198
|
-
"Invalid XSLT stylesheet: root element must be xsl:stylesheet or xsl:transform",
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Collect namespaces from root
|
|
203
|
-
this.collectNamespaces(root);
|
|
204
|
-
|
|
205
|
-
// XSLT 1.0: xsl:import elements MUST come first and are processed with lower precedence
|
|
206
|
-
// Process imports first (they have lower precedence than the importing stylesheet)
|
|
207
|
-
const imports = [];
|
|
208
|
-
const otherElements = [];
|
|
209
|
-
|
|
210
|
-
for (const child of root.childNodes) {
|
|
211
|
-
if (child.nodeType !== 1) continue;
|
|
212
|
-
|
|
213
|
-
if (this.isXsltElement(child, "import")) {
|
|
214
|
-
imports.push(child);
|
|
215
|
-
} else {
|
|
216
|
-
otherElements.push(child);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Process imports (lower precedence - process before current stylesheet)
|
|
221
|
-
for (const importNode of imports) {
|
|
222
|
-
this.processImport(importNode, stylesheetUri || this.baseUri);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Process other top-level elements (including includes)
|
|
226
|
-
for (const child of otherElements) {
|
|
227
|
-
if (this.isXsltElement(child, "template")) {
|
|
228
|
-
this.registerTemplate(child);
|
|
229
|
-
} else if (this.isXsltElement(child, "output")) {
|
|
230
|
-
this.processOutput(child);
|
|
231
|
-
} else if (this.isXsltElement(child, "variable")) {
|
|
232
|
-
this.processGlobalVariable(child);
|
|
233
|
-
} else if (this.isXsltElement(child, "param")) {
|
|
234
|
-
this.processGlobalParam(child);
|
|
235
|
-
} else if (this.isXsltElement(child, "key")) {
|
|
236
|
-
this.processKey(child);
|
|
237
|
-
} else if (this.isXsltElement(child, "decimal-format")) {
|
|
238
|
-
this.processDecimalFormat(child);
|
|
239
|
-
} else if (this.isXsltElement(child, "namespace-alias")) {
|
|
240
|
-
this.processNamespaceAlias(child);
|
|
241
|
-
} else if (this.isXsltElement(child, "attribute-set")) {
|
|
242
|
-
this.processAttributeSet(child);
|
|
243
|
-
} else if (this.isXsltElement(child, "strip-space")) {
|
|
244
|
-
this.processStripSpace(child);
|
|
245
|
-
} else if (this.isXsltElement(child, "preserve-space")) {
|
|
246
|
-
this.processPreserveSpace(child);
|
|
247
|
-
} else if (this.isXsltElement(child, "include")) {
|
|
248
|
-
this.processInclude(child, stylesheetUri || this.baseUri);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Increment import precedence after processing this stylesheet
|
|
253
|
-
if (isMainStylesheet) {
|
|
254
|
-
this.currentImportPrecedence++;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Process xsl:include element
|
|
260
|
-
* Includes are merged at the same import precedence level
|
|
261
|
-
*/
|
|
262
|
-
processInclude(node, baseUri) {
|
|
263
|
-
const href = node.getAttribute("href");
|
|
264
|
-
if (!href) {
|
|
265
|
-
throw new Error("xsl:include requires an href attribute");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const resolvedUri = this.resolveUri(href, baseUri);
|
|
269
|
-
|
|
270
|
-
// Check for circular includes
|
|
271
|
-
if (this.processedStylesheets.has(resolvedUri)) {
|
|
272
|
-
throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
this.processedStylesheets.add(resolvedUri);
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
|
|
279
|
-
|
|
280
|
-
// Parse if string
|
|
281
|
-
let doc = stylesheetDoc;
|
|
282
|
-
if (typeof stylesheetDoc === "string") {
|
|
283
|
-
doc = this.parseXmlString(stylesheetDoc);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Process the included stylesheet at the same import precedence
|
|
287
|
-
const savedPrecedence = this.currentImportPrecedence;
|
|
288
|
-
this.processIncludedStylesheet(doc, resolvedUri);
|
|
289
|
-
this.currentImportPrecedence = savedPrecedence;
|
|
290
|
-
} catch (error) {
|
|
291
|
-
throw new Error(
|
|
292
|
-
`Failed to include stylesheet "${href}": ${error.message}`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Process xsl:import element
|
|
299
|
-
* Imports have lower precedence than the importing stylesheet
|
|
300
|
-
*/
|
|
301
|
-
processImport(node, baseUri) {
|
|
302
|
-
const href = node.getAttribute("href");
|
|
303
|
-
if (!href) {
|
|
304
|
-
throw new Error("xsl:import requires an href attribute");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const resolvedUri = this.resolveUri(href, baseUri);
|
|
308
|
-
|
|
309
|
-
// Check for circular imports
|
|
310
|
-
if (this.processedStylesheets.has(resolvedUri)) {
|
|
311
|
-
throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
this.processedStylesheets.add(resolvedUri);
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
|
|
318
|
-
|
|
319
|
-
// Parse if string
|
|
320
|
-
let doc = stylesheetDoc;
|
|
321
|
-
if (typeof stylesheetDoc === "string") {
|
|
322
|
-
doc = this.parseXmlString(stylesheetDoc);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Process the imported stylesheet (imports have lower precedence)
|
|
326
|
-
// Don't increment precedence yet - imported templates get current (lower) precedence
|
|
327
|
-
this.processIncludedStylesheet(doc, resolvedUri);
|
|
328
|
-
|
|
329
|
-
// After processing import, increment precedence for next imports and main stylesheet
|
|
330
|
-
this.currentImportPrecedence++;
|
|
331
|
-
} catch (error) {
|
|
332
|
-
throw new Error(
|
|
333
|
-
`Failed to import stylesheet "${href}": ${error.message}`,
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Process an included/imported stylesheet document
|
|
340
|
-
*/
|
|
341
|
-
processIncludedStylesheet(stylesheetDoc, stylesheetUri) {
|
|
342
|
-
const root = stylesheetDoc.documentElement || stylesheetDoc;
|
|
343
|
-
|
|
344
|
-
// Validate stylesheet
|
|
345
|
-
if (
|
|
346
|
-
!this.isXsltElement(root, "stylesheet") &&
|
|
347
|
-
!this.isXsltElement(root, "transform")
|
|
348
|
-
) {
|
|
349
|
-
throw new Error(
|
|
350
|
-
"Included/imported document is not a valid XSLT stylesheet",
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Collect namespaces
|
|
355
|
-
this.collectNamespaces(root);
|
|
356
|
-
|
|
357
|
-
// Process imports first (they have lower precedence)
|
|
358
|
-
const imports = [];
|
|
359
|
-
const otherElements = [];
|
|
360
|
-
|
|
361
|
-
for (const child of root.childNodes) {
|
|
362
|
-
if (child.nodeType !== 1) continue;
|
|
363
|
-
|
|
364
|
-
if (this.isXsltElement(child, "import")) {
|
|
365
|
-
imports.push(child);
|
|
366
|
-
} else {
|
|
367
|
-
otherElements.push(child);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Process nested imports
|
|
372
|
-
for (const importNode of imports) {
|
|
373
|
-
this.processImport(importNode, stylesheetUri);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Process other elements
|
|
377
|
-
for (const child of otherElements) {
|
|
378
|
-
if (this.isXsltElement(child, "template")) {
|
|
379
|
-
this.registerTemplate(child);
|
|
380
|
-
} else if (this.isXsltElement(child, "output")) {
|
|
381
|
-
this.processOutput(child);
|
|
382
|
-
} else if (this.isXsltElement(child, "variable")) {
|
|
383
|
-
this.processGlobalVariable(child);
|
|
384
|
-
} else if (this.isXsltElement(child, "param")) {
|
|
385
|
-
this.processGlobalParam(child);
|
|
386
|
-
} else if (this.isXsltElement(child, "key")) {
|
|
387
|
-
this.processKey(child);
|
|
388
|
-
} else if (this.isXsltElement(child, "decimal-format")) {
|
|
389
|
-
this.processDecimalFormat(child);
|
|
390
|
-
} else if (this.isXsltElement(child, "namespace-alias")) {
|
|
391
|
-
this.processNamespaceAlias(child);
|
|
392
|
-
} else if (this.isXsltElement(child, "attribute-set")) {
|
|
393
|
-
this.processAttributeSet(child);
|
|
394
|
-
} else if (this.isXsltElement(child, "strip-space")) {
|
|
395
|
-
this.processStripSpace(child);
|
|
396
|
-
} else if (this.isXsltElement(child, "preserve-space")) {
|
|
397
|
-
this.processPreserveSpace(child);
|
|
398
|
-
} else if (this.isXsltElement(child, "include")) {
|
|
399
|
-
this.processInclude(child, stylesheetUri);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
processLiteralResultStylesheet(root) {
|
|
405
|
-
// Simplified stylesheet - entire document is one template matching /
|
|
406
|
-
this.templates.push({
|
|
407
|
-
match: "/",
|
|
408
|
-
name: null,
|
|
409
|
-
mode: null,
|
|
410
|
-
priority: 0.5,
|
|
411
|
-
node: root,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
collectNamespaces(node) {
|
|
416
|
-
if (!node.attributes) return;
|
|
417
|
-
|
|
418
|
-
for (const attr of node.attributes) {
|
|
419
|
-
if (attr.name.startsWith("xmlns:")) {
|
|
420
|
-
const prefix = attr.name.substring(6);
|
|
421
|
-
if (attr.value !== XSLT_NS) {
|
|
422
|
-
this.namespaces[prefix] = attr.value;
|
|
423
|
-
}
|
|
424
|
-
} else if (attr.name === "xmlns" && attr.value !== XSLT_NS) {
|
|
425
|
-
this.namespaces[""] = attr.value;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
registerTemplate(node) {
|
|
431
|
-
const match = node.getAttribute("match");
|
|
432
|
-
const name = node.getAttribute("name");
|
|
433
|
-
const mode = node.getAttribute("mode") || null;
|
|
434
|
-
const priorityAttr = node.getAttribute("priority");
|
|
435
|
-
const priority = priorityAttr
|
|
436
|
-
? parseFloat(priorityAttr)
|
|
437
|
-
: this.calculatePriority(match);
|
|
438
|
-
|
|
439
|
-
this.templates.push({
|
|
440
|
-
match,
|
|
441
|
-
name,
|
|
442
|
-
mode,
|
|
443
|
-
priority,
|
|
444
|
-
importPrecedence: this.currentImportPrecedence,
|
|
445
|
-
node,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
137
|
|
|
449
|
-
|
|
450
|
-
|
|
138
|
+
// document() support
|
|
139
|
+
this.documentLoader = options.documentLoader || null;
|
|
140
|
+
this.loadedDocuments = new Map();
|
|
451
141
|
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
// - NCName:* has priority -0.25
|
|
455
|
-
// - QName has priority 0
|
|
456
|
-
// - Other patterns have priority 0.5
|
|
142
|
+
// Parser for XML strings returned by the loaders (see domParsing.js)
|
|
143
|
+
this.domParser = options.domParser ?? null;
|
|
457
144
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
matchPattern === "text()" ||
|
|
462
|
-
matchPattern === "comment()" ||
|
|
463
|
-
matchPattern === "processing-instruction()"
|
|
464
|
-
) {
|
|
465
|
-
return -0.5;
|
|
466
|
-
}
|
|
145
|
+
// generate-id() support
|
|
146
|
+
this.generatedIds = new WeakMap();
|
|
147
|
+
this.generatedIdCount = 0;
|
|
467
148
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
149
|
+
// Compiled XSLT patterns (template match, xsl:key, xsl:number)
|
|
150
|
+
this.patternMatcher = new PatternMatcher(this.xpathEvaluator);
|
|
471
151
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return 0.5;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
processOutput(node) {
|
|
480
|
-
const method = node.getAttribute("method");
|
|
481
|
-
if (method) this.outputSettings.method = method;
|
|
482
|
-
|
|
483
|
-
const encoding = node.getAttribute("encoding");
|
|
484
|
-
if (encoding) this.outputSettings.encoding = encoding;
|
|
485
|
-
|
|
486
|
-
const indent = node.getAttribute("indent");
|
|
487
|
-
if (indent) this.outputSettings.indent = indent;
|
|
488
|
-
|
|
489
|
-
const omit = node.getAttribute("omit-xml-declaration");
|
|
490
|
-
if (omit) this.outputSettings.omitXmlDeclaration = omit;
|
|
491
|
-
|
|
492
|
-
const doctypePublic = node.getAttribute("doctype-public");
|
|
493
|
-
if (doctypePublic) this.outputSettings.doctypePublic = doctypePublic;
|
|
494
|
-
|
|
495
|
-
const doctypeSystem = node.getAttribute("doctype-system");
|
|
496
|
-
if (doctypeSystem) this.outputSettings.doctypeSystem = doctypeSystem;
|
|
497
|
-
|
|
498
|
-
const mediaType = node.getAttribute("media-type");
|
|
499
|
-
if (mediaType) this.outputSettings.mediaType = mediaType;
|
|
500
|
-
|
|
501
|
-
const cdataElements = node.getAttribute("cdata-section-elements");
|
|
502
|
-
if (cdataElements) {
|
|
503
|
-
this.outputSettings.cdataSectionElements = cdataElements
|
|
504
|
-
.split(/\s+/)
|
|
505
|
-
.filter(Boolean);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
processGlobalVariable(node) {
|
|
510
|
-
const name = node.getAttribute("name");
|
|
511
|
-
const select = node.getAttribute("select");
|
|
512
|
-
|
|
513
|
-
this.globalVariables[name] = { node, select };
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
processGlobalParam(node) {
|
|
517
|
-
const name = node.getAttribute("name");
|
|
518
|
-
const select = node.getAttribute("select");
|
|
519
|
-
|
|
520
|
-
this.globalParameters[name] = { node, select };
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
processKey(node) {
|
|
524
|
-
const name = node.getAttribute("name");
|
|
525
|
-
const match = node.getAttribute("match");
|
|
526
|
-
const use = node.getAttribute("use");
|
|
527
|
-
|
|
528
|
-
this.keys[name] = { match, use };
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
processDecimalFormat(node) {
|
|
532
|
-
const name = node.getAttribute("name") || "";
|
|
533
|
-
|
|
534
|
-
this.decimalFormats[name] = {
|
|
535
|
-
decimalSeparator: node.getAttribute("decimal-separator") || ".",
|
|
536
|
-
groupingSeparator: node.getAttribute("grouping-separator") || ",",
|
|
537
|
-
percent: node.getAttribute("percent") || "%",
|
|
538
|
-
perMille: node.getAttribute("per-mille") || "\u2030",
|
|
539
|
-
zeroDigit: node.getAttribute("zero-digit") || "0",
|
|
540
|
-
digit: node.getAttribute("digit") || "#",
|
|
541
|
-
patternSeparator: node.getAttribute("pattern-separator") || ";",
|
|
542
|
-
infinity: node.getAttribute("infinity") || "Infinity",
|
|
543
|
-
nan: node.getAttribute("NaN") || "NaN",
|
|
544
|
-
minusSign: node.getAttribute("minus-sign") || "-",
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
processNamespaceAlias(node) {
|
|
549
|
-
const stylesheet = node.getAttribute("stylesheet-prefix");
|
|
550
|
-
const result = node.getAttribute("result-prefix");
|
|
551
|
-
this.namespaceAliases[stylesheet] = result;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
processAttributeSet(node) {
|
|
555
|
-
const name = node.getAttribute("name");
|
|
556
|
-
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
557
|
-
|
|
558
|
-
this.attributeSets[name] = {
|
|
559
|
-
node,
|
|
560
|
-
useAttributeSets: useAttributeSets
|
|
561
|
-
? useAttributeSets.split(/\s+/).filter(Boolean)
|
|
562
|
-
: [],
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
processStripSpace(node) {
|
|
567
|
-
const elements = node.getAttribute("elements");
|
|
568
|
-
if (elements) {
|
|
569
|
-
this.stripSpace.push(...elements.split(/\s+/).filter(Boolean));
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
processPreserveSpace(node) {
|
|
574
|
-
const elements = node.getAttribute("elements");
|
|
575
|
-
if (elements) {
|
|
576
|
-
this.preserveSpace.push(...elements.split(/\s+/).filter(Boolean));
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Transform a source document
|
|
582
|
-
*/
|
|
583
|
-
transform(sourceNode, ownerDocument) {
|
|
584
|
-
const doc =
|
|
585
|
-
ownerDocument || (typeof document !== "undefined" ? document : null);
|
|
586
|
-
|
|
587
|
-
if (!doc) {
|
|
588
|
-
throw new Error("No output document available");
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Create context - use document node as initial context for "/" template matching
|
|
592
|
-
// XPath paths like "RootElement/child" expect to start from document node
|
|
593
|
-
const context = new XsltContext({
|
|
594
|
-
currentNode: sourceNode,
|
|
595
|
-
currentNodeList: [sourceNode],
|
|
596
|
-
position: 1,
|
|
597
|
-
outputDocument: doc,
|
|
598
|
-
stylesheet: this.stylesheetDoc,
|
|
599
|
-
namespaces: { ...this.namespaces },
|
|
600
|
-
templates: this.templates,
|
|
152
|
+
// key() support
|
|
153
|
+
this.rootContext = null;
|
|
154
|
+
this.keyRegistry = new KeyIndexRegistry({
|
|
601
155
|
keys: this.keys,
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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),
|
|
605
165
|
});
|
|
606
166
|
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
if (!(name in context.parameters)) {
|
|
610
|
-
context.parameters[name] = this.evaluateVariable(def, context);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
for (const [name, def] of Object.entries(this.globalVariables)) {
|
|
615
|
-
context.variables[name] = this.evaluateVariable(def, context);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Create result document fragment
|
|
619
|
-
const fragment = doc.createDocumentFragment();
|
|
620
|
-
|
|
621
|
-
// Apply templates to document node (not documentElement)
|
|
622
|
-
// This ensures "/" template has document as context, so paths like
|
|
623
|
-
// "RootElement/child" work correctly
|
|
624
|
-
this.applyTemplates([sourceNode], null, context, fragment);
|
|
625
|
-
|
|
626
|
-
return fragment;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* Transform to a complete document
|
|
631
|
-
*/
|
|
632
|
-
transformToDocument(sourceNode) {
|
|
633
|
-
// For Node.js environments, we need a document implementation
|
|
634
|
-
const doc = this.createDocument();
|
|
635
|
-
const fragment = this.transform(sourceNode, doc);
|
|
636
|
-
|
|
637
|
-
// Move fragment contents to document
|
|
638
|
-
while (fragment.firstChild) {
|
|
639
|
-
doc.appendChild(fragment.firstChild);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return doc;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
createDocument() {
|
|
646
|
-
if (typeof document !== "undefined") {
|
|
647
|
-
return document.implementation.createDocument(null, null, null);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// For Node.js - would need JSDOM or similar
|
|
651
|
-
throw new Error("Document creation not available in this environment");
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
evaluateVariable(def, context) {
|
|
655
|
-
if (def.select) {
|
|
656
|
-
return this.evaluateXPath(def.select, context);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// If no select, evaluate content as result tree fragment
|
|
660
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
661
|
-
this.processChildren(def.node, context, fragment);
|
|
662
|
-
return fragment;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Apply templates to a node list
|
|
667
|
-
*/
|
|
668
|
-
applyTemplates(nodes, mode, context, output) {
|
|
669
|
-
const nodeList = Array.isArray(nodes) ? nodes : [nodes];
|
|
670
|
-
|
|
671
|
-
for (let i = 0; i < nodeList.length; i++) {
|
|
672
|
-
const node = nodeList[i];
|
|
673
|
-
const template = this.findMatchingTemplate(node, mode, context);
|
|
674
|
-
|
|
675
|
-
if (template) {
|
|
676
|
-
const newContext = context.clone({
|
|
677
|
-
currentNode: node,
|
|
678
|
-
currentNodeList: nodeList,
|
|
679
|
-
position: i + 1,
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
this.processTemplate(template.node, newContext, output);
|
|
683
|
-
} else {
|
|
684
|
-
// Built-in templates
|
|
685
|
-
this.applyBuiltinTemplate(node, mode, context, output);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Find the best matching template for a node
|
|
692
|
-
*/
|
|
693
|
-
findMatchingTemplate(node, mode, context) {
|
|
694
|
-
let bestMatch = null;
|
|
695
|
-
let bestPriority = -Infinity;
|
|
696
|
-
let bestImportPrecedence = -Infinity;
|
|
697
|
-
|
|
698
|
-
for (const template of this.templates) {
|
|
699
|
-
if (template.mode !== mode) continue;
|
|
700
|
-
if (!template.match) continue;
|
|
701
|
-
|
|
702
|
-
if (this.matchesPattern(node, template.match, context)) {
|
|
703
|
-
const priority = template.priority;
|
|
704
|
-
const importPrecedence = template.importPrecedence || 0;
|
|
705
|
-
|
|
706
|
-
if (
|
|
707
|
-
importPrecedence > bestImportPrecedence ||
|
|
708
|
-
(importPrecedence === bestImportPrecedence && priority > bestPriority)
|
|
709
|
-
) {
|
|
710
|
-
bestMatch = template;
|
|
711
|
-
bestPriority = priority;
|
|
712
|
-
bestImportPrecedence = importPrecedence;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return bestMatch;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Check if a node matches an XSLT pattern
|
|
722
|
-
*/
|
|
723
|
-
matchesPattern(node, pattern, context) {
|
|
724
|
-
// Split union patterns
|
|
725
|
-
const patterns = this.splitUnionPattern(pattern);
|
|
726
|
-
|
|
727
|
-
for (const p of patterns) {
|
|
728
|
-
if (this.matchesSinglePattern(node, p.trim(), context)) {
|
|
729
|
-
return true;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
return false;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
splitUnionPattern(pattern) {
|
|
737
|
-
// Simple split on | not inside predicates or strings
|
|
738
|
-
const parts = [];
|
|
739
|
-
let current = "";
|
|
740
|
-
let depth = 0;
|
|
741
|
-
let inString = false;
|
|
742
|
-
let stringChar = "";
|
|
743
|
-
|
|
744
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
745
|
-
const char = pattern[i];
|
|
746
|
-
|
|
747
|
-
if (inString) {
|
|
748
|
-
current += char;
|
|
749
|
-
if (char === stringChar) {
|
|
750
|
-
inString = false;
|
|
751
|
-
}
|
|
752
|
-
} else if (char === '"' || char === "'") {
|
|
753
|
-
inString = true;
|
|
754
|
-
stringChar = char;
|
|
755
|
-
current += char;
|
|
756
|
-
} else if (char === "[") {
|
|
757
|
-
depth++;
|
|
758
|
-
current += char;
|
|
759
|
-
} else if (char === "]") {
|
|
760
|
-
depth--;
|
|
761
|
-
current += char;
|
|
762
|
-
} else if (char === "|" && depth === 0) {
|
|
763
|
-
parts.push(current);
|
|
764
|
-
current = "";
|
|
765
|
-
} else {
|
|
766
|
-
current += char;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (current) {
|
|
771
|
-
parts.push(current);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
return parts;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
matchesSinglePattern(node, pattern, context) {
|
|
778
|
-
try {
|
|
779
|
-
// Handle root pattern
|
|
780
|
-
if (pattern === "/") {
|
|
781
|
-
return (
|
|
782
|
-
node.nodeType === 9 || node === node.ownerDocument?.documentElement
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Handle patterns like "item" (child match)
|
|
787
|
-
// Need to check if node would be selected by pattern from parent
|
|
788
|
-
const ast = parseXPath(pattern);
|
|
789
|
-
|
|
790
|
-
// For patterns starting with /, evaluate from root
|
|
791
|
-
if (pattern.startsWith("/")) {
|
|
792
|
-
const doc = node.ownerDocument || node;
|
|
793
|
-
const xpathContext = new XPathContext(
|
|
794
|
-
doc,
|
|
795
|
-
1,
|
|
796
|
-
1,
|
|
797
|
-
{ ...context.variables, ...context.parameters },
|
|
798
|
-
context.namespaces,
|
|
799
|
-
);
|
|
800
|
-
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
801
|
-
const nodes = Array.isArray(result) ? result : [result];
|
|
802
|
-
return nodes.includes(node);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// For relative patterns, check if this node matches when evaluated from parent
|
|
806
|
-
if (node.parentNode) {
|
|
807
|
-
const xpathContext = new XPathContext(
|
|
808
|
-
node.parentNode,
|
|
809
|
-
1,
|
|
810
|
-
1,
|
|
811
|
-
{ ...context.variables, ...context.parameters },
|
|
812
|
-
context.namespaces,
|
|
813
|
-
);
|
|
814
|
-
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
815
|
-
const nodes = Array.isArray(result) ? result : [result];
|
|
816
|
-
return nodes.includes(node);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// For document node without parent
|
|
820
|
-
const xpathContext = new XPathContext(
|
|
821
|
-
node,
|
|
822
|
-
1,
|
|
823
|
-
1,
|
|
824
|
-
{ ...context.variables, ...context.parameters },
|
|
825
|
-
context.namespaces,
|
|
826
|
-
);
|
|
827
|
-
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
828
|
-
const nodes = Array.isArray(result) ? result : [result];
|
|
829
|
-
return nodes.includes(node);
|
|
830
|
-
} catch {
|
|
831
|
-
return false;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Apply built-in template rules
|
|
837
|
-
*/
|
|
838
|
-
applyBuiltinTemplate(node, mode, context, output) {
|
|
839
|
-
switch (node.nodeType) {
|
|
840
|
-
case 1: // Element
|
|
841
|
-
case 9: // Document
|
|
842
|
-
case 11: // Document Fragment
|
|
843
|
-
// Process children
|
|
844
|
-
this.applyTemplates(Array.from(node.childNodes), mode, context, output);
|
|
845
|
-
break;
|
|
846
|
-
|
|
847
|
-
case 3: // Text
|
|
848
|
-
case 4: {
|
|
849
|
-
// CDATA
|
|
850
|
-
// Copy text value
|
|
851
|
-
const text = context.outputDocument.createTextNode(
|
|
852
|
-
node.nodeValue || "",
|
|
853
|
-
);
|
|
854
|
-
output.appendChild(text);
|
|
855
|
-
break;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
case 2: {
|
|
859
|
-
// Attribute
|
|
860
|
-
// Copy attribute value as text
|
|
861
|
-
const attrText = context.outputDocument.createTextNode(
|
|
862
|
-
node.nodeValue || "",
|
|
863
|
-
);
|
|
864
|
-
output.appendChild(attrText);
|
|
865
|
-
break;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// Comments and PIs have no built-in template
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* Process template content
|
|
874
|
-
*/
|
|
875
|
-
processTemplate(templateNode, context, output) {
|
|
876
|
-
// Process template parameters first
|
|
877
|
-
const localContext = context.clone();
|
|
878
|
-
|
|
879
|
-
for (const child of templateNode.childNodes) {
|
|
880
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "param")) {
|
|
881
|
-
const name = child.getAttribute("name");
|
|
882
|
-
if (!(name in localContext.parameters)) {
|
|
883
|
-
localContext.parameters[name] = this.evaluateVariable(
|
|
884
|
-
{ node: child, select: child.getAttribute("select") },
|
|
885
|
-
localContext,
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
this.processChildren(templateNode, localContext, output);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* Process child nodes of an XSLT element
|
|
896
|
-
*/
|
|
897
|
-
processChildren(node, context, output) {
|
|
898
|
-
for (const child of node.childNodes) {
|
|
899
|
-
this.processNode(child, context, output);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Process a single node in the stylesheet
|
|
905
|
-
*/
|
|
906
|
-
processNode(node, context, output) {
|
|
907
|
-
switch (node.nodeType) {
|
|
908
|
-
case 1: // Element
|
|
909
|
-
this.processElement(node, context, output);
|
|
910
|
-
break;
|
|
911
|
-
|
|
912
|
-
case 3: // Text
|
|
913
|
-
case 4: {
|
|
914
|
-
// CDATA
|
|
915
|
-
// Output text if not whitespace only (or if preserving space)
|
|
916
|
-
const text = node.nodeValue;
|
|
917
|
-
if (text && (text.trim() || this.shouldPreserveSpace(node))) {
|
|
918
|
-
const textNode = context.outputDocument.createTextNode(text);
|
|
919
|
-
output.appendChild(textNode);
|
|
920
|
-
}
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
shouldPreserveSpace(node) {
|
|
927
|
-
// Check xml:space attribute on ancestors
|
|
928
|
-
let current = node.parentNode;
|
|
929
|
-
while (current && current.nodeType === 1) {
|
|
930
|
-
const space = current.getAttribute("xml:space");
|
|
931
|
-
if (space === "preserve") return true;
|
|
932
|
-
if (space === "default") return false;
|
|
933
|
-
current = current.parentNode;
|
|
934
|
-
}
|
|
935
|
-
return false;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* Process an element in the stylesheet
|
|
940
|
-
*/
|
|
941
|
-
processElement(node, context, output) {
|
|
942
|
-
// Check if XSLT element
|
|
943
|
-
if (this.isXsltNamespace(node)) {
|
|
944
|
-
this.processXsltElement(node, context, output);
|
|
945
|
-
} else {
|
|
946
|
-
// Literal result element
|
|
947
|
-
this.processLiteralResultElement(node, context, output);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Process an XSLT instruction element
|
|
953
|
-
*/
|
|
954
|
-
processXsltElement(node, context, output) {
|
|
955
|
-
const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
|
|
956
|
-
|
|
957
|
-
switch (localName) {
|
|
958
|
-
case "apply-templates":
|
|
959
|
-
this.xslApplyTemplates(node, context, output);
|
|
960
|
-
break;
|
|
961
|
-
|
|
962
|
-
case "call-template":
|
|
963
|
-
this.xslCallTemplate(node, context, output);
|
|
964
|
-
break;
|
|
965
|
-
|
|
966
|
-
case "value-of":
|
|
967
|
-
this.xslValueOf(node, context, output);
|
|
968
|
-
break;
|
|
969
|
-
|
|
970
|
-
case "text":
|
|
971
|
-
this.xslText(node, context, output);
|
|
972
|
-
break;
|
|
973
|
-
|
|
974
|
-
case "element":
|
|
975
|
-
this.xslElement(node, context, output);
|
|
976
|
-
break;
|
|
977
|
-
|
|
978
|
-
case "attribute":
|
|
979
|
-
this.xslAttribute(node, context, output);
|
|
980
|
-
break;
|
|
981
|
-
|
|
982
|
-
case "if":
|
|
983
|
-
this.xslIf(node, context, output);
|
|
984
|
-
break;
|
|
985
|
-
|
|
986
|
-
case "choose":
|
|
987
|
-
this.xslChoose(node, context, output);
|
|
988
|
-
break;
|
|
989
|
-
|
|
990
|
-
case "for-each":
|
|
991
|
-
this.xslForEach(node, context, output);
|
|
992
|
-
break;
|
|
993
|
-
|
|
994
|
-
case "copy":
|
|
995
|
-
this.xslCopy(node, context, output);
|
|
996
|
-
break;
|
|
997
|
-
|
|
998
|
-
case "copy-of":
|
|
999
|
-
this.xslCopyOf(node, context, output);
|
|
1000
|
-
break;
|
|
1001
|
-
|
|
1002
|
-
case "variable":
|
|
1003
|
-
this.xslVariable(node, context, output);
|
|
1004
|
-
break;
|
|
1005
|
-
|
|
1006
|
-
case "param":
|
|
1007
|
-
// Params are processed at template start
|
|
1008
|
-
break;
|
|
1009
|
-
|
|
1010
|
-
case "comment":
|
|
1011
|
-
this.xslComment(node, context, output);
|
|
1012
|
-
break;
|
|
1013
|
-
|
|
1014
|
-
case "processing-instruction":
|
|
1015
|
-
this.xslProcessingInstruction(node, context, output);
|
|
1016
|
-
break;
|
|
1017
|
-
|
|
1018
|
-
case "number":
|
|
1019
|
-
this.xslNumber(node, context, output);
|
|
1020
|
-
break;
|
|
1021
|
-
|
|
1022
|
-
case "sort":
|
|
1023
|
-
// Handled by apply-templates and for-each
|
|
1024
|
-
break;
|
|
1025
|
-
|
|
1026
|
-
case "with-param":
|
|
1027
|
-
// Handled by call-template and apply-templates
|
|
1028
|
-
break;
|
|
1029
|
-
|
|
1030
|
-
case "message":
|
|
1031
|
-
this.xslMessage(node, context, output);
|
|
1032
|
-
break;
|
|
1033
|
-
|
|
1034
|
-
case "fallback":
|
|
1035
|
-
// Used for forward compatibility
|
|
1036
|
-
break;
|
|
1037
|
-
|
|
1038
|
-
default:
|
|
1039
|
-
console.warn(`Unknown XSLT element: ${localName}`);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* Process a literal result element (non-XSLT)
|
|
1045
|
-
*/
|
|
1046
|
-
processLiteralResultElement(node, context, output) {
|
|
1047
|
-
// Create element in output
|
|
1048
|
-
let outputElement;
|
|
1049
|
-
const namespaceURI = node.namespaceURI;
|
|
1050
|
-
const nodeName = node.nodeName;
|
|
1051
|
-
|
|
1052
|
-
// Apply namespace aliases
|
|
1053
|
-
let resolvedNS = namespaceURI;
|
|
1054
|
-
if (namespaceURI) {
|
|
1055
|
-
for (const [from, to] of Object.entries(this.namespaceAliases)) {
|
|
1056
|
-
if (this.namespaces[from] === namespaceURI) {
|
|
1057
|
-
resolvedNS = this.namespaces[to] || to;
|
|
1058
|
-
break;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (resolvedNS && context.outputDocument.createElementNS) {
|
|
1064
|
-
outputElement = context.outputDocument.createElementNS(
|
|
1065
|
-
resolvedNS,
|
|
1066
|
-
nodeName,
|
|
1067
|
-
);
|
|
1068
|
-
} else {
|
|
1069
|
-
outputElement = context.outputDocument.createElement(nodeName);
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Copy attributes (except XSLT namespace)
|
|
1073
|
-
if (node.attributes) {
|
|
1074
|
-
for (const attr of node.attributes) {
|
|
1075
|
-
if (attr.namespaceURI === XSLT_NS) continue;
|
|
1076
|
-
if (attr.name.startsWith("xmlns")) continue;
|
|
167
|
+
// Recoverable errors already reported, so each is reported once
|
|
168
|
+
this.reportedWarnings = new Set();
|
|
1077
169
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
outputElement.setAttribute(attr.name, value);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
170
|
+
// Extension element implementations by expanded name `{uri}local`
|
|
171
|
+
this.extensionElements = new Map();
|
|
1083
172
|
|
|
1084
|
-
|
|
1085
|
-
this.processChildren(node, context, outputElement);
|
|
1086
|
-
|
|
1087
|
-
output.appendChild(outputElement);
|
|
173
|
+
this.xpathEvaluator.registerFunctions(createXsltFunctions(this));
|
|
1088
174
|
}
|
|
1089
175
|
|
|
1090
176
|
/**
|
|
1091
|
-
*
|
|
177
|
+
* Evaluate an XPath expression in an XSLT context.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} expr - The expression
|
|
180
|
+
* @param {XsltContext} context - The current context
|
|
181
|
+
* @returns {*} The value
|
|
1092
182
|
*/
|
|
1093
|
-
processAttributeValueTemplate(value, context) {
|
|
1094
|
-
if (!value.includes("{")) return value;
|
|
1095
|
-
|
|
1096
|
-
let result = "";
|
|
1097
|
-
let i = 0;
|
|
1098
|
-
|
|
1099
|
-
while (i < value.length) {
|
|
1100
|
-
if (value[i] === "{") {
|
|
1101
|
-
if (value[i + 1] === "{") {
|
|
1102
|
-
// Escaped brace
|
|
1103
|
-
result += "{";
|
|
1104
|
-
i += 2;
|
|
1105
|
-
} else {
|
|
1106
|
-
// Find closing brace
|
|
1107
|
-
let depth = 1;
|
|
1108
|
-
let j = i + 1;
|
|
1109
|
-
while (j < value.length && depth > 0) {
|
|
1110
|
-
if (value[j] === "{") depth++;
|
|
1111
|
-
else if (value[j] === "}") depth--;
|
|
1112
|
-
j++;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const expr = value.substring(i + 1, j - 1);
|
|
1116
|
-
const evalResult = this.evaluateXPath(expr, context);
|
|
1117
|
-
result += this.xpathEvaluator.toString(evalResult);
|
|
1118
|
-
i = j;
|
|
1119
|
-
}
|
|
1120
|
-
} else if (value[i] === "}") {
|
|
1121
|
-
if (value[i + 1] === "}") {
|
|
1122
|
-
// Escaped brace
|
|
1123
|
-
result += "}";
|
|
1124
|
-
i += 2;
|
|
1125
|
-
} else {
|
|
1126
|
-
throw new Error("Unmatched } in attribute value template");
|
|
1127
|
-
}
|
|
1128
|
-
} else {
|
|
1129
|
-
result += value[i];
|
|
1130
|
-
i++;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
return result;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// XSLT Instructions
|
|
1138
|
-
xslApplyTemplates(node, context, output) {
|
|
1139
|
-
const select = node.getAttribute("select") || "node()";
|
|
1140
|
-
const mode = node.getAttribute("mode") || null;
|
|
1141
|
-
|
|
1142
|
-
// Evaluate select expression
|
|
1143
|
-
let nodes = this.evaluateXPath(select, context);
|
|
1144
|
-
if (!Array.isArray(nodes)) {
|
|
1145
|
-
nodes = nodes ? [nodes] : [];
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// Collect sort specifications
|
|
1149
|
-
const sortSpecs = [];
|
|
1150
|
-
for (const child of node.childNodes) {
|
|
1151
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
1152
|
-
sortSpecs.push({
|
|
1153
|
-
select: child.getAttribute("select") || ".",
|
|
1154
|
-
order: child.getAttribute("order") || "ascending",
|
|
1155
|
-
dataType: child.getAttribute("data-type") || "text",
|
|
1156
|
-
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
1157
|
-
lang: child.getAttribute("lang"),
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Apply sorting
|
|
1163
|
-
if (sortSpecs.length > 0) {
|
|
1164
|
-
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// Collect with-param values
|
|
1168
|
-
const params = {};
|
|
1169
|
-
for (const child of node.childNodes) {
|
|
1170
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
1171
|
-
const name = child.getAttribute("name");
|
|
1172
|
-
const selectAttr = child.getAttribute("select");
|
|
1173
|
-
if (selectAttr) {
|
|
1174
|
-
params[name] = this.evaluateXPath(selectAttr, context);
|
|
1175
|
-
} else {
|
|
1176
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1177
|
-
this.processChildren(child, context, fragment);
|
|
1178
|
-
params[name] = fragment;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Apply templates with new context including params
|
|
1184
|
-
const newContext = context.clone({
|
|
1185
|
-
parameters: { ...context.parameters, ...params },
|
|
1186
|
-
});
|
|
1187
|
-
this.applyTemplates(nodes, mode, newContext, output);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
xslCallTemplate(node, context, output) {
|
|
1191
|
-
const name = node.getAttribute("name");
|
|
1192
|
-
|
|
1193
|
-
// Find named template
|
|
1194
|
-
const template = this.templates.find((t) => t.name === name);
|
|
1195
|
-
if (!template) {
|
|
1196
|
-
throw new Error(`Template not found: ${name}`);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
// Collect with-param values
|
|
1200
|
-
const params = {};
|
|
1201
|
-
for (const child of node.childNodes) {
|
|
1202
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
1203
|
-
const paramName = child.getAttribute("name");
|
|
1204
|
-
const selectAttr = child.getAttribute("select");
|
|
1205
|
-
if (selectAttr) {
|
|
1206
|
-
params[paramName] = this.evaluateXPath(selectAttr, context);
|
|
1207
|
-
} else {
|
|
1208
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1209
|
-
this.processChildren(child, context, fragment);
|
|
1210
|
-
params[paramName] = fragment;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// Call template with params
|
|
1216
|
-
const newContext = context.clone({
|
|
1217
|
-
parameters: { ...context.parameters, ...params },
|
|
1218
|
-
});
|
|
1219
|
-
this.processTemplate(template.node, newContext, output);
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
xslValueOf(node, context, output) {
|
|
1223
|
-
const select = node.getAttribute("select");
|
|
1224
|
-
const disableOutputEscaping =
|
|
1225
|
-
node.getAttribute("disable-output-escaping") === "yes";
|
|
1226
|
-
|
|
1227
|
-
const result = this.evaluateXPath(select, context);
|
|
1228
|
-
const text = this.xpathEvaluator.toString(result);
|
|
1229
|
-
|
|
1230
|
-
if (text) {
|
|
1231
|
-
const textNode = context.outputDocument.createTextNode(text);
|
|
1232
|
-
if (disableOutputEscaping) {
|
|
1233
|
-
textNode._disableOutputEscaping = true;
|
|
1234
|
-
}
|
|
1235
|
-
output.appendChild(textNode);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
xslText(node, context, output) {
|
|
1240
|
-
const disableOutputEscaping =
|
|
1241
|
-
node.getAttribute("disable-output-escaping") === "yes";
|
|
1242
|
-
let text = "";
|
|
1243
|
-
|
|
1244
|
-
for (const child of node.childNodes) {
|
|
1245
|
-
if (child.nodeType === 3 || child.nodeType === 4) {
|
|
1246
|
-
text += child.nodeValue || "";
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
if (text) {
|
|
1251
|
-
const textNode = context.outputDocument.createTextNode(text);
|
|
1252
|
-
if (disableOutputEscaping) {
|
|
1253
|
-
textNode._disableOutputEscaping = true;
|
|
1254
|
-
}
|
|
1255
|
-
output.appendChild(textNode);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
xslElement(node, context, output) {
|
|
1260
|
-
const name = this.processAttributeValueTemplate(
|
|
1261
|
-
node.getAttribute("name"),
|
|
1262
|
-
context,
|
|
1263
|
-
);
|
|
1264
|
-
const namespace = node.getAttribute("namespace");
|
|
1265
|
-
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
1266
|
-
|
|
1267
|
-
let element;
|
|
1268
|
-
if (namespace) {
|
|
1269
|
-
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
1270
|
-
element = context.outputDocument.createElementNS(ns, name);
|
|
1271
|
-
} else {
|
|
1272
|
-
element = context.outputDocument.createElement(name);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// Apply attribute sets
|
|
1276
|
-
if (useAttributeSets) {
|
|
1277
|
-
this.applyAttributeSets(useAttributeSets, context, element);
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
this.processChildren(node, context, element);
|
|
1281
|
-
output.appendChild(element);
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
xslAttribute(node, context, output) {
|
|
1285
|
-
const name = this.processAttributeValueTemplate(
|
|
1286
|
-
node.getAttribute("name"),
|
|
1287
|
-
context,
|
|
1288
|
-
);
|
|
1289
|
-
const namespace = node.getAttribute("namespace");
|
|
1290
|
-
|
|
1291
|
-
// Collect content
|
|
1292
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1293
|
-
this.processChildren(node, context, fragment);
|
|
1294
|
-
|
|
1295
|
-
// Get text content
|
|
1296
|
-
let value = "";
|
|
1297
|
-
const getText = (n) => {
|
|
1298
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1299
|
-
value += n.nodeValue || "";
|
|
1300
|
-
} else if (n.childNodes) {
|
|
1301
|
-
for (const child of n.childNodes) {
|
|
1302
|
-
getText(child);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
};
|
|
1306
|
-
getText(fragment);
|
|
1307
|
-
|
|
1308
|
-
// Add attribute to parent element
|
|
1309
|
-
if (output.nodeType === 1) {
|
|
1310
|
-
if (namespace) {
|
|
1311
|
-
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
1312
|
-
output.setAttributeNS(ns, name, value);
|
|
1313
|
-
} else {
|
|
1314
|
-
output.setAttribute(name, value);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
xslIf(node, context, output) {
|
|
1320
|
-
const test = node.getAttribute("test");
|
|
1321
|
-
const result = this.evaluateXPath(test, context);
|
|
1322
|
-
|
|
1323
|
-
if (this.xpathEvaluator.toBoolean(result)) {
|
|
1324
|
-
this.processChildren(node, context, output);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
xslChoose(node, context, output) {
|
|
1329
|
-
for (const child of node.childNodes) {
|
|
1330
|
-
if (child.nodeType !== 1) continue;
|
|
1331
|
-
|
|
1332
|
-
if (this.isXsltElement(child, "when")) {
|
|
1333
|
-
const test = child.getAttribute("test");
|
|
1334
|
-
const result = this.evaluateXPath(test, context);
|
|
1335
|
-
|
|
1336
|
-
if (this.xpathEvaluator.toBoolean(result)) {
|
|
1337
|
-
this.processChildren(child, context, output);
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
} else if (this.isXsltElement(child, "otherwise")) {
|
|
1341
|
-
this.processChildren(child, context, output);
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
xslForEach(node, context, output) {
|
|
1348
|
-
const select = node.getAttribute("select");
|
|
1349
|
-
|
|
1350
|
-
let nodes = this.evaluateXPath(select, context);
|
|
1351
|
-
if (!Array.isArray(nodes)) {
|
|
1352
|
-
nodes = nodes ? [nodes] : [];
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
// Collect sort specifications
|
|
1356
|
-
const sortSpecs = [];
|
|
1357
|
-
for (const child of node.childNodes) {
|
|
1358
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
1359
|
-
sortSpecs.push({
|
|
1360
|
-
select: child.getAttribute("select") || ".",
|
|
1361
|
-
order: child.getAttribute("order") || "ascending",
|
|
1362
|
-
dataType: child.getAttribute("data-type") || "text",
|
|
1363
|
-
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
1364
|
-
lang: child.getAttribute("lang"),
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// Apply sorting
|
|
1370
|
-
if (sortSpecs.length > 0) {
|
|
1371
|
-
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// Process each node
|
|
1375
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1376
|
-
const newContext = context.clone({
|
|
1377
|
-
currentNode: nodes[i],
|
|
1378
|
-
currentNodeList: nodes,
|
|
1379
|
-
position: i + 1,
|
|
1380
|
-
});
|
|
1381
|
-
|
|
1382
|
-
this.processChildren(node, newContext, output);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
xslCopy(node, context, output) {
|
|
1387
|
-
const currentNode = context.currentNode;
|
|
1388
|
-
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
1389
|
-
|
|
1390
|
-
switch (currentNode.nodeType) {
|
|
1391
|
-
case 1: {
|
|
1392
|
-
// Element
|
|
1393
|
-
let copy;
|
|
1394
|
-
if (currentNode.namespaceURI) {
|
|
1395
|
-
copy = context.outputDocument.createElementNS(
|
|
1396
|
-
currentNode.namespaceURI,
|
|
1397
|
-
currentNode.nodeName,
|
|
1398
|
-
);
|
|
1399
|
-
} else {
|
|
1400
|
-
copy = context.outputDocument.createElement(currentNode.nodeName);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
if (useAttributeSets) {
|
|
1404
|
-
this.applyAttributeSets(useAttributeSets, context, copy);
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
this.processChildren(node, context, copy);
|
|
1408
|
-
output.appendChild(copy);
|
|
1409
|
-
break;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
case 2: // Attribute
|
|
1413
|
-
if (output.nodeType === 1) {
|
|
1414
|
-
output.setAttribute(currentNode.name, currentNode.value);
|
|
1415
|
-
}
|
|
1416
|
-
break;
|
|
1417
|
-
|
|
1418
|
-
case 3: // Text
|
|
1419
|
-
case 4: {
|
|
1420
|
-
// CDATA
|
|
1421
|
-
const textCopy = context.outputDocument.createTextNode(
|
|
1422
|
-
currentNode.nodeValue || "",
|
|
1423
|
-
);
|
|
1424
|
-
output.appendChild(textCopy);
|
|
1425
|
-
break;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
case 7: {
|
|
1429
|
-
// Processing Instruction
|
|
1430
|
-
const piCopy = context.outputDocument.createProcessingInstruction(
|
|
1431
|
-
currentNode.target,
|
|
1432
|
-
currentNode.data,
|
|
1433
|
-
);
|
|
1434
|
-
output.appendChild(piCopy);
|
|
1435
|
-
break;
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
case 8: {
|
|
1439
|
-
// Comment
|
|
1440
|
-
const commentCopy = context.outputDocument.createComment(
|
|
1441
|
-
currentNode.nodeValue || "",
|
|
1442
|
-
);
|
|
1443
|
-
output.appendChild(commentCopy);
|
|
1444
|
-
break;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
case 9: // Document
|
|
1448
|
-
case 11: // Document Fragment
|
|
1449
|
-
this.processChildren(node, context, output);
|
|
1450
|
-
break;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
xslCopyOf(node, context, output) {
|
|
1455
|
-
const select = node.getAttribute("select");
|
|
1456
|
-
const result = this.evaluateXPath(select, context);
|
|
1457
|
-
|
|
1458
|
-
this.copyToOutput(result, context, output);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
copyToOutput(value, context, output) {
|
|
1462
|
-
if (Array.isArray(value)) {
|
|
1463
|
-
for (const item of value) {
|
|
1464
|
-
this.copyToOutput(item, context, output);
|
|
1465
|
-
}
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
if (value && value.nodeType) {
|
|
1470
|
-
// Deep copy node
|
|
1471
|
-
const clone = this.deepCloneNode(value, context.outputDocument);
|
|
1472
|
-
output.appendChild(clone);
|
|
1473
|
-
} else if (
|
|
1474
|
-
typeof value === "string" ||
|
|
1475
|
-
typeof value === "number" ||
|
|
1476
|
-
typeof value === "boolean"
|
|
1477
|
-
) {
|
|
1478
|
-
const text = context.outputDocument.createTextNode(String(value));
|
|
1479
|
-
output.appendChild(text);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
deepCloneNode(node, targetDoc) {
|
|
1484
|
-
switch (node.nodeType) {
|
|
1485
|
-
case 1: {
|
|
1486
|
-
// Element
|
|
1487
|
-
let clone;
|
|
1488
|
-
if (node.namespaceURI && targetDoc.createElementNS) {
|
|
1489
|
-
clone = targetDoc.createElementNS(node.namespaceURI, node.nodeName);
|
|
1490
|
-
} else {
|
|
1491
|
-
clone = targetDoc.createElement(node.nodeName);
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
if (node.attributes) {
|
|
1495
|
-
for (const attr of node.attributes) {
|
|
1496
|
-
clone.setAttribute(attr.name, attr.value);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
for (const child of node.childNodes) {
|
|
1501
|
-
clone.appendChild(this.deepCloneNode(child, targetDoc));
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
return clone;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
case 3: // Text
|
|
1508
|
-
case 4: // CDATA
|
|
1509
|
-
return targetDoc.createTextNode(node.nodeValue || "");
|
|
1510
|
-
|
|
1511
|
-
case 7: // Processing Instruction
|
|
1512
|
-
return targetDoc.createProcessingInstruction(node.target, node.data);
|
|
1513
|
-
|
|
1514
|
-
case 8: // Comment
|
|
1515
|
-
return targetDoc.createComment(node.nodeValue || "");
|
|
1516
|
-
|
|
1517
|
-
case 11: {
|
|
1518
|
-
// Document Fragment
|
|
1519
|
-
const frag = targetDoc.createDocumentFragment();
|
|
1520
|
-
for (const child of node.childNodes) {
|
|
1521
|
-
frag.appendChild(this.deepCloneNode(child, targetDoc));
|
|
1522
|
-
}
|
|
1523
|
-
return frag;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
default:
|
|
1527
|
-
return targetDoc.createTextNode("");
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
xslVariable(node, context, _output) {
|
|
1532
|
-
const name = node.getAttribute("name");
|
|
1533
|
-
const select = node.getAttribute("select");
|
|
1534
|
-
|
|
1535
|
-
let value;
|
|
1536
|
-
if (select) {
|
|
1537
|
-
value = this.evaluateXPath(select, context);
|
|
1538
|
-
} else {
|
|
1539
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1540
|
-
this.processChildren(node, context, fragment);
|
|
1541
|
-
value = fragment;
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
context.setVariable(name, value);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
xslComment(node, context, output) {
|
|
1548
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1549
|
-
this.processChildren(node, context, fragment);
|
|
1550
|
-
|
|
1551
|
-
let text = "";
|
|
1552
|
-
const getText = (n) => {
|
|
1553
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1554
|
-
text += n.nodeValue || "";
|
|
1555
|
-
} else if (n.childNodes) {
|
|
1556
|
-
for (const child of n.childNodes) {
|
|
1557
|
-
getText(child);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
};
|
|
1561
|
-
getText(fragment);
|
|
1562
|
-
|
|
1563
|
-
const comment = context.outputDocument.createComment(text);
|
|
1564
|
-
output.appendChild(comment);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
xslProcessingInstruction(node, context, output) {
|
|
1568
|
-
const name = this.processAttributeValueTemplate(
|
|
1569
|
-
node.getAttribute("name"),
|
|
1570
|
-
context,
|
|
1571
|
-
);
|
|
1572
|
-
|
|
1573
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1574
|
-
this.processChildren(node, context, fragment);
|
|
1575
|
-
|
|
1576
|
-
let data = "";
|
|
1577
|
-
const getText = (n) => {
|
|
1578
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1579
|
-
data += n.nodeValue || "";
|
|
1580
|
-
} else if (n.childNodes) {
|
|
1581
|
-
for (const child of n.childNodes) {
|
|
1582
|
-
getText(child);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
};
|
|
1586
|
-
getText(fragment);
|
|
1587
|
-
|
|
1588
|
-
const pi = context.outputDocument.createProcessingInstruction(name, data);
|
|
1589
|
-
output.appendChild(pi);
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
xslNumber(node, context, output) {
|
|
1593
|
-
const value = node.getAttribute("value");
|
|
1594
|
-
const format = node.getAttribute("format") || "1";
|
|
1595
|
-
const level = node.getAttribute("level") || "single";
|
|
1596
|
-
|
|
1597
|
-
let number;
|
|
1598
|
-
if (value) {
|
|
1599
|
-
number = Math.round(
|
|
1600
|
-
this.xpathEvaluator.toNumber(this.evaluateXPath(value, context)),
|
|
1601
|
-
);
|
|
1602
|
-
} else {
|
|
1603
|
-
// Count based on level
|
|
1604
|
-
number = this.countNumber(context.currentNode, level, node, context);
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
const formatted = this.formatNumber(number, format);
|
|
1608
|
-
const text = context.outputDocument.createTextNode(formatted);
|
|
1609
|
-
output.appendChild(text);
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
countNumber(node, level, spec, context) {
|
|
1613
|
-
const count = spec.getAttribute("count");
|
|
1614
|
-
const _from = spec.getAttribute("from");
|
|
1615
|
-
|
|
1616
|
-
// Simplified implementation
|
|
1617
|
-
if (level === "single") {
|
|
1618
|
-
// Count preceding siblings matching pattern
|
|
1619
|
-
let n = 1;
|
|
1620
|
-
let sibling = node.previousSibling;
|
|
1621
|
-
while (sibling) {
|
|
1622
|
-
if (sibling.nodeType === 1) {
|
|
1623
|
-
if (!count || this.matchesPattern(sibling, count, context)) {
|
|
1624
|
-
n++;
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
sibling = sibling.previousSibling;
|
|
1628
|
-
}
|
|
1629
|
-
return n;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
return 1;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
formatNumber(number, format) {
|
|
1636
|
-
// Simple format implementation
|
|
1637
|
-
if (/^[0-9]+$/.test(format)) {
|
|
1638
|
-
return String(number).padStart(format.length, "0");
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
if (format === "a") {
|
|
1642
|
-
return String.fromCharCode(96 + ((number - 1) % 26) + 1);
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
if (format === "A") {
|
|
1646
|
-
return String.fromCharCode(64 + ((number - 1) % 26) + 1);
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
if (format === "i") {
|
|
1650
|
-
return this.toRoman(number).toLowerCase();
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (format === "I") {
|
|
1654
|
-
return this.toRoman(number);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
return String(number);
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
toRoman(num) {
|
|
1661
|
-
const romanNumerals = [
|
|
1662
|
-
["M", 1000],
|
|
1663
|
-
["CM", 900],
|
|
1664
|
-
["D", 500],
|
|
1665
|
-
["CD", 400],
|
|
1666
|
-
["C", 100],
|
|
1667
|
-
["XC", 90],
|
|
1668
|
-
["L", 50],
|
|
1669
|
-
["XL", 40],
|
|
1670
|
-
["X", 10],
|
|
1671
|
-
["IX", 9],
|
|
1672
|
-
["V", 5],
|
|
1673
|
-
["IV", 4],
|
|
1674
|
-
["I", 1],
|
|
1675
|
-
];
|
|
1676
|
-
|
|
1677
|
-
let result = "";
|
|
1678
|
-
for (const [numeral, value] of romanNumerals) {
|
|
1679
|
-
while (num >= value) {
|
|
1680
|
-
result += numeral;
|
|
1681
|
-
num -= value;
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
return result;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
xslMessage(node, context, _output) {
|
|
1688
|
-
const terminate = node.getAttribute("terminate") === "yes";
|
|
1689
|
-
|
|
1690
|
-
const fragment = context.outputDocument.createDocumentFragment();
|
|
1691
|
-
this.processChildren(node, context, fragment);
|
|
1692
|
-
|
|
1693
|
-
let text = "";
|
|
1694
|
-
const getText = (n) => {
|
|
1695
|
-
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1696
|
-
text += n.nodeValue || "";
|
|
1697
|
-
} else if (n.childNodes) {
|
|
1698
|
-
for (const child of n.childNodes) {
|
|
1699
|
-
getText(child);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
};
|
|
1703
|
-
getText(fragment);
|
|
1704
|
-
|
|
1705
|
-
console.log("XSLT Message:", text);
|
|
1706
|
-
|
|
1707
|
-
if (terminate) {
|
|
1708
|
-
throw new Error(`XSLT terminated: ${text}`);
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
applyAttributeSets(names, context, element) {
|
|
1713
|
-
const setNames = names.split(/\s+/).filter(Boolean);
|
|
1714
|
-
|
|
1715
|
-
for (const name of setNames) {
|
|
1716
|
-
const attrSet = this.attributeSets[name];
|
|
1717
|
-
if (attrSet) {
|
|
1718
|
-
// Apply inherited sets first
|
|
1719
|
-
if (attrSet.useAttributeSets.length > 0) {
|
|
1720
|
-
this.applyAttributeSets(
|
|
1721
|
-
attrSet.useAttributeSets.join(" "),
|
|
1722
|
-
context,
|
|
1723
|
-
element,
|
|
1724
|
-
);
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
// Apply attributes from this set
|
|
1728
|
-
for (const child of attrSet.node.childNodes) {
|
|
1729
|
-
if (child.nodeType === 1 && this.isXsltElement(child, "attribute")) {
|
|
1730
|
-
this.xslAttribute(child, context, element);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
sortNodes(nodes, sortSpecs, context) {
|
|
1738
|
-
return [...nodes].sort((a, b) => {
|
|
1739
|
-
for (const spec of sortSpecs) {
|
|
1740
|
-
const contextA = context.clone({ currentNode: a });
|
|
1741
|
-
const contextB = context.clone({ currentNode: b });
|
|
1742
|
-
|
|
1743
|
-
let valueA = this.evaluateXPath(spec.select, contextA);
|
|
1744
|
-
let valueB = this.evaluateXPath(spec.select, contextB);
|
|
1745
|
-
|
|
1746
|
-
// Convert to string for comparison (ensure non-null values)
|
|
1747
|
-
valueA = this.xpathEvaluator.toString(valueA) || "";
|
|
1748
|
-
valueB = this.xpathEvaluator.toString(valueB) || "";
|
|
1749
|
-
|
|
1750
|
-
if (spec.dataType === "number") {
|
|
1751
|
-
valueA = parseFloat(valueA) || 0;
|
|
1752
|
-
valueB = parseFloat(valueB) || 0;
|
|
1753
|
-
} else {
|
|
1754
|
-
// Text comparison
|
|
1755
|
-
if (spec.caseOrder === "lower-first") {
|
|
1756
|
-
valueA = valueA.toLowerCase();
|
|
1757
|
-
valueB = valueB.toLowerCase();
|
|
1758
|
-
} else {
|
|
1759
|
-
valueA = valueA.toUpperCase();
|
|
1760
|
-
valueB = valueB.toUpperCase();
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
let cmp;
|
|
1765
|
-
if (typeof valueA === "number") {
|
|
1766
|
-
cmp = valueA - valueB;
|
|
1767
|
-
} else {
|
|
1768
|
-
cmp = valueA.localeCompare(valueB, spec.lang || undefined);
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
if (spec.order === "descending") {
|
|
1772
|
-
cmp = -cmp;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
if (cmp !== 0) return cmp;
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
return 0;
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
183
|
evaluateXPath(expr, context) {
|
|
1783
184
|
const ast = parseXPath(expr);
|
|
1784
185
|
const xpathContext = new XPathContext(
|
|
1785
186
|
context.currentNode,
|
|
1786
187
|
context.position,
|
|
1787
188
|
context.currentNodeList.length,
|
|
1788
|
-
|
|
189
|
+
context.xpathVariables,
|
|
1789
190
|
context.namespaces,
|
|
191
|
+
context,
|
|
1790
192
|
);
|
|
1791
193
|
return this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
1792
194
|
}
|
|
1793
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Whether a node is in the XSLT namespace (or uses the xsl prefix).
|
|
198
|
+
*
|
|
199
|
+
* @param {Node} node - A stylesheet node
|
|
200
|
+
* @returns {boolean} True for an XSLT element
|
|
201
|
+
*/
|
|
1794
202
|
isXsltNamespace(node) {
|
|
1795
203
|
return (
|
|
1796
|
-
node.namespaceURI ===
|
|
204
|
+
node.namespaceURI === XSLT_NAMESPACE ||
|
|
1797
205
|
(node.nodeName && node.nodeName.startsWith("xsl:"))
|
|
1798
206
|
);
|
|
1799
207
|
}
|
|
1800
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Whether a node is the XSLT element of a local name.
|
|
211
|
+
*
|
|
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
|
|
215
|
+
*/
|
|
1801
216
|
isXsltElement(node, localName) {
|
|
1802
217
|
if (node.nodeType !== 1) return false;
|
|
1803
218
|
|
|
1804
219
|
const nodeName = node.localName || node.nodeName;
|
|
1805
220
|
return (
|
|
1806
|
-
(node.namespaceURI ===
|
|
221
|
+
(node.namespaceURI === XSLT_NAMESPACE && nodeName === localName) ||
|
|
1807
222
|
node.nodeName === `xsl:${localName}`
|
|
1808
223
|
);
|
|
1809
224
|
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Report a recoverable stylesheet error with console.warn, once per engine
|
|
228
|
+
* and message (libxslt reports these errors and goes on).
|
|
229
|
+
*
|
|
230
|
+
* @param {string} message - The error description
|
|
231
|
+
* @returns {void}
|
|
232
|
+
*/
|
|
233
|
+
warnOnce(message) {
|
|
234
|
+
if (this.reportedWarnings.has(message)) return;
|
|
235
|
+
this.reportedWarnings.add(message);
|
|
236
|
+
console.warn(`XSLT: ${message}`);
|
|
237
|
+
}
|
|
1810
238
|
}
|
|
239
|
+
|
|
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
|
+
);
|