@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,3271 @@
|
|
|
1
|
+
// src/xpath/tokenizer.js
|
|
2
|
+
var TokenType = {
|
|
3
|
+
// Literals
|
|
4
|
+
NUMBER: "NUMBER",
|
|
5
|
+
LITERAL: "LITERAL",
|
|
6
|
+
// Operators
|
|
7
|
+
SLASH: "SLASH",
|
|
8
|
+
DOUBLE_SLASH: "DOUBLE_SLASH",
|
|
9
|
+
PIPE: "PIPE",
|
|
10
|
+
PLUS: "PLUS",
|
|
11
|
+
MINUS: "MINUS",
|
|
12
|
+
STAR: "STAR",
|
|
13
|
+
DIV: "DIV",
|
|
14
|
+
MOD: "MOD",
|
|
15
|
+
EQUALS: "EQUALS",
|
|
16
|
+
NOT_EQUALS: "NOT_EQUALS",
|
|
17
|
+
LT: "LT",
|
|
18
|
+
LTE: "LTE",
|
|
19
|
+
GT: "GT",
|
|
20
|
+
GTE: "GTE",
|
|
21
|
+
AND: "AND",
|
|
22
|
+
OR: "OR",
|
|
23
|
+
// Brackets
|
|
24
|
+
LPAREN: "LPAREN",
|
|
25
|
+
RPAREN: "RPAREN",
|
|
26
|
+
LBRACKET: "LBRACKET",
|
|
27
|
+
RBRACKET: "RBRACKET",
|
|
28
|
+
// Axes
|
|
29
|
+
AXIS: "AXIS",
|
|
30
|
+
AT: "AT",
|
|
31
|
+
DOT: "DOT",
|
|
32
|
+
DOUBLE_DOT: "DOUBLE_DOT",
|
|
33
|
+
DOUBLE_COLON: "DOUBLE_COLON",
|
|
34
|
+
// Names
|
|
35
|
+
NAME: "NAME",
|
|
36
|
+
NCNAME: "NCNAME",
|
|
37
|
+
PREFIX: "PREFIX",
|
|
38
|
+
FUNCTION: "FUNCTION",
|
|
39
|
+
NODE_TYPE: "NODE_TYPE",
|
|
40
|
+
// Other
|
|
41
|
+
COMMA: "COMMA",
|
|
42
|
+
DOLLAR: "DOLLAR",
|
|
43
|
+
COLON: "COLON",
|
|
44
|
+
EOF: "EOF"
|
|
45
|
+
};
|
|
46
|
+
var AXIS_NAMES = /* @__PURE__ */ new Set([
|
|
47
|
+
"ancestor",
|
|
48
|
+
"ancestor-or-self",
|
|
49
|
+
"attribute",
|
|
50
|
+
"child",
|
|
51
|
+
"descendant",
|
|
52
|
+
"descendant-or-self",
|
|
53
|
+
"following",
|
|
54
|
+
"following-sibling",
|
|
55
|
+
"namespace",
|
|
56
|
+
"parent",
|
|
57
|
+
"preceding",
|
|
58
|
+
"preceding-sibling",
|
|
59
|
+
"self"
|
|
60
|
+
]);
|
|
61
|
+
var NODE_TYPES = /* @__PURE__ */ new Set([
|
|
62
|
+
"comment",
|
|
63
|
+
"text",
|
|
64
|
+
"processing-instruction",
|
|
65
|
+
"node"
|
|
66
|
+
]);
|
|
67
|
+
var OPERATORS = /* @__PURE__ */ new Set(["and", "or", "mod", "div"]);
|
|
68
|
+
var Token = class {
|
|
69
|
+
constructor(type, value, position) {
|
|
70
|
+
this.type = type;
|
|
71
|
+
this.value = value;
|
|
72
|
+
this.position = position;
|
|
73
|
+
}
|
|
74
|
+
toString() {
|
|
75
|
+
return `Token(${this.type}, ${JSON.stringify(this.value)}, pos=${this.position})`;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var OPERATOR_CONTEXT_BLOCKERS = /* @__PURE__ */ new Set([
|
|
79
|
+
TokenType.AT,
|
|
80
|
+
TokenType.DOUBLE_COLON,
|
|
81
|
+
TokenType.LPAREN,
|
|
82
|
+
TokenType.LBRACKET,
|
|
83
|
+
TokenType.COMMA,
|
|
84
|
+
// Operators - after an operator, next name token should be a name, not an operator
|
|
85
|
+
TokenType.SLASH,
|
|
86
|
+
TokenType.DOUBLE_SLASH,
|
|
87
|
+
TokenType.PIPE,
|
|
88
|
+
TokenType.PLUS,
|
|
89
|
+
TokenType.MINUS,
|
|
90
|
+
TokenType.STAR,
|
|
91
|
+
TokenType.DIV,
|
|
92
|
+
TokenType.MOD,
|
|
93
|
+
TokenType.EQUALS,
|
|
94
|
+
TokenType.NOT_EQUALS,
|
|
95
|
+
TokenType.LT,
|
|
96
|
+
TokenType.LTE,
|
|
97
|
+
TokenType.GT,
|
|
98
|
+
TokenType.GTE,
|
|
99
|
+
TokenType.AND,
|
|
100
|
+
TokenType.OR
|
|
101
|
+
]);
|
|
102
|
+
var XPathTokenizer = class {
|
|
103
|
+
constructor(expression) {
|
|
104
|
+
this.expression = expression;
|
|
105
|
+
this.position = 0;
|
|
106
|
+
this.tokens = [];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get the last token that was added (if any)
|
|
110
|
+
*/
|
|
111
|
+
getLastToken() {
|
|
112
|
+
return this.tokens.length > 0 ? this.tokens[this.tokens.length - 1] : null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if the current context allows operator names (div, mod, and, or)
|
|
116
|
+
* Per XPath 1.0 spec disambiguation rules.
|
|
117
|
+
*/
|
|
118
|
+
isOperatorContext() {
|
|
119
|
+
const lastToken = this.getLastToken();
|
|
120
|
+
if (!lastToken) return false;
|
|
121
|
+
if (OPERATOR_CONTEXT_BLOCKERS.has(lastToken.type)) return false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
tokenize() {
|
|
125
|
+
this.tokens = [];
|
|
126
|
+
this.position = 0;
|
|
127
|
+
while (this.position < this.expression.length) {
|
|
128
|
+
this.skipWhitespace();
|
|
129
|
+
if (this.position >= this.expression.length) break;
|
|
130
|
+
const token = this.nextToken();
|
|
131
|
+
if (token) {
|
|
132
|
+
this.tokens.push(token);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.tokens.push(new Token(TokenType.EOF, null, this.position));
|
|
136
|
+
return this.tokens;
|
|
137
|
+
}
|
|
138
|
+
skipWhitespace() {
|
|
139
|
+
while (this.position < this.expression.length && /\s/.test(this.expression[this.position])) {
|
|
140
|
+
this.position++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
peek(offset = 0) {
|
|
144
|
+
return this.expression[this.position + offset];
|
|
145
|
+
}
|
|
146
|
+
consume() {
|
|
147
|
+
return this.expression[this.position++];
|
|
148
|
+
}
|
|
149
|
+
nextToken() {
|
|
150
|
+
const startPos = this.position;
|
|
151
|
+
const char = this.peek();
|
|
152
|
+
if (char === '"' || char === "'") {
|
|
153
|
+
return this.readStringLiteral();
|
|
154
|
+
}
|
|
155
|
+
if (/[0-9]/.test(char) || char === "." && /[0-9]/.test(this.peek(1))) {
|
|
156
|
+
return this.readNumber();
|
|
157
|
+
}
|
|
158
|
+
if (char === "/" && this.peek(1) === "/") {
|
|
159
|
+
this.position += 2;
|
|
160
|
+
return new Token(TokenType.DOUBLE_SLASH, "//", startPos);
|
|
161
|
+
}
|
|
162
|
+
if (char === "." && this.peek(1) === ".") {
|
|
163
|
+
this.position += 2;
|
|
164
|
+
return new Token(TokenType.DOUBLE_DOT, "..", startPos);
|
|
165
|
+
}
|
|
166
|
+
if (char === ":" && this.peek(1) === ":") {
|
|
167
|
+
this.position += 2;
|
|
168
|
+
return new Token(TokenType.DOUBLE_COLON, "::", startPos);
|
|
169
|
+
}
|
|
170
|
+
if (char === "!" && this.peek(1) === "=") {
|
|
171
|
+
this.position += 2;
|
|
172
|
+
return new Token(TokenType.NOT_EQUALS, "!=", startPos);
|
|
173
|
+
}
|
|
174
|
+
if (char === "<" && this.peek(1) === "=") {
|
|
175
|
+
this.position += 2;
|
|
176
|
+
return new Token(TokenType.LTE, "<=", startPos);
|
|
177
|
+
}
|
|
178
|
+
if (char === ">" && this.peek(1) === "=") {
|
|
179
|
+
this.position += 2;
|
|
180
|
+
return new Token(TokenType.GTE, ">=", startPos);
|
|
181
|
+
}
|
|
182
|
+
const singleCharTokens = {
|
|
183
|
+
"/": TokenType.SLASH,
|
|
184
|
+
"|": TokenType.PIPE,
|
|
185
|
+
"+": TokenType.PLUS,
|
|
186
|
+
"-": TokenType.MINUS,
|
|
187
|
+
"*": TokenType.STAR,
|
|
188
|
+
"=": TokenType.EQUALS,
|
|
189
|
+
"<": TokenType.LT,
|
|
190
|
+
">": TokenType.GT,
|
|
191
|
+
"(": TokenType.LPAREN,
|
|
192
|
+
")": TokenType.RPAREN,
|
|
193
|
+
"[": TokenType.LBRACKET,
|
|
194
|
+
"]": TokenType.RBRACKET,
|
|
195
|
+
"@": TokenType.AT,
|
|
196
|
+
".": TokenType.DOT,
|
|
197
|
+
",": TokenType.COMMA,
|
|
198
|
+
$: TokenType.DOLLAR,
|
|
199
|
+
":": TokenType.COLON
|
|
200
|
+
};
|
|
201
|
+
if (singleCharTokens[char]) {
|
|
202
|
+
this.position++;
|
|
203
|
+
return new Token(singleCharTokens[char], char, startPos);
|
|
204
|
+
}
|
|
205
|
+
if (this.isNameStartChar(char)) {
|
|
206
|
+
return this.readName();
|
|
207
|
+
}
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Unexpected character '${char}' at position ${this.position} in expression: ${this.expression}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
readStringLiteral() {
|
|
213
|
+
const startPos = this.position;
|
|
214
|
+
const quote = this.consume();
|
|
215
|
+
let value = "";
|
|
216
|
+
while (this.position < this.expression.length && this.peek() !== quote) {
|
|
217
|
+
value += this.consume();
|
|
218
|
+
}
|
|
219
|
+
if (this.position >= this.expression.length) {
|
|
220
|
+
throw new Error(`Unterminated string literal at position ${startPos}`);
|
|
221
|
+
}
|
|
222
|
+
this.consume();
|
|
223
|
+
return new Token(TokenType.LITERAL, value, startPos);
|
|
224
|
+
}
|
|
225
|
+
readNumber() {
|
|
226
|
+
const startPos = this.position;
|
|
227
|
+
let value = "";
|
|
228
|
+
while (this.position < this.expression.length && /[0-9]/.test(this.peek())) {
|
|
229
|
+
value += this.consume();
|
|
230
|
+
}
|
|
231
|
+
if (this.peek() === "." && /[0-9]/.test(this.peek(1))) {
|
|
232
|
+
value += this.consume();
|
|
233
|
+
while (this.position < this.expression.length && /[0-9]/.test(this.peek())) {
|
|
234
|
+
value += this.consume();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return new Token(TokenType.NUMBER, parseFloat(value), startPos);
|
|
238
|
+
}
|
|
239
|
+
readName() {
|
|
240
|
+
const startPos = this.position;
|
|
241
|
+
let value = "";
|
|
242
|
+
while (this.position < this.expression.length && this.isNameChar(this.peek())) {
|
|
243
|
+
value += this.consume();
|
|
244
|
+
}
|
|
245
|
+
this.skipWhitespace();
|
|
246
|
+
if (AXIS_NAMES.has(value) && this.peek() === ":" && this.peek(1) === ":") {
|
|
247
|
+
return new Token(TokenType.AXIS, value, startPos);
|
|
248
|
+
}
|
|
249
|
+
const savedPos = this.position;
|
|
250
|
+
this.skipWhitespace();
|
|
251
|
+
if (this.peek() === "(") {
|
|
252
|
+
if (NODE_TYPES.has(value)) {
|
|
253
|
+
return new Token(TokenType.NODE_TYPE, value, startPos);
|
|
254
|
+
}
|
|
255
|
+
return new Token(TokenType.FUNCTION, value, startPos);
|
|
256
|
+
}
|
|
257
|
+
this.position = savedPos;
|
|
258
|
+
if (OPERATORS.has(value) && this.isOperatorContext()) {
|
|
259
|
+
const opTokens = {
|
|
260
|
+
and: TokenType.AND,
|
|
261
|
+
or: TokenType.OR,
|
|
262
|
+
mod: TokenType.MOD,
|
|
263
|
+
div: TokenType.DIV
|
|
264
|
+
};
|
|
265
|
+
return new Token(opTokens[value], value, startPos);
|
|
266
|
+
}
|
|
267
|
+
return new Token(TokenType.NAME, value, startPos);
|
|
268
|
+
}
|
|
269
|
+
isNameStartChar(char) {
|
|
270
|
+
if (!char) return false;
|
|
271
|
+
const code = char.charCodeAt(0);
|
|
272
|
+
return char === "_" || code >= 65 && code <= 90 || // A-Z
|
|
273
|
+
code >= 97 && code <= 122 || // a-z
|
|
274
|
+
code >= 192;
|
|
275
|
+
}
|
|
276
|
+
isNameChar(char) {
|
|
277
|
+
if (!char) return false;
|
|
278
|
+
const code = char.charCodeAt(0);
|
|
279
|
+
return this.isNameStartChar(char) || char === "-" || char === "." || code >= 48 && code <= 57;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
function tokenize(expression) {
|
|
283
|
+
const tokenizer = new XPathTokenizer(expression);
|
|
284
|
+
return tokenizer.tokenize();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/xpath/parser.js
|
|
288
|
+
var NodeType = {
|
|
289
|
+
// Expression types
|
|
290
|
+
OR_EXPR: "OrExpr",
|
|
291
|
+
AND_EXPR: "AndExpr",
|
|
292
|
+
EQUALITY_EXPR: "EqualityExpr",
|
|
293
|
+
RELATIONAL_EXPR: "RelationalExpr",
|
|
294
|
+
ADDITIVE_EXPR: "AdditiveExpr",
|
|
295
|
+
MULTIPLICATIVE_EXPR: "MultiplicativeExpr",
|
|
296
|
+
UNARY_EXPR: "UnaryExpr",
|
|
297
|
+
UNION_EXPR: "UnionExpr",
|
|
298
|
+
// Path expressions
|
|
299
|
+
PATH_EXPR: "PathExpr",
|
|
300
|
+
LOCATION_PATH: "LocationPath",
|
|
301
|
+
STEP: "Step",
|
|
302
|
+
PREDICATE: "Predicate",
|
|
303
|
+
// Primaries
|
|
304
|
+
VARIABLE_REF: "VariableRef",
|
|
305
|
+
LITERAL: "Literal",
|
|
306
|
+
NUMBER: "Number",
|
|
307
|
+
FUNCTION_CALL: "FunctionCall",
|
|
308
|
+
// Node tests
|
|
309
|
+
NAME_TEST: "NameTest",
|
|
310
|
+
NODE_TYPE_TEST: "NodeTypeTest",
|
|
311
|
+
PI_TEST: "ProcessingInstructionTest"
|
|
312
|
+
};
|
|
313
|
+
var ASTNode = class {
|
|
314
|
+
constructor(type, props = {}) {
|
|
315
|
+
this.type = type;
|
|
316
|
+
Object.assign(this, props);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
var XPathParser = class {
|
|
320
|
+
constructor(tokens) {
|
|
321
|
+
this.tokens = tokens;
|
|
322
|
+
this.position = 0;
|
|
323
|
+
}
|
|
324
|
+
parse() {
|
|
325
|
+
const expr = this.parseExpr();
|
|
326
|
+
if (!this.isAtEnd()) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Unexpected token ${this.peek().type} at position ${this.peek().position}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return expr;
|
|
332
|
+
}
|
|
333
|
+
// Expr ::= OrExpr
|
|
334
|
+
parseExpr() {
|
|
335
|
+
return this.parseOrExpr();
|
|
336
|
+
}
|
|
337
|
+
// OrExpr ::= AndExpr | OrExpr 'or' AndExpr
|
|
338
|
+
parseOrExpr() {
|
|
339
|
+
let left = this.parseAndExpr();
|
|
340
|
+
while (this.match(TokenType.OR)) {
|
|
341
|
+
const right = this.parseAndExpr();
|
|
342
|
+
left = new ASTNode(NodeType.OR_EXPR, { left, right });
|
|
343
|
+
}
|
|
344
|
+
return left;
|
|
345
|
+
}
|
|
346
|
+
// AndExpr ::= EqualityExpr | AndExpr 'and' EqualityExpr
|
|
347
|
+
parseAndExpr() {
|
|
348
|
+
let left = this.parseEqualityExpr();
|
|
349
|
+
while (this.match(TokenType.AND)) {
|
|
350
|
+
const right = this.parseEqualityExpr();
|
|
351
|
+
left = new ASTNode(NodeType.AND_EXPR, { left, right });
|
|
352
|
+
}
|
|
353
|
+
return left;
|
|
354
|
+
}
|
|
355
|
+
// EqualityExpr ::= RelationalExpr | EqualityExpr '=' RelationalExpr | EqualityExpr '!=' RelationalExpr
|
|
356
|
+
parseEqualityExpr() {
|
|
357
|
+
let left = this.parseRelationalExpr();
|
|
358
|
+
while (true) {
|
|
359
|
+
if (this.match(TokenType.EQUALS)) {
|
|
360
|
+
const right = this.parseRelationalExpr();
|
|
361
|
+
left = new ASTNode(NodeType.EQUALITY_EXPR, {
|
|
362
|
+
operator: "=",
|
|
363
|
+
left,
|
|
364
|
+
right
|
|
365
|
+
});
|
|
366
|
+
} else if (this.match(TokenType.NOT_EQUALS)) {
|
|
367
|
+
const right = this.parseRelationalExpr();
|
|
368
|
+
left = new ASTNode(NodeType.EQUALITY_EXPR, {
|
|
369
|
+
operator: "!=",
|
|
370
|
+
left,
|
|
371
|
+
right
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return left;
|
|
378
|
+
}
|
|
379
|
+
// RelationalExpr ::= AdditiveExpr | RelationalExpr '<' AdditiveExpr | ...
|
|
380
|
+
parseRelationalExpr() {
|
|
381
|
+
let left = this.parseAdditiveExpr();
|
|
382
|
+
while (true) {
|
|
383
|
+
if (this.match(TokenType.LT)) {
|
|
384
|
+
const right = this.parseAdditiveExpr();
|
|
385
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
386
|
+
operator: "<",
|
|
387
|
+
left,
|
|
388
|
+
right
|
|
389
|
+
});
|
|
390
|
+
} else if (this.match(TokenType.LTE)) {
|
|
391
|
+
const right = this.parseAdditiveExpr();
|
|
392
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
393
|
+
operator: "<=",
|
|
394
|
+
left,
|
|
395
|
+
right
|
|
396
|
+
});
|
|
397
|
+
} else if (this.match(TokenType.GT)) {
|
|
398
|
+
const right = this.parseAdditiveExpr();
|
|
399
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
400
|
+
operator: ">",
|
|
401
|
+
left,
|
|
402
|
+
right
|
|
403
|
+
});
|
|
404
|
+
} else if (this.match(TokenType.GTE)) {
|
|
405
|
+
const right = this.parseAdditiveExpr();
|
|
406
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
407
|
+
operator: ">=",
|
|
408
|
+
left,
|
|
409
|
+
right
|
|
410
|
+
});
|
|
411
|
+
} else {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return left;
|
|
416
|
+
}
|
|
417
|
+
// AdditiveExpr ::= MultiplicativeExpr | AdditiveExpr '+' MultiplicativeExpr | AdditiveExpr '-' MultiplicativeExpr
|
|
418
|
+
parseAdditiveExpr() {
|
|
419
|
+
let left = this.parseMultiplicativeExpr();
|
|
420
|
+
while (true) {
|
|
421
|
+
if (this.match(TokenType.PLUS)) {
|
|
422
|
+
const right = this.parseMultiplicativeExpr();
|
|
423
|
+
left = new ASTNode(NodeType.ADDITIVE_EXPR, {
|
|
424
|
+
operator: "+",
|
|
425
|
+
left,
|
|
426
|
+
right
|
|
427
|
+
});
|
|
428
|
+
} else if (this.match(TokenType.MINUS)) {
|
|
429
|
+
const right = this.parseMultiplicativeExpr();
|
|
430
|
+
left = new ASTNode(NodeType.ADDITIVE_EXPR, {
|
|
431
|
+
operator: "-",
|
|
432
|
+
left,
|
|
433
|
+
right
|
|
434
|
+
});
|
|
435
|
+
} else {
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return left;
|
|
440
|
+
}
|
|
441
|
+
// MultiplicativeExpr ::= UnaryExpr | MultiplicativeExpr '*' UnaryExpr | ...
|
|
442
|
+
parseMultiplicativeExpr() {
|
|
443
|
+
let left = this.parseUnaryExpr();
|
|
444
|
+
while (true) {
|
|
445
|
+
if (this.match(TokenType.STAR)) {
|
|
446
|
+
const right = this.parseUnaryExpr();
|
|
447
|
+
left = new ASTNode(NodeType.MULTIPLICATIVE_EXPR, {
|
|
448
|
+
operator: "*",
|
|
449
|
+
left,
|
|
450
|
+
right
|
|
451
|
+
});
|
|
452
|
+
} else if (this.match(TokenType.DIV)) {
|
|
453
|
+
const right = this.parseUnaryExpr();
|
|
454
|
+
left = new ASTNode(NodeType.MULTIPLICATIVE_EXPR, {
|
|
455
|
+
operator: "div",
|
|
456
|
+
left,
|
|
457
|
+
right
|
|
458
|
+
});
|
|
459
|
+
} else if (this.match(TokenType.MOD)) {
|
|
460
|
+
const right = this.parseUnaryExpr();
|
|
461
|
+
left = new ASTNode(NodeType.MULTIPLICATIVE_EXPR, {
|
|
462
|
+
operator: "mod",
|
|
463
|
+
left,
|
|
464
|
+
right
|
|
465
|
+
});
|
|
466
|
+
} else {
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return left;
|
|
471
|
+
}
|
|
472
|
+
// UnaryExpr ::= UnionExpr | '-' UnaryExpr
|
|
473
|
+
parseUnaryExpr() {
|
|
474
|
+
if (this.match(TokenType.MINUS)) {
|
|
475
|
+
const operand = this.parseUnaryExpr();
|
|
476
|
+
return new ASTNode(NodeType.UNARY_EXPR, { operator: "-", operand });
|
|
477
|
+
}
|
|
478
|
+
return this.parseUnionExpr();
|
|
479
|
+
}
|
|
480
|
+
// UnionExpr ::= PathExpr | UnionExpr '|' PathExpr
|
|
481
|
+
parseUnionExpr() {
|
|
482
|
+
let left = this.parsePathExpr();
|
|
483
|
+
while (this.match(TokenType.PIPE)) {
|
|
484
|
+
const right = this.parsePathExpr();
|
|
485
|
+
left = new ASTNode(NodeType.UNION_EXPR, { left, right });
|
|
486
|
+
}
|
|
487
|
+
return left;
|
|
488
|
+
}
|
|
489
|
+
// PathExpr ::= LocationPath | FilterExpr | FilterExpr '/' RelativeLocationPath | FilterExpr '//' RelativeLocationPath
|
|
490
|
+
parsePathExpr() {
|
|
491
|
+
if (this.check(TokenType.SLASH) || this.check(TokenType.DOUBLE_SLASH)) {
|
|
492
|
+
return this.parseLocationPath();
|
|
493
|
+
}
|
|
494
|
+
if (this.isStepStart()) {
|
|
495
|
+
return this.parseLocationPath();
|
|
496
|
+
}
|
|
497
|
+
const filter = this.parseFilterExpr();
|
|
498
|
+
if (this.check(TokenType.SLASH) || this.check(TokenType.DOUBLE_SLASH)) {
|
|
499
|
+
const steps = [];
|
|
500
|
+
while (this.check(TokenType.SLASH) || this.check(TokenType.DOUBLE_SLASH)) {
|
|
501
|
+
const isDescendant = this.match(TokenType.DOUBLE_SLASH);
|
|
502
|
+
if (!isDescendant) this.advance();
|
|
503
|
+
if (isDescendant) {
|
|
504
|
+
steps.push(
|
|
505
|
+
new ASTNode(NodeType.STEP, {
|
|
506
|
+
axis: "descendant-or-self",
|
|
507
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, {
|
|
508
|
+
nodeType: "node"
|
|
509
|
+
}),
|
|
510
|
+
predicates: []
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
steps.push(this.parseStep());
|
|
515
|
+
}
|
|
516
|
+
return new ASTNode(NodeType.PATH_EXPR, { filter, steps });
|
|
517
|
+
}
|
|
518
|
+
return filter;
|
|
519
|
+
}
|
|
520
|
+
// LocationPath ::= RelativeLocationPath | AbsoluteLocationPath
|
|
521
|
+
parseLocationPath() {
|
|
522
|
+
let absolute = false;
|
|
523
|
+
const steps = [];
|
|
524
|
+
if (this.match(TokenType.DOUBLE_SLASH)) {
|
|
525
|
+
absolute = true;
|
|
526
|
+
steps.push(
|
|
527
|
+
new ASTNode(NodeType.STEP, {
|
|
528
|
+
axis: "descendant-or-self",
|
|
529
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType: "node" }),
|
|
530
|
+
predicates: []
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
if (!this.isAtEnd() && !this.check(TokenType.EOF)) {
|
|
534
|
+
steps.push(this.parseStep());
|
|
535
|
+
}
|
|
536
|
+
} else if (this.match(TokenType.SLASH)) {
|
|
537
|
+
absolute = true;
|
|
538
|
+
if (this.isStepStart()) {
|
|
539
|
+
steps.push(this.parseStep());
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
steps.push(this.parseStep());
|
|
543
|
+
}
|
|
544
|
+
while (this.check(TokenType.SLASH) || this.check(TokenType.DOUBLE_SLASH)) {
|
|
545
|
+
const isDescendant = this.match(TokenType.DOUBLE_SLASH);
|
|
546
|
+
if (!isDescendant) this.advance();
|
|
547
|
+
if (isDescendant) {
|
|
548
|
+
steps.push(
|
|
549
|
+
new ASTNode(NodeType.STEP, {
|
|
550
|
+
axis: "descendant-or-self",
|
|
551
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, {
|
|
552
|
+
nodeType: "node"
|
|
553
|
+
}),
|
|
554
|
+
predicates: []
|
|
555
|
+
})
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
steps.push(this.parseStep());
|
|
559
|
+
}
|
|
560
|
+
return new ASTNode(NodeType.LOCATION_PATH, { absolute, steps });
|
|
561
|
+
}
|
|
562
|
+
// Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
|
|
563
|
+
parseStep() {
|
|
564
|
+
if (this.match(TokenType.DOT)) {
|
|
565
|
+
return new ASTNode(NodeType.STEP, {
|
|
566
|
+
axis: "self",
|
|
567
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType: "node" }),
|
|
568
|
+
predicates: []
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (this.match(TokenType.DOUBLE_DOT)) {
|
|
572
|
+
return new ASTNode(NodeType.STEP, {
|
|
573
|
+
axis: "parent",
|
|
574
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType: "node" }),
|
|
575
|
+
predicates: []
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
let axis = "child";
|
|
579
|
+
if (this.match(TokenType.AT)) {
|
|
580
|
+
axis = "attribute";
|
|
581
|
+
} else if (this.check(TokenType.AXIS)) {
|
|
582
|
+
axis = this.advance().value;
|
|
583
|
+
this.expect(TokenType.DOUBLE_COLON);
|
|
584
|
+
}
|
|
585
|
+
const nodeTest = this.parseNodeTest();
|
|
586
|
+
const predicates = [];
|
|
587
|
+
while (this.check(TokenType.LBRACKET)) {
|
|
588
|
+
predicates.push(this.parsePredicate());
|
|
589
|
+
}
|
|
590
|
+
return new ASTNode(NodeType.STEP, { axis, nodeTest, predicates });
|
|
591
|
+
}
|
|
592
|
+
// NodeTest ::= NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')'
|
|
593
|
+
parseNodeTest() {
|
|
594
|
+
if (this.check(TokenType.NODE_TYPE)) {
|
|
595
|
+
const nodeType = this.advance().value;
|
|
596
|
+
this.expect(TokenType.LPAREN);
|
|
597
|
+
if (nodeType === "processing-instruction" && this.check(TokenType.LITERAL)) {
|
|
598
|
+
const name = this.advance().value;
|
|
599
|
+
this.expect(TokenType.RPAREN);
|
|
600
|
+
return new ASTNode(NodeType.PI_TEST, { name });
|
|
601
|
+
}
|
|
602
|
+
this.expect(TokenType.RPAREN);
|
|
603
|
+
return new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType });
|
|
604
|
+
}
|
|
605
|
+
return this.parseNameTest();
|
|
606
|
+
}
|
|
607
|
+
// NameTest ::= '*' | NCName ':' '*' | QName
|
|
608
|
+
parseNameTest() {
|
|
609
|
+
if (this.match(TokenType.STAR)) {
|
|
610
|
+
return new ASTNode(NodeType.NAME_TEST, { name: "*", prefix: null });
|
|
611
|
+
}
|
|
612
|
+
if (!this.check(TokenType.NAME)) {
|
|
613
|
+
throw new Error(`Expected name at position ${this.peek().position}`);
|
|
614
|
+
}
|
|
615
|
+
const name = this.advance().value;
|
|
616
|
+
if (this.match(TokenType.COLON)) {
|
|
617
|
+
if (this.match(TokenType.STAR)) {
|
|
618
|
+
return new ASTNode(NodeType.NAME_TEST, { name: "*", prefix: name });
|
|
619
|
+
}
|
|
620
|
+
if (this.check(TokenType.NAME)) {
|
|
621
|
+
const localName = this.advance().value;
|
|
622
|
+
return new ASTNode(NodeType.NAME_TEST, {
|
|
623
|
+
name: localName,
|
|
624
|
+
prefix: name
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Expected name or * after : at position ${this.peek().position}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return new ASTNode(NodeType.NAME_TEST, { name, prefix: null });
|
|
632
|
+
}
|
|
633
|
+
// Predicate ::= '[' Expr ']'
|
|
634
|
+
parsePredicate() {
|
|
635
|
+
this.expect(TokenType.LBRACKET);
|
|
636
|
+
const expr = this.parseExpr();
|
|
637
|
+
this.expect(TokenType.RBRACKET);
|
|
638
|
+
return new ASTNode(NodeType.PREDICATE, { expr });
|
|
639
|
+
}
|
|
640
|
+
// FilterExpr ::= PrimaryExpr | FilterExpr Predicate
|
|
641
|
+
parseFilterExpr() {
|
|
642
|
+
let primary = this.parsePrimaryExpr();
|
|
643
|
+
while (this.check(TokenType.LBRACKET)) {
|
|
644
|
+
const predicate = this.parsePredicate();
|
|
645
|
+
primary = new ASTNode(NodeType.PATH_EXPR, {
|
|
646
|
+
filter: primary,
|
|
647
|
+
predicates: [predicate]
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return primary;
|
|
651
|
+
}
|
|
652
|
+
// PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
|
|
653
|
+
parsePrimaryExpr() {
|
|
654
|
+
if (this.match(TokenType.DOLLAR)) {
|
|
655
|
+
if (!this.check(TokenType.NAME)) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`Expected variable name at position ${this.peek().position}`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const name = this.advance().value;
|
|
661
|
+
let prefix = null;
|
|
662
|
+
if (this.match(TokenType.COLON)) {
|
|
663
|
+
prefix = name;
|
|
664
|
+
if (!this.check(TokenType.NAME)) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`Expected local name at position ${this.peek().position}`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const localName = this.advance().value;
|
|
670
|
+
return new ASTNode(NodeType.VARIABLE_REF, { name: localName, prefix });
|
|
671
|
+
}
|
|
672
|
+
return new ASTNode(NodeType.VARIABLE_REF, { name, prefix });
|
|
673
|
+
}
|
|
674
|
+
if (this.match(TokenType.LPAREN)) {
|
|
675
|
+
const expr = this.parseExpr();
|
|
676
|
+
this.expect(TokenType.RPAREN);
|
|
677
|
+
return expr;
|
|
678
|
+
}
|
|
679
|
+
if (this.check(TokenType.LITERAL)) {
|
|
680
|
+
return new ASTNode(NodeType.LITERAL, { value: this.advance().value });
|
|
681
|
+
}
|
|
682
|
+
if (this.check(TokenType.NUMBER)) {
|
|
683
|
+
return new ASTNode(NodeType.NUMBER, { value: this.advance().value });
|
|
684
|
+
}
|
|
685
|
+
if (this.check(TokenType.FUNCTION)) {
|
|
686
|
+
return this.parseFunctionCall();
|
|
687
|
+
}
|
|
688
|
+
throw new Error(
|
|
689
|
+
`Unexpected token ${this.peek().type} at position ${this.peek().position}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
// FunctionCall ::= FunctionName '(' ( Argument ( ',' Argument )* )? ')'
|
|
693
|
+
// Note: Prefixed function calls (prefix:fn()) are not supported because
|
|
694
|
+
// the tokenizer identifies functions by NAME followed by '(' - prefixed
|
|
695
|
+
// names like 'fn:name()' are tokenized as NAME:NAME() which is parsed
|
|
696
|
+
// as a location path, not a function call.
|
|
697
|
+
parseFunctionCall() {
|
|
698
|
+
const name = this.advance().value;
|
|
699
|
+
return this.parseFunctionCallArgs(name, null);
|
|
700
|
+
}
|
|
701
|
+
parseFunctionCallArgs(name, prefix) {
|
|
702
|
+
this.expect(TokenType.LPAREN);
|
|
703
|
+
const args = [];
|
|
704
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
705
|
+
args.push(this.parseExpr());
|
|
706
|
+
while (this.match(TokenType.COMMA)) {
|
|
707
|
+
args.push(this.parseExpr());
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
this.expect(TokenType.RPAREN);
|
|
711
|
+
return new ASTNode(NodeType.FUNCTION_CALL, { name, prefix, args });
|
|
712
|
+
}
|
|
713
|
+
// Helper methods
|
|
714
|
+
isStepStart() {
|
|
715
|
+
const type = this.peek().type;
|
|
716
|
+
return type === TokenType.NAME || type === TokenType.STAR || type === TokenType.AT || type === TokenType.DOT || type === TokenType.DOUBLE_DOT || type === TokenType.AXIS || type === TokenType.NODE_TYPE;
|
|
717
|
+
}
|
|
718
|
+
peek() {
|
|
719
|
+
return this.tokens[this.position];
|
|
720
|
+
}
|
|
721
|
+
advance() {
|
|
722
|
+
if (!this.isAtEnd()) {
|
|
723
|
+
return this.tokens[this.position++];
|
|
724
|
+
}
|
|
725
|
+
return this.tokens[this.position];
|
|
726
|
+
}
|
|
727
|
+
check(type) {
|
|
728
|
+
if (this.isAtEnd()) return false;
|
|
729
|
+
return this.peek().type === type;
|
|
730
|
+
}
|
|
731
|
+
match(type) {
|
|
732
|
+
if (this.check(type)) {
|
|
733
|
+
this.advance();
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
expect(type) {
|
|
739
|
+
if (!this.check(type)) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
`Expected ${type} but got ${this.peek().type} at position ${this.peek().position}`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
return this.advance();
|
|
745
|
+
}
|
|
746
|
+
isAtEnd() {
|
|
747
|
+
return this.peek().type === TokenType.EOF;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
function parse(expression) {
|
|
751
|
+
const tokens = tokenize(expression);
|
|
752
|
+
const parser = new XPathParser(tokens);
|
|
753
|
+
return parser.parse();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/xpath/evaluator.js
|
|
757
|
+
var XPathResultType = {
|
|
758
|
+
ANY_TYPE: 0,
|
|
759
|
+
NUMBER_TYPE: 1,
|
|
760
|
+
STRING_TYPE: 2,
|
|
761
|
+
BOOLEAN_TYPE: 3,
|
|
762
|
+
UNORDERED_NODE_ITERATOR_TYPE: 4,
|
|
763
|
+
ORDERED_NODE_ITERATOR_TYPE: 5,
|
|
764
|
+
UNORDERED_NODE_SNAPSHOT_TYPE: 6,
|
|
765
|
+
ORDERED_NODE_SNAPSHOT_TYPE: 7,
|
|
766
|
+
ANY_UNORDERED_NODE_TYPE: 8,
|
|
767
|
+
FIRST_ORDERED_NODE_TYPE: 9
|
|
768
|
+
};
|
|
769
|
+
var XPathLimits = {
|
|
770
|
+
MAX_RECURSION_DEPTH: 100,
|
|
771
|
+
MAX_RESULT_SIZE: 1e4,
|
|
772
|
+
MAX_STRING_LENGTH: 1e6
|
|
773
|
+
};
|
|
774
|
+
var FORBIDDEN_VARIABLE_NAMES = Object.freeze([
|
|
775
|
+
"__proto__",
|
|
776
|
+
"constructor",
|
|
777
|
+
"prototype",
|
|
778
|
+
"__defineGetter__",
|
|
779
|
+
"__defineSetter__",
|
|
780
|
+
"__lookupGetter__",
|
|
781
|
+
"__lookupSetter__"
|
|
782
|
+
]);
|
|
783
|
+
var XPathContext = class _XPathContext {
|
|
784
|
+
constructor(node, position = 1, size = 1, variables = {}, namespaces = {}) {
|
|
785
|
+
this.node = node;
|
|
786
|
+
this.position = position;
|
|
787
|
+
this.size = size;
|
|
788
|
+
this.variables = variables;
|
|
789
|
+
this.namespaces = namespaces;
|
|
790
|
+
}
|
|
791
|
+
clone(overrides = {}) {
|
|
792
|
+
return new _XPathContext(
|
|
793
|
+
overrides.node ?? this.node,
|
|
794
|
+
overrides.position ?? this.position,
|
|
795
|
+
overrides.size ?? this.size,
|
|
796
|
+
overrides.variables ?? this.variables,
|
|
797
|
+
overrides.namespaces ?? this.namespaces
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
var XPathEvaluator = class {
|
|
802
|
+
constructor(options = {}) {
|
|
803
|
+
this.functions = this.initCoreFunctions();
|
|
804
|
+
this.maxRecursionDepth = options.maxRecursionDepth ?? XPathLimits.MAX_RECURSION_DEPTH;
|
|
805
|
+
this.maxResultSize = options.maxResultSize ?? XPathLimits.MAX_RESULT_SIZE;
|
|
806
|
+
this.maxStringLength = options.maxStringLength ?? XPathLimits.MAX_STRING_LENGTH;
|
|
807
|
+
this.recursionDepth = 0;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Evaluate XPath expression against a context
|
|
811
|
+
* @throws {Error} If recursion depth exceeds limit
|
|
812
|
+
* @throws {Error} If AST is invalid
|
|
813
|
+
*/
|
|
814
|
+
evaluate(ast, context) {
|
|
815
|
+
if (!ast || typeof ast !== "object") {
|
|
816
|
+
throw new Error("Invalid AST: expected object");
|
|
817
|
+
}
|
|
818
|
+
if (!ast.type) {
|
|
819
|
+
throw new Error("Invalid AST: missing type property");
|
|
820
|
+
}
|
|
821
|
+
this.recursionDepth++;
|
|
822
|
+
if (this.recursionDepth > this.maxRecursionDepth) {
|
|
823
|
+
this.recursionDepth = 0;
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Maximum recursion depth exceeded (${this.maxRecursionDepth})`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
return this.evaluateInternal(ast, context);
|
|
830
|
+
} finally {
|
|
831
|
+
this.recursionDepth--;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Internal evaluation (after validation)
|
|
836
|
+
*/
|
|
837
|
+
evaluateInternal(ast, context) {
|
|
838
|
+
switch (ast.type) {
|
|
839
|
+
case NodeType.OR_EXPR:
|
|
840
|
+
return this.evalOrExpr(ast, context);
|
|
841
|
+
case NodeType.AND_EXPR:
|
|
842
|
+
return this.evalAndExpr(ast, context);
|
|
843
|
+
case NodeType.EQUALITY_EXPR:
|
|
844
|
+
return this.evalEqualityExpr(ast, context);
|
|
845
|
+
case NodeType.RELATIONAL_EXPR:
|
|
846
|
+
return this.evalRelationalExpr(ast, context);
|
|
847
|
+
case NodeType.ADDITIVE_EXPR:
|
|
848
|
+
return this.evalAdditiveExpr(ast, context);
|
|
849
|
+
case NodeType.MULTIPLICATIVE_EXPR:
|
|
850
|
+
return this.evalMultiplicativeExpr(ast, context);
|
|
851
|
+
case NodeType.UNARY_EXPR:
|
|
852
|
+
return this.evalUnaryExpr(ast, context);
|
|
853
|
+
case NodeType.UNION_EXPR:
|
|
854
|
+
return this.evalUnionExpr(ast, context);
|
|
855
|
+
case NodeType.PATH_EXPR:
|
|
856
|
+
return this.evalPathExpr(ast, context);
|
|
857
|
+
case NodeType.LOCATION_PATH:
|
|
858
|
+
return this.evalLocationPath(ast, context);
|
|
859
|
+
case NodeType.VARIABLE_REF:
|
|
860
|
+
return this.evalVariableRef(ast, context);
|
|
861
|
+
case NodeType.LITERAL:
|
|
862
|
+
return this.validateString(ast.value);
|
|
863
|
+
case NodeType.NUMBER:
|
|
864
|
+
return ast.value;
|
|
865
|
+
case NodeType.FUNCTION_CALL:
|
|
866
|
+
return this.evalFunctionCall(ast, context);
|
|
867
|
+
default:
|
|
868
|
+
throw new Error(`Unknown AST node type: ${String(ast.type)}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Validate and limit string length
|
|
873
|
+
*/
|
|
874
|
+
validateString(str) {
|
|
875
|
+
if (typeof str === "string" && str.length > this.maxStringLength) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
`String exceeds maximum length (${this.maxStringLength})`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return str;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Validate result size
|
|
884
|
+
*/
|
|
885
|
+
validateResultSize(nodes) {
|
|
886
|
+
if (Array.isArray(nodes) && nodes.length > this.maxResultSize) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`Result set exceeds maximum size (${this.maxResultSize})`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return nodes;
|
|
892
|
+
}
|
|
893
|
+
evalOrExpr(ast, context) {
|
|
894
|
+
return this.toBoolean(this.evaluate(ast.left, context)) || this.toBoolean(this.evaluate(ast.right, context));
|
|
895
|
+
}
|
|
896
|
+
evalAndExpr(ast, context) {
|
|
897
|
+
return this.toBoolean(this.evaluate(ast.left, context)) && this.toBoolean(this.evaluate(ast.right, context));
|
|
898
|
+
}
|
|
899
|
+
evalEqualityExpr(ast, context) {
|
|
900
|
+
const left = this.evaluate(ast.left, context);
|
|
901
|
+
const right = this.evaluate(ast.right, context);
|
|
902
|
+
const isEqual = this.compareValues(left, right, "=");
|
|
903
|
+
return ast.operator === "=" ? isEqual : !isEqual;
|
|
904
|
+
}
|
|
905
|
+
evalRelationalExpr(ast, context) {
|
|
906
|
+
const left = this.evaluate(ast.left, context);
|
|
907
|
+
const right = this.evaluate(ast.right, context);
|
|
908
|
+
return this.compareValues(left, right, ast.operator);
|
|
909
|
+
}
|
|
910
|
+
evalAdditiveExpr(ast, context) {
|
|
911
|
+
const left = this.toNumber(this.evaluate(ast.left, context));
|
|
912
|
+
const right = this.toNumber(this.evaluate(ast.right, context));
|
|
913
|
+
return ast.operator === "+" ? left + right : left - right;
|
|
914
|
+
}
|
|
915
|
+
evalMultiplicativeExpr(ast, context) {
|
|
916
|
+
const left = this.toNumber(this.evaluate(ast.left, context));
|
|
917
|
+
const right = this.toNumber(this.evaluate(ast.right, context));
|
|
918
|
+
switch (ast.operator) {
|
|
919
|
+
case "*":
|
|
920
|
+
return left * right;
|
|
921
|
+
case "div":
|
|
922
|
+
return left / right;
|
|
923
|
+
case "mod":
|
|
924
|
+
return left % right;
|
|
925
|
+
default:
|
|
926
|
+
throw new Error(`Unknown multiplicative operator: ${ast.operator}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
evalUnaryExpr(ast, context) {
|
|
930
|
+
const value = this.toNumber(this.evaluate(ast.operand, context));
|
|
931
|
+
return -value;
|
|
932
|
+
}
|
|
933
|
+
evalUnionExpr(ast, context) {
|
|
934
|
+
const left = this.evaluate(ast.left, context);
|
|
935
|
+
const right = this.evaluate(ast.right, context);
|
|
936
|
+
const leftNodes = Array.isArray(left) ? left : [left];
|
|
937
|
+
const rightNodes = Array.isArray(right) ? right : [right];
|
|
938
|
+
const seen = /* @__PURE__ */ new Set();
|
|
939
|
+
const result = [];
|
|
940
|
+
for (const node of [...leftNodes, ...rightNodes]) {
|
|
941
|
+
if (!seen.has(node)) {
|
|
942
|
+
seen.add(node);
|
|
943
|
+
result.push(node);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
this.validateResultSize(result);
|
|
947
|
+
return this.sortByDocumentOrder(result);
|
|
948
|
+
}
|
|
949
|
+
evalPathExpr(ast, context) {
|
|
950
|
+
if (ast.filter) {
|
|
951
|
+
let result = this.evaluate(ast.filter, context);
|
|
952
|
+
if (ast.predicates) {
|
|
953
|
+
for (const pred of ast.predicates) {
|
|
954
|
+
result = this.filterByPredicate(result, pred, context);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (ast.steps) {
|
|
958
|
+
for (const step of ast.steps) {
|
|
959
|
+
result = this.evalStepOnNodes(step, result, context);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return result;
|
|
963
|
+
}
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
evalLocationPath(ast, context) {
|
|
967
|
+
let nodes;
|
|
968
|
+
if (ast.absolute) {
|
|
969
|
+
const doc = context.node.ownerDocument || context.node;
|
|
970
|
+
nodes = [doc];
|
|
971
|
+
} else {
|
|
972
|
+
nodes = [context.node];
|
|
973
|
+
}
|
|
974
|
+
for (const step of ast.steps) {
|
|
975
|
+
nodes = this.evalStepOnNodes(step, nodes, context);
|
|
976
|
+
this.validateResultSize(nodes);
|
|
977
|
+
}
|
|
978
|
+
return nodes;
|
|
979
|
+
}
|
|
980
|
+
evalStepOnNodes(step, nodes, context) {
|
|
981
|
+
const allNodes = Array.isArray(nodes) ? nodes : [nodes];
|
|
982
|
+
let result = [];
|
|
983
|
+
for (const node of allNodes) {
|
|
984
|
+
const stepNodes = this.evalStep(step, context.clone({ node }));
|
|
985
|
+
result = result.concat(stepNodes);
|
|
986
|
+
if (result.length > this.maxResultSize * 2) {
|
|
987
|
+
this.validateResultSize(result);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const uniqueResult = [...new Set(result)];
|
|
991
|
+
this.validateResultSize(uniqueResult);
|
|
992
|
+
return this.sortByDocumentOrder(uniqueResult);
|
|
993
|
+
}
|
|
994
|
+
evalStep(step, context) {
|
|
995
|
+
let nodes = this.getAxisNodes(step.axis, context.node);
|
|
996
|
+
nodes = nodes.filter((n) => this.matchNodeTest(step.nodeTest, n, context));
|
|
997
|
+
for (const predicate of step.predicates) {
|
|
998
|
+
nodes = this.filterByPredicate(nodes, predicate, context);
|
|
999
|
+
}
|
|
1000
|
+
return nodes;
|
|
1001
|
+
}
|
|
1002
|
+
getAxisNodes(axis, node) {
|
|
1003
|
+
switch (axis) {
|
|
1004
|
+
case "child":
|
|
1005
|
+
return Array.from(node.childNodes || []);
|
|
1006
|
+
case "parent":
|
|
1007
|
+
return node.parentNode ? [node.parentNode] : [];
|
|
1008
|
+
case "self":
|
|
1009
|
+
return [node];
|
|
1010
|
+
case "descendant":
|
|
1011
|
+
return this.getDescendants(node, false);
|
|
1012
|
+
case "descendant-or-self":
|
|
1013
|
+
return this.getDescendants(node, true);
|
|
1014
|
+
case "ancestor":
|
|
1015
|
+
return this.getAncestors(node, false);
|
|
1016
|
+
case "ancestor-or-self":
|
|
1017
|
+
return this.getAncestors(node, true);
|
|
1018
|
+
case "following-sibling":
|
|
1019
|
+
return this.getFollowingSiblings(node);
|
|
1020
|
+
case "preceding-sibling":
|
|
1021
|
+
return this.getPrecedingSiblings(node);
|
|
1022
|
+
case "following":
|
|
1023
|
+
return this.getFollowing(node);
|
|
1024
|
+
case "preceding":
|
|
1025
|
+
return this.getPreceding(node);
|
|
1026
|
+
case "attribute":
|
|
1027
|
+
if (node.attributes) {
|
|
1028
|
+
return Array.from(node.attributes);
|
|
1029
|
+
}
|
|
1030
|
+
return [];
|
|
1031
|
+
case "namespace":
|
|
1032
|
+
return [];
|
|
1033
|
+
default:
|
|
1034
|
+
throw new Error(`Unknown axis: ${axis}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
getDescendants(node, includeSelf) {
|
|
1038
|
+
const result = includeSelf ? [node] : [];
|
|
1039
|
+
const stack = Array.from(node.childNodes || []).reverse();
|
|
1040
|
+
while (stack.length > 0) {
|
|
1041
|
+
const current = stack.pop();
|
|
1042
|
+
result.push(current);
|
|
1043
|
+
if (current.childNodes) {
|
|
1044
|
+
for (let i = current.childNodes.length - 1; i >= 0; i--) {
|
|
1045
|
+
stack.push(current.childNodes[i]);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return result;
|
|
1050
|
+
}
|
|
1051
|
+
getAncestors(node, includeSelf) {
|
|
1052
|
+
const result = includeSelf ? [node] : [];
|
|
1053
|
+
let current = node.parentNode;
|
|
1054
|
+
while (current) {
|
|
1055
|
+
result.push(current);
|
|
1056
|
+
current = current.parentNode;
|
|
1057
|
+
}
|
|
1058
|
+
return result;
|
|
1059
|
+
}
|
|
1060
|
+
getFollowingSiblings(node) {
|
|
1061
|
+
const result = [];
|
|
1062
|
+
if (!node) return result;
|
|
1063
|
+
let current = node.nextSibling;
|
|
1064
|
+
while (current) {
|
|
1065
|
+
result.push(current);
|
|
1066
|
+
current = current.nextSibling;
|
|
1067
|
+
}
|
|
1068
|
+
return result;
|
|
1069
|
+
}
|
|
1070
|
+
getPrecedingSiblings(node) {
|
|
1071
|
+
const result = [];
|
|
1072
|
+
if (!node) return result;
|
|
1073
|
+
let current = node.previousSibling;
|
|
1074
|
+
while (current) {
|
|
1075
|
+
result.push(current);
|
|
1076
|
+
current = current.previousSibling;
|
|
1077
|
+
}
|
|
1078
|
+
return result.reverse();
|
|
1079
|
+
}
|
|
1080
|
+
getFollowing(node) {
|
|
1081
|
+
const result = [];
|
|
1082
|
+
let current = node;
|
|
1083
|
+
while (current) {
|
|
1084
|
+
if (current.nextSibling) {
|
|
1085
|
+
current = current.nextSibling;
|
|
1086
|
+
result.push(current);
|
|
1087
|
+
result.push(...this.getDescendants(current, false));
|
|
1088
|
+
} else {
|
|
1089
|
+
current = current.parentNode;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return result;
|
|
1093
|
+
}
|
|
1094
|
+
getPreceding(node) {
|
|
1095
|
+
const result = [];
|
|
1096
|
+
let current = node;
|
|
1097
|
+
while (current) {
|
|
1098
|
+
if (current.previousSibling) {
|
|
1099
|
+
current = current.previousSibling;
|
|
1100
|
+
const descendants = this.getDescendants(current, false);
|
|
1101
|
+
result.unshift(...descendants.reverse());
|
|
1102
|
+
result.unshift(current);
|
|
1103
|
+
} else {
|
|
1104
|
+
current = current.parentNode;
|
|
1105
|
+
if (current && current.nodeType !== 9) {
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return result;
|
|
1110
|
+
}
|
|
1111
|
+
matchNodeTest(nodeTest, node, context) {
|
|
1112
|
+
switch (nodeTest.type) {
|
|
1113
|
+
case NodeType.NAME_TEST:
|
|
1114
|
+
return this.matchNameTest(nodeTest, node, context);
|
|
1115
|
+
case NodeType.NODE_TYPE_TEST:
|
|
1116
|
+
return this.matchNodeTypeTest(nodeTest.nodeType, node);
|
|
1117
|
+
case NodeType.PI_TEST:
|
|
1118
|
+
return node.nodeType === 7 && node.nodeName === nodeTest.name;
|
|
1119
|
+
default:
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
matchNameTest(nodeTest, node, context) {
|
|
1124
|
+
if (node.nodeType !== 1 && node.nodeType !== 2) {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
const name = nodeTest.name;
|
|
1128
|
+
const prefix = nodeTest.prefix;
|
|
1129
|
+
if (name === "*" && !prefix) {
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
const nodeName = node.localName || node.nodeName;
|
|
1133
|
+
const nodeNs = node.namespaceURI || null;
|
|
1134
|
+
if (name === "*" && prefix) {
|
|
1135
|
+
const ns2 = context.namespaces[prefix];
|
|
1136
|
+
return nodeNs === ns2;
|
|
1137
|
+
}
|
|
1138
|
+
if (!prefix) {
|
|
1139
|
+
const doc = node.ownerDocument;
|
|
1140
|
+
if (doc && doc.contentType === "text/html" && node.nodeType === 1) {
|
|
1141
|
+
return nodeName.toLowerCase() === name.toLowerCase();
|
|
1142
|
+
}
|
|
1143
|
+
return nodeName === name;
|
|
1144
|
+
}
|
|
1145
|
+
const ns = context.namespaces[prefix];
|
|
1146
|
+
return nodeName === name && nodeNs === ns;
|
|
1147
|
+
}
|
|
1148
|
+
matchNodeTypeTest(nodeType, node) {
|
|
1149
|
+
switch (nodeType) {
|
|
1150
|
+
case "node":
|
|
1151
|
+
return true;
|
|
1152
|
+
case "text":
|
|
1153
|
+
return node.nodeType === 3;
|
|
1154
|
+
case "comment":
|
|
1155
|
+
return node.nodeType === 8;
|
|
1156
|
+
case "processing-instruction":
|
|
1157
|
+
return node.nodeType === 7;
|
|
1158
|
+
default:
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
filterByPredicate(nodes, predicate, context) {
|
|
1163
|
+
const result = [];
|
|
1164
|
+
const nodeArray = Array.isArray(nodes) ? nodes : [nodes];
|
|
1165
|
+
const size = nodeArray.length;
|
|
1166
|
+
for (let i = 0; i < nodeArray.length; i++) {
|
|
1167
|
+
const node = nodeArray[i];
|
|
1168
|
+
const predicateContext = context.clone({
|
|
1169
|
+
node,
|
|
1170
|
+
position: i + 1,
|
|
1171
|
+
size
|
|
1172
|
+
});
|
|
1173
|
+
const value = this.evaluate(predicate.expr, predicateContext);
|
|
1174
|
+
if (typeof value === "number") {
|
|
1175
|
+
if (value === i + 1) {
|
|
1176
|
+
result.push(node);
|
|
1177
|
+
}
|
|
1178
|
+
} else if (this.toBoolean(value)) {
|
|
1179
|
+
result.push(node);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return result;
|
|
1183
|
+
}
|
|
1184
|
+
evalVariableRef(ast, context) {
|
|
1185
|
+
const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
|
|
1186
|
+
if (FORBIDDEN_VARIABLE_NAMES.includes(name) || FORBIDDEN_VARIABLE_NAMES.includes(ast.name)) {
|
|
1187
|
+
throw new Error(`Forbidden variable name: $${name}`);
|
|
1188
|
+
}
|
|
1189
|
+
if (!Object.prototype.hasOwnProperty.call(context.variables, name)) {
|
|
1190
|
+
throw new Error(`Undefined variable: $${name}`);
|
|
1191
|
+
}
|
|
1192
|
+
return context.variables[name];
|
|
1193
|
+
}
|
|
1194
|
+
evalFunctionCall(ast, context) {
|
|
1195
|
+
const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
|
|
1196
|
+
const fn = this.functions[name];
|
|
1197
|
+
if (!fn) {
|
|
1198
|
+
throw new Error(`Unknown function: ${name}`);
|
|
1199
|
+
}
|
|
1200
|
+
return fn.call(this, ast.args, context);
|
|
1201
|
+
}
|
|
1202
|
+
// Type conversion functions
|
|
1203
|
+
toBoolean(value) {
|
|
1204
|
+
if (typeof value === "boolean") return value;
|
|
1205
|
+
if (typeof value === "number") return value !== 0 && !isNaN(value);
|
|
1206
|
+
if (typeof value === "string") return value.length > 0;
|
|
1207
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
1208
|
+
if (value && value.nodeType) return true;
|
|
1209
|
+
return Boolean(value);
|
|
1210
|
+
}
|
|
1211
|
+
toNumber(value) {
|
|
1212
|
+
if (typeof value === "number") return value;
|
|
1213
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
1214
|
+
if (typeof value === "string") {
|
|
1215
|
+
const trimmed = value.trim();
|
|
1216
|
+
if (trimmed === "") return NaN;
|
|
1217
|
+
const num = Number(trimmed);
|
|
1218
|
+
return num;
|
|
1219
|
+
}
|
|
1220
|
+
if (Array.isArray(value)) {
|
|
1221
|
+
return this.toNumber(this.toString(value));
|
|
1222
|
+
}
|
|
1223
|
+
if (value && value.nodeType) {
|
|
1224
|
+
return this.toNumber(this.getStringValue(value));
|
|
1225
|
+
}
|
|
1226
|
+
return NaN;
|
|
1227
|
+
}
|
|
1228
|
+
toString(value) {
|
|
1229
|
+
if (typeof value === "string") return value;
|
|
1230
|
+
if (typeof value === "number") {
|
|
1231
|
+
if (isNaN(value)) return "NaN";
|
|
1232
|
+
if (value === Infinity) return "Infinity";
|
|
1233
|
+
if (value === -Infinity) return "-Infinity";
|
|
1234
|
+
if (value === 0) return "0";
|
|
1235
|
+
return String(value);
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1238
|
+
if (Array.isArray(value)) {
|
|
1239
|
+
if (value.length === 0) return "";
|
|
1240
|
+
return this.getStringValue(value[0]);
|
|
1241
|
+
}
|
|
1242
|
+
if (value && value.nodeType) {
|
|
1243
|
+
return this.getStringValue(value);
|
|
1244
|
+
}
|
|
1245
|
+
return String(value);
|
|
1246
|
+
}
|
|
1247
|
+
getStringValue(node) {
|
|
1248
|
+
if (!node) return "";
|
|
1249
|
+
switch (node.nodeType) {
|
|
1250
|
+
case 1:
|
|
1251
|
+
// Element
|
|
1252
|
+
case 9:
|
|
1253
|
+
// Document
|
|
1254
|
+
case 11: {
|
|
1255
|
+
let text = "";
|
|
1256
|
+
const walker = (n) => {
|
|
1257
|
+
if (n.nodeType === 3) {
|
|
1258
|
+
text += n.nodeValue || "";
|
|
1259
|
+
} else if (n.childNodes) {
|
|
1260
|
+
for (const child of n.childNodes) {
|
|
1261
|
+
walker(child);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
walker(node);
|
|
1266
|
+
return text;
|
|
1267
|
+
}
|
|
1268
|
+
case 2:
|
|
1269
|
+
// Attribute
|
|
1270
|
+
case 3:
|
|
1271
|
+
// Text
|
|
1272
|
+
case 4:
|
|
1273
|
+
// CDATA
|
|
1274
|
+
case 7:
|
|
1275
|
+
// Processing Instruction
|
|
1276
|
+
case 8:
|
|
1277
|
+
return node.nodeValue || "";
|
|
1278
|
+
default:
|
|
1279
|
+
return "";
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// Comparison helper
|
|
1283
|
+
compareValues(left, right, operator) {
|
|
1284
|
+
const leftIsNodeSet = Array.isArray(left);
|
|
1285
|
+
const rightIsNodeSet = Array.isArray(right);
|
|
1286
|
+
if (leftIsNodeSet && rightIsNodeSet) {
|
|
1287
|
+
for (const l of left) {
|
|
1288
|
+
for (const r of right) {
|
|
1289
|
+
if (this.comparePrimitive(
|
|
1290
|
+
this.getStringValue(l),
|
|
1291
|
+
this.getStringValue(r),
|
|
1292
|
+
operator
|
|
1293
|
+
)) {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
if (leftIsNodeSet) {
|
|
1301
|
+
for (const l of left) {
|
|
1302
|
+
if (this.comparePrimitive(this.getStringValue(l), right, operator)) {
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
if (rightIsNodeSet) {
|
|
1309
|
+
for (const r of right) {
|
|
1310
|
+
if (this.comparePrimitive(left, this.getStringValue(r), operator)) {
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
return this.comparePrimitive(left, right, operator);
|
|
1317
|
+
}
|
|
1318
|
+
comparePrimitive(left, right, operator) {
|
|
1319
|
+
if (operator === "=" || operator === "!=") {
|
|
1320
|
+
if (typeof left === "boolean" || typeof right === "boolean") {
|
|
1321
|
+
const result2 = this.toBoolean(left) === this.toBoolean(right);
|
|
1322
|
+
return operator === "=" ? result2 : !result2;
|
|
1323
|
+
}
|
|
1324
|
+
if (typeof left === "number" || typeof right === "number") {
|
|
1325
|
+
const result2 = this.toNumber(left) === this.toNumber(right);
|
|
1326
|
+
return operator === "=" ? result2 : !result2;
|
|
1327
|
+
}
|
|
1328
|
+
const result = this.toString(left) === this.toString(right);
|
|
1329
|
+
return operator === "=" ? result : !result;
|
|
1330
|
+
}
|
|
1331
|
+
const leftNum = this.toNumber(left);
|
|
1332
|
+
const rightNum = this.toNumber(right);
|
|
1333
|
+
switch (operator) {
|
|
1334
|
+
case "<":
|
|
1335
|
+
return leftNum < rightNum;
|
|
1336
|
+
case "<=":
|
|
1337
|
+
return leftNum <= rightNum;
|
|
1338
|
+
case ">":
|
|
1339
|
+
return leftNum > rightNum;
|
|
1340
|
+
case ">=":
|
|
1341
|
+
return leftNum >= rightNum;
|
|
1342
|
+
default:
|
|
1343
|
+
throw new Error(`Unknown comparison operator: ${operator}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
sortByDocumentOrder(nodes) {
|
|
1347
|
+
if (nodes.length <= 1) return nodes;
|
|
1348
|
+
return nodes.sort((a, b) => {
|
|
1349
|
+
if (a === b) return 0;
|
|
1350
|
+
const position = a.compareDocumentPosition ? a.compareDocumentPosition(b) : this.compareDocumentPositionFallback(a, b);
|
|
1351
|
+
if (position & 4) return -1;
|
|
1352
|
+
if (position & 2) return 1;
|
|
1353
|
+
return 0;
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
compareDocumentPositionFallback(a, b) {
|
|
1357
|
+
const getPath = (node) => {
|
|
1358
|
+
const path = [];
|
|
1359
|
+
let current = node;
|
|
1360
|
+
while (current) {
|
|
1361
|
+
if (current.parentNode) {
|
|
1362
|
+
const siblings = Array.from(current.parentNode.childNodes);
|
|
1363
|
+
path.unshift(siblings.indexOf(current));
|
|
1364
|
+
}
|
|
1365
|
+
current = current.parentNode;
|
|
1366
|
+
}
|
|
1367
|
+
return path;
|
|
1368
|
+
};
|
|
1369
|
+
const pathA = getPath(a);
|
|
1370
|
+
const pathB = getPath(b);
|
|
1371
|
+
for (let i = 0; i < Math.min(pathA.length, pathB.length); i++) {
|
|
1372
|
+
if (pathA[i] < pathB[i]) return 4;
|
|
1373
|
+
if (pathA[i] > pathB[i]) return 2;
|
|
1374
|
+
}
|
|
1375
|
+
return pathA.length < pathB.length ? 4 : 2;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Initialize XPath 1.0 core functions
|
|
1379
|
+
*/
|
|
1380
|
+
initCoreFunctions() {
|
|
1381
|
+
return {
|
|
1382
|
+
// Node set functions
|
|
1383
|
+
last: (args, ctx) => ctx.size,
|
|
1384
|
+
position: (args, ctx) => ctx.position,
|
|
1385
|
+
count: (args, ctx) => {
|
|
1386
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
1387
|
+
return Array.isArray(nodeSet) ? nodeSet.length : 1;
|
|
1388
|
+
},
|
|
1389
|
+
id: (args, ctx) => {
|
|
1390
|
+
const value = this.toString(this.evaluate(args[0], ctx));
|
|
1391
|
+
const doc = ctx.node.ownerDocument || ctx.node;
|
|
1392
|
+
const ids = value.split(/\s+/).filter((id) => id);
|
|
1393
|
+
const result = [];
|
|
1394
|
+
for (const id of ids) {
|
|
1395
|
+
const el = doc.getElementById(id);
|
|
1396
|
+
if (el) result.push(el);
|
|
1397
|
+
}
|
|
1398
|
+
return result;
|
|
1399
|
+
},
|
|
1400
|
+
"local-name": (args, ctx) => {
|
|
1401
|
+
let node;
|
|
1402
|
+
if (args.length === 0) {
|
|
1403
|
+
node = ctx.node;
|
|
1404
|
+
} else {
|
|
1405
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
1406
|
+
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
1407
|
+
}
|
|
1408
|
+
if (!node) return "";
|
|
1409
|
+
return node.localName || node.nodeName || "";
|
|
1410
|
+
},
|
|
1411
|
+
"namespace-uri": (args, ctx) => {
|
|
1412
|
+
let node;
|
|
1413
|
+
if (args.length === 0) {
|
|
1414
|
+
node = ctx.node;
|
|
1415
|
+
} else {
|
|
1416
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
1417
|
+
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
1418
|
+
}
|
|
1419
|
+
if (!node) return "";
|
|
1420
|
+
return node.namespaceURI || "";
|
|
1421
|
+
},
|
|
1422
|
+
name: (args, ctx) => {
|
|
1423
|
+
let node;
|
|
1424
|
+
if (args.length === 0) {
|
|
1425
|
+
node = ctx.node;
|
|
1426
|
+
} else {
|
|
1427
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
1428
|
+
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
1429
|
+
}
|
|
1430
|
+
if (!node) return "";
|
|
1431
|
+
return node.nodeName || "";
|
|
1432
|
+
},
|
|
1433
|
+
// String functions
|
|
1434
|
+
string: (args, ctx) => {
|
|
1435
|
+
if (args.length === 0) {
|
|
1436
|
+
return this.toString([ctx.node]);
|
|
1437
|
+
}
|
|
1438
|
+
return this.toString(this.evaluate(args[0], ctx));
|
|
1439
|
+
},
|
|
1440
|
+
concat: (args, ctx) => {
|
|
1441
|
+
return args.map((arg) => this.toString(this.evaluate(arg, ctx))).join("");
|
|
1442
|
+
},
|
|
1443
|
+
"starts-with": (args, ctx) => {
|
|
1444
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
1445
|
+
const prefix = this.toString(this.evaluate(args[1], ctx));
|
|
1446
|
+
return str.startsWith(prefix);
|
|
1447
|
+
},
|
|
1448
|
+
contains: (args, ctx) => {
|
|
1449
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
1450
|
+
const substr = this.toString(this.evaluate(args[1], ctx));
|
|
1451
|
+
return str.includes(substr);
|
|
1452
|
+
},
|
|
1453
|
+
"substring-before": (args, ctx) => {
|
|
1454
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
1455
|
+
const substr = this.toString(this.evaluate(args[1], ctx));
|
|
1456
|
+
const idx = str.indexOf(substr);
|
|
1457
|
+
return idx === -1 ? "" : str.substring(0, idx);
|
|
1458
|
+
},
|
|
1459
|
+
"substring-after": (args, ctx) => {
|
|
1460
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
1461
|
+
const substr = this.toString(this.evaluate(args[1], ctx));
|
|
1462
|
+
const idx = str.indexOf(substr);
|
|
1463
|
+
return idx === -1 ? "" : str.substring(idx + substr.length);
|
|
1464
|
+
},
|
|
1465
|
+
substring: (args, ctx) => {
|
|
1466
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
1467
|
+
let start = Math.round(this.toNumber(this.evaluate(args[1], ctx)));
|
|
1468
|
+
let length;
|
|
1469
|
+
if (args.length > 2) {
|
|
1470
|
+
length = Math.round(this.toNumber(this.evaluate(args[2], ctx)));
|
|
1471
|
+
}
|
|
1472
|
+
start = start - 1;
|
|
1473
|
+
if (isNaN(start)) return "";
|
|
1474
|
+
if (start < 0) {
|
|
1475
|
+
if (length !== void 0) {
|
|
1476
|
+
length = length + start;
|
|
1477
|
+
}
|
|
1478
|
+
start = 0;
|
|
1479
|
+
}
|
|
1480
|
+
if (length !== void 0) {
|
|
1481
|
+
if (isNaN(length) || length <= 0) return "";
|
|
1482
|
+
return str.substring(start, start + length);
|
|
1483
|
+
}
|
|
1484
|
+
return str.substring(start);
|
|
1485
|
+
},
|
|
1486
|
+
"string-length": (args, ctx) => {
|
|
1487
|
+
const str = args.length === 0 ? this.toString([ctx.node]) : this.toString(this.evaluate(args[0], ctx));
|
|
1488
|
+
return str.length;
|
|
1489
|
+
},
|
|
1490
|
+
"normalize-space": (args, ctx) => {
|
|
1491
|
+
const str = args.length === 0 ? this.toString([ctx.node]) : this.toString(this.evaluate(args[0], ctx));
|
|
1492
|
+
return str.trim().replace(/\s+/g, " ");
|
|
1493
|
+
},
|
|
1494
|
+
translate: (args, ctx) => {
|
|
1495
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
1496
|
+
const from = this.toString(this.evaluate(args[1], ctx));
|
|
1497
|
+
const to = this.toString(this.evaluate(args[2], ctx));
|
|
1498
|
+
let result = "";
|
|
1499
|
+
for (const char of str) {
|
|
1500
|
+
const idx = from.indexOf(char);
|
|
1501
|
+
if (idx === -1) {
|
|
1502
|
+
result += char;
|
|
1503
|
+
} else if (idx < to.length) {
|
|
1504
|
+
result += to[idx];
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return result;
|
|
1508
|
+
},
|
|
1509
|
+
// Boolean functions
|
|
1510
|
+
boolean: (args, ctx) => {
|
|
1511
|
+
return this.toBoolean(this.evaluate(args[0], ctx));
|
|
1512
|
+
},
|
|
1513
|
+
not: (args, ctx) => {
|
|
1514
|
+
return !this.toBoolean(this.evaluate(args[0], ctx));
|
|
1515
|
+
},
|
|
1516
|
+
true: () => true,
|
|
1517
|
+
false: () => false,
|
|
1518
|
+
lang: (args, ctx) => {
|
|
1519
|
+
const lang = this.toString(this.evaluate(args[0], ctx)).toLowerCase();
|
|
1520
|
+
let node = ctx.node;
|
|
1521
|
+
while (node && node.nodeType === 1) {
|
|
1522
|
+
const xmlLang = node.getAttribute("xml:lang") || node.getAttribute("lang");
|
|
1523
|
+
if (xmlLang) {
|
|
1524
|
+
const nodeLang = xmlLang.toLowerCase();
|
|
1525
|
+
return nodeLang === lang || nodeLang.startsWith(lang + "-");
|
|
1526
|
+
}
|
|
1527
|
+
node = node.parentNode;
|
|
1528
|
+
}
|
|
1529
|
+
return false;
|
|
1530
|
+
},
|
|
1531
|
+
// Number functions
|
|
1532
|
+
number: (args, ctx) => {
|
|
1533
|
+
if (args.length === 0) {
|
|
1534
|
+
return this.toNumber([ctx.node]);
|
|
1535
|
+
}
|
|
1536
|
+
return this.toNumber(this.evaluate(args[0], ctx));
|
|
1537
|
+
},
|
|
1538
|
+
sum: (args, ctx) => {
|
|
1539
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
1540
|
+
if (!Array.isArray(nodeSet)) return NaN;
|
|
1541
|
+
return nodeSet.reduce(
|
|
1542
|
+
(sum, node) => sum + this.toNumber(this.getStringValue(node)),
|
|
1543
|
+
0
|
|
1544
|
+
);
|
|
1545
|
+
},
|
|
1546
|
+
floor: (args, ctx) => {
|
|
1547
|
+
return Math.floor(this.toNumber(this.evaluate(args[0], ctx)));
|
|
1548
|
+
},
|
|
1549
|
+
ceiling: (args, ctx) => {
|
|
1550
|
+
return Math.ceil(this.toNumber(this.evaluate(args[0], ctx)));
|
|
1551
|
+
},
|
|
1552
|
+
round: (args, ctx) => {
|
|
1553
|
+
const num = this.toNumber(this.evaluate(args[0], ctx));
|
|
1554
|
+
if (isNaN(num)) return NaN;
|
|
1555
|
+
if (num === -0.5) return -0;
|
|
1556
|
+
return Math.round(num);
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
// src/xslt/engine.js
|
|
1563
|
+
var XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
|
|
1564
|
+
var XsltContext = class _XsltContext {
|
|
1565
|
+
constructor(options = {}) {
|
|
1566
|
+
this.currentNode = options.currentNode;
|
|
1567
|
+
this.currentNodeList = options.currentNodeList || [];
|
|
1568
|
+
this.position = options.position || 1;
|
|
1569
|
+
this.variables = { ...options.variables };
|
|
1570
|
+
this.parameters = { ...options.parameters };
|
|
1571
|
+
this.outputDocument = options.outputDocument;
|
|
1572
|
+
this.stylesheet = options.stylesheet;
|
|
1573
|
+
this.namespaces = { ...options.namespaces };
|
|
1574
|
+
this.templates = options.templates || [];
|
|
1575
|
+
this.keys = options.keys || {};
|
|
1576
|
+
this.decimalFormats = options.decimalFormats || {};
|
|
1577
|
+
this.outputMethod = options.outputMethod || "xml";
|
|
1578
|
+
this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
|
|
1579
|
+
}
|
|
1580
|
+
clone(overrides = {}) {
|
|
1581
|
+
return new _XsltContext({
|
|
1582
|
+
currentNode: overrides.currentNode ?? this.currentNode,
|
|
1583
|
+
currentNodeList: overrides.currentNodeList ?? this.currentNodeList,
|
|
1584
|
+
position: overrides.position ?? this.position,
|
|
1585
|
+
variables: overrides.variables ? { ...this.variables, ...overrides.variables } : { ...this.variables },
|
|
1586
|
+
parameters: overrides.parameters ? { ...this.parameters, ...overrides.parameters } : { ...this.parameters },
|
|
1587
|
+
outputDocument: this.outputDocument,
|
|
1588
|
+
stylesheet: this.stylesheet,
|
|
1589
|
+
namespaces: overrides.namespaces ? { ...this.namespaces, ...overrides.namespaces } : { ...this.namespaces },
|
|
1590
|
+
templates: this.templates,
|
|
1591
|
+
keys: this.keys,
|
|
1592
|
+
decimalFormats: this.decimalFormats,
|
|
1593
|
+
outputMethod: this.outputMethod,
|
|
1594
|
+
xpathEvaluator: this.xpathEvaluator
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
getVariable(name) {
|
|
1598
|
+
if (name in this.variables) {
|
|
1599
|
+
return this.variables[name];
|
|
1600
|
+
}
|
|
1601
|
+
if (name in this.parameters) {
|
|
1602
|
+
return this.parameters[name];
|
|
1603
|
+
}
|
|
1604
|
+
throw new Error(`Undefined variable: $${name}`);
|
|
1605
|
+
}
|
|
1606
|
+
setVariable(name, value) {
|
|
1607
|
+
this.variables[name] = value;
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
var XsltEngine = class {
|
|
1611
|
+
constructor(options = {}) {
|
|
1612
|
+
this.xpathEvaluator = new XPathEvaluator();
|
|
1613
|
+
this.templates = [];
|
|
1614
|
+
this.keys = {};
|
|
1615
|
+
this.globalVariables = {};
|
|
1616
|
+
this.globalParameters = {};
|
|
1617
|
+
this.outputSettings = {
|
|
1618
|
+
method: "xml",
|
|
1619
|
+
encoding: "UTF-8",
|
|
1620
|
+
indent: "no",
|
|
1621
|
+
omitXmlDeclaration: "no",
|
|
1622
|
+
doctypePublic: null,
|
|
1623
|
+
doctypeSystem: null,
|
|
1624
|
+
mediaType: null,
|
|
1625
|
+
cdataSectionElements: []
|
|
1626
|
+
};
|
|
1627
|
+
this.namespaces = {};
|
|
1628
|
+
this.decimalFormats = {};
|
|
1629
|
+
this.stylesheetDoc = null;
|
|
1630
|
+
this.attributeSets = {};
|
|
1631
|
+
this.namespaceAliases = {};
|
|
1632
|
+
this.stripSpace = [];
|
|
1633
|
+
this.preserveSpace = [];
|
|
1634
|
+
this.stylesheetLoader = options.stylesheetLoader || null;
|
|
1635
|
+
this.currentImportPrecedence = 0;
|
|
1636
|
+
this.processedStylesheets = /* @__PURE__ */ new Set();
|
|
1637
|
+
this.baseUri = options.baseUri || "";
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Set the stylesheet loader function for xsl:import and xsl:include
|
|
1641
|
+
* @param {Function} loader - Function(href, baseUri) => Document or string (XML)
|
|
1642
|
+
*/
|
|
1643
|
+
setStylesheetLoader(loader) {
|
|
1644
|
+
this.stylesheetLoader = loader;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Resolve a relative URI against a base URI
|
|
1648
|
+
*/
|
|
1649
|
+
resolveUri(href, baseUri) {
|
|
1650
|
+
if (!baseUri || href.startsWith("http://") || href.startsWith("https://") || href.startsWith("/")) {
|
|
1651
|
+
return href;
|
|
1652
|
+
}
|
|
1653
|
+
const lastSlash = baseUri.lastIndexOf("/");
|
|
1654
|
+
const baseDir = lastSlash >= 0 ? baseUri.substring(0, lastSlash + 1) : "";
|
|
1655
|
+
return baseDir + href;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Load an external stylesheet document
|
|
1659
|
+
*/
|
|
1660
|
+
loadStylesheet(href, baseUri) {
|
|
1661
|
+
if (!this.stylesheetLoader) {
|
|
1662
|
+
throw new Error(
|
|
1663
|
+
`Cannot load stylesheet "${href}": no stylesheetLoader configured. Use engine.setStylesheetLoader(fn) to provide a loader function.`
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
const resolvedUri = this.resolveUri(href, baseUri);
|
|
1667
|
+
const result = this.stylesheetLoader(resolvedUri, baseUri);
|
|
1668
|
+
return { document: result, uri: resolvedUri };
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Parse XML string to document (helper for stylesheet loading)
|
|
1672
|
+
*/
|
|
1673
|
+
parseXmlString(xmlString) {
|
|
1674
|
+
if (typeof DOMParser !== "undefined") {
|
|
1675
|
+
const parser = new DOMParser();
|
|
1676
|
+
const doc = parser.parseFromString(xmlString, "application/xml");
|
|
1677
|
+
const parseError = doc.querySelector("parsererror");
|
|
1678
|
+
if (parseError) {
|
|
1679
|
+
throw new Error(`XML parse error: ${parseError.textContent}`);
|
|
1680
|
+
}
|
|
1681
|
+
return doc;
|
|
1682
|
+
}
|
|
1683
|
+
throw new Error("XML parsing not available in this environment");
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Import and compile an XSLT stylesheet
|
|
1687
|
+
* @param {Document|Element} stylesheetNode - The stylesheet document or root element
|
|
1688
|
+
* @param {string} [stylesheetUri] - Optional URI of the stylesheet for resolving imports
|
|
1689
|
+
*/
|
|
1690
|
+
importStylesheet(stylesheetNode, stylesheetUri) {
|
|
1691
|
+
const isMainStylesheet = this.stylesheetDoc === null;
|
|
1692
|
+
if (isMainStylesheet) {
|
|
1693
|
+
this.stylesheetDoc = stylesheetNode.ownerDocument || stylesheetNode;
|
|
1694
|
+
if (stylesheetUri) {
|
|
1695
|
+
this.baseUri = stylesheetUri;
|
|
1696
|
+
this.processedStylesheets.add(stylesheetUri);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const root = stylesheetNode.documentElement || stylesheetNode;
|
|
1700
|
+
if (!this.isXsltElement(root, "stylesheet") && !this.isXsltElement(root, "transform")) {
|
|
1701
|
+
if (root.getAttribute && root.getAttribute("xsl:version")) {
|
|
1702
|
+
this.processLiteralResultStylesheet(root);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
throw new Error(
|
|
1706
|
+
"Invalid XSLT stylesheet: root element must be xsl:stylesheet or xsl:transform"
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
this.collectNamespaces(root);
|
|
1710
|
+
const imports = [];
|
|
1711
|
+
const otherElements = [];
|
|
1712
|
+
for (const child of root.childNodes) {
|
|
1713
|
+
if (child.nodeType !== 1) continue;
|
|
1714
|
+
if (this.isXsltElement(child, "import")) {
|
|
1715
|
+
imports.push(child);
|
|
1716
|
+
} else {
|
|
1717
|
+
otherElements.push(child);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
for (const importNode of imports) {
|
|
1721
|
+
this.processImport(importNode, stylesheetUri || this.baseUri);
|
|
1722
|
+
}
|
|
1723
|
+
for (const child of otherElements) {
|
|
1724
|
+
if (this.isXsltElement(child, "template")) {
|
|
1725
|
+
this.registerTemplate(child);
|
|
1726
|
+
} else if (this.isXsltElement(child, "output")) {
|
|
1727
|
+
this.processOutput(child);
|
|
1728
|
+
} else if (this.isXsltElement(child, "variable")) {
|
|
1729
|
+
this.processGlobalVariable(child);
|
|
1730
|
+
} else if (this.isXsltElement(child, "param")) {
|
|
1731
|
+
this.processGlobalParam(child);
|
|
1732
|
+
} else if (this.isXsltElement(child, "key")) {
|
|
1733
|
+
this.processKey(child);
|
|
1734
|
+
} else if (this.isXsltElement(child, "decimal-format")) {
|
|
1735
|
+
this.processDecimalFormat(child);
|
|
1736
|
+
} else if (this.isXsltElement(child, "namespace-alias")) {
|
|
1737
|
+
this.processNamespaceAlias(child);
|
|
1738
|
+
} else if (this.isXsltElement(child, "attribute-set")) {
|
|
1739
|
+
this.processAttributeSet(child);
|
|
1740
|
+
} else if (this.isXsltElement(child, "strip-space")) {
|
|
1741
|
+
this.processStripSpace(child);
|
|
1742
|
+
} else if (this.isXsltElement(child, "preserve-space")) {
|
|
1743
|
+
this.processPreserveSpace(child);
|
|
1744
|
+
} else if (this.isXsltElement(child, "include")) {
|
|
1745
|
+
this.processInclude(child, stylesheetUri || this.baseUri);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (isMainStylesheet) {
|
|
1749
|
+
this.currentImportPrecedence++;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Process xsl:include element
|
|
1754
|
+
* Includes are merged at the same import precedence level
|
|
1755
|
+
*/
|
|
1756
|
+
processInclude(node, baseUri) {
|
|
1757
|
+
const href = node.getAttribute("href");
|
|
1758
|
+
if (!href) {
|
|
1759
|
+
throw new Error("xsl:include requires an href attribute");
|
|
1760
|
+
}
|
|
1761
|
+
const resolvedUri = this.resolveUri(href, baseUri);
|
|
1762
|
+
if (this.processedStylesheets.has(resolvedUri)) {
|
|
1763
|
+
throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
|
|
1764
|
+
}
|
|
1765
|
+
this.processedStylesheets.add(resolvedUri);
|
|
1766
|
+
try {
|
|
1767
|
+
const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
|
|
1768
|
+
let doc = stylesheetDoc;
|
|
1769
|
+
if (typeof stylesheetDoc === "string") {
|
|
1770
|
+
doc = this.parseXmlString(stylesheetDoc);
|
|
1771
|
+
}
|
|
1772
|
+
const savedPrecedence = this.currentImportPrecedence;
|
|
1773
|
+
this.processIncludedStylesheet(doc, resolvedUri);
|
|
1774
|
+
this.currentImportPrecedence = savedPrecedence;
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`Failed to include stylesheet "${href}": ${error.message}`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Process xsl:import element
|
|
1783
|
+
* Imports have lower precedence than the importing stylesheet
|
|
1784
|
+
*/
|
|
1785
|
+
processImport(node, baseUri) {
|
|
1786
|
+
const href = node.getAttribute("href");
|
|
1787
|
+
if (!href) {
|
|
1788
|
+
throw new Error("xsl:import requires an href attribute");
|
|
1789
|
+
}
|
|
1790
|
+
const resolvedUri = this.resolveUri(href, baseUri);
|
|
1791
|
+
if (this.processedStylesheets.has(resolvedUri)) {
|
|
1792
|
+
throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
|
|
1793
|
+
}
|
|
1794
|
+
this.processedStylesheets.add(resolvedUri);
|
|
1795
|
+
try {
|
|
1796
|
+
const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
|
|
1797
|
+
let doc = stylesheetDoc;
|
|
1798
|
+
if (typeof stylesheetDoc === "string") {
|
|
1799
|
+
doc = this.parseXmlString(stylesheetDoc);
|
|
1800
|
+
}
|
|
1801
|
+
this.processIncludedStylesheet(doc, resolvedUri);
|
|
1802
|
+
this.currentImportPrecedence++;
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`Failed to import stylesheet "${href}": ${error.message}`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Process an included/imported stylesheet document
|
|
1811
|
+
*/
|
|
1812
|
+
processIncludedStylesheet(stylesheetDoc, stylesheetUri) {
|
|
1813
|
+
const root = stylesheetDoc.documentElement || stylesheetDoc;
|
|
1814
|
+
if (!this.isXsltElement(root, "stylesheet") && !this.isXsltElement(root, "transform")) {
|
|
1815
|
+
throw new Error(
|
|
1816
|
+
"Included/imported document is not a valid XSLT stylesheet"
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
this.collectNamespaces(root);
|
|
1820
|
+
const imports = [];
|
|
1821
|
+
const otherElements = [];
|
|
1822
|
+
for (const child of root.childNodes) {
|
|
1823
|
+
if (child.nodeType !== 1) continue;
|
|
1824
|
+
if (this.isXsltElement(child, "import")) {
|
|
1825
|
+
imports.push(child);
|
|
1826
|
+
} else {
|
|
1827
|
+
otherElements.push(child);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
for (const importNode of imports) {
|
|
1831
|
+
this.processImport(importNode, stylesheetUri);
|
|
1832
|
+
}
|
|
1833
|
+
for (const child of otherElements) {
|
|
1834
|
+
if (this.isXsltElement(child, "template")) {
|
|
1835
|
+
this.registerTemplate(child);
|
|
1836
|
+
} else if (this.isXsltElement(child, "output")) {
|
|
1837
|
+
this.processOutput(child);
|
|
1838
|
+
} else if (this.isXsltElement(child, "variable")) {
|
|
1839
|
+
this.processGlobalVariable(child);
|
|
1840
|
+
} else if (this.isXsltElement(child, "param")) {
|
|
1841
|
+
this.processGlobalParam(child);
|
|
1842
|
+
} else if (this.isXsltElement(child, "key")) {
|
|
1843
|
+
this.processKey(child);
|
|
1844
|
+
} else if (this.isXsltElement(child, "decimal-format")) {
|
|
1845
|
+
this.processDecimalFormat(child);
|
|
1846
|
+
} else if (this.isXsltElement(child, "namespace-alias")) {
|
|
1847
|
+
this.processNamespaceAlias(child);
|
|
1848
|
+
} else if (this.isXsltElement(child, "attribute-set")) {
|
|
1849
|
+
this.processAttributeSet(child);
|
|
1850
|
+
} else if (this.isXsltElement(child, "strip-space")) {
|
|
1851
|
+
this.processStripSpace(child);
|
|
1852
|
+
} else if (this.isXsltElement(child, "preserve-space")) {
|
|
1853
|
+
this.processPreserveSpace(child);
|
|
1854
|
+
} else if (this.isXsltElement(child, "include")) {
|
|
1855
|
+
this.processInclude(child, stylesheetUri);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
processLiteralResultStylesheet(root) {
|
|
1860
|
+
this.templates.push({
|
|
1861
|
+
match: "/",
|
|
1862
|
+
name: null,
|
|
1863
|
+
mode: null,
|
|
1864
|
+
priority: 0.5,
|
|
1865
|
+
node: root
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
collectNamespaces(node) {
|
|
1869
|
+
if (!node.attributes) return;
|
|
1870
|
+
for (const attr of node.attributes) {
|
|
1871
|
+
if (attr.name.startsWith("xmlns:")) {
|
|
1872
|
+
const prefix = attr.name.substring(6);
|
|
1873
|
+
if (attr.value !== XSLT_NS) {
|
|
1874
|
+
this.namespaces[prefix] = attr.value;
|
|
1875
|
+
}
|
|
1876
|
+
} else if (attr.name === "xmlns" && attr.value !== XSLT_NS) {
|
|
1877
|
+
this.namespaces[""] = attr.value;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
registerTemplate(node) {
|
|
1882
|
+
const match = node.getAttribute("match");
|
|
1883
|
+
const name = node.getAttribute("name");
|
|
1884
|
+
const mode = node.getAttribute("mode") || null;
|
|
1885
|
+
const priorityAttr = node.getAttribute("priority");
|
|
1886
|
+
const priority = priorityAttr ? parseFloat(priorityAttr) : this.calculatePriority(match);
|
|
1887
|
+
this.templates.push({
|
|
1888
|
+
match,
|
|
1889
|
+
name,
|
|
1890
|
+
mode,
|
|
1891
|
+
priority,
|
|
1892
|
+
importPrecedence: this.currentImportPrecedence,
|
|
1893
|
+
node
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
calculatePriority(matchPattern) {
|
|
1897
|
+
if (!matchPattern) return 0.5;
|
|
1898
|
+
if (matchPattern === "*" || matchPattern === "node()" || matchPattern === "text()" || matchPattern === "comment()" || matchPattern === "processing-instruction()") {
|
|
1899
|
+
return -0.5;
|
|
1900
|
+
}
|
|
1901
|
+
if (matchPattern.includes(":*")) {
|
|
1902
|
+
return -0.25;
|
|
1903
|
+
}
|
|
1904
|
+
if (/^[a-zA-Z_][\w.-]*$/.test(matchPattern)) {
|
|
1905
|
+
return 0;
|
|
1906
|
+
}
|
|
1907
|
+
return 0.5;
|
|
1908
|
+
}
|
|
1909
|
+
processOutput(node) {
|
|
1910
|
+
const method = node.getAttribute("method");
|
|
1911
|
+
if (method) this.outputSettings.method = method;
|
|
1912
|
+
const encoding = node.getAttribute("encoding");
|
|
1913
|
+
if (encoding) this.outputSettings.encoding = encoding;
|
|
1914
|
+
const indent = node.getAttribute("indent");
|
|
1915
|
+
if (indent) this.outputSettings.indent = indent;
|
|
1916
|
+
const omit = node.getAttribute("omit-xml-declaration");
|
|
1917
|
+
if (omit) this.outputSettings.omitXmlDeclaration = omit;
|
|
1918
|
+
const doctypePublic = node.getAttribute("doctype-public");
|
|
1919
|
+
if (doctypePublic) this.outputSettings.doctypePublic = doctypePublic;
|
|
1920
|
+
const doctypeSystem = node.getAttribute("doctype-system");
|
|
1921
|
+
if (doctypeSystem) this.outputSettings.doctypeSystem = doctypeSystem;
|
|
1922
|
+
const mediaType = node.getAttribute("media-type");
|
|
1923
|
+
if (mediaType) this.outputSettings.mediaType = mediaType;
|
|
1924
|
+
const cdataElements = node.getAttribute("cdata-section-elements");
|
|
1925
|
+
if (cdataElements) {
|
|
1926
|
+
this.outputSettings.cdataSectionElements = cdataElements.split(/\s+/).filter(Boolean);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
processGlobalVariable(node) {
|
|
1930
|
+
const name = node.getAttribute("name");
|
|
1931
|
+
const select2 = node.getAttribute("select");
|
|
1932
|
+
this.globalVariables[name] = { node, select: select2 };
|
|
1933
|
+
}
|
|
1934
|
+
processGlobalParam(node) {
|
|
1935
|
+
const name = node.getAttribute("name");
|
|
1936
|
+
const select2 = node.getAttribute("select");
|
|
1937
|
+
this.globalParameters[name] = { node, select: select2 };
|
|
1938
|
+
}
|
|
1939
|
+
processKey(node) {
|
|
1940
|
+
const name = node.getAttribute("name");
|
|
1941
|
+
const match = node.getAttribute("match");
|
|
1942
|
+
const use = node.getAttribute("use");
|
|
1943
|
+
this.keys[name] = { match, use };
|
|
1944
|
+
}
|
|
1945
|
+
processDecimalFormat(node) {
|
|
1946
|
+
const name = node.getAttribute("name") || "";
|
|
1947
|
+
this.decimalFormats[name] = {
|
|
1948
|
+
decimalSeparator: node.getAttribute("decimal-separator") || ".",
|
|
1949
|
+
groupingSeparator: node.getAttribute("grouping-separator") || ",",
|
|
1950
|
+
percent: node.getAttribute("percent") || "%",
|
|
1951
|
+
perMille: node.getAttribute("per-mille") || "\u2030",
|
|
1952
|
+
zeroDigit: node.getAttribute("zero-digit") || "0",
|
|
1953
|
+
digit: node.getAttribute("digit") || "#",
|
|
1954
|
+
patternSeparator: node.getAttribute("pattern-separator") || ";",
|
|
1955
|
+
infinity: node.getAttribute("infinity") || "Infinity",
|
|
1956
|
+
nan: node.getAttribute("NaN") || "NaN",
|
|
1957
|
+
minusSign: node.getAttribute("minus-sign") || "-"
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
processNamespaceAlias(node) {
|
|
1961
|
+
const stylesheet = node.getAttribute("stylesheet-prefix");
|
|
1962
|
+
const result = node.getAttribute("result-prefix");
|
|
1963
|
+
this.namespaceAliases[stylesheet] = result;
|
|
1964
|
+
}
|
|
1965
|
+
processAttributeSet(node) {
|
|
1966
|
+
const name = node.getAttribute("name");
|
|
1967
|
+
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
1968
|
+
this.attributeSets[name] = {
|
|
1969
|
+
node,
|
|
1970
|
+
useAttributeSets: useAttributeSets ? useAttributeSets.split(/\s+/).filter(Boolean) : []
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
processStripSpace(node) {
|
|
1974
|
+
const elements = node.getAttribute("elements");
|
|
1975
|
+
if (elements) {
|
|
1976
|
+
this.stripSpace.push(...elements.split(/\s+/).filter(Boolean));
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
processPreserveSpace(node) {
|
|
1980
|
+
const elements = node.getAttribute("elements");
|
|
1981
|
+
if (elements) {
|
|
1982
|
+
this.preserveSpace.push(...elements.split(/\s+/).filter(Boolean));
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Transform a source document
|
|
1987
|
+
*/
|
|
1988
|
+
transform(sourceNode, ownerDocument) {
|
|
1989
|
+
const doc = ownerDocument || (typeof document !== "undefined" ? document : null);
|
|
1990
|
+
if (!doc) {
|
|
1991
|
+
throw new Error("No output document available");
|
|
1992
|
+
}
|
|
1993
|
+
const context = new XsltContext({
|
|
1994
|
+
currentNode: sourceNode.documentElement || sourceNode,
|
|
1995
|
+
currentNodeList: [sourceNode.documentElement || sourceNode],
|
|
1996
|
+
position: 1,
|
|
1997
|
+
outputDocument: doc,
|
|
1998
|
+
stylesheet: this.stylesheetDoc,
|
|
1999
|
+
namespaces: { ...this.namespaces },
|
|
2000
|
+
templates: this.templates,
|
|
2001
|
+
keys: this.keys,
|
|
2002
|
+
decimalFormats: this.decimalFormats,
|
|
2003
|
+
outputMethod: this.outputSettings.method,
|
|
2004
|
+
xpathEvaluator: this.xpathEvaluator
|
|
2005
|
+
});
|
|
2006
|
+
for (const [name, def] of Object.entries(this.globalParameters)) {
|
|
2007
|
+
if (!(name in context.parameters)) {
|
|
2008
|
+
context.parameters[name] = this.evaluateVariable(def, context);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
for (const [name, def] of Object.entries(this.globalVariables)) {
|
|
2012
|
+
context.variables[name] = this.evaluateVariable(def, context);
|
|
2013
|
+
}
|
|
2014
|
+
const fragment = doc.createDocumentFragment();
|
|
2015
|
+
this.applyTemplates(
|
|
2016
|
+
[sourceNode.documentElement || sourceNode],
|
|
2017
|
+
null,
|
|
2018
|
+
context,
|
|
2019
|
+
fragment
|
|
2020
|
+
);
|
|
2021
|
+
return fragment;
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Transform to a complete document
|
|
2025
|
+
*/
|
|
2026
|
+
transformToDocument(sourceNode) {
|
|
2027
|
+
const doc = this.createDocument();
|
|
2028
|
+
const fragment = this.transform(sourceNode, doc);
|
|
2029
|
+
while (fragment.firstChild) {
|
|
2030
|
+
doc.appendChild(fragment.firstChild);
|
|
2031
|
+
}
|
|
2032
|
+
return doc;
|
|
2033
|
+
}
|
|
2034
|
+
createDocument() {
|
|
2035
|
+
if (typeof document !== "undefined") {
|
|
2036
|
+
return document.implementation.createDocument(null, null, null);
|
|
2037
|
+
}
|
|
2038
|
+
throw new Error("Document creation not available in this environment");
|
|
2039
|
+
}
|
|
2040
|
+
evaluateVariable(def, context) {
|
|
2041
|
+
if (def.select) {
|
|
2042
|
+
return this.evaluateXPath(def.select, context);
|
|
2043
|
+
}
|
|
2044
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2045
|
+
this.processChildren(def.node, context, fragment);
|
|
2046
|
+
return fragment;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Apply templates to a node list
|
|
2050
|
+
*/
|
|
2051
|
+
applyTemplates(nodes, mode, context, output) {
|
|
2052
|
+
const nodeList = Array.isArray(nodes) ? nodes : [nodes];
|
|
2053
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
2054
|
+
const node = nodeList[i];
|
|
2055
|
+
const template = this.findMatchingTemplate(node, mode, context);
|
|
2056
|
+
if (template) {
|
|
2057
|
+
const newContext = context.clone({
|
|
2058
|
+
currentNode: node,
|
|
2059
|
+
currentNodeList: nodeList,
|
|
2060
|
+
position: i + 1
|
|
2061
|
+
});
|
|
2062
|
+
this.processTemplate(template.node, newContext, output);
|
|
2063
|
+
} else {
|
|
2064
|
+
this.applyBuiltinTemplate(node, mode, context, output);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Find the best matching template for a node
|
|
2070
|
+
*/
|
|
2071
|
+
findMatchingTemplate(node, mode, context) {
|
|
2072
|
+
let bestMatch = null;
|
|
2073
|
+
let bestPriority = -Infinity;
|
|
2074
|
+
let bestImportPrecedence = -Infinity;
|
|
2075
|
+
for (const template of this.templates) {
|
|
2076
|
+
if (template.mode !== mode) continue;
|
|
2077
|
+
if (!template.match) continue;
|
|
2078
|
+
if (this.matchesPattern(node, template.match, context)) {
|
|
2079
|
+
const priority = template.priority;
|
|
2080
|
+
const importPrecedence = template.importPrecedence || 0;
|
|
2081
|
+
if (importPrecedence > bestImportPrecedence || importPrecedence === bestImportPrecedence && priority > bestPriority) {
|
|
2082
|
+
bestMatch = template;
|
|
2083
|
+
bestPriority = priority;
|
|
2084
|
+
bestImportPrecedence = importPrecedence;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return bestMatch;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Check if a node matches an XSLT pattern
|
|
2092
|
+
*/
|
|
2093
|
+
matchesPattern(node, pattern, context) {
|
|
2094
|
+
const patterns = this.splitUnionPattern(pattern);
|
|
2095
|
+
for (const p of patterns) {
|
|
2096
|
+
if (this.matchesSinglePattern(node, p.trim(), context)) {
|
|
2097
|
+
return true;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
splitUnionPattern(pattern) {
|
|
2103
|
+
const parts = [];
|
|
2104
|
+
let current = "";
|
|
2105
|
+
let depth = 0;
|
|
2106
|
+
let inString = false;
|
|
2107
|
+
let stringChar = "";
|
|
2108
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
2109
|
+
const char = pattern[i];
|
|
2110
|
+
if (inString) {
|
|
2111
|
+
current += char;
|
|
2112
|
+
if (char === stringChar) {
|
|
2113
|
+
inString = false;
|
|
2114
|
+
}
|
|
2115
|
+
} else if (char === '"' || char === "'") {
|
|
2116
|
+
inString = true;
|
|
2117
|
+
stringChar = char;
|
|
2118
|
+
current += char;
|
|
2119
|
+
} else if (char === "[") {
|
|
2120
|
+
depth++;
|
|
2121
|
+
current += char;
|
|
2122
|
+
} else if (char === "]") {
|
|
2123
|
+
depth--;
|
|
2124
|
+
current += char;
|
|
2125
|
+
} else if (char === "|" && depth === 0) {
|
|
2126
|
+
parts.push(current);
|
|
2127
|
+
current = "";
|
|
2128
|
+
} else {
|
|
2129
|
+
current += char;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (current) {
|
|
2133
|
+
parts.push(current);
|
|
2134
|
+
}
|
|
2135
|
+
return parts;
|
|
2136
|
+
}
|
|
2137
|
+
matchesSinglePattern(node, pattern, context) {
|
|
2138
|
+
try {
|
|
2139
|
+
if (pattern === "/") {
|
|
2140
|
+
return node.nodeType === 9 || node === node.ownerDocument?.documentElement;
|
|
2141
|
+
}
|
|
2142
|
+
const ast = parse(pattern);
|
|
2143
|
+
if (pattern.startsWith("/")) {
|
|
2144
|
+
const doc = node.ownerDocument || node;
|
|
2145
|
+
const xpathContext2 = new XPathContext(
|
|
2146
|
+
doc,
|
|
2147
|
+
1,
|
|
2148
|
+
1,
|
|
2149
|
+
{ ...context.variables, ...context.parameters },
|
|
2150
|
+
context.namespaces
|
|
2151
|
+
);
|
|
2152
|
+
const result2 = this.xpathEvaluator.evaluate(ast, xpathContext2);
|
|
2153
|
+
const nodes2 = Array.isArray(result2) ? result2 : [result2];
|
|
2154
|
+
return nodes2.includes(node);
|
|
2155
|
+
}
|
|
2156
|
+
if (node.parentNode) {
|
|
2157
|
+
const xpathContext2 = new XPathContext(
|
|
2158
|
+
node.parentNode,
|
|
2159
|
+
1,
|
|
2160
|
+
1,
|
|
2161
|
+
{ ...context.variables, ...context.parameters },
|
|
2162
|
+
context.namespaces
|
|
2163
|
+
);
|
|
2164
|
+
const result2 = this.xpathEvaluator.evaluate(ast, xpathContext2);
|
|
2165
|
+
const nodes2 = Array.isArray(result2) ? result2 : [result2];
|
|
2166
|
+
return nodes2.includes(node);
|
|
2167
|
+
}
|
|
2168
|
+
const xpathContext = new XPathContext(
|
|
2169
|
+
node,
|
|
2170
|
+
1,
|
|
2171
|
+
1,
|
|
2172
|
+
{ ...context.variables, ...context.parameters },
|
|
2173
|
+
context.namespaces
|
|
2174
|
+
);
|
|
2175
|
+
const result = this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
2176
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
2177
|
+
return nodes.includes(node);
|
|
2178
|
+
} catch {
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Apply built-in template rules
|
|
2184
|
+
*/
|
|
2185
|
+
applyBuiltinTemplate(node, mode, context, output) {
|
|
2186
|
+
switch (node.nodeType) {
|
|
2187
|
+
case 1:
|
|
2188
|
+
// Element
|
|
2189
|
+
case 9:
|
|
2190
|
+
// Document
|
|
2191
|
+
case 11:
|
|
2192
|
+
this.applyTemplates(Array.from(node.childNodes), mode, context, output);
|
|
2193
|
+
break;
|
|
2194
|
+
case 3:
|
|
2195
|
+
// Text
|
|
2196
|
+
case 4: {
|
|
2197
|
+
const text = context.outputDocument.createTextNode(
|
|
2198
|
+
node.nodeValue || ""
|
|
2199
|
+
);
|
|
2200
|
+
output.appendChild(text);
|
|
2201
|
+
break;
|
|
2202
|
+
}
|
|
2203
|
+
case 2: {
|
|
2204
|
+
const attrText = context.outputDocument.createTextNode(
|
|
2205
|
+
node.nodeValue || ""
|
|
2206
|
+
);
|
|
2207
|
+
output.appendChild(attrText);
|
|
2208
|
+
break;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Process template content
|
|
2214
|
+
*/
|
|
2215
|
+
processTemplate(templateNode, context, output) {
|
|
2216
|
+
const localContext = context.clone();
|
|
2217
|
+
for (const child of templateNode.childNodes) {
|
|
2218
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "param")) {
|
|
2219
|
+
const name = child.getAttribute("name");
|
|
2220
|
+
if (!(name in localContext.parameters)) {
|
|
2221
|
+
localContext.parameters[name] = this.evaluateVariable(
|
|
2222
|
+
{ node: child, select: child.getAttribute("select") },
|
|
2223
|
+
localContext
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
this.processChildren(templateNode, localContext, output);
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Process child nodes of an XSLT element
|
|
2232
|
+
*/
|
|
2233
|
+
processChildren(node, context, output) {
|
|
2234
|
+
for (const child of node.childNodes) {
|
|
2235
|
+
this.processNode(child, context, output);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Process a single node in the stylesheet
|
|
2240
|
+
*/
|
|
2241
|
+
processNode(node, context, output) {
|
|
2242
|
+
switch (node.nodeType) {
|
|
2243
|
+
case 1:
|
|
2244
|
+
this.processElement(node, context, output);
|
|
2245
|
+
break;
|
|
2246
|
+
case 3:
|
|
2247
|
+
// Text
|
|
2248
|
+
case 4: {
|
|
2249
|
+
const text = node.nodeValue;
|
|
2250
|
+
if (text && (text.trim() || this.shouldPreserveSpace(node))) {
|
|
2251
|
+
const textNode = context.outputDocument.createTextNode(text);
|
|
2252
|
+
output.appendChild(textNode);
|
|
2253
|
+
}
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
shouldPreserveSpace(node) {
|
|
2259
|
+
let current = node.parentNode;
|
|
2260
|
+
while (current && current.nodeType === 1) {
|
|
2261
|
+
const space = current.getAttribute("xml:space");
|
|
2262
|
+
if (space === "preserve") return true;
|
|
2263
|
+
if (space === "default") return false;
|
|
2264
|
+
current = current.parentNode;
|
|
2265
|
+
}
|
|
2266
|
+
return false;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Process an element in the stylesheet
|
|
2270
|
+
*/
|
|
2271
|
+
processElement(node, context, output) {
|
|
2272
|
+
if (this.isXsltNamespace(node)) {
|
|
2273
|
+
this.processXsltElement(node, context, output);
|
|
2274
|
+
} else {
|
|
2275
|
+
this.processLiteralResultElement(node, context, output);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Process an XSLT instruction element
|
|
2280
|
+
*/
|
|
2281
|
+
processXsltElement(node, context, output) {
|
|
2282
|
+
const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
|
|
2283
|
+
switch (localName) {
|
|
2284
|
+
case "apply-templates":
|
|
2285
|
+
this.xslApplyTemplates(node, context, output);
|
|
2286
|
+
break;
|
|
2287
|
+
case "call-template":
|
|
2288
|
+
this.xslCallTemplate(node, context, output);
|
|
2289
|
+
break;
|
|
2290
|
+
case "value-of":
|
|
2291
|
+
this.xslValueOf(node, context, output);
|
|
2292
|
+
break;
|
|
2293
|
+
case "text":
|
|
2294
|
+
this.xslText(node, context, output);
|
|
2295
|
+
break;
|
|
2296
|
+
case "element":
|
|
2297
|
+
this.xslElement(node, context, output);
|
|
2298
|
+
break;
|
|
2299
|
+
case "attribute":
|
|
2300
|
+
this.xslAttribute(node, context, output);
|
|
2301
|
+
break;
|
|
2302
|
+
case "if":
|
|
2303
|
+
this.xslIf(node, context, output);
|
|
2304
|
+
break;
|
|
2305
|
+
case "choose":
|
|
2306
|
+
this.xslChoose(node, context, output);
|
|
2307
|
+
break;
|
|
2308
|
+
case "for-each":
|
|
2309
|
+
this.xslForEach(node, context, output);
|
|
2310
|
+
break;
|
|
2311
|
+
case "copy":
|
|
2312
|
+
this.xslCopy(node, context, output);
|
|
2313
|
+
break;
|
|
2314
|
+
case "copy-of":
|
|
2315
|
+
this.xslCopyOf(node, context, output);
|
|
2316
|
+
break;
|
|
2317
|
+
case "variable":
|
|
2318
|
+
this.xslVariable(node, context, output);
|
|
2319
|
+
break;
|
|
2320
|
+
case "param":
|
|
2321
|
+
break;
|
|
2322
|
+
case "comment":
|
|
2323
|
+
this.xslComment(node, context, output);
|
|
2324
|
+
break;
|
|
2325
|
+
case "processing-instruction":
|
|
2326
|
+
this.xslProcessingInstruction(node, context, output);
|
|
2327
|
+
break;
|
|
2328
|
+
case "number":
|
|
2329
|
+
this.xslNumber(node, context, output);
|
|
2330
|
+
break;
|
|
2331
|
+
case "sort":
|
|
2332
|
+
break;
|
|
2333
|
+
case "with-param":
|
|
2334
|
+
break;
|
|
2335
|
+
case "message":
|
|
2336
|
+
this.xslMessage(node, context, output);
|
|
2337
|
+
break;
|
|
2338
|
+
case "fallback":
|
|
2339
|
+
break;
|
|
2340
|
+
default:
|
|
2341
|
+
console.warn(`Unknown XSLT element: ${localName}`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Process a literal result element (non-XSLT)
|
|
2346
|
+
*/
|
|
2347
|
+
processLiteralResultElement(node, context, output) {
|
|
2348
|
+
let outputElement;
|
|
2349
|
+
const namespaceURI = node.namespaceURI;
|
|
2350
|
+
const nodeName = node.nodeName;
|
|
2351
|
+
let resolvedNS = namespaceURI;
|
|
2352
|
+
if (namespaceURI) {
|
|
2353
|
+
for (const [from, to] of Object.entries(this.namespaceAliases)) {
|
|
2354
|
+
if (this.namespaces[from] === namespaceURI) {
|
|
2355
|
+
resolvedNS = this.namespaces[to] || to;
|
|
2356
|
+
break;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (resolvedNS && context.outputDocument.createElementNS) {
|
|
2361
|
+
outputElement = context.outputDocument.createElementNS(
|
|
2362
|
+
resolvedNS,
|
|
2363
|
+
nodeName
|
|
2364
|
+
);
|
|
2365
|
+
} else {
|
|
2366
|
+
outputElement = context.outputDocument.createElement(nodeName);
|
|
2367
|
+
}
|
|
2368
|
+
if (node.attributes) {
|
|
2369
|
+
for (const attr of node.attributes) {
|
|
2370
|
+
if (attr.namespaceURI === XSLT_NS) continue;
|
|
2371
|
+
if (attr.name.startsWith("xmlns")) continue;
|
|
2372
|
+
const value = this.processAttributeValueTemplate(attr.value, context);
|
|
2373
|
+
outputElement.setAttribute(attr.name, value);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
this.processChildren(node, context, outputElement);
|
|
2377
|
+
output.appendChild(outputElement);
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Process attribute value templates (expressions in curly braces)
|
|
2381
|
+
*/
|
|
2382
|
+
processAttributeValueTemplate(value, context) {
|
|
2383
|
+
if (!value.includes("{")) return value;
|
|
2384
|
+
let result = "";
|
|
2385
|
+
let i = 0;
|
|
2386
|
+
while (i < value.length) {
|
|
2387
|
+
if (value[i] === "{") {
|
|
2388
|
+
if (value[i + 1] === "{") {
|
|
2389
|
+
result += "{";
|
|
2390
|
+
i += 2;
|
|
2391
|
+
} else {
|
|
2392
|
+
let depth = 1;
|
|
2393
|
+
let j = i + 1;
|
|
2394
|
+
while (j < value.length && depth > 0) {
|
|
2395
|
+
if (value[j] === "{") depth++;
|
|
2396
|
+
else if (value[j] === "}") depth--;
|
|
2397
|
+
j++;
|
|
2398
|
+
}
|
|
2399
|
+
const expr = value.substring(i + 1, j - 1);
|
|
2400
|
+
const evalResult = this.evaluateXPath(expr, context);
|
|
2401
|
+
result += this.xpathEvaluator.toString(evalResult);
|
|
2402
|
+
i = j;
|
|
2403
|
+
}
|
|
2404
|
+
} else if (value[i] === "}") {
|
|
2405
|
+
if (value[i + 1] === "}") {
|
|
2406
|
+
result += "}";
|
|
2407
|
+
i += 2;
|
|
2408
|
+
} else {
|
|
2409
|
+
throw new Error("Unmatched } in attribute value template");
|
|
2410
|
+
}
|
|
2411
|
+
} else {
|
|
2412
|
+
result += value[i];
|
|
2413
|
+
i++;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
return result;
|
|
2417
|
+
}
|
|
2418
|
+
// XSLT Instructions
|
|
2419
|
+
xslApplyTemplates(node, context, output) {
|
|
2420
|
+
const select2 = node.getAttribute("select") || "node()";
|
|
2421
|
+
const mode = node.getAttribute("mode") || null;
|
|
2422
|
+
let nodes = this.evaluateXPath(select2, context);
|
|
2423
|
+
if (!Array.isArray(nodes)) {
|
|
2424
|
+
nodes = nodes ? [nodes] : [];
|
|
2425
|
+
}
|
|
2426
|
+
const sortSpecs = [];
|
|
2427
|
+
for (const child of node.childNodes) {
|
|
2428
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
2429
|
+
sortSpecs.push({
|
|
2430
|
+
select: child.getAttribute("select") || ".",
|
|
2431
|
+
order: child.getAttribute("order") || "ascending",
|
|
2432
|
+
dataType: child.getAttribute("data-type") || "text",
|
|
2433
|
+
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
2434
|
+
lang: child.getAttribute("lang")
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
if (sortSpecs.length > 0) {
|
|
2439
|
+
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
2440
|
+
}
|
|
2441
|
+
const params = {};
|
|
2442
|
+
for (const child of node.childNodes) {
|
|
2443
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
2444
|
+
const name = child.getAttribute("name");
|
|
2445
|
+
const selectAttr = child.getAttribute("select");
|
|
2446
|
+
if (selectAttr) {
|
|
2447
|
+
params[name] = this.evaluateXPath(selectAttr, context);
|
|
2448
|
+
} else {
|
|
2449
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2450
|
+
this.processChildren(child, context, fragment);
|
|
2451
|
+
params[name] = fragment;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
const newContext = context.clone({
|
|
2456
|
+
parameters: { ...context.parameters, ...params }
|
|
2457
|
+
});
|
|
2458
|
+
this.applyTemplates(nodes, mode, newContext, output);
|
|
2459
|
+
}
|
|
2460
|
+
xslCallTemplate(node, context, output) {
|
|
2461
|
+
const name = node.getAttribute("name");
|
|
2462
|
+
const template = this.templates.find((t) => t.name === name);
|
|
2463
|
+
if (!template) {
|
|
2464
|
+
throw new Error(`Template not found: ${name}`);
|
|
2465
|
+
}
|
|
2466
|
+
const params = {};
|
|
2467
|
+
for (const child of node.childNodes) {
|
|
2468
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
|
|
2469
|
+
const paramName = child.getAttribute("name");
|
|
2470
|
+
const selectAttr = child.getAttribute("select");
|
|
2471
|
+
if (selectAttr) {
|
|
2472
|
+
params[paramName] = this.evaluateXPath(selectAttr, context);
|
|
2473
|
+
} else {
|
|
2474
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2475
|
+
this.processChildren(child, context, fragment);
|
|
2476
|
+
params[paramName] = fragment;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const newContext = context.clone({
|
|
2481
|
+
parameters: { ...context.parameters, ...params }
|
|
2482
|
+
});
|
|
2483
|
+
this.processTemplate(template.node, newContext, output);
|
|
2484
|
+
}
|
|
2485
|
+
xslValueOf(node, context, output) {
|
|
2486
|
+
const select2 = node.getAttribute("select");
|
|
2487
|
+
const disableOutputEscaping = node.getAttribute("disable-output-escaping") === "yes";
|
|
2488
|
+
const result = this.evaluateXPath(select2, context);
|
|
2489
|
+
const text = this.xpathEvaluator.toString(result);
|
|
2490
|
+
if (text) {
|
|
2491
|
+
const textNode = context.outputDocument.createTextNode(text);
|
|
2492
|
+
if (disableOutputEscaping) {
|
|
2493
|
+
textNode._disableOutputEscaping = true;
|
|
2494
|
+
}
|
|
2495
|
+
output.appendChild(textNode);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
xslText(node, context, output) {
|
|
2499
|
+
const disableOutputEscaping = node.getAttribute("disable-output-escaping") === "yes";
|
|
2500
|
+
let text = "";
|
|
2501
|
+
for (const child of node.childNodes) {
|
|
2502
|
+
if (child.nodeType === 3 || child.nodeType === 4) {
|
|
2503
|
+
text += child.nodeValue || "";
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
if (text) {
|
|
2507
|
+
const textNode = context.outputDocument.createTextNode(text);
|
|
2508
|
+
if (disableOutputEscaping) {
|
|
2509
|
+
textNode._disableOutputEscaping = true;
|
|
2510
|
+
}
|
|
2511
|
+
output.appendChild(textNode);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
xslElement(node, context, output) {
|
|
2515
|
+
const name = this.processAttributeValueTemplate(
|
|
2516
|
+
node.getAttribute("name"),
|
|
2517
|
+
context
|
|
2518
|
+
);
|
|
2519
|
+
const namespace = node.getAttribute("namespace");
|
|
2520
|
+
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
2521
|
+
let element;
|
|
2522
|
+
if (namespace) {
|
|
2523
|
+
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
2524
|
+
element = context.outputDocument.createElementNS(ns, name);
|
|
2525
|
+
} else {
|
|
2526
|
+
element = context.outputDocument.createElement(name);
|
|
2527
|
+
}
|
|
2528
|
+
if (useAttributeSets) {
|
|
2529
|
+
this.applyAttributeSets(useAttributeSets, context, element);
|
|
2530
|
+
}
|
|
2531
|
+
this.processChildren(node, context, element);
|
|
2532
|
+
output.appendChild(element);
|
|
2533
|
+
}
|
|
2534
|
+
xslAttribute(node, context, output) {
|
|
2535
|
+
const name = this.processAttributeValueTemplate(
|
|
2536
|
+
node.getAttribute("name"),
|
|
2537
|
+
context
|
|
2538
|
+
);
|
|
2539
|
+
const namespace = node.getAttribute("namespace");
|
|
2540
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2541
|
+
this.processChildren(node, context, fragment);
|
|
2542
|
+
let value = "";
|
|
2543
|
+
const getText = (n) => {
|
|
2544
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
2545
|
+
value += n.nodeValue || "";
|
|
2546
|
+
} else if (n.childNodes) {
|
|
2547
|
+
for (const child of n.childNodes) {
|
|
2548
|
+
getText(child);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
};
|
|
2552
|
+
getText(fragment);
|
|
2553
|
+
if (output.nodeType === 1) {
|
|
2554
|
+
if (namespace) {
|
|
2555
|
+
const ns = this.processAttributeValueTemplate(namespace, context);
|
|
2556
|
+
output.setAttributeNS(ns, name, value);
|
|
2557
|
+
} else {
|
|
2558
|
+
output.setAttribute(name, value);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
xslIf(node, context, output) {
|
|
2563
|
+
const test = node.getAttribute("test");
|
|
2564
|
+
const result = this.evaluateXPath(test, context);
|
|
2565
|
+
if (this.xpathEvaluator.toBoolean(result)) {
|
|
2566
|
+
this.processChildren(node, context, output);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
xslChoose(node, context, output) {
|
|
2570
|
+
for (const child of node.childNodes) {
|
|
2571
|
+
if (child.nodeType !== 1) continue;
|
|
2572
|
+
if (this.isXsltElement(child, "when")) {
|
|
2573
|
+
const test = child.getAttribute("test");
|
|
2574
|
+
const result = this.evaluateXPath(test, context);
|
|
2575
|
+
if (this.xpathEvaluator.toBoolean(result)) {
|
|
2576
|
+
this.processChildren(child, context, output);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
} else if (this.isXsltElement(child, "otherwise")) {
|
|
2580
|
+
this.processChildren(child, context, output);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
xslForEach(node, context, output) {
|
|
2586
|
+
const select2 = node.getAttribute("select");
|
|
2587
|
+
let nodes = this.evaluateXPath(select2, context);
|
|
2588
|
+
if (!Array.isArray(nodes)) {
|
|
2589
|
+
nodes = nodes ? [nodes] : [];
|
|
2590
|
+
}
|
|
2591
|
+
const sortSpecs = [];
|
|
2592
|
+
for (const child of node.childNodes) {
|
|
2593
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
|
|
2594
|
+
sortSpecs.push({
|
|
2595
|
+
select: child.getAttribute("select") || ".",
|
|
2596
|
+
order: child.getAttribute("order") || "ascending",
|
|
2597
|
+
dataType: child.getAttribute("data-type") || "text",
|
|
2598
|
+
caseOrder: child.getAttribute("case-order") || "upper-first",
|
|
2599
|
+
lang: child.getAttribute("lang")
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
if (sortSpecs.length > 0) {
|
|
2604
|
+
nodes = this.sortNodes(nodes, sortSpecs, context);
|
|
2605
|
+
}
|
|
2606
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2607
|
+
const newContext = context.clone({
|
|
2608
|
+
currentNode: nodes[i],
|
|
2609
|
+
currentNodeList: nodes,
|
|
2610
|
+
position: i + 1
|
|
2611
|
+
});
|
|
2612
|
+
this.processChildren(node, newContext, output);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
xslCopy(node, context, output) {
|
|
2616
|
+
const currentNode = context.currentNode;
|
|
2617
|
+
const useAttributeSets = node.getAttribute("use-attribute-sets");
|
|
2618
|
+
switch (currentNode.nodeType) {
|
|
2619
|
+
case 1: {
|
|
2620
|
+
let copy;
|
|
2621
|
+
if (currentNode.namespaceURI) {
|
|
2622
|
+
copy = context.outputDocument.createElementNS(
|
|
2623
|
+
currentNode.namespaceURI,
|
|
2624
|
+
currentNode.nodeName
|
|
2625
|
+
);
|
|
2626
|
+
} else {
|
|
2627
|
+
copy = context.outputDocument.createElement(currentNode.nodeName);
|
|
2628
|
+
}
|
|
2629
|
+
if (useAttributeSets) {
|
|
2630
|
+
this.applyAttributeSets(useAttributeSets, context, copy);
|
|
2631
|
+
}
|
|
2632
|
+
this.processChildren(node, context, copy);
|
|
2633
|
+
output.appendChild(copy);
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2636
|
+
case 2:
|
|
2637
|
+
if (output.nodeType === 1) {
|
|
2638
|
+
output.setAttribute(currentNode.name, currentNode.value);
|
|
2639
|
+
}
|
|
2640
|
+
break;
|
|
2641
|
+
case 3:
|
|
2642
|
+
// Text
|
|
2643
|
+
case 4: {
|
|
2644
|
+
const textCopy = context.outputDocument.createTextNode(
|
|
2645
|
+
currentNode.nodeValue || ""
|
|
2646
|
+
);
|
|
2647
|
+
output.appendChild(textCopy);
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2650
|
+
case 7: {
|
|
2651
|
+
const piCopy = context.outputDocument.createProcessingInstruction(
|
|
2652
|
+
currentNode.target,
|
|
2653
|
+
currentNode.data
|
|
2654
|
+
);
|
|
2655
|
+
output.appendChild(piCopy);
|
|
2656
|
+
break;
|
|
2657
|
+
}
|
|
2658
|
+
case 8: {
|
|
2659
|
+
const commentCopy = context.outputDocument.createComment(
|
|
2660
|
+
currentNode.nodeValue || ""
|
|
2661
|
+
);
|
|
2662
|
+
output.appendChild(commentCopy);
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
case 9:
|
|
2666
|
+
// Document
|
|
2667
|
+
case 11:
|
|
2668
|
+
this.processChildren(node, context, output);
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
xslCopyOf(node, context, output) {
|
|
2673
|
+
const select2 = node.getAttribute("select");
|
|
2674
|
+
const result = this.evaluateXPath(select2, context);
|
|
2675
|
+
this.copyToOutput(result, context, output);
|
|
2676
|
+
}
|
|
2677
|
+
copyToOutput(value, context, output) {
|
|
2678
|
+
if (Array.isArray(value)) {
|
|
2679
|
+
for (const item of value) {
|
|
2680
|
+
this.copyToOutput(item, context, output);
|
|
2681
|
+
}
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
if (value && value.nodeType) {
|
|
2685
|
+
const clone = this.deepCloneNode(value, context.outputDocument);
|
|
2686
|
+
output.appendChild(clone);
|
|
2687
|
+
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2688
|
+
const text = context.outputDocument.createTextNode(String(value));
|
|
2689
|
+
output.appendChild(text);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
deepCloneNode(node, targetDoc) {
|
|
2693
|
+
switch (node.nodeType) {
|
|
2694
|
+
case 1: {
|
|
2695
|
+
let clone;
|
|
2696
|
+
if (node.namespaceURI && targetDoc.createElementNS) {
|
|
2697
|
+
clone = targetDoc.createElementNS(node.namespaceURI, node.nodeName);
|
|
2698
|
+
} else {
|
|
2699
|
+
clone = targetDoc.createElement(node.nodeName);
|
|
2700
|
+
}
|
|
2701
|
+
if (node.attributes) {
|
|
2702
|
+
for (const attr of node.attributes) {
|
|
2703
|
+
clone.setAttribute(attr.name, attr.value);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
for (const child of node.childNodes) {
|
|
2707
|
+
clone.appendChild(this.deepCloneNode(child, targetDoc));
|
|
2708
|
+
}
|
|
2709
|
+
return clone;
|
|
2710
|
+
}
|
|
2711
|
+
case 3:
|
|
2712
|
+
// Text
|
|
2713
|
+
case 4:
|
|
2714
|
+
return targetDoc.createTextNode(node.nodeValue || "");
|
|
2715
|
+
case 7:
|
|
2716
|
+
return targetDoc.createProcessingInstruction(node.target, node.data);
|
|
2717
|
+
case 8:
|
|
2718
|
+
return targetDoc.createComment(node.nodeValue || "");
|
|
2719
|
+
case 11: {
|
|
2720
|
+
const frag = targetDoc.createDocumentFragment();
|
|
2721
|
+
for (const child of node.childNodes) {
|
|
2722
|
+
frag.appendChild(this.deepCloneNode(child, targetDoc));
|
|
2723
|
+
}
|
|
2724
|
+
return frag;
|
|
2725
|
+
}
|
|
2726
|
+
default:
|
|
2727
|
+
return targetDoc.createTextNode("");
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
xslVariable(node, context, _output) {
|
|
2731
|
+
const name = node.getAttribute("name");
|
|
2732
|
+
const select2 = node.getAttribute("select");
|
|
2733
|
+
let value;
|
|
2734
|
+
if (select2) {
|
|
2735
|
+
value = this.evaluateXPath(select2, context);
|
|
2736
|
+
} else {
|
|
2737
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2738
|
+
this.processChildren(node, context, fragment);
|
|
2739
|
+
value = fragment;
|
|
2740
|
+
}
|
|
2741
|
+
context.setVariable(name, value);
|
|
2742
|
+
}
|
|
2743
|
+
xslComment(node, context, output) {
|
|
2744
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2745
|
+
this.processChildren(node, context, fragment);
|
|
2746
|
+
let text = "";
|
|
2747
|
+
const getText = (n) => {
|
|
2748
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
2749
|
+
text += n.nodeValue || "";
|
|
2750
|
+
} else if (n.childNodes) {
|
|
2751
|
+
for (const child of n.childNodes) {
|
|
2752
|
+
getText(child);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
getText(fragment);
|
|
2757
|
+
const comment = context.outputDocument.createComment(text);
|
|
2758
|
+
output.appendChild(comment);
|
|
2759
|
+
}
|
|
2760
|
+
xslProcessingInstruction(node, context, output) {
|
|
2761
|
+
const name = this.processAttributeValueTemplate(
|
|
2762
|
+
node.getAttribute("name"),
|
|
2763
|
+
context
|
|
2764
|
+
);
|
|
2765
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2766
|
+
this.processChildren(node, context, fragment);
|
|
2767
|
+
let data = "";
|
|
2768
|
+
const getText = (n) => {
|
|
2769
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
2770
|
+
data += n.nodeValue || "";
|
|
2771
|
+
} else if (n.childNodes) {
|
|
2772
|
+
for (const child of n.childNodes) {
|
|
2773
|
+
getText(child);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
};
|
|
2777
|
+
getText(fragment);
|
|
2778
|
+
const pi = context.outputDocument.createProcessingInstruction(name, data);
|
|
2779
|
+
output.appendChild(pi);
|
|
2780
|
+
}
|
|
2781
|
+
xslNumber(node, context, output) {
|
|
2782
|
+
const value = node.getAttribute("value");
|
|
2783
|
+
const format = node.getAttribute("format") || "1";
|
|
2784
|
+
const level = node.getAttribute("level") || "single";
|
|
2785
|
+
let number;
|
|
2786
|
+
if (value) {
|
|
2787
|
+
number = Math.round(
|
|
2788
|
+
this.xpathEvaluator.toNumber(this.evaluateXPath(value, context))
|
|
2789
|
+
);
|
|
2790
|
+
} else {
|
|
2791
|
+
number = this.countNumber(context.currentNode, level, node, context);
|
|
2792
|
+
}
|
|
2793
|
+
const formatted = this.formatNumber(number, format);
|
|
2794
|
+
const text = context.outputDocument.createTextNode(formatted);
|
|
2795
|
+
output.appendChild(text);
|
|
2796
|
+
}
|
|
2797
|
+
countNumber(node, level, spec, context) {
|
|
2798
|
+
const count = spec.getAttribute("count");
|
|
2799
|
+
const _from = spec.getAttribute("from");
|
|
2800
|
+
if (level === "single") {
|
|
2801
|
+
let n = 1;
|
|
2802
|
+
let sibling = node.previousSibling;
|
|
2803
|
+
while (sibling) {
|
|
2804
|
+
if (sibling.nodeType === 1) {
|
|
2805
|
+
if (!count || this.matchesPattern(sibling, count, context)) {
|
|
2806
|
+
n++;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
sibling = sibling.previousSibling;
|
|
2810
|
+
}
|
|
2811
|
+
return n;
|
|
2812
|
+
}
|
|
2813
|
+
return 1;
|
|
2814
|
+
}
|
|
2815
|
+
formatNumber(number, format) {
|
|
2816
|
+
if (/^[0-9]+$/.test(format)) {
|
|
2817
|
+
return String(number).padStart(format.length, "0");
|
|
2818
|
+
}
|
|
2819
|
+
if (format === "a") {
|
|
2820
|
+
return String.fromCharCode(96 + (number - 1) % 26 + 1);
|
|
2821
|
+
}
|
|
2822
|
+
if (format === "A") {
|
|
2823
|
+
return String.fromCharCode(64 + (number - 1) % 26 + 1);
|
|
2824
|
+
}
|
|
2825
|
+
if (format === "i") {
|
|
2826
|
+
return this.toRoman(number).toLowerCase();
|
|
2827
|
+
}
|
|
2828
|
+
if (format === "I") {
|
|
2829
|
+
return this.toRoman(number);
|
|
2830
|
+
}
|
|
2831
|
+
return String(number);
|
|
2832
|
+
}
|
|
2833
|
+
toRoman(num) {
|
|
2834
|
+
const romanNumerals = [
|
|
2835
|
+
["M", 1e3],
|
|
2836
|
+
["CM", 900],
|
|
2837
|
+
["D", 500],
|
|
2838
|
+
["CD", 400],
|
|
2839
|
+
["C", 100],
|
|
2840
|
+
["XC", 90],
|
|
2841
|
+
["L", 50],
|
|
2842
|
+
["XL", 40],
|
|
2843
|
+
["X", 10],
|
|
2844
|
+
["IX", 9],
|
|
2845
|
+
["V", 5],
|
|
2846
|
+
["IV", 4],
|
|
2847
|
+
["I", 1]
|
|
2848
|
+
];
|
|
2849
|
+
let result = "";
|
|
2850
|
+
for (const [numeral, value] of romanNumerals) {
|
|
2851
|
+
while (num >= value) {
|
|
2852
|
+
result += numeral;
|
|
2853
|
+
num -= value;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return result;
|
|
2857
|
+
}
|
|
2858
|
+
xslMessage(node, context, _output) {
|
|
2859
|
+
const terminate = node.getAttribute("terminate") === "yes";
|
|
2860
|
+
const fragment = context.outputDocument.createDocumentFragment();
|
|
2861
|
+
this.processChildren(node, context, fragment);
|
|
2862
|
+
let text = "";
|
|
2863
|
+
const getText = (n) => {
|
|
2864
|
+
if (n.nodeType === 3 || n.nodeType === 4) {
|
|
2865
|
+
text += n.nodeValue || "";
|
|
2866
|
+
} else if (n.childNodes) {
|
|
2867
|
+
for (const child of n.childNodes) {
|
|
2868
|
+
getText(child);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
getText(fragment);
|
|
2873
|
+
console.log("XSLT Message:", text);
|
|
2874
|
+
if (terminate) {
|
|
2875
|
+
throw new Error(`XSLT terminated: ${text}`);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
applyAttributeSets(names, context, element) {
|
|
2879
|
+
const setNames = names.split(/\s+/).filter(Boolean);
|
|
2880
|
+
for (const name of setNames) {
|
|
2881
|
+
const attrSet = this.attributeSets[name];
|
|
2882
|
+
if (attrSet) {
|
|
2883
|
+
if (attrSet.useAttributeSets.length > 0) {
|
|
2884
|
+
this.applyAttributeSets(
|
|
2885
|
+
attrSet.useAttributeSets.join(" "),
|
|
2886
|
+
context,
|
|
2887
|
+
element
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
for (const child of attrSet.node.childNodes) {
|
|
2891
|
+
if (child.nodeType === 1 && this.isXsltElement(child, "attribute")) {
|
|
2892
|
+
this.xslAttribute(child, context, element);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
sortNodes(nodes, sortSpecs, context) {
|
|
2899
|
+
return [...nodes].sort((a, b) => {
|
|
2900
|
+
for (const spec of sortSpecs) {
|
|
2901
|
+
const contextA = context.clone({ currentNode: a });
|
|
2902
|
+
const contextB = context.clone({ currentNode: b });
|
|
2903
|
+
let valueA = this.evaluateXPath(spec.select, contextA);
|
|
2904
|
+
let valueB = this.evaluateXPath(spec.select, contextB);
|
|
2905
|
+
valueA = this.xpathEvaluator.toString(valueA) || "";
|
|
2906
|
+
valueB = this.xpathEvaluator.toString(valueB) || "";
|
|
2907
|
+
if (spec.dataType === "number") {
|
|
2908
|
+
valueA = parseFloat(valueA) || 0;
|
|
2909
|
+
valueB = parseFloat(valueB) || 0;
|
|
2910
|
+
} else {
|
|
2911
|
+
if (spec.caseOrder === "lower-first") {
|
|
2912
|
+
valueA = valueA.toLowerCase();
|
|
2913
|
+
valueB = valueB.toLowerCase();
|
|
2914
|
+
} else {
|
|
2915
|
+
valueA = valueA.toUpperCase();
|
|
2916
|
+
valueB = valueB.toUpperCase();
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
let cmp;
|
|
2920
|
+
if (typeof valueA === "number") {
|
|
2921
|
+
cmp = valueA - valueB;
|
|
2922
|
+
} else {
|
|
2923
|
+
cmp = valueA.localeCompare(valueB, spec.lang || void 0);
|
|
2924
|
+
}
|
|
2925
|
+
if (spec.order === "descending") {
|
|
2926
|
+
cmp = -cmp;
|
|
2927
|
+
}
|
|
2928
|
+
if (cmp !== 0) return cmp;
|
|
2929
|
+
}
|
|
2930
|
+
return 0;
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
evaluateXPath(expr, context) {
|
|
2934
|
+
const ast = parse(expr);
|
|
2935
|
+
const xpathContext = new XPathContext(
|
|
2936
|
+
context.currentNode,
|
|
2937
|
+
context.position,
|
|
2938
|
+
context.currentNodeList.length,
|
|
2939
|
+
{ ...context.variables, ...context.parameters },
|
|
2940
|
+
context.namespaces
|
|
2941
|
+
);
|
|
2942
|
+
return this.xpathEvaluator.evaluate(ast, xpathContext);
|
|
2943
|
+
}
|
|
2944
|
+
isXsltNamespace(node) {
|
|
2945
|
+
return node.namespaceURI === XSLT_NS || node.nodeName && node.nodeName.startsWith("xsl:");
|
|
2946
|
+
}
|
|
2947
|
+
isXsltElement(node, localName) {
|
|
2948
|
+
if (node.nodeType !== 1) return false;
|
|
2949
|
+
const nodeName = node.localName || node.nodeName;
|
|
2950
|
+
return node.namespaceURI === XSLT_NS && nodeName === localName || node.nodeName === `xsl:${localName}`;
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
|
|
2954
|
+
// src/XSLTProcessor.js
|
|
2955
|
+
var XSLTProcessor = class {
|
|
2956
|
+
constructor() {
|
|
2957
|
+
this._engine = null;
|
|
2958
|
+
this._stylesheet = null;
|
|
2959
|
+
this._parameters = /* @__PURE__ */ new Map();
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Imports the XSLT stylesheet.
|
|
2963
|
+
*
|
|
2964
|
+
* If the given node is a document node, you can pass in a full XSL Transform
|
|
2965
|
+
* or a literal result element transform; otherwise, it must be an
|
|
2966
|
+
* <xsl:stylesheet> or <xsl:transform> element.
|
|
2967
|
+
*
|
|
2968
|
+
* @param {Node} style - The XSLT stylesheet to import (Document or Element)
|
|
2969
|
+
* @returns {void}
|
|
2970
|
+
*
|
|
2971
|
+
* @example
|
|
2972
|
+
* const parser = new DOMParser();
|
|
2973
|
+
* const xslDoc = parser.parseFromString(xslText, 'application/xml');
|
|
2974
|
+
* processor.importStylesheet(xslDoc);
|
|
2975
|
+
*/
|
|
2976
|
+
importStylesheet(style) {
|
|
2977
|
+
if (!style) {
|
|
2978
|
+
throw new TypeError(
|
|
2979
|
+
"Failed to execute 'importStylesheet' on 'XSLTProcessor': 1 argument required, but only 0 present."
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
if (style.nodeType !== 1 && style.nodeType !== 9) {
|
|
2983
|
+
throw new TypeError(
|
|
2984
|
+
"Failed to execute 'importStylesheet' on 'XSLTProcessor': The node provided is not a Document or Element."
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
const errorNode = style.querySelector ? style.querySelector("parsererror") : null;
|
|
2988
|
+
if (errorNode) {
|
|
2989
|
+
throw new Error("XSLT stylesheet contains parse errors");
|
|
2990
|
+
}
|
|
2991
|
+
this._stylesheet = style;
|
|
2992
|
+
this._engine = new XsltEngine();
|
|
2993
|
+
for (const [key, value] of this._parameters) {
|
|
2994
|
+
this._engine.globalParameters[key] = { value };
|
|
2995
|
+
}
|
|
2996
|
+
this._engine.importStylesheet(style);
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Transforms the node source by applying the XSLT stylesheet.
|
|
3000
|
+
* Returns a document fragment.
|
|
3001
|
+
*
|
|
3002
|
+
* @param {Node} source - The XML document to transform
|
|
3003
|
+
* @param {Document} output - The document that will own the generated fragment
|
|
3004
|
+
* @returns {DocumentFragment} The transformed result as a DocumentFragment
|
|
3005
|
+
*
|
|
3006
|
+
* @example
|
|
3007
|
+
* const fragment = processor.transformToFragment(xmlDoc, document);
|
|
3008
|
+
* document.getElementById('output').appendChild(fragment);
|
|
3009
|
+
*/
|
|
3010
|
+
transformToFragment(source, output) {
|
|
3011
|
+
if (!source) {
|
|
3012
|
+
throw new TypeError(
|
|
3013
|
+
"Failed to execute 'transformToFragment' on 'XSLTProcessor': 2 arguments required, but only 0 present."
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
if (!output) {
|
|
3017
|
+
throw new TypeError(
|
|
3018
|
+
"Failed to execute 'transformToFragment' on 'XSLTProcessor': 2 arguments required, but only 1 present."
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
if (!this._engine || !this._stylesheet) {
|
|
3022
|
+
throw new Error(
|
|
3023
|
+
"Failed to execute 'transformToFragment' on 'XSLTProcessor': No stylesheet has been imported."
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
if (source.nodeType !== 1 && source.nodeType !== 9 && source.nodeType !== 11) {
|
|
3027
|
+
throw new TypeError(
|
|
3028
|
+
"Failed to execute 'transformToFragment' on 'XSLTProcessor': The source is not a valid node type."
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
if (output.nodeType !== 9) {
|
|
3032
|
+
throw new TypeError(
|
|
3033
|
+
"Failed to execute 'transformToFragment' on 'XSLTProcessor': The output is not a Document."
|
|
3034
|
+
);
|
|
3035
|
+
}
|
|
3036
|
+
try {
|
|
3037
|
+
return this._engine.transform(source, output);
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
console.error("XSLT transformation error:", error);
|
|
3040
|
+
return null;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Transforms the node source by applying the XSLT stylesheet.
|
|
3045
|
+
* Returns a full XML document.
|
|
3046
|
+
*
|
|
3047
|
+
* @param {Node} source - The XML document to transform
|
|
3048
|
+
* @returns {XMLDocument} The transformed result as an XMLDocument
|
|
3049
|
+
*
|
|
3050
|
+
* @example
|
|
3051
|
+
* const resultDoc = processor.transformToDocument(xmlDoc);
|
|
3052
|
+
* const serialized = new XMLSerializer().serializeToString(resultDoc);
|
|
3053
|
+
*/
|
|
3054
|
+
transformToDocument(source) {
|
|
3055
|
+
if (!source) {
|
|
3056
|
+
throw new TypeError(
|
|
3057
|
+
"Failed to execute 'transformToDocument' on 'XSLTProcessor': 1 argument required, but only 0 present."
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
if (!this._engine || !this._stylesheet) {
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
"Failed to execute 'transformToDocument' on 'XSLTProcessor': No stylesheet has been imported."
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
if (source.nodeType !== 1 && source.nodeType !== 9 && source.nodeType !== 11) {
|
|
3066
|
+
throw new TypeError(
|
|
3067
|
+
"Failed to execute 'transformToDocument' on 'XSLTProcessor': The source is not a valid node type."
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
try {
|
|
3071
|
+
return this._engine.transformToDocument(source);
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
console.error("XSLT transformation error:", error);
|
|
3074
|
+
return null;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Sets a parameter in the XSLT stylesheet.
|
|
3079
|
+
*
|
|
3080
|
+
* @param {string|null} namespaceURI - The namespace URI of the XSLT parameter (use null for no namespace)
|
|
3081
|
+
* @param {string} localName - The local name of the parameter
|
|
3082
|
+
* @param {*} value - The value to set (string, number, boolean, or node-set)
|
|
3083
|
+
* @returns {void}
|
|
3084
|
+
*
|
|
3085
|
+
* @example
|
|
3086
|
+
* processor.setParameter(null, 'sortOrder', 'ascending');
|
|
3087
|
+
* processor.setParameter('http://example.com/ns', 'limit', 10);
|
|
3088
|
+
*/
|
|
3089
|
+
setParameter(namespaceURI, localName, value) {
|
|
3090
|
+
if (arguments.length < 3) {
|
|
3091
|
+
throw new TypeError(
|
|
3092
|
+
`Failed to execute 'setParameter' on 'XSLTProcessor': 3 arguments required, but only ${arguments.length} present.`
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
if (typeof localName !== "string" || localName === "") {
|
|
3096
|
+
throw new TypeError(
|
|
3097
|
+
"Failed to execute 'setParameter' on 'XSLTProcessor': The localName argument must be a non-empty string."
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
|
|
3101
|
+
this._parameters.set(key, value);
|
|
3102
|
+
if (this._engine) {
|
|
3103
|
+
this._engine.globalParameters[key] = { value };
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Gets the value of a parameter from the XSLT stylesheet.
|
|
3108
|
+
*
|
|
3109
|
+
* @param {string|null} namespaceURI - The namespace URI of the parameter
|
|
3110
|
+
* @param {string} localName - The local name of the parameter
|
|
3111
|
+
* @returns {*} The parameter value, or empty string if not set
|
|
3112
|
+
*
|
|
3113
|
+
* @example
|
|
3114
|
+
* const sortOrder = processor.getParameter(null, 'sortOrder');
|
|
3115
|
+
*/
|
|
3116
|
+
getParameter(namespaceURI, localName) {
|
|
3117
|
+
if (arguments.length < 2) {
|
|
3118
|
+
throw new TypeError(
|
|
3119
|
+
`Failed to execute 'getParameter' on 'XSLTProcessor': 2 arguments required, but only ${arguments.length} present.`
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
if (typeof localName !== "string") {
|
|
3123
|
+
throw new TypeError(
|
|
3124
|
+
"Failed to execute 'getParameter' on 'XSLTProcessor': The localName argument must be a string."
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
|
|
3128
|
+
if (this._parameters.has(key)) {
|
|
3129
|
+
return this._parameters.get(key);
|
|
3130
|
+
}
|
|
3131
|
+
return "";
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Removes a parameter from the XSLT processor.
|
|
3135
|
+
*
|
|
3136
|
+
* The XSLTProcessor will use the default value for the parameter
|
|
3137
|
+
* as specified in the XSLT stylesheet.
|
|
3138
|
+
*
|
|
3139
|
+
* @param {string|null} namespaceURI - The namespace URI of the parameter
|
|
3140
|
+
* @param {string} localName - The local name of the parameter
|
|
3141
|
+
* @returns {void}
|
|
3142
|
+
*
|
|
3143
|
+
* @example
|
|
3144
|
+
* processor.removeParameter(null, 'sortOrder');
|
|
3145
|
+
*/
|
|
3146
|
+
removeParameter(namespaceURI, localName) {
|
|
3147
|
+
if (arguments.length < 2) {
|
|
3148
|
+
throw new TypeError(
|
|
3149
|
+
`Failed to execute 'removeParameter' on 'XSLTProcessor': 2 arguments required, but only ${arguments.length} present.`
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
if (typeof localName !== "string") {
|
|
3153
|
+
throw new TypeError(
|
|
3154
|
+
"Failed to execute 'removeParameter' on 'XSLTProcessor': The localName argument must be a string."
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
|
|
3158
|
+
this._parameters.delete(key);
|
|
3159
|
+
if (this._engine) {
|
|
3160
|
+
delete this._engine.globalParameters[key];
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Removes all set parameters from the XSLTProcessor.
|
|
3165
|
+
*
|
|
3166
|
+
* The processor will use default values specified in the XSLT stylesheet.
|
|
3167
|
+
*
|
|
3168
|
+
* @returns {void}
|
|
3169
|
+
*
|
|
3170
|
+
* @example
|
|
3171
|
+
* processor.clearParameters();
|
|
3172
|
+
*/
|
|
3173
|
+
clearParameters() {
|
|
3174
|
+
this._parameters.clear();
|
|
3175
|
+
if (this._engine) {
|
|
3176
|
+
this._engine.globalParameters = {};
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Removes all parameters and stylesheets from the XSLTProcessor.
|
|
3181
|
+
*
|
|
3182
|
+
* @returns {void}
|
|
3183
|
+
*
|
|
3184
|
+
* @example
|
|
3185
|
+
* processor.reset();
|
|
3186
|
+
* // Now need to call importStylesheet() again before transforming
|
|
3187
|
+
*/
|
|
3188
|
+
reset() {
|
|
3189
|
+
this._engine = null;
|
|
3190
|
+
this._stylesheet = null;
|
|
3191
|
+
this._parameters.clear();
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
3194
|
+
function isNativeXSLTSupported() {
|
|
3195
|
+
if (typeof globalThis.XSLTProcessor === "undefined") {
|
|
3196
|
+
return false;
|
|
3197
|
+
}
|
|
3198
|
+
try {
|
|
3199
|
+
const processor = new globalThis.XSLTProcessor();
|
|
3200
|
+
const parser = new DOMParser();
|
|
3201
|
+
const xslt = parser.parseFromString(
|
|
3202
|
+
`<?xml version="1.0"?>
|
|
3203
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
3204
|
+
<xsl:template match="/"><test/></xsl:template>
|
|
3205
|
+
</xsl:stylesheet>`,
|
|
3206
|
+
"application/xml"
|
|
3207
|
+
);
|
|
3208
|
+
processor.importStylesheet(xslt);
|
|
3209
|
+
const xml = parser.parseFromString("<root/>", "application/xml");
|
|
3210
|
+
const result = processor.transformToFragment(xml, document);
|
|
3211
|
+
return result !== null && result.childNodes.length > 0;
|
|
3212
|
+
} catch {
|
|
3213
|
+
return false;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
function installGlobal(force = false) {
|
|
3217
|
+
if (!force && isNativeXSLTSupported()) {
|
|
3218
|
+
return false;
|
|
3219
|
+
}
|
|
3220
|
+
globalThis.XSLTProcessor = XSLTProcessor;
|
|
3221
|
+
return true;
|
|
3222
|
+
}
|
|
3223
|
+
var XSLTProcessor_default = XSLTProcessor;
|
|
3224
|
+
|
|
3225
|
+
// src/xpath/index.js
|
|
3226
|
+
function evaluate(expression, contextNode, options = {}) {
|
|
3227
|
+
const ast = parse(expression);
|
|
3228
|
+
const evaluator = new XPathEvaluator();
|
|
3229
|
+
const context = new XPathContext(
|
|
3230
|
+
contextNode,
|
|
3231
|
+
1,
|
|
3232
|
+
1,
|
|
3233
|
+
options.variables || {},
|
|
3234
|
+
options.namespaces || {}
|
|
3235
|
+
);
|
|
3236
|
+
return evaluator.evaluate(ast, context);
|
|
3237
|
+
}
|
|
3238
|
+
function select(expression, contextNode, options = {}) {
|
|
3239
|
+
const result = evaluate(expression, contextNode, options);
|
|
3240
|
+
if (Array.isArray(result)) return result;
|
|
3241
|
+
if (result && result.nodeType) return [result];
|
|
3242
|
+
return [];
|
|
3243
|
+
}
|
|
3244
|
+
function selectFirst(expression, contextNode, options = {}) {
|
|
3245
|
+
const nodes = select(expression, contextNode, options);
|
|
3246
|
+
return nodes.length > 0 ? nodes[0] : null;
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// src/index.js
|
|
3250
|
+
var VERSION = "1.0.0";
|
|
3251
|
+
var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
3252
|
+
var isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
3253
|
+
export {
|
|
3254
|
+
VERSION,
|
|
3255
|
+
XPathContext,
|
|
3256
|
+
XPathEvaluator,
|
|
3257
|
+
XPathResultType,
|
|
3258
|
+
XSLTProcessor,
|
|
3259
|
+
XsltContext,
|
|
3260
|
+
XsltEngine,
|
|
3261
|
+
XSLTProcessor_default as default,
|
|
3262
|
+
evaluate as evaluateXPath,
|
|
3263
|
+
installGlobal,
|
|
3264
|
+
isBrowser,
|
|
3265
|
+
isNativeXSLTSupported,
|
|
3266
|
+
isNode,
|
|
3267
|
+
parse as parseXPath,
|
|
3268
|
+
selectFirst as selectFirstXPath,
|
|
3269
|
+
select as selectXPath
|
|
3270
|
+
};
|
|
3271
|
+
//# sourceMappingURL=xslt-processor.js.map
|