@tradik/xslt-processor 1.0.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/README.md +110 -520
- package/bin/lib/decode.js +15 -0
- package/bin/lib/dom.js +177 -0
- package/bin/lib/loaders.js +127 -0
- package/bin/lib/options.js +131 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +206 -0
- package/bin/xslt.js +73 -168
- package/dist/xslt-processor.browser.js +9564 -1585
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +9572 -1586
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +658 -0
- package/dist/xslt-processor.d.ts +459 -12
- package/dist/xslt-processor.js +9546 -1582
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +71 -20
- package/src/XSLTProcessor.js +494 -48
- package/src/async/abort.js +63 -0
- package/src/async/documentUris.js +128 -0
- package/src/async/loaders.js +134 -0
- package/src/async/preload.js +159 -0
- package/src/async/processor.js +206 -0
- package/src/async/stream.js +125 -0
- package/src/bridge/engine.js +221 -0
- package/src/bridge/loader.js +78 -0
- package/src/bridge/results.js +75 -0
- package/src/bridge/version.js +63 -0
- package/src/index.js +26 -8
- package/src/io/decode.js +140 -0
- package/src/io/readSource.js +167 -0
- package/src/xpath/axes.js +562 -0
- package/src/xpath/documentOrder.js +270 -0
- package/src/xpath/evaluator.js +518 -357
- package/src/xpath/index.js +8 -2
- package/src/xpath/namespaceNodes.js +172 -0
- package/src/xpath/nodeSetFunctions.js +169 -0
- package/src/xpath/parser.js +30 -5
- package/src/xpath/strings.js +183 -0
- package/src/xpath/tokenizer.js +37 -23
- package/src/xslt/attributeSets.js +95 -0
- package/src/xslt/avt.js +103 -0
- package/src/xslt/computedNames.js +91 -0
- package/src/xslt/copying.js +212 -0
- package/src/xslt/declarationNames.js +80 -0
- package/src/xslt/domParsing.js +95 -0
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine/bindings.js +195 -0
- package/src/xslt/engine/context.js +105 -0
- package/src/xslt/engine/controlFlow.js +145 -0
- package/src/xslt/engine/copyInstructions.js +133 -0
- package/src/xslt/engine/declarations.js +233 -0
- package/src/xslt/engine/functionSupport.js +103 -0
- package/src/xslt/engine/methods.js +33 -0
- package/src/xslt/engine/nodeConstruction.js +187 -0
- package/src/xslt/engine/numbering.js +104 -0
- package/src/xslt/engine/outputDeclaration.js +77 -0
- package/src/xslt/engine/sequenceConstructor.js +228 -0
- package/src/xslt/engine/stylesheetLoading.js +208 -0
- package/src/xslt/engine/templateInvocation.js +253 -0
- package/src/xslt/engine/templateRules.js +243 -0
- package/src/xslt/engine/textInstructions.js +171 -0
- package/src/xslt/engine/topLevel.js +130 -0
- package/src/xslt/engine/transformation.js +263 -0
- package/src/xslt/engine/workStack.js +245 -0
- package/src/xslt/engine.js +184 -1736
- package/src/xslt/exslt/arguments.js +99 -0
- package/src/xslt/exslt/calendar.js +120 -0
- package/src/xslt/exslt/common.js +44 -0
- package/src/xslt/exslt/dateCalc.js +261 -0
- package/src/xslt/exslt/dateFormat.js +150 -0
- package/src/xslt/exslt/dateParse.js +265 -0
- package/src/xslt/exslt/dates.js +259 -0
- package/src/xslt/exslt/duration.js +207 -0
- package/src/xslt/exslt/dynamic.js +59 -0
- package/src/xslt/exslt/index.js +59 -0
- package/src/xslt/exslt/math.js +177 -0
- package/src/xslt/exslt/sets.js +96 -0
- package/src/xslt/exslt/stringOps.js +163 -0
- package/src/xslt/exslt/strings.js +147 -0
- package/src/xslt/exslt/uri.js +92 -0
- package/src/xslt/formatNumber.js +233 -0
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +270 -0
- package/src/xslt/index.js +38 -1
- package/src/xslt/keys.js +164 -0
- package/src/xslt/literalResult.js +223 -0
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +271 -0
- package/src/xslt/numberFormat.js +253 -0
- package/src/xslt/outputNames.js +58 -0
- package/src/xslt/patternCompiler.js +175 -0
- package/src/xslt/patterns.js +324 -0
- package/src/xslt/qname.js +90 -0
- package/src/xslt/resultDocument.js +98 -0
- package/src/xslt/resultNamespaces.js +219 -0
- package/src/xslt/resultTree.js +211 -0
- package/src/xslt/serializer/baseWriter.js +390 -0
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +92 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +135 -0
- package/src/xslt/serializer/frames.js +168 -0
- package/src/xslt/serializer/htmlDoctype.js +102 -0
- package/src/xslt/serializer/htmlEntities.js +77 -0
- package/src/xslt/serializer/htmlSerializer.js +239 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +179 -0
- package/src/xslt/serializer/textSerializer.js +77 -0
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +227 -0
- package/src/xslt/serializer.js +90 -0
- package/src/xslt/sort.js +151 -0
- package/src/xslt/spaceNameTests.js +115 -0
- package/src/xslt/stylesheetChecks.js +206 -0
- package/src/xslt/stylesheetNamespaces.js +266 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +200 -0
- package/LICENSE +0 -29
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- package/src/xslt/engine.test.js +0 -3130
package/src/xpath/tokenizer.js
CHANGED
|
@@ -82,6 +82,14 @@ const NODE_TYPES = new Set([
|
|
|
82
82
|
|
|
83
83
|
const OPERATORS = new Set(["and", "or", "mod", "div"]);
|
|
84
84
|
|
|
85
|
+
/** Token type of each OperatorName. */
|
|
86
|
+
const OPERATOR_TOKEN_TYPES = Object.freeze({
|
|
87
|
+
and: TokenType.AND,
|
|
88
|
+
or: TokenType.OR,
|
|
89
|
+
mod: TokenType.MOD,
|
|
90
|
+
div: TokenType.DIV,
|
|
91
|
+
});
|
|
92
|
+
|
|
85
93
|
export class Token {
|
|
86
94
|
constructor(type, value, position) {
|
|
87
95
|
this.type = type;
|
|
@@ -145,6 +153,8 @@ export class XPathTokenizer {
|
|
|
145
153
|
const lastToken = this.getLastToken();
|
|
146
154
|
// No preceding token means we're at the start - not an operator context
|
|
147
155
|
if (!lastToken) return false;
|
|
156
|
+
// The local part of a QName (`html:div`) is a name, never an operator
|
|
157
|
+
if (lastToken.type === TokenType.COLON) return false;
|
|
148
158
|
// If preceding token is a "blocker", it's not an operator context
|
|
149
159
|
if (OPERATOR_CONTEXT_BLOCKERS.has(lastToken.type)) return false;
|
|
150
160
|
// Otherwise, it IS an operator context (after names, numbers, closing brackets, etc.)
|
|
@@ -296,9 +306,9 @@ export class XPathTokenizer {
|
|
|
296
306
|
value += this.consume();
|
|
297
307
|
}
|
|
298
308
|
|
|
299
|
-
// Decimal part
|
|
300
|
-
//
|
|
301
|
-
if (this.peek() === "." &&
|
|
309
|
+
// Decimal part: Number ::= Digits ('.' Digits?)? | '.' Digits, so `5.`
|
|
310
|
+
// is a number too. A `..` after digits is left alone (never valid there).
|
|
311
|
+
if (this.peek() === "." && this.peek(1) !== ".") {
|
|
302
312
|
value += this.consume(); // .
|
|
303
313
|
while (
|
|
304
314
|
this.position < this.expression.length &&
|
|
@@ -311,6 +321,20 @@ export class XPathTokenizer {
|
|
|
311
321
|
return new Token(TokenType.NUMBER, parseFloat(value), startPos);
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Read an NCName and classify it using the XPath 1.0 lexical
|
|
326
|
+
* disambiguation rules (section 3.7), applied in the order the
|
|
327
|
+
* specification lists them:
|
|
328
|
+
*
|
|
329
|
+
* 1. After an operand (the preceding token is not `@`, `::`, `(`, `[`, `,`
|
|
330
|
+
* or an operator) `and`, `or`, `div` and `mod` are OperatorNames, even
|
|
331
|
+
* when a `(` follows, as in `(a) or (b)`.
|
|
332
|
+
* 2. A name followed by `(` is a NodeType or a FunctionName.
|
|
333
|
+
* 3. A name followed by `::` is an AxisName.
|
|
334
|
+
* 4. Anything else is a NameTest.
|
|
335
|
+
*
|
|
336
|
+
* @returns {Token} The classified token
|
|
337
|
+
*/
|
|
314
338
|
readName() {
|
|
315
339
|
const startPos = this.position;
|
|
316
340
|
let value = "";
|
|
@@ -322,35 +346,25 @@ export class XPathTokenizer {
|
|
|
322
346
|
value += this.consume();
|
|
323
347
|
}
|
|
324
348
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (AXIS_NAMES.has(value) && this.peek() === ":" && this.peek(1) === ":") {
|
|
328
|
-
return new Token(TokenType.AXIS, value, startPos);
|
|
349
|
+
if (OPERATORS.has(value) && this.isOperatorContext()) {
|
|
350
|
+
return new Token(OPERATOR_TOKEN_TYPES[value], value, startPos);
|
|
329
351
|
}
|
|
330
352
|
|
|
331
|
-
// Check for function call (followed by '(')
|
|
332
353
|
const savedPos = this.position;
|
|
333
354
|
this.skipWhitespace();
|
|
355
|
+
|
|
334
356
|
if (this.peek() === "(") {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return new Token(TokenType.FUNCTION, value, startPos);
|
|
357
|
+
const type = NODE_TYPES.has(value)
|
|
358
|
+
? TokenType.NODE_TYPE
|
|
359
|
+
: TokenType.FUNCTION;
|
|
360
|
+
return new Token(type, value, startPos);
|
|
340
361
|
}
|
|
341
|
-
this.position = savedPos;
|
|
342
362
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const opTokens = {
|
|
346
|
-
and: TokenType.AND,
|
|
347
|
-
or: TokenType.OR,
|
|
348
|
-
mod: TokenType.MOD,
|
|
349
|
-
div: TokenType.DIV,
|
|
350
|
-
};
|
|
351
|
-
return new Token(opTokens[value], value, startPos);
|
|
363
|
+
if (AXIS_NAMES.has(value) && this.peek() === ":" && this.peek(1) === ":") {
|
|
364
|
+
return new Token(TokenType.AXIS, value, startPos);
|
|
352
365
|
}
|
|
353
366
|
|
|
367
|
+
this.position = savedPos;
|
|
354
368
|
return new Token(TokenType.NAME, value, startPos);
|
|
355
369
|
}
|
|
356
370
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named attribute sets (XSLT 1.0 section 7.1.4).
|
|
3
|
+
*
|
|
4
|
+
* Every `xsl:attribute-set` declaration is kept: declarations with the same
|
|
5
|
+
* expanded name are merged into one set. Applying a set applies its
|
|
6
|
+
* declarations from the lowest import precedence to the highest (stylesheet
|
|
7
|
+
* order within one precedence), each one its `use-attribute-sets` first and
|
|
8
|
+
* then its own `xsl:attribute` children. A later attribute of the same name
|
|
9
|
+
* replaces an earlier one, so attributes of a higher import precedence win
|
|
10
|
+
* over imported ones and a declaration's own attributes win over the sets it
|
|
11
|
+
* uses, as in libxslt.
|
|
12
|
+
*
|
|
13
|
+
* @module xslt/attributeSets
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
requireExpandedName,
|
|
20
|
+
requireExpandedNames,
|
|
21
|
+
} from "./declarationNames.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} AttributeSetDeclaration
|
|
25
|
+
* @property {Element} node - The xsl:attribute-set element
|
|
26
|
+
* @property {string[]} uses - Expanded names of its use-attribute-sets
|
|
27
|
+
* @property {number} importPrecedence - Import precedence of its stylesheet
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register an `xsl:attribute-set` declaration.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object<string, AttributeSetDeclaration[]>} sets - Declarations by
|
|
34
|
+
* expanded name; updated in place
|
|
35
|
+
* @param {Element} node - The xsl:attribute-set element
|
|
36
|
+
* @param {number} importPrecedence - Import precedence of its stylesheet
|
|
37
|
+
* @returns {void}
|
|
38
|
+
* @throws {Error} When the name or a used name is not a QName with a
|
|
39
|
+
* declared prefix (libxslt rejects such a stylesheet)
|
|
40
|
+
*/
|
|
41
|
+
export function registerAttributeSet(sets, node, importPrecedence) {
|
|
42
|
+
const key = requireExpandedName(
|
|
43
|
+
node.getAttribute("name") ?? "",
|
|
44
|
+
node,
|
|
45
|
+
"xsl:attribute-set name",
|
|
46
|
+
);
|
|
47
|
+
const uses = requireExpandedNames(
|
|
48
|
+
node.getAttribute("use-attribute-sets"),
|
|
49
|
+
node,
|
|
50
|
+
"xsl:attribute-set use-attribute-sets",
|
|
51
|
+
);
|
|
52
|
+
const declarations = Object.hasOwn(sets, key) ? sets[key] : [];
|
|
53
|
+
declarations.push({ node, uses, importPrecedence });
|
|
54
|
+
// Stable sort: stylesheet order is kept within one import precedence
|
|
55
|
+
declarations.sort((a, b) => a.importPrecedence - b.importPrecedence);
|
|
56
|
+
sets[key] = declarations;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Apply attribute sets to a result element.
|
|
61
|
+
*
|
|
62
|
+
* @param {Object<string, AttributeSetDeclaration[]>} sets - Declarations by expanded name
|
|
63
|
+
* @param {string[]} keys - Expanded names of the sets to apply, in order
|
|
64
|
+
* @param {(node: Element) => void} applyDeclaration - Instantiates the
|
|
65
|
+
* xsl:attribute children of one declaration
|
|
66
|
+
* @param {Set<string>} [active] - Sets being applied (recursion guard)
|
|
67
|
+
* @returns {void}
|
|
68
|
+
* @throws {Error} When a set uses itself, directly or indirectly (an error
|
|
69
|
+
* in XSLT 1.0 section 7.1.4)
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* applyAttributeSets(engine.attributeSets, ["common"], (node) =>
|
|
73
|
+
* engine.processChildren(node, context, element));
|
|
74
|
+
*/
|
|
75
|
+
export function applyAttributeSets(
|
|
76
|
+
sets,
|
|
77
|
+
keys,
|
|
78
|
+
applyDeclaration,
|
|
79
|
+
active = new Set(),
|
|
80
|
+
) {
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
if (!Object.hasOwn(sets, key)) continue;
|
|
83
|
+
if (active.has(key)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`xsl:attribute-set ${key} uses itself (XSLT 1.0 section 7.1.4)`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
active.add(key);
|
|
89
|
+
for (const declaration of sets[key]) {
|
|
90
|
+
applyAttributeSets(sets, declaration.uses, applyDeclaration, active);
|
|
91
|
+
applyDeclaration(declaration.node);
|
|
92
|
+
}
|
|
93
|
+
active.delete(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/xslt/avt.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute value templates (XSLT 1.0 section 7.6.2).
|
|
3
|
+
*
|
|
4
|
+
* An AVT is literal text with XPath expressions in curly braces. A brace
|
|
5
|
+
* inside a string literal of an expression is part of the literal, so the
|
|
6
|
+
* template is split with a small scanner that knows '...' and "..." literals;
|
|
7
|
+
* `{{` and `}}` outside expressions stand for single braces. A lone `}` in the
|
|
8
|
+
* literal text is an error in the recommendation; it is kept as a character
|
|
9
|
+
* instead, like browsers do. Parsed templates are cached per string.
|
|
10
|
+
*
|
|
11
|
+
* @module xslt/avt
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {string | {expr: string}} AvtPart
|
|
18
|
+
* A literal text part, or an expression part.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const cache = new Map();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find the `}` closing an expression that starts at `start`.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} value - The whole template
|
|
27
|
+
* @param {number} start - Index just after the opening `{`
|
|
28
|
+
* @returns {number} Index of the closing `}`
|
|
29
|
+
* @throws {Error} When the expression is not closed
|
|
30
|
+
*/
|
|
31
|
+
function closingBrace(value, start) {
|
|
32
|
+
let quote = null;
|
|
33
|
+
for (let i = start; i < value.length; i++) {
|
|
34
|
+
const char = value[i];
|
|
35
|
+
if (quote) {
|
|
36
|
+
if (char === quote) quote = null;
|
|
37
|
+
} else if (char === "'" || char === '"') {
|
|
38
|
+
quote = char;
|
|
39
|
+
} else if (char === "}") {
|
|
40
|
+
return i;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Unclosed expression in attribute value template: ${value}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Split an attribute value template into literal and expression parts.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} value - The attribute value
|
|
50
|
+
* @returns {AvtPart[]} The parts, adjacent literals merged
|
|
51
|
+
* @throws {Error} When an expression is not closed
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* parseAvt("a{@b}c"); // ["a", { expr: "@b" }, "c"]
|
|
55
|
+
*/
|
|
56
|
+
export function parseAvt(value) {
|
|
57
|
+
let parts = cache.get(value);
|
|
58
|
+
if (parts) return parts;
|
|
59
|
+
|
|
60
|
+
parts = [];
|
|
61
|
+
let text = "";
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < value.length) {
|
|
64
|
+
const char = value[i];
|
|
65
|
+
if ((char === "{" || char === "}") && value[i + 1] === char) {
|
|
66
|
+
text += char;
|
|
67
|
+
i += 2;
|
|
68
|
+
} else if (char === "{") {
|
|
69
|
+
const end = closingBrace(value, i + 1);
|
|
70
|
+
if (text) parts.push(text);
|
|
71
|
+
text = "";
|
|
72
|
+
parts.push({ expr: value.slice(i + 1, end) });
|
|
73
|
+
i = end + 1;
|
|
74
|
+
} else {
|
|
75
|
+
text += char;
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (text) parts.push(text);
|
|
80
|
+
|
|
81
|
+
cache.set(value, parts);
|
|
82
|
+
return parts;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Evaluate an attribute value template.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} value - The attribute value
|
|
89
|
+
* @param {(expr: string) => string} evaluate - Evaluates one expression to a string
|
|
90
|
+
* @returns {string} The resulting text
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* evaluateAvt("{1+1}px", (e) => String(eval(e))); // "2px"
|
|
94
|
+
*/
|
|
95
|
+
export function evaluateAvt(value, evaluate) {
|
|
96
|
+
if (!value.includes("{") && !value.includes("}")) return value;
|
|
97
|
+
|
|
98
|
+
let result = "";
|
|
99
|
+
for (const part of parseAvt(value)) {
|
|
100
|
+
result += typeof part === "string" ? part : evaluate(part.expr);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation of the names computed by `xsl:element` and `xsl:attribute`
|
|
3
|
+
* (XSLT 1.0 sections 7.1.2 and 7.1.3).
|
|
4
|
+
*
|
|
5
|
+
* The `name` attribute is an attribute value template, so its value is only
|
|
6
|
+
* known at run time. It must be a QName whose prefix (when there is no
|
|
7
|
+
* `namespace` attribute) is declared in scope on the instruction, and an
|
|
8
|
+
* attribute may not be called `xmlns` (nor use the `xmlns` prefix). Like
|
|
9
|
+
* libxslt, the processor recovers from these errors by reporting them and not
|
|
10
|
+
* creating the element (with its content) or the attribute.
|
|
11
|
+
*
|
|
12
|
+
* @module xslt/computedNames
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
import { isQName } from "./qname.js";
|
|
18
|
+
import { attributeName, elementName, splitQName } from "./resultNamespaces.js";
|
|
19
|
+
import { resolvePrefix } from "./stylesheetNamespaces.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} ComputedName
|
|
23
|
+
* @property {{namespaceUri: (string|null), qname: string}} [name] - The
|
|
24
|
+
* expanded name, when valid
|
|
25
|
+
* @property {string} [error] - Why no node can be created, when invalid
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check the parts shared by element and attribute names.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} kind - "element" or "attribute", used in messages
|
|
32
|
+
* @param {string} qname - The evaluated `name` attribute
|
|
33
|
+
* @param {string|null} namespace - The evaluated `namespace` attribute, null when absent
|
|
34
|
+
* @param {Object<string, string>} scope - Namespaces in scope on the instruction
|
|
35
|
+
* @returns {string|null} The error message, or null when the name is usable
|
|
36
|
+
*/
|
|
37
|
+
function nameError(kind, qname, namespace, scope) {
|
|
38
|
+
if (!isQName(qname)) {
|
|
39
|
+
return `xsl:${kind}: "${qname}" is not a valid ${kind} name (QName), the ${kind} is not created`;
|
|
40
|
+
}
|
|
41
|
+
const { prefix } = splitQName(qname);
|
|
42
|
+
if (namespace === null && prefix && !resolvePrefix(scope, prefix)) {
|
|
43
|
+
return `xsl:${kind}: undefined namespace prefix "${prefix}" in "${qname}", the ${kind} is not created`;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the name computed by `xsl:element`.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} qname - The evaluated `name` attribute
|
|
52
|
+
* @param {string|null} namespace - The evaluated `namespace` attribute, null when absent
|
|
53
|
+
* @param {Object<string, string>} scope - Namespaces in scope on the instruction
|
|
54
|
+
* @returns {ComputedName} The expanded name, or the error to report
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* computedElementName("p:e", null, { p: "urn:p" }).name;
|
|
58
|
+
* // { namespaceUri: "urn:p", qname: "p:e" }
|
|
59
|
+
* computedElementName("x{", null, {}).error; // 'xsl:element: "x{" is not ...'
|
|
60
|
+
*/
|
|
61
|
+
export function computedElementName(qname, namespace, scope) {
|
|
62
|
+
const error = nameError("element", qname, namespace, scope);
|
|
63
|
+
return error ? { error } : { name: elementName(qname, namespace, scope) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the name computed by `xsl:attribute`; `xmlns` and `xmlns:*` are
|
|
68
|
+
* rejected because namespace declarations are not attributes.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} qname - The evaluated `name` attribute
|
|
71
|
+
* @param {string|null} namespace - The evaluated `namespace` attribute, null when absent
|
|
72
|
+
* @param {Object<string, string>} scope - Namespaces in scope on the instruction
|
|
73
|
+
* @returns {ComputedName} The expanded name, or the error to report
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* computedAttributeName("xmlns", null, {}).error;
|
|
77
|
+
* // 'xsl:attribute: "xmlns" cannot be used as an attribute name ...'
|
|
78
|
+
*/
|
|
79
|
+
export function computedAttributeName(qname, namespace, scope) {
|
|
80
|
+
// With a namespace attribute, the prefix of `xmlns:a` is only a hint and
|
|
81
|
+
// is replaced (libxslt REC/test-7.1.3)
|
|
82
|
+
const xmlnsName =
|
|
83
|
+
qname === "xmlns" || (namespace === null && qname.startsWith("xmlns:"));
|
|
84
|
+
if (xmlnsName) {
|
|
85
|
+
return {
|
|
86
|
+
error: `xsl:attribute: "${qname}" cannot be used as an attribute name (XSLT 1.0 section 7.1.3), the attribute is not created`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const error = nameError("attribute", qname, namespace, scope);
|
|
90
|
+
return error ? { error } : { name: attributeName(qname, namespace, scope) };
|
|
91
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copying source nodes to the result tree: `xsl:copy-of` (XSLT 1.0 section
|
|
3
|
+
* 11.3) and the node kinds of `xsl:copy` (section 7.5).
|
|
4
|
+
*
|
|
5
|
+
* Copies follow the XPath data model rather than the raw DOM: a run of
|
|
6
|
+
* adjacent Text/CDATA nodes is one text node (its first DOM node stands for
|
|
7
|
+
* the run), copying a root node copies its children, and copying an attribute
|
|
8
|
+
* or a namespace node adds it to the result element being built. Names keep their namespace and
|
|
9
|
+
* elements keep their namespace declarations.
|
|
10
|
+
*
|
|
11
|
+
* @module xslt/copying
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
import { childAxis } from "../xpath/axes.js";
|
|
17
|
+
import { NAMESPACE_NODE, inScopeBindings } from "../xpath/namespaceNodes.js";
|
|
18
|
+
import { copyNamespaceDeclarations } from "./resultNamespaces.js";
|
|
19
|
+
import { XMLNS_NAMESPACE } from "./stylesheetNamespaces.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Copy an attribute node onto a result element. A namespace declaration or a
|
|
23
|
+
* target that is not an element is ignored; so is an attribute added after
|
|
24
|
+
* the element got children (see {@link canAddAttribute}).
|
|
25
|
+
*
|
|
26
|
+
* @param {Attr} attribute - The source attribute
|
|
27
|
+
* @param {Node} target - The result node receiving it
|
|
28
|
+
* @param {(element: Node) => boolean} canAddAttribute - Guard for late attributes
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
31
|
+
export function copyAttribute(attribute, target, canAddAttribute) {
|
|
32
|
+
if (attribute.namespaceURI === XMLNS_NAMESPACE) return;
|
|
33
|
+
if (!canAddAttribute(target)) return;
|
|
34
|
+
if (attribute.namespaceURI) {
|
|
35
|
+
target.setAttributeNS(
|
|
36
|
+
attribute.namespaceURI,
|
|
37
|
+
attribute.name,
|
|
38
|
+
attribute.value,
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
target.setAttribute(attribute.name, attribute.value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Copy a namespace node onto a result element as a namespace declaration
|
|
47
|
+
* (XSLT 1.0 sections 7.5 and 11.3). As in libxslt, nothing is declared for
|
|
48
|
+
* the implicit `xml` binding, for a prefix the element's own name binds to
|
|
49
|
+
* another namespace, for a prefix the element already declares differently,
|
|
50
|
+
* or when the element already has children (see `canAddAttribute`).
|
|
51
|
+
*
|
|
52
|
+
* @param {import('../xpath/namespaceNodes.js').NamespaceNode} namespace - The namespace node
|
|
53
|
+
* @param {Node} target - The result node receiving it
|
|
54
|
+
* @param {(element: Node) => boolean} canAddNamespace - Guard for late nodes
|
|
55
|
+
* @returns {void}
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* copyNamespaceNode(nsNode, resultElement, () => true); // xmlns:a="urn:a"
|
|
59
|
+
*/
|
|
60
|
+
export function copyNamespaceNode(namespace, target, canAddNamespace) {
|
|
61
|
+
const prefix = namespace.localName;
|
|
62
|
+
if (prefix === "xml" || !canAddNamespace(target)) return;
|
|
63
|
+
const uri = namespace.nodeValue;
|
|
64
|
+
if ((target.prefix ?? "") === prefix && (target.namespaceURI ?? "") !== uri) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const declared = target.getAttributeNS(XMLNS_NAMESPACE, prefix || "xmlns");
|
|
68
|
+
if (declared !== null && declared !== uri) return;
|
|
69
|
+
target.setAttributeNS(
|
|
70
|
+
XMLNS_NAMESPACE,
|
|
71
|
+
prefix ? `xmlns:${prefix}` : "xmlns",
|
|
72
|
+
uri,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a shallow copy of an element: same expanded name and prefix, and the
|
|
78
|
+
* same namespace declarations.
|
|
79
|
+
*
|
|
80
|
+
* @param {Element} element - The source element
|
|
81
|
+
* @param {Document} doc - The result document
|
|
82
|
+
* @returns {Element} The empty copy
|
|
83
|
+
*/
|
|
84
|
+
export function shallowCopyElement(element, doc) {
|
|
85
|
+
const copy = element.namespaceURI
|
|
86
|
+
? doc.createElementNS(element.namespaceURI, element.nodeName)
|
|
87
|
+
: doc.createElement(element.nodeName);
|
|
88
|
+
copyNamespaceDeclarations(element, copy);
|
|
89
|
+
return copy;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Deep copy of a node that can be a child in the result tree.
|
|
94
|
+
*
|
|
95
|
+
* @param {Node} node - Element, text, CDATA, comment, PI or document fragment
|
|
96
|
+
* @param {Document} doc - The result document
|
|
97
|
+
* @param {(node: Node) => string} stringValue - XPath string value (text runs)
|
|
98
|
+
* @returns {Node|null} The copy, or null for nodes that cannot be children
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* cloneNode(sourceElement, resultDocument, (n) => evaluator.getStringValue(n));
|
|
102
|
+
*/
|
|
103
|
+
export function cloneNode(node, doc, stringValue) {
|
|
104
|
+
switch (node.nodeType) {
|
|
105
|
+
case 1: {
|
|
106
|
+
const copy = shallowCopyElement(node, doc);
|
|
107
|
+
for (const attribute of node.attributes) {
|
|
108
|
+
copyAttribute(attribute, copy, () => true);
|
|
109
|
+
}
|
|
110
|
+
appendChildCopies(node, copy, doc, stringValue);
|
|
111
|
+
return copy;
|
|
112
|
+
}
|
|
113
|
+
case 3:
|
|
114
|
+
case 4:
|
|
115
|
+
return doc.createTextNode(stringValue(node));
|
|
116
|
+
case 7:
|
|
117
|
+
return doc.createProcessingInstruction(node.target, node.data);
|
|
118
|
+
case 8:
|
|
119
|
+
return doc.createComment(node.nodeValue);
|
|
120
|
+
case 11: {
|
|
121
|
+
const fragment = doc.createDocumentFragment();
|
|
122
|
+
appendChildCopies(node, fragment, doc, stringValue);
|
|
123
|
+
return fragment;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Append deep copies of the (XPath) children of a node.
|
|
132
|
+
*
|
|
133
|
+
* @param {Node} node - The source parent
|
|
134
|
+
* @param {Node} target - The result parent
|
|
135
|
+
* @param {Document} doc - The result document
|
|
136
|
+
* @param {(node: Node) => string} stringValue - XPath string value
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
function appendChildCopies(node, target, doc, stringValue) {
|
|
140
|
+
for (const child of childAxis(node)) {
|
|
141
|
+
const copy = cloneNode(child, doc, stringValue);
|
|
142
|
+
if (copy) target.appendChild(copy);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Declare on the copy of an element every namespace in scope on the source
|
|
148
|
+
* element that is not in scope on the result parent already: `xsl:copy-of`
|
|
149
|
+
* copies the namespace nodes of an element (XSLT 1.0 section 11.3), those
|
|
150
|
+
* declared on its ancestors included, as libxslt does for the top element of
|
|
151
|
+
* a copied tree (its descendants inherit them in the result).
|
|
152
|
+
*
|
|
153
|
+
* @param {Element} source - The copied source element
|
|
154
|
+
* @param {Element} copy - Its copy, not yet attached
|
|
155
|
+
* @param {Node} output - The result node receiving the copy
|
|
156
|
+
* @returns {void}
|
|
157
|
+
*/
|
|
158
|
+
function copyInScopeNamespaces(source, copy, output) {
|
|
159
|
+
const inOutput = (prefix, uri) =>
|
|
160
|
+
output.nodeType === 1 && output.lookupNamespaceURI(prefix || null) === uri;
|
|
161
|
+
for (const [prefix, uri] of inScopeBindings(source)) {
|
|
162
|
+
if (!inOutput(prefix, uri)) {
|
|
163
|
+
copyNamespaceNode(
|
|
164
|
+
{ localName: prefix, nodeValue: uri },
|
|
165
|
+
copy,
|
|
166
|
+
() => true,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Copy the result of an `xsl:copy-of` select expression to the result tree.
|
|
174
|
+
*
|
|
175
|
+
* Node-sets are copied node by node (roots as their children, attributes onto
|
|
176
|
+
* the current result element); any other value is written as text.
|
|
177
|
+
*
|
|
178
|
+
* @param {*} value - The evaluated expression
|
|
179
|
+
* @param {Node} output - The result node receiving the copy
|
|
180
|
+
* @param {object} host - Engine services
|
|
181
|
+
* @param {Document} host.doc - The result document
|
|
182
|
+
* @param {(node: Node) => string} host.stringValue - XPath string value
|
|
183
|
+
* @param {(value: *) => string} host.toString - XPath string() conversion
|
|
184
|
+
* @param {(element: Node) => boolean} host.canAddAttribute - Guard for attributes
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
export function copyOf(value, output, host) {
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
for (const item of value) copyOf(item, output, host);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (value === null || value === undefined) return;
|
|
194
|
+
if (!value.nodeType) {
|
|
195
|
+
const text = host.toString(value);
|
|
196
|
+
if (text) output.appendChild(host.doc.createTextNode(text));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (value.nodeType === 2) {
|
|
201
|
+
copyAttribute(value, output, host.canAddAttribute);
|
|
202
|
+
} else if (value.nodeType === NAMESPACE_NODE) {
|
|
203
|
+
copyNamespaceNode(value, output, host.canAddAttribute);
|
|
204
|
+
} else if (value.nodeType === 9 || value.nodeType === 11) {
|
|
205
|
+
// Children one by one: xmldom mishandles appending a DocumentFragment
|
|
206
|
+
appendChildCopies(value, output, host.doc, host.stringValue);
|
|
207
|
+
} else {
|
|
208
|
+
const copy = cloneNode(value, host.doc, host.stringValue);
|
|
209
|
+
if (value.nodeType === 1) copyInScopeNamespaces(value, copy, output);
|
|
210
|
+
if (copy) output.appendChild(copy);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names of named stylesheet objects (XSLT 1.0 section 2.4).
|
|
3
|
+
*
|
|
4
|
+
* Named templates, attribute sets and decimal formats are named by QNames
|
|
5
|
+
* that are expanded with the namespace declarations in scope on the element
|
|
6
|
+
* carrying the name; the default namespace is not used for unprefixed names.
|
|
7
|
+
* Two names are the same when their expanded names are, whatever prefixes
|
|
8
|
+
* they use, so every table of such objects is keyed by the expanded name in
|
|
9
|
+
* Clark notation (`{uri}local`, or the bare local name in no namespace).
|
|
10
|
+
*
|
|
11
|
+
* @module xslt/declarationNames
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
import { isQName } from "./qname.js";
|
|
17
|
+
import { splitQName } from "./resultNamespaces.js";
|
|
18
|
+
import { inScopeNamespaces, resolvePrefix } from "./stylesheetNamespaces.js";
|
|
19
|
+
import { expandedNameKey } from "./serializer/settings.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Expand a QName-valued attribute of a stylesheet element.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} qname - The QName, e.g. `p:format` or `common`
|
|
25
|
+
* @param {Element} element - The element in whose scope the prefix resolves
|
|
26
|
+
* @returns {{key: string}|{error: string}} The expanded name key, or why the
|
|
27
|
+
* value is not a QName with a declared prefix
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // <xsl:decimal-format xmlns:p="urn:p" name="p:f"/>
|
|
31
|
+
* expandName("p:f", element); // { key: "{urn:p}f" }
|
|
32
|
+
* expandName("q:f", element); // { error: 'undeclared namespace prefix "q" in "q:f"' }
|
|
33
|
+
*/
|
|
34
|
+
export function expandName(qname, element) {
|
|
35
|
+
if (!isQName(qname)) return { error: `invalid QName "${qname}"` };
|
|
36
|
+
const { prefix, localName } = splitQName(qname);
|
|
37
|
+
if (!prefix) return { key: localName };
|
|
38
|
+
const namespaceUri = resolvePrefix(inScopeNamespaces(element), prefix);
|
|
39
|
+
if (!namespaceUri) {
|
|
40
|
+
return { error: `undeclared namespace prefix "${prefix}" in "${qname}"` };
|
|
41
|
+
}
|
|
42
|
+
return { key: expandedNameKey(namespaceUri, localName) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Expand a QName that must be valid, failing with a message naming where it
|
|
47
|
+
* comes from.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} qname - The QName
|
|
50
|
+
* @param {Element} element - The element in whose scope the prefix resolves
|
|
51
|
+
* @param {string} where - The attribute holding it, e.g. "xsl:template name"
|
|
52
|
+
* @returns {string} The expanded name key
|
|
53
|
+
* @throws {Error} When the value is not a QName or its prefix is undeclared
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* requireExpandedName("p:t", templateElement, "xsl:template name"); // "{urn:p}t"
|
|
57
|
+
*/
|
|
58
|
+
export function requireExpandedName(qname, element, where) {
|
|
59
|
+
const expanded = expandName(qname, element);
|
|
60
|
+
if (expanded.error) throw new Error(`${where}: ${expanded.error}`);
|
|
61
|
+
return expanded.key;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Expand the whitespace separated QNames of an attribute such as
|
|
66
|
+
* `use-attribute-sets`.
|
|
67
|
+
*
|
|
68
|
+
* @param {string|null} value - The attribute value
|
|
69
|
+
* @param {Element} element - The element carrying the attribute
|
|
70
|
+
* @param {string} where - Attribute description for error messages
|
|
71
|
+
* @returns {string[]} The expanded name keys, in order
|
|
72
|
+
* @throws {Error} When one of the names is not a QName with a declared prefix
|
|
73
|
+
*/
|
|
74
|
+
export function requireExpandedNames(value, element, where) {
|
|
75
|
+
if (!value) return [];
|
|
76
|
+
return value
|
|
77
|
+
.split(/[ \t\r\n]+/)
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map((qname) => requireExpandedName(qname, element, where));
|
|
80
|
+
}
|