@tradik/xslt-processor 1.0.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 +29 -0
- package/LICENSE.md +28 -0
- package/README.md +634 -0
- package/bin/xslt.js +208 -0
- package/dist/xslt-processor.browser.js +3302 -0
- package/dist/xslt-processor.browser.js.map +7 -0
- package/dist/xslt-processor.browser.min.js +6 -0
- package/dist/xslt-processor.browser.min.js.map +7 -0
- package/dist/xslt-processor.cjs +3311 -0
- package/dist/xslt-processor.cjs.map +7 -0
- package/dist/xslt-processor.d.ts +211 -0
- package/dist/xslt-processor.js +3271 -0
- package/dist/xslt-processor.js.map +7 -0
- package/package.json +68 -0
- package/src/XSLTProcessor.js +368 -0
- package/src/XSLTProcessor.test.js +930 -0
- package/src/index.js +66 -0
- package/src/xpath/evaluator.js +1012 -0
- package/src/xpath/evaluator.test.js +1852 -0
- package/src/xpath/index.js +67 -0
- package/src/xpath/parser.js +595 -0
- package/src/xpath/tokenizer.js +383 -0
- package/src/xpath/tokenizer.test.js +224 -0
- package/src/xslt/engine.js +1812 -0
- package/src/xslt/engine.test.js +3130 -0
- package/src/xslt/index.js +6 -0
|
@@ -0,0 +1,1812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSLT 1.0 Processing Engine
|
|
3
|
+
* Based on W3C XSLT 1.0 Specification: http://www.w3.org/TR/1999/REC-xslt-19991116
|
|
4
|
+
*
|
|
5
|
+
* Processes XSLT stylesheets and transforms XML documents.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parse as parseXPath } from "../xpath/parser.js";
|
|
9
|
+
import { XPathEvaluator, XPathContext } from "../xpath/evaluator.js";
|
|
10
|
+
|
|
11
|
+
const XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* XSLT Processing Context
|
|
15
|
+
*/
|
|
16
|
+
export class XsltContext {
|
|
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
|
+
}
|
|
32
|
+
|
|
33
|
+
clone(overrides = {}) {
|
|
34
|
+
return new XsltContext({
|
|
35
|
+
currentNode: overrides.currentNode ?? this.currentNode,
|
|
36
|
+
currentNodeList: overrides.currentNodeList ?? this.currentNodeList,
|
|
37
|
+
position: overrides.position ?? this.position,
|
|
38
|
+
variables: overrides.variables
|
|
39
|
+
? { ...this.variables, ...overrides.variables }
|
|
40
|
+
: { ...this.variables },
|
|
41
|
+
parameters: overrides.parameters
|
|
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
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* XSLT Engine
|
|
74
|
+
*/
|
|
75
|
+
export class XsltEngine {
|
|
76
|
+
constructor(options = {}) {
|
|
77
|
+
this.xpathEvaluator = new XPathEvaluator();
|
|
78
|
+
this.templates = [];
|
|
79
|
+
this.keys = {};
|
|
80
|
+
this.globalVariables = {};
|
|
81
|
+
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
|
+
};
|
|
92
|
+
this.namespaces = {};
|
|
93
|
+
this.decimalFormats = {};
|
|
94
|
+
this.stylesheetDoc = null;
|
|
95
|
+
this.attributeSets = {};
|
|
96
|
+
this.namespaceAliases = {};
|
|
97
|
+
this.stripSpace = [];
|
|
98
|
+
this.preserveSpace = [];
|
|
99
|
+
|
|
100
|
+
// Import/Include support
|
|
101
|
+
this.stylesheetLoader = options.stylesheetLoader || null;
|
|
102
|
+
this.currentImportPrecedence = 0;
|
|
103
|
+
this.processedStylesheets = new Set();
|
|
104
|
+
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
|
+
|
|
449
|
+
calculatePriority(matchPattern) {
|
|
450
|
+
if (!matchPattern) return 0.5;
|
|
451
|
+
|
|
452
|
+
// Simplified priority calculation based on XPath 1.0 spec
|
|
453
|
+
// - NodeType or * have priority -0.5
|
|
454
|
+
// - NCName:* has priority -0.25
|
|
455
|
+
// - QName has priority 0
|
|
456
|
+
// - Other patterns have priority 0.5
|
|
457
|
+
|
|
458
|
+
if (
|
|
459
|
+
matchPattern === "*" ||
|
|
460
|
+
matchPattern === "node()" ||
|
|
461
|
+
matchPattern === "text()" ||
|
|
462
|
+
matchPattern === "comment()" ||
|
|
463
|
+
matchPattern === "processing-instruction()"
|
|
464
|
+
) {
|
|
465
|
+
return -0.5;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (matchPattern.includes(":*")) {
|
|
469
|
+
return -0.25;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (/^[a-zA-Z_][\w.-]*$/.test(matchPattern)) {
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return 0.5;
|
|
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
|
|
592
|
+
const context = new XsltContext({
|
|
593
|
+
currentNode: sourceNode.documentElement || sourceNode,
|
|
594
|
+
currentNodeList: [sourceNode.documentElement || sourceNode],
|
|
595
|
+
position: 1,
|
|
596
|
+
outputDocument: doc,
|
|
597
|
+
stylesheet: this.stylesheetDoc,
|
|
598
|
+
namespaces: { ...this.namespaces },
|
|
599
|
+
templates: this.templates,
|
|
600
|
+
keys: this.keys,
|
|
601
|
+
decimalFormats: this.decimalFormats,
|
|
602
|
+
outputMethod: this.outputSettings.method,
|
|
603
|
+
xpathEvaluator: this.xpathEvaluator,
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Evaluate global variables
|
|
607
|
+
for (const [name, def] of Object.entries(this.globalParameters)) {
|
|
608
|
+
if (!(name in context.parameters)) {
|
|
609
|
+
context.parameters[name] = this.evaluateVariable(def, context);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const [name, def] of Object.entries(this.globalVariables)) {
|
|
614
|
+
context.variables[name] = this.evaluateVariable(def, context);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Create result document fragment
|
|
618
|
+
const fragment = doc.createDocumentFragment();
|
|
619
|
+
|
|
620
|
+
// Apply templates to root
|
|
621
|
+
this.applyTemplates(
|
|
622
|
+
[sourceNode.documentElement || sourceNode],
|
|
623
|
+
null,
|
|
624
|
+
context,
|
|
625
|
+
fragment,
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
return fragment;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Transform to a complete document
|
|
633
|
+
*/
|
|
634
|
+
transformToDocument(sourceNode) {
|
|
635
|
+
// For Node.js environments, we need a document implementation
|
|
636
|
+
const doc = this.createDocument();
|
|
637
|
+
const fragment = this.transform(sourceNode, doc);
|
|
638
|
+
|
|
639
|
+
// Move fragment contents to document
|
|
640
|
+
while (fragment.firstChild) {
|
|
641
|
+
doc.appendChild(fragment.firstChild);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return doc;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
createDocument() {
|
|
648
|
+
if (typeof document !== "undefined") {
|
|
649
|
+
return document.implementation.createDocument(null, null, null);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// For Node.js - would need JSDOM or similar
|
|
653
|
+
throw new Error("Document creation not available in this environment");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
evaluateVariable(def, context) {
|
|
657
|
+
if (def.select) {
|
|
658
|
+
return this.evaluateXPath(def.select, context);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// If no select, evaluate content as result tree fragment
|
|
662
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
663
|
+
this.processChildren(def.node, context, fragment);
|
|
664
|
+
return fragment;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Apply templates to a node list
|
|
669
|
+
*/
|
|
670
|
+
applyTemplates(nodes, mode, context, output) {
|
|
671
|
+
const nodeList = Array.isArray(nodes) ? nodes : [nodes];
|
|
672
|
+
|
|
673
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
674
|
+
const node = nodeList[i];
|
|
675
|
+
const template = this.findMatchingTemplate(node, mode, context);
|
|
676
|
+
|
|
677
|
+
if (template) {
|
|
678
|
+
const newContext = context.clone({
|
|
679
|
+
currentNode: node,
|
|
680
|
+
currentNodeList: nodeList,
|
|
681
|
+
position: i + 1,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
this.processTemplate(template.node, newContext, output);
|
|
685
|
+
} else {
|
|
686
|
+
// Built-in templates
|
|
687
|
+
this.applyBuiltinTemplate(node, mode, context, output);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Find the best matching template for a node
|
|
694
|
+
*/
|
|
695
|
+
findMatchingTemplate(node, mode, context) {
|
|
696
|
+
let bestMatch = null;
|
|
697
|
+
let bestPriority = -Infinity;
|
|
698
|
+
let bestImportPrecedence = -Infinity;
|
|
699
|
+
|
|
700
|
+
for (const template of this.templates) {
|
|
701
|
+
if (template.mode !== mode) continue;
|
|
702
|
+
if (!template.match) continue;
|
|
703
|
+
|
|
704
|
+
if (this.matchesPattern(node, template.match, context)) {
|
|
705
|
+
const priority = template.priority;
|
|
706
|
+
const importPrecedence = template.importPrecedence || 0;
|
|
707
|
+
|
|
708
|
+
if (
|
|
709
|
+
importPrecedence > bestImportPrecedence ||
|
|
710
|
+
(importPrecedence === bestImportPrecedence && priority > bestPriority)
|
|
711
|
+
) {
|
|
712
|
+
bestMatch = template;
|
|
713
|
+
bestPriority = priority;
|
|
714
|
+
bestImportPrecedence = importPrecedence;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return bestMatch;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Check if a node matches an XSLT pattern
|
|
724
|
+
*/
|
|
725
|
+
matchesPattern(node, pattern, context) {
|
|
726
|
+
// Split union patterns
|
|
727
|
+
const patterns = this.splitUnionPattern(pattern);
|
|
728
|
+
|
|
729
|
+
for (const p of patterns) {
|
|
730
|
+
if (this.matchesSinglePattern(node, p.trim(), context)) {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
splitUnionPattern(pattern) {
|
|
739
|
+
// Simple split on | not inside predicates or strings
|
|
740
|
+
const parts = [];
|
|
741
|
+
let current = "";
|
|
742
|
+
let depth = 0;
|
|
743
|
+
let inString = false;
|
|
744
|
+
let stringChar = "";
|
|
745
|
+
|
|
746
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
747
|
+
const char = pattern[i];
|
|
748
|
+
|
|
749
|
+
if (inString) {
|
|
750
|
+
current += char;
|
|
751
|
+
if (char === stringChar) {
|
|
752
|
+
inString = false;
|
|
753
|
+
}
|
|
754
|
+
} else if (char === '"' || char === "'") {
|
|
755
|
+
inString = true;
|
|
756
|
+
stringChar = char;
|
|
757
|
+
current += char;
|
|
758
|
+
} else if (char === "[") {
|
|
759
|
+
depth++;
|
|
760
|
+
current += char;
|
|
761
|
+
} else if (char === "]") {
|
|
762
|
+
depth--;
|
|
763
|
+
current += char;
|
|
764
|
+
} else if (char === "|" && depth === 0) {
|
|
765
|
+
parts.push(current);
|
|
766
|
+
current = "";
|
|
767
|
+
} else {
|
|
768
|
+
current += char;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (current) {
|
|
773
|
+
parts.push(current);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return parts;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
matchesSinglePattern(node, pattern, context) {
|
|
780
|
+
try {
|
|
781
|
+
// Handle root pattern
|
|
782
|
+
if (pattern === "/") {
|
|
783
|
+
return (
|
|
784
|
+
node.nodeType === 9 || node === node.ownerDocument?.documentElement
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Handle patterns like "item" (child match)
|
|
789
|
+
// Need to check if node would be selected by pattern from parent
|
|
790
|
+
const ast = parseXPath(pattern);
|
|
791
|
+
|
|
792
|
+
// For patterns starting with /, evaluate from root
|
|
793
|
+
if (pattern.startsWith("/")) {
|
|
794
|
+
const doc = node.ownerDocument || node;
|
|
795
|
+
const xpathContext = new XPathContext(
|
|
796
|
+
doc,
|
|
797
|
+
1,
|
|
798
|
+
1,
|
|
799
|
+
{ ...context.variables, ...context.parameters },
|
|
800
|
+
context.namespaces,
|
|
801
|
+
);
|
|
802
|
+
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
803
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
804
|
+
return nodes.includes(node);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// For relative patterns, check if this node matches when evaluated from parent
|
|
808
|
+
if (node.parentNode) {
|
|
809
|
+
const xpathContext = new XPathContext(
|
|
810
|
+
node.parentNode,
|
|
811
|
+
1,
|
|
812
|
+
1,
|
|
813
|
+
{ ...context.variables, ...context.parameters },
|
|
814
|
+
context.namespaces,
|
|
815
|
+
);
|
|
816
|
+
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
817
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
818
|
+
return nodes.includes(node);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// For document node without parent
|
|
822
|
+
const xpathContext = new XPathContext(
|
|
823
|
+
node,
|
|
824
|
+
1,
|
|
825
|
+
1,
|
|
826
|
+
{ ...context.variables, ...context.parameters },
|
|
827
|
+
context.namespaces,
|
|
828
|
+
);
|
|
829
|
+
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
830
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
831
|
+
return nodes.includes(node);
|
|
832
|
+
} catch {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Apply built-in template rules
|
|
839
|
+
*/
|
|
840
|
+
applyBuiltinTemplate(node, mode, context, output) {
|
|
841
|
+
switch (node.nodeType) {
|
|
842
|
+
case 1: // Element
|
|
843
|
+
case 9: // Document
|
|
844
|
+
case 11: // Document Fragment
|
|
845
|
+
// Process children
|
|
846
|
+
this.applyTemplates(Array.from(node.childNodes), mode, context, output);
|
|
847
|
+
break;
|
|
848
|
+
|
|
849
|
+
case 3: // Text
|
|
850
|
+
case 4: {
|
|
851
|
+
// CDATA
|
|
852
|
+
// Copy text value
|
|
853
|
+
const text = context.outputDocument.createTextNode(
|
|
854
|
+
node.nodeValue || "",
|
|
855
|
+
);
|
|
856
|
+
output.appendChild(text);
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
case 2: {
|
|
861
|
+
// Attribute
|
|
862
|
+
// Copy attribute value as text
|
|
863
|
+
const attrText = context.outputDocument.createTextNode(
|
|
864
|
+
node.nodeValue || "",
|
|
865
|
+
);
|
|
866
|
+
output.appendChild(attrText);
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Comments and PIs have no built-in template
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Process template content
|
|
876
|
+
*/
|
|
877
|
+
processTemplate(templateNode, context, output) {
|
|
878
|
+
// Process template parameters first
|
|
879
|
+
const localContext = context.clone();
|
|
880
|
+
|
|
881
|
+
for (const child of templateNode.childNodes) {
|
|
882
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "param")) {
|
|
883
|
+
const name = child.getAttribute("name");
|
|
884
|
+
if (!(name in localContext.parameters)) {
|
|
885
|
+
localContext.parameters[name] = this.evaluateVariable(
|
|
886
|
+
{ node: child, select: child.getAttribute("select") },
|
|
887
|
+
localContext,
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
this.processChildren(templateNode, localContext, output);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Process child nodes of an XSLT element
|
|
898
|
+
*/
|
|
899
|
+
processChildren(node, context, output) {
|
|
900
|
+
for (const child of node.childNodes) {
|
|
901
|
+
this.processNode(child, context, output);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Process a single node in the stylesheet
|
|
907
|
+
*/
|
|
908
|
+
processNode(node, context, output) {
|
|
909
|
+
switch (node.nodeType) {
|
|
910
|
+
case 1: // Element
|
|
911
|
+
this.processElement(node, context, output);
|
|
912
|
+
break;
|
|
913
|
+
|
|
914
|
+
case 3: // Text
|
|
915
|
+
case 4: {
|
|
916
|
+
// CDATA
|
|
917
|
+
// Output text if not whitespace only (or if preserving space)
|
|
918
|
+
const text = node.nodeValue;
|
|
919
|
+
if (text && (text.trim() || this.shouldPreserveSpace(node))) {
|
|
920
|
+
const textNode = context.outputDocument.createTextNode(text);
|
|
921
|
+
output.appendChild(textNode);
|
|
922
|
+
}
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
shouldPreserveSpace(node) {
|
|
929
|
+
// Check xml:space attribute on ancestors
|
|
930
|
+
let current = node.parentNode;
|
|
931
|
+
while (current && current.nodeType === 1) {
|
|
932
|
+
const space = current.getAttribute("xml:space");
|
|
933
|
+
if (space === "preserve") return true;
|
|
934
|
+
if (space === "default") return false;
|
|
935
|
+
current = current.parentNode;
|
|
936
|
+
}
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Process an element in the stylesheet
|
|
942
|
+
*/
|
|
943
|
+
processElement(node, context, output) {
|
|
944
|
+
// Check if XSLT element
|
|
945
|
+
if (this.isXsltNamespace(node)) {
|
|
946
|
+
this.processXsltElement(node, context, output);
|
|
947
|
+
} else {
|
|
948
|
+
// Literal result element
|
|
949
|
+
this.processLiteralResultElement(node, context, output);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Process an XSLT instruction element
|
|
955
|
+
*/
|
|
956
|
+
processXsltElement(node, context, output) {
|
|
957
|
+
const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
|
|
958
|
+
|
|
959
|
+
switch (localName) {
|
|
960
|
+
case "apply-templates":
|
|
961
|
+
this.xslApplyTemplates(node, context, output);
|
|
962
|
+
break;
|
|
963
|
+
|
|
964
|
+
case "call-template":
|
|
965
|
+
this.xslCallTemplate(node, context, output);
|
|
966
|
+
break;
|
|
967
|
+
|
|
968
|
+
case "value-of":
|
|
969
|
+
this.xslValueOf(node, context, output);
|
|
970
|
+
break;
|
|
971
|
+
|
|
972
|
+
case "text":
|
|
973
|
+
this.xslText(node, context, output);
|
|
974
|
+
break;
|
|
975
|
+
|
|
976
|
+
case "element":
|
|
977
|
+
this.xslElement(node, context, output);
|
|
978
|
+
break;
|
|
979
|
+
|
|
980
|
+
case "attribute":
|
|
981
|
+
this.xslAttribute(node, context, output);
|
|
982
|
+
break;
|
|
983
|
+
|
|
984
|
+
case "if":
|
|
985
|
+
this.xslIf(node, context, output);
|
|
986
|
+
break;
|
|
987
|
+
|
|
988
|
+
case "choose":
|
|
989
|
+
this.xslChoose(node, context, output);
|
|
990
|
+
break;
|
|
991
|
+
|
|
992
|
+
case "for-each":
|
|
993
|
+
this.xslForEach(node, context, output);
|
|
994
|
+
break;
|
|
995
|
+
|
|
996
|
+
case "copy":
|
|
997
|
+
this.xslCopy(node, context, output);
|
|
998
|
+
break;
|
|
999
|
+
|
|
1000
|
+
case "copy-of":
|
|
1001
|
+
this.xslCopyOf(node, context, output);
|
|
1002
|
+
break;
|
|
1003
|
+
|
|
1004
|
+
case "variable":
|
|
1005
|
+
this.xslVariable(node, context, output);
|
|
1006
|
+
break;
|
|
1007
|
+
|
|
1008
|
+
case "param":
|
|
1009
|
+
// Params are processed at template start
|
|
1010
|
+
break;
|
|
1011
|
+
|
|
1012
|
+
case "comment":
|
|
1013
|
+
this.xslComment(node, context, output);
|
|
1014
|
+
break;
|
|
1015
|
+
|
|
1016
|
+
case "processing-instruction":
|
|
1017
|
+
this.xslProcessingInstruction(node, context, output);
|
|
1018
|
+
break;
|
|
1019
|
+
|
|
1020
|
+
case "number":
|
|
1021
|
+
this.xslNumber(node, context, output);
|
|
1022
|
+
break;
|
|
1023
|
+
|
|
1024
|
+
case "sort":
|
|
1025
|
+
// Handled by apply-templates and for-each
|
|
1026
|
+
break;
|
|
1027
|
+
|
|
1028
|
+
case "with-param":
|
|
1029
|
+
// Handled by call-template and apply-templates
|
|
1030
|
+
break;
|
|
1031
|
+
|
|
1032
|
+
case "message":
|
|
1033
|
+
this.xslMessage(node, context, output);
|
|
1034
|
+
break;
|
|
1035
|
+
|
|
1036
|
+
case "fallback":
|
|
1037
|
+
// Used for forward compatibility
|
|
1038
|
+
break;
|
|
1039
|
+
|
|
1040
|
+
default:
|
|
1041
|
+
console.warn(`Unknown XSLT element: ${localName}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Process a literal result element (non-XSLT)
|
|
1047
|
+
*/
|
|
1048
|
+
processLiteralResultElement(node, context, output) {
|
|
1049
|
+
// Create element in output
|
|
1050
|
+
let outputElement;
|
|
1051
|
+
const namespaceURI = node.namespaceURI;
|
|
1052
|
+
const nodeName = node.nodeName;
|
|
1053
|
+
|
|
1054
|
+
// Apply namespace aliases
|
|
1055
|
+
let resolvedNS = namespaceURI;
|
|
1056
|
+
if (namespaceURI) {
|
|
1057
|
+
for (const [from, to] of Object.entries(this.namespaceAliases)) {
|
|
1058
|
+
if (this.namespaces[from] === namespaceURI) {
|
|
1059
|
+
resolvedNS = this.namespaces[to] || to;
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (resolvedNS && context.outputDocument.createElementNS) {
|
|
1066
|
+
outputElement = context.outputDocument.createElementNS(
|
|
1067
|
+
resolvedNS,
|
|
1068
|
+
nodeName,
|
|
1069
|
+
);
|
|
1070
|
+
} else {
|
|
1071
|
+
outputElement = context.outputDocument.createElement(nodeName);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Copy attributes (except XSLT namespace)
|
|
1075
|
+
if (node.attributes) {
|
|
1076
|
+
for (const attr of node.attributes) {
|
|
1077
|
+
if (attr.namespaceURI === XSLT_NS) continue;
|
|
1078
|
+
if (attr.name.startsWith("xmlns")) continue;
|
|
1079
|
+
|
|
1080
|
+
// Process attribute value templates
|
|
1081
|
+
const value = this.processAttributeValueTemplate(attr.value, context);
|
|
1082
|
+
outputElement.setAttribute(attr.name, value);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Process children
|
|
1087
|
+
this.processChildren(node, context, outputElement);
|
|
1088
|
+
|
|
1089
|
+
output.appendChild(outputElement);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Process attribute value templates (expressions in curly braces)
|
|
1094
|
+
*/
|
|
1095
|
+
processAttributeValueTemplate(value, context) {
|
|
1096
|
+
if (!value.includes("{")) return value;
|
|
1097
|
+
|
|
1098
|
+
let result = "";
|
|
1099
|
+
let i = 0;
|
|
1100
|
+
|
|
1101
|
+
while (i < value.length) {
|
|
1102
|
+
if (value[i] === "{") {
|
|
1103
|
+
if (value[i + 1] === "{") {
|
|
1104
|
+
// Escaped brace
|
|
1105
|
+
result += "{";
|
|
1106
|
+
i += 2;
|
|
1107
|
+
} else {
|
|
1108
|
+
// Find closing brace
|
|
1109
|
+
let depth = 1;
|
|
1110
|
+
let j = i + 1;
|
|
1111
|
+
while (j < value.length && depth > 0) {
|
|
1112
|
+
if (value[j] === "{") depth++;
|
|
1113
|
+
else if (value[j] === "}") depth--;
|
|
1114
|
+
j++;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const expr = value.substring(i + 1, j - 1);
|
|
1118
|
+
const evalResult = this.evaluateXPath(expr, context);
|
|
1119
|
+
result += this.xpathEvaluator.toString(evalResult);
|
|
1120
|
+
i = j;
|
|
1121
|
+
}
|
|
1122
|
+
} else if (value[i] === "}") {
|
|
1123
|
+
if (value[i + 1] === "}") {
|
|
1124
|
+
// Escaped brace
|
|
1125
|
+
result += "}";
|
|
1126
|
+
i += 2;
|
|
1127
|
+
} else {
|
|
1128
|
+
throw new Error("Unmatched } in attribute value template");
|
|
1129
|
+
}
|
|
1130
|
+
} else {
|
|
1131
|
+
result += value[i];
|
|
1132
|
+
i++;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// XSLT Instructions
|
|
1140
|
+
xslApplyTemplates(node, context, output) {
|
|
1141
|
+
const select = node.getAttribute("select") || "node()";
|
|
1142
|
+
const mode = node.getAttribute("mode") || null;
|
|
1143
|
+
|
|
1144
|
+
// Evaluate select expression
|
|
1145
|
+
let nodes = this.evaluateXPath(select, context);
|
|
1146
|
+
if (!Array.isArray(nodes)) {
|
|
1147
|
+
nodes = nodes ? [nodes] : [];
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Collect sort specifications
|
|
1151
|
+
const sortSpecs = [];
|
|
1152
|
+
for (const child of node.childNodes) {
|
|
1153
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
1154
|
+
sortSpecs.push({
|
|
1155
|
+
select: child.getAttribute("select") || ".",
|
|
1156
|
+
order: child.getAttribute("order") || "ascending",
|
|
1157
|
+
dataType: child.getAttribute("data-type") || "text",
|
|
1158
|
+
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
1159
|
+
lang: child.getAttribute("lang"),
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Apply sorting
|
|
1165
|
+
if (sortSpecs.length > 0) {
|
|
1166
|
+
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Collect with-param values
|
|
1170
|
+
const params = {};
|
|
1171
|
+
for (const child of node.childNodes) {
|
|
1172
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
1173
|
+
const name = child.getAttribute("name");
|
|
1174
|
+
const selectAttr = child.getAttribute("select");
|
|
1175
|
+
if (selectAttr) {
|
|
1176
|
+
params[name] = this.evaluateXPath(selectAttr, context);
|
|
1177
|
+
} else {
|
|
1178
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
1179
|
+
this.processChildren(child, context, fragment);
|
|
1180
|
+
params[name] = fragment;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Apply templates with new context including params
|
|
1186
|
+
const newContext = context.clone({
|
|
1187
|
+
parameters: { ...context.parameters, ...params },
|
|
1188
|
+
});
|
|
1189
|
+
this.applyTemplates(nodes, mode, newContext, output);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
xslCallTemplate(node, context, output) {
|
|
1193
|
+
const name = node.getAttribute("name");
|
|
1194
|
+
|
|
1195
|
+
// Find named template
|
|
1196
|
+
const template = this.templates.find((t) => t.name === name);
|
|
1197
|
+
if (!template) {
|
|
1198
|
+
throw new Error(`Template not found: ${name}`);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Collect with-param values
|
|
1202
|
+
const params = {};
|
|
1203
|
+
for (const child of node.childNodes) {
|
|
1204
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
1205
|
+
const paramName = child.getAttribute("name");
|
|
1206
|
+
const selectAttr = child.getAttribute("select");
|
|
1207
|
+
if (selectAttr) {
|
|
1208
|
+
params[paramName] = this.evaluateXPath(selectAttr, context);
|
|
1209
|
+
} else {
|
|
1210
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
1211
|
+
this.processChildren(child, context, fragment);
|
|
1212
|
+
params[paramName] = fragment;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Call template with params
|
|
1218
|
+
const newContext = context.clone({
|
|
1219
|
+
parameters: { ...context.parameters, ...params },
|
|
1220
|
+
});
|
|
1221
|
+
this.processTemplate(template.node, newContext, output);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
xslValueOf(node, context, output) {
|
|
1225
|
+
const select = node.getAttribute("select");
|
|
1226
|
+
const disableOutputEscaping =
|
|
1227
|
+
node.getAttribute("disable-output-escaping") === "yes";
|
|
1228
|
+
|
|
1229
|
+
const result = this.evaluateXPath(select, context);
|
|
1230
|
+
const text = this.xpathEvaluator.toString(result);
|
|
1231
|
+
|
|
1232
|
+
if (text) {
|
|
1233
|
+
const textNode = context.outputDocument.createTextNode(text);
|
|
1234
|
+
if (disableOutputEscaping) {
|
|
1235
|
+
textNode._disableOutputEscaping = true;
|
|
1236
|
+
}
|
|
1237
|
+
output.appendChild(textNode);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
xslText(node, context, output) {
|
|
1242
|
+
const disableOutputEscaping =
|
|
1243
|
+
node.getAttribute("disable-output-escaping") === "yes";
|
|
1244
|
+
let text = "";
|
|
1245
|
+
|
|
1246
|
+
for (const child of node.childNodes) {
|
|
1247
|
+
if (child.nodeType === 3 || child.nodeType === 4) {
|
|
1248
|
+
text += child.nodeValue || "";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (text) {
|
|
1253
|
+
const textNode = context.outputDocument.createTextNode(text);
|
|
1254
|
+
if (disableOutputEscaping) {
|
|
1255
|
+
textNode._disableOutputEscaping = true;
|
|
1256
|
+
}
|
|
1257
|
+
output.appendChild(textNode);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
xslElement(node, context, output) {
|
|
1262
|
+
const name = this.processAttributeValueTemplate(
|
|
1263
|
+
node.getAttribute("name"),
|
|
1264
|
+
context,
|
|
1265
|
+
);
|
|
1266
|
+
const namespace = node.getAttribute("namespace");
|
|
1267
|
+
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
1268
|
+
|
|
1269
|
+
let element;
|
|
1270
|
+
if (namespace) {
|
|
1271
|
+
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
1272
|
+
element = context.outputDocument.createElementNS(ns, name);
|
|
1273
|
+
} else {
|
|
1274
|
+
element = context.outputDocument.createElement(name);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Apply attribute sets
|
|
1278
|
+
if (useAttributeSets) {
|
|
1279
|
+
this.applyAttributeSets(useAttributeSets, context, element);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
this.processChildren(node, context, element);
|
|
1283
|
+
output.appendChild(element);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
xslAttribute(node, context, output) {
|
|
1287
|
+
const name = this.processAttributeValueTemplate(
|
|
1288
|
+
node.getAttribute("name"),
|
|
1289
|
+
context,
|
|
1290
|
+
);
|
|
1291
|
+
const namespace = node.getAttribute("namespace");
|
|
1292
|
+
|
|
1293
|
+
// Collect content
|
|
1294
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
1295
|
+
this.processChildren(node, context, fragment);
|
|
1296
|
+
|
|
1297
|
+
// Get text content
|
|
1298
|
+
let value = "";
|
|
1299
|
+
const getText = (n) => {
|
|
1300
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1301
|
+
value += n.nodeValue || "";
|
|
1302
|
+
} else if (n.childNodes) {
|
|
1303
|
+
for (const child of n.childNodes) {
|
|
1304
|
+
getText(child);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
getText(fragment);
|
|
1309
|
+
|
|
1310
|
+
// Add attribute to parent element
|
|
1311
|
+
if (output.nodeType === 1) {
|
|
1312
|
+
if (namespace) {
|
|
1313
|
+
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
1314
|
+
output.setAttributeNS(ns, name, value);
|
|
1315
|
+
} else {
|
|
1316
|
+
output.setAttribute(name, value);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
xslIf(node, context, output) {
|
|
1322
|
+
const test = node.getAttribute("test");
|
|
1323
|
+
const result = this.evaluateXPath(test, context);
|
|
1324
|
+
|
|
1325
|
+
if (this.xpathEvaluator.toBoolean(result)) {
|
|
1326
|
+
this.processChildren(node, context, output);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
xslChoose(node, context, output) {
|
|
1331
|
+
for (const child of node.childNodes) {
|
|
1332
|
+
if (child.nodeType !== 1) continue;
|
|
1333
|
+
|
|
1334
|
+
if (this.isXsltElement(child, "when")) {
|
|
1335
|
+
const test = child.getAttribute("test");
|
|
1336
|
+
const result = this.evaluateXPath(test, context);
|
|
1337
|
+
|
|
1338
|
+
if (this.xpathEvaluator.toBoolean(result)) {
|
|
1339
|
+
this.processChildren(child, context, output);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
} else if (this.isXsltElement(child, "otherwise")) {
|
|
1343
|
+
this.processChildren(child, context, output);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
xslForEach(node, context, output) {
|
|
1350
|
+
const select = node.getAttribute("select");
|
|
1351
|
+
|
|
1352
|
+
let nodes = this.evaluateXPath(select, context);
|
|
1353
|
+
if (!Array.isArray(nodes)) {
|
|
1354
|
+
nodes = nodes ? [nodes] : [];
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Collect sort specifications
|
|
1358
|
+
const sortSpecs = [];
|
|
1359
|
+
for (const child of node.childNodes) {
|
|
1360
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
1361
|
+
sortSpecs.push({
|
|
1362
|
+
select: child.getAttribute("select") || ".",
|
|
1363
|
+
order: child.getAttribute("order") || "ascending",
|
|
1364
|
+
dataType: child.getAttribute("data-type") || "text",
|
|
1365
|
+
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
1366
|
+
lang: child.getAttribute("lang"),
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Apply sorting
|
|
1372
|
+
if (sortSpecs.length > 0) {
|
|
1373
|
+
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Process each node
|
|
1377
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1378
|
+
const newContext = context.clone({
|
|
1379
|
+
currentNode: nodes[i],
|
|
1380
|
+
currentNodeList: nodes,
|
|
1381
|
+
position: i + 1,
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
this.processChildren(node, newContext, output);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
xslCopy(node, context, output) {
|
|
1389
|
+
const currentNode = context.currentNode;
|
|
1390
|
+
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
1391
|
+
|
|
1392
|
+
switch (currentNode.nodeType) {
|
|
1393
|
+
case 1: {
|
|
1394
|
+
// Element
|
|
1395
|
+
let copy;
|
|
1396
|
+
if (currentNode.namespaceURI) {
|
|
1397
|
+
copy = context.outputDocument.createElementNS(
|
|
1398
|
+
currentNode.namespaceURI,
|
|
1399
|
+
currentNode.nodeName,
|
|
1400
|
+
);
|
|
1401
|
+
} else {
|
|
1402
|
+
copy = context.outputDocument.createElement(currentNode.nodeName);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (useAttributeSets) {
|
|
1406
|
+
this.applyAttributeSets(useAttributeSets, context, copy);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
this.processChildren(node, context, copy);
|
|
1410
|
+
output.appendChild(copy);
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
case 2: // Attribute
|
|
1415
|
+
if (output.nodeType === 1) {
|
|
1416
|
+
output.setAttribute(currentNode.name, currentNode.value);
|
|
1417
|
+
}
|
|
1418
|
+
break;
|
|
1419
|
+
|
|
1420
|
+
case 3: // Text
|
|
1421
|
+
case 4: {
|
|
1422
|
+
// CDATA
|
|
1423
|
+
const textCopy = context.outputDocument.createTextNode(
|
|
1424
|
+
currentNode.nodeValue || "",
|
|
1425
|
+
);
|
|
1426
|
+
output.appendChild(textCopy);
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
case 7: {
|
|
1431
|
+
// Processing Instruction
|
|
1432
|
+
const piCopy = context.outputDocument.createProcessingInstruction(
|
|
1433
|
+
currentNode.target,
|
|
1434
|
+
currentNode.data,
|
|
1435
|
+
);
|
|
1436
|
+
output.appendChild(piCopy);
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
case 8: {
|
|
1441
|
+
// Comment
|
|
1442
|
+
const commentCopy = context.outputDocument.createComment(
|
|
1443
|
+
currentNode.nodeValue || "",
|
|
1444
|
+
);
|
|
1445
|
+
output.appendChild(commentCopy);
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
case 9: // Document
|
|
1450
|
+
case 11: // Document Fragment
|
|
1451
|
+
this.processChildren(node, context, output);
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
xslCopyOf(node, context, output) {
|
|
1457
|
+
const select = node.getAttribute("select");
|
|
1458
|
+
const result = this.evaluateXPath(select, context);
|
|
1459
|
+
|
|
1460
|
+
this.copyToOutput(result, context, output);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
copyToOutput(value, context, output) {
|
|
1464
|
+
if (Array.isArray(value)) {
|
|
1465
|
+
for (const item of value) {
|
|
1466
|
+
this.copyToOutput(item, context, output);
|
|
1467
|
+
}
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (value && value.nodeType) {
|
|
1472
|
+
// Deep copy node
|
|
1473
|
+
const clone = this.deepCloneNode(value, context.outputDocument);
|
|
1474
|
+
output.appendChild(clone);
|
|
1475
|
+
} else if (
|
|
1476
|
+
typeof value === "string" ||
|
|
1477
|
+
typeof value === "number" ||
|
|
1478
|
+
typeof value === "boolean"
|
|
1479
|
+
) {
|
|
1480
|
+
const text = context.outputDocument.createTextNode(String(value));
|
|
1481
|
+
output.appendChild(text);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
deepCloneNode(node, targetDoc) {
|
|
1486
|
+
switch (node.nodeType) {
|
|
1487
|
+
case 1: {
|
|
1488
|
+
// Element
|
|
1489
|
+
let clone;
|
|
1490
|
+
if (node.namespaceURI && targetDoc.createElementNS) {
|
|
1491
|
+
clone = targetDoc.createElementNS(node.namespaceURI, node.nodeName);
|
|
1492
|
+
} else {
|
|
1493
|
+
clone = targetDoc.createElement(node.nodeName);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (node.attributes) {
|
|
1497
|
+
for (const attr of node.attributes) {
|
|
1498
|
+
clone.setAttribute(attr.name, attr.value);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
for (const child of node.childNodes) {
|
|
1503
|
+
clone.appendChild(this.deepCloneNode(child, targetDoc));
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return clone;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
case 3: // Text
|
|
1510
|
+
case 4: // CDATA
|
|
1511
|
+
return targetDoc.createTextNode(node.nodeValue || "");
|
|
1512
|
+
|
|
1513
|
+
case 7: // Processing Instruction
|
|
1514
|
+
return targetDoc.createProcessingInstruction(node.target, node.data);
|
|
1515
|
+
|
|
1516
|
+
case 8: // Comment
|
|
1517
|
+
return targetDoc.createComment(node.nodeValue || "");
|
|
1518
|
+
|
|
1519
|
+
case 11: {
|
|
1520
|
+
// Document Fragment
|
|
1521
|
+
const frag = targetDoc.createDocumentFragment();
|
|
1522
|
+
for (const child of node.childNodes) {
|
|
1523
|
+
frag.appendChild(this.deepCloneNode(child, targetDoc));
|
|
1524
|
+
}
|
|
1525
|
+
return frag;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
default:
|
|
1529
|
+
return targetDoc.createTextNode("");
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
xslVariable(node, context, _output) {
|
|
1534
|
+
const name = node.getAttribute("name");
|
|
1535
|
+
const select = node.getAttribute("select");
|
|
1536
|
+
|
|
1537
|
+
let value;
|
|
1538
|
+
if (select) {
|
|
1539
|
+
value = this.evaluateXPath(select, context);
|
|
1540
|
+
} else {
|
|
1541
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
1542
|
+
this.processChildren(node, context, fragment);
|
|
1543
|
+
value = fragment;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
context.setVariable(name, value);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
xslComment(node, context, output) {
|
|
1550
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
1551
|
+
this.processChildren(node, context, fragment);
|
|
1552
|
+
|
|
1553
|
+
let text = "";
|
|
1554
|
+
const getText = (n) => {
|
|
1555
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1556
|
+
text += n.nodeValue || "";
|
|
1557
|
+
} else if (n.childNodes) {
|
|
1558
|
+
for (const child of n.childNodes) {
|
|
1559
|
+
getText(child);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
getText(fragment);
|
|
1564
|
+
|
|
1565
|
+
const comment = context.outputDocument.createComment(text);
|
|
1566
|
+
output.appendChild(comment);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
xslProcessingInstruction(node, context, output) {
|
|
1570
|
+
const name = this.processAttributeValueTemplate(
|
|
1571
|
+
node.getAttribute("name"),
|
|
1572
|
+
context,
|
|
1573
|
+
);
|
|
1574
|
+
|
|
1575
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
1576
|
+
this.processChildren(node, context, fragment);
|
|
1577
|
+
|
|
1578
|
+
let data = "";
|
|
1579
|
+
const getText = (n) => {
|
|
1580
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1581
|
+
data += n.nodeValue || "";
|
|
1582
|
+
} else if (n.childNodes) {
|
|
1583
|
+
for (const child of n.childNodes) {
|
|
1584
|
+
getText(child);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
getText(fragment);
|
|
1589
|
+
|
|
1590
|
+
const pi = context.outputDocument.createProcessingInstruction(name, data);
|
|
1591
|
+
output.appendChild(pi);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
xslNumber(node, context, output) {
|
|
1595
|
+
const value = node.getAttribute("value");
|
|
1596
|
+
const format = node.getAttribute("format") || "1";
|
|
1597
|
+
const level = node.getAttribute("level") || "single";
|
|
1598
|
+
|
|
1599
|
+
let number;
|
|
1600
|
+
if (value) {
|
|
1601
|
+
number = Math.round(
|
|
1602
|
+
this.xpathEvaluator.toNumber(this.evaluateXPath(value, context)),
|
|
1603
|
+
);
|
|
1604
|
+
} else {
|
|
1605
|
+
// Count based on level
|
|
1606
|
+
number = this.countNumber(context.currentNode, level, node, context);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const formatted = this.formatNumber(number, format);
|
|
1610
|
+
const text = context.outputDocument.createTextNode(formatted);
|
|
1611
|
+
output.appendChild(text);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
countNumber(node, level, spec, context) {
|
|
1615
|
+
const count = spec.getAttribute("count");
|
|
1616
|
+
const _from = spec.getAttribute("from");
|
|
1617
|
+
|
|
1618
|
+
// Simplified implementation
|
|
1619
|
+
if (level === "single") {
|
|
1620
|
+
// Count preceding siblings matching pattern
|
|
1621
|
+
let n = 1;
|
|
1622
|
+
let sibling = node.previousSibling;
|
|
1623
|
+
while (sibling) {
|
|
1624
|
+
if (sibling.nodeType === 1) {
|
|
1625
|
+
if (!count || this.matchesPattern(sibling, count, context)) {
|
|
1626
|
+
n++;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
sibling = sibling.previousSibling;
|
|
1630
|
+
}
|
|
1631
|
+
return n;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
return 1;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
formatNumber(number, format) {
|
|
1638
|
+
// Simple format implementation
|
|
1639
|
+
if (/^[0-9]+$/.test(format)) {
|
|
1640
|
+
return String(number).padStart(format.length, "0");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (format === "a") {
|
|
1644
|
+
return String.fromCharCode(96 + ((number - 1) % 26) + 1);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (format === "A") {
|
|
1648
|
+
return String.fromCharCode(64 + ((number - 1) % 26) + 1);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (format === "i") {
|
|
1652
|
+
return this.toRoman(number).toLowerCase();
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (format === "I") {
|
|
1656
|
+
return this.toRoman(number);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
return String(number);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
toRoman(num) {
|
|
1663
|
+
const romanNumerals = [
|
|
1664
|
+
["M", 1000],
|
|
1665
|
+
["CM", 900],
|
|
1666
|
+
["D", 500],
|
|
1667
|
+
["CD", 400],
|
|
1668
|
+
["C", 100],
|
|
1669
|
+
["XC", 90],
|
|
1670
|
+
["L", 50],
|
|
1671
|
+
["XL", 40],
|
|
1672
|
+
["X", 10],
|
|
1673
|
+
["IX", 9],
|
|
1674
|
+
["V", 5],
|
|
1675
|
+
["IV", 4],
|
|
1676
|
+
["I", 1],
|
|
1677
|
+
];
|
|
1678
|
+
|
|
1679
|
+
let result = "";
|
|
1680
|
+
for (const [numeral, value] of romanNumerals) {
|
|
1681
|
+
while (num >= value) {
|
|
1682
|
+
result += numeral;
|
|
1683
|
+
num -= value;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return result;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
xslMessage(node, context, _output) {
|
|
1690
|
+
const terminate = node.getAttribute("terminate") === "yes";
|
|
1691
|
+
|
|
1692
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
1693
|
+
this.processChildren(node, context, fragment);
|
|
1694
|
+
|
|
1695
|
+
let text = "";
|
|
1696
|
+
const getText = (n) => {
|
|
1697
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
1698
|
+
text += n.nodeValue || "";
|
|
1699
|
+
} else if (n.childNodes) {
|
|
1700
|
+
for (const child of n.childNodes) {
|
|
1701
|
+
getText(child);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
getText(fragment);
|
|
1706
|
+
|
|
1707
|
+
console.log("XSLT Message:", text);
|
|
1708
|
+
|
|
1709
|
+
if (terminate) {
|
|
1710
|
+
throw new Error(`XSLT terminated: ${text}`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
applyAttributeSets(names, context, element) {
|
|
1715
|
+
const setNames = names.split(/\s+/).filter(Boolean);
|
|
1716
|
+
|
|
1717
|
+
for (const name of setNames) {
|
|
1718
|
+
const attrSet = this.attributeSets[name];
|
|
1719
|
+
if (attrSet) {
|
|
1720
|
+
// Apply inherited sets first
|
|
1721
|
+
if (attrSet.useAttributeSets.length > 0) {
|
|
1722
|
+
this.applyAttributeSets(
|
|
1723
|
+
attrSet.useAttributeSets.join(" "),
|
|
1724
|
+
context,
|
|
1725
|
+
element,
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Apply attributes from this set
|
|
1730
|
+
for (const child of attrSet.node.childNodes) {
|
|
1731
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "attribute")) {
|
|
1732
|
+
this.xslAttribute(child, context, element);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
sortNodes(nodes, sortSpecs, context) {
|
|
1740
|
+
return [...nodes].sort((a, b) => {
|
|
1741
|
+
for (const spec of sortSpecs) {
|
|
1742
|
+
const contextA = context.clone({ currentNode: a });
|
|
1743
|
+
const contextB = context.clone({ currentNode: b });
|
|
1744
|
+
|
|
1745
|
+
let valueA = this.evaluateXPath(spec.select, contextA);
|
|
1746
|
+
let valueB = this.evaluateXPath(spec.select, contextB);
|
|
1747
|
+
|
|
1748
|
+
// Convert to string for comparison (ensure non-null values)
|
|
1749
|
+
valueA = this.xpathEvaluator.toString(valueA) || "";
|
|
1750
|
+
valueB = this.xpathEvaluator.toString(valueB) || "";
|
|
1751
|
+
|
|
1752
|
+
if (spec.dataType === "number") {
|
|
1753
|
+
valueA = parseFloat(valueA) || 0;
|
|
1754
|
+
valueB = parseFloat(valueB) || 0;
|
|
1755
|
+
} else {
|
|
1756
|
+
// Text comparison
|
|
1757
|
+
if (spec.caseOrder === "lower-first") {
|
|
1758
|
+
valueA = valueA.toLowerCase();
|
|
1759
|
+
valueB = valueB.toLowerCase();
|
|
1760
|
+
} else {
|
|
1761
|
+
valueA = valueA.toUpperCase();
|
|
1762
|
+
valueB = valueB.toUpperCase();
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
let cmp;
|
|
1767
|
+
if (typeof valueA === "number") {
|
|
1768
|
+
cmp = valueA - valueB;
|
|
1769
|
+
} else {
|
|
1770
|
+
cmp = valueA.localeCompare(valueB, spec.lang || undefined);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (spec.order === "descending") {
|
|
1774
|
+
cmp = -cmp;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (cmp !== 0) return cmp;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
return 0;
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
evaluateXPath(expr, context) {
|
|
1785
|
+
const ast = parseXPath(expr);
|
|
1786
|
+
const xpathContext = new XPathContext(
|
|
1787
|
+
context.currentNode,
|
|
1788
|
+
context.position,
|
|
1789
|
+
context.currentNodeList.length,
|
|
1790
|
+
{ ...context.variables, ...context.parameters },
|
|
1791
|
+
context.namespaces,
|
|
1792
|
+
);
|
|
1793
|
+
return this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
isXsltNamespace(node) {
|
|
1797
|
+
return (
|
|
1798
|
+
node.namespaceURI === XSLT_NS ||
|
|
1799
|
+
(node.nodeName && node.nodeName.startsWith("xsl:"))
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
isXsltElement(node, localName) {
|
|
1804
|
+
if (node.nodeType !== 1) return false;
|
|
1805
|
+
|
|
1806
|
+
const nodeName = node.localName || node.nodeName;
|
|
1807
|
+
return (
|
|
1808
|
+
(node.namespaceURI === XSLT_NS && nodeName === localName) ||
|
|
1809
|
+
node.nodeName === `xsl:${localName}`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|