@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forwards-compatible processing and xsl:fallback (XSLT 1.0 sections 2.5
|
|
3
|
+
* and 15).
|
|
4
|
+
*
|
|
5
|
+
* An element is processed in forwards-compatible mode when the nearest
|
|
6
|
+
* enclosing `version` declaration (the `version` attribute of
|
|
7
|
+
* `xsl:stylesheet`/`xsl:transform`, or the `xsl:version` attribute of a
|
|
8
|
+
* literal result element) is not "1.0". In that mode unknown top-level XSLT
|
|
9
|
+
* elements are ignored. An unknown XSLT instruction is replaced by its
|
|
10
|
+
* `xsl:fallback` children when it has some (in any mode); otherwise it is an
|
|
11
|
+
* error only if it is actually instantiated.
|
|
12
|
+
*
|
|
13
|
+
* @module xslt/forwardsCompatible
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
import { XSLT_NAMESPACE } from "./elements.js";
|
|
19
|
+
import { xsltLocalName } from "./stylesheetChecks.js";
|
|
20
|
+
|
|
21
|
+
const modes = new WeakMap();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The version declared on one stylesheet element, if any.
|
|
25
|
+
*
|
|
26
|
+
* @param {Element} element - A stylesheet element
|
|
27
|
+
* @returns {string|null} The declared version, null when none
|
|
28
|
+
*/
|
|
29
|
+
export function declaredVersion(element) {
|
|
30
|
+
const localName = xsltLocalName(element);
|
|
31
|
+
if (localName === "stylesheet" || localName === "transform") {
|
|
32
|
+
return element.getAttribute("version");
|
|
33
|
+
}
|
|
34
|
+
if (localName !== null) return null;
|
|
35
|
+
return element.getAttributeNS?.(XSLT_NAMESPACE, "version") ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether a stylesheet element is processed in forwards-compatible mode.
|
|
40
|
+
* Cached per element.
|
|
41
|
+
*
|
|
42
|
+
* @param {Node|null} node - A stylesheet node
|
|
43
|
+
* @returns {boolean} True when the version in effect is not "1.0"
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* isForwardsCompatible(instruction); // true under version="2.0"
|
|
47
|
+
*/
|
|
48
|
+
export function isForwardsCompatible(node) {
|
|
49
|
+
if (node?.nodeType !== 1) return false;
|
|
50
|
+
|
|
51
|
+
let forwards = modes.get(node);
|
|
52
|
+
if (forwards === undefined) {
|
|
53
|
+
const version = declaredVersion(node);
|
|
54
|
+
forwards =
|
|
55
|
+
version === null || version === ""
|
|
56
|
+
? isForwardsCompatible(node.parentNode)
|
|
57
|
+
: version.trim() !== "1.0";
|
|
58
|
+
modes.set(node, forwards);
|
|
59
|
+
}
|
|
60
|
+
return forwards;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The `xsl:fallback` children of an instruction, in document order.
|
|
65
|
+
*
|
|
66
|
+
* @param {Element} instruction - An XSLT instruction
|
|
67
|
+
* @returns {Element[]} The fallback elements
|
|
68
|
+
*/
|
|
69
|
+
export function fallbackChildren(instruction) {
|
|
70
|
+
const fallbacks = [];
|
|
71
|
+
for (let child = instruction.firstChild; child; child = child.nextSibling) {
|
|
72
|
+
if (xsltLocalName(child) === "fallback") fallbacks.push(child);
|
|
73
|
+
}
|
|
74
|
+
return fallbacks;
|
|
75
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSLT-defined XPath functions.
|
|
3
|
+
*
|
|
4
|
+
* XSLT 1.0 section 12 adds functions to the XPath function library, and the
|
|
5
|
+
* EXSLT `node-set()` extension (also under the msxsl namespace) is registered
|
|
6
|
+
* by expanded name next to them, together with the other EXSLT functions of
|
|
7
|
+
* `./exslt/index.js`. They live here rather than in `src/xpath` so
|
|
8
|
+
* that module stays a pure XPath 1.0 implementation; the engine registers this
|
|
9
|
+
* map on its evaluator through
|
|
10
|
+
* {@link XPathEvaluator#registerFunctions}.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
import { formatNumber, DEFAULT_DECIMAL_FORMAT } from "./formatNumber.js";
|
|
16
|
+
import { isXsltElementAvailable, XSLT_NAMESPACE } from "./elements.js";
|
|
17
|
+
import {
|
|
18
|
+
expandedFunctionName,
|
|
19
|
+
resolveNamespacePrefix,
|
|
20
|
+
} from "../xpath/evaluator.js";
|
|
21
|
+
import { rootNodeOf } from "../xpath/axes.js";
|
|
22
|
+
import { EXSLT_COMMON, createExsltFunctions } from "./exslt/index.js";
|
|
23
|
+
|
|
24
|
+
/** Namespace of the EXSLT common module (`exsl:node-set()`). */
|
|
25
|
+
export const EXSLT_COMMON_NAMESPACE = EXSLT_COMMON;
|
|
26
|
+
|
|
27
|
+
/** Namespace of the MSXML extension functions (`msxsl:node-set()`). */
|
|
28
|
+
export const MSXSL_NAMESPACE = "urn:schemas-microsoft-com:xslt";
|
|
29
|
+
|
|
30
|
+
/** Vendor identification reported by `system-property()`. */
|
|
31
|
+
export const VENDOR = "@tradik/xslt-processor";
|
|
32
|
+
|
|
33
|
+
/** Vendor URL reported by `system-property()`. */
|
|
34
|
+
export const VENDOR_URL = "https://github.com/spagu/XSLT-Processor";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Values reported by `system-property()`, keyed by property name.
|
|
38
|
+
*
|
|
39
|
+
* The XSLT version is reported as the string `"1"`; XPath 1.0 converts it to
|
|
40
|
+
* the number 1 wherever a numeric comparison or arithmetic is used.
|
|
41
|
+
*/
|
|
42
|
+
const SYSTEM_PROPERTIES = Object.freeze({
|
|
43
|
+
"xsl:version": "1",
|
|
44
|
+
"xsl:vendor": VENDOR,
|
|
45
|
+
"xsl:vendor-url": VENDOR_URL,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the document that owns a node.
|
|
50
|
+
*
|
|
51
|
+
* @param {Node} node - Any node
|
|
52
|
+
* @returns {Document} The owning document, or the node when it is a document
|
|
53
|
+
*/
|
|
54
|
+
function ownerDocumentOf(node) {
|
|
55
|
+
return node.ownerDocument || node;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert an evaluated argument to the list of strings it denotes.
|
|
60
|
+
*
|
|
61
|
+
* Node-sets yield the string value of every node, other types yield one string.
|
|
62
|
+
*
|
|
63
|
+
* @param {XPathEvaluator} evaluator - The evaluator providing the conversions
|
|
64
|
+
* @param {(value: *) => string} stringify - The XPath `string()` conversion
|
|
65
|
+
* @param {*} value - An evaluated XPath value
|
|
66
|
+
* @returns {string[]} The string values
|
|
67
|
+
*/
|
|
68
|
+
function toStringList(evaluator, stringify, value) {
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return value.map((node) => evaluator.getStringValue(node));
|
|
71
|
+
}
|
|
72
|
+
return [stringify(value)];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Split a QName into its prefix and local part.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} qname - A possibly prefixed name
|
|
79
|
+
* @returns {{prefix: (string|null), localName: string}} The parts of the name
|
|
80
|
+
*/
|
|
81
|
+
function splitQName(qname) {
|
|
82
|
+
const colon = qname.indexOf(":");
|
|
83
|
+
if (colon === -1) return { prefix: null, localName: qname };
|
|
84
|
+
return {
|
|
85
|
+
prefix: qname.substring(0, colon),
|
|
86
|
+
localName: qname.substring(colon + 1),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Expanded name key of the decimal format named by the third argument of
|
|
92
|
+
* `format-number()`, with the prefix resolved in the expression's scope
|
|
93
|
+
* (XSLT 1.0 section 12.3); the table is keyed the same way (see
|
|
94
|
+
* declarationNames.js).
|
|
95
|
+
*
|
|
96
|
+
* @param {string} qname - The decimal format QName
|
|
97
|
+
* @param {{namespaces: Object<string, string>}} ctx - The XPath context
|
|
98
|
+
* @returns {string} `{uri}local`, or the local name in no namespace
|
|
99
|
+
* @throws {Error} When the prefix is not declared
|
|
100
|
+
*/
|
|
101
|
+
function decimalFormatKey(qname, ctx) {
|
|
102
|
+
const { prefix, localName } = splitQName(qname);
|
|
103
|
+
if (prefix === null) return localName;
|
|
104
|
+
const namespaceUri = resolveNamespacePrefix(prefix, ctx.namespaces);
|
|
105
|
+
if (!namespaceUri) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`format-number(): undeclared namespace prefix "${prefix}" in "${qname}"`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return `{${namespaceUri}}${localName}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build the XSLT function map for an engine.
|
|
115
|
+
*
|
|
116
|
+
* @param {import('./engine.js').XsltEngine} engine - The engine providing loaders, keys and formats
|
|
117
|
+
* @returns {Object<string, Function>} Functions ready for `registerFunctions`
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* evaluator.registerFunctions(createXsltFunctions(engine));
|
|
121
|
+
*/
|
|
122
|
+
export function createXsltFunctions(engine) {
|
|
123
|
+
const evaluator = engine.xpathEvaluator;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The XPath `string()` conversion of the evaluator.
|
|
127
|
+
*
|
|
128
|
+
* `XPathEvaluator#toString` shadows `Object#toString` and takes the value to
|
|
129
|
+
* convert as its argument, so it is bound once under an unambiguous name.
|
|
130
|
+
*
|
|
131
|
+
* @type {(value: *) => string}
|
|
132
|
+
*/
|
|
133
|
+
const stringify = evaluator.toString.bind(evaluator);
|
|
134
|
+
const evaluate = (arg, ctx) => evaluator.evaluate(arg, ctx);
|
|
135
|
+
const asString = (arg, ctx) => stringify(evaluate(arg, ctx));
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `exsl:node-set(object)` - EXSLT common, also known as `msxsl:node-set()`.
|
|
139
|
+
*
|
|
140
|
+
* A result tree fragment (a DocumentFragment in this engine) becomes a
|
|
141
|
+
* node-set holding its root node, so `exsl:node-set($rtf)/item` selects the
|
|
142
|
+
* fragment's top-level `item` elements. A node-set is returned unchanged; any
|
|
143
|
+
* other value becomes a node-set holding one text node with its string value.
|
|
144
|
+
*
|
|
145
|
+
* @param {Array} args - Argument expressions
|
|
146
|
+
* @param {import('../xpath/evaluator.js').XPathContext} ctx - Evaluation context
|
|
147
|
+
* @returns {Node[]} The node-set
|
|
148
|
+
*/
|
|
149
|
+
const nodeSet = (args, ctx) => {
|
|
150
|
+
const value = evaluate(args[0], ctx);
|
|
151
|
+
if (Array.isArray(value)) return value;
|
|
152
|
+
if (value?.nodeType) return [value];
|
|
153
|
+
return [ownerDocumentOf(ctx.node).createTextNode(stringify(value))];
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
...createExsltFunctions(engine),
|
|
158
|
+
[expandedFunctionName(EXSLT_COMMON_NAMESPACE, "node-set")]: nodeSet,
|
|
159
|
+
[expandedFunctionName(MSXSL_NAMESPACE, "node-set")]: nodeSet,
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `document(object, base?)` - load external XML documents.
|
|
163
|
+
*
|
|
164
|
+
* An empty URI denotes the stylesheet itself. Without a document loader, or
|
|
165
|
+
* when the loader returns null, the result is an empty node-set. The
|
|
166
|
+
* optional second argument is read as a base URI string.
|
|
167
|
+
*/
|
|
168
|
+
document: (args, ctx) => {
|
|
169
|
+
const baseUri = args.length > 1 ? asString(args[1], ctx) : engine.baseUri;
|
|
170
|
+
const uris = toStringList(evaluator, stringify, evaluate(args[0], ctx));
|
|
171
|
+
const result = [];
|
|
172
|
+
|
|
173
|
+
for (const uri of uris) {
|
|
174
|
+
const doc = engine.loadDocument(uri, baseUri || engine.baseUri);
|
|
175
|
+
if (doc && !result.includes(doc)) result.push(doc);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `key(name, value)` - look up nodes through an `xsl:key` index of the
|
|
183
|
+
* tree containing the context node: its document, or the fragment of a
|
|
184
|
+
* result tree fragment converted with `exsl:node-set()`.
|
|
185
|
+
*/
|
|
186
|
+
key: (args, ctx) => {
|
|
187
|
+
const name = asString(args[0], ctx);
|
|
188
|
+
const values = toStringList(evaluator, stringify, evaluate(args[1], ctx));
|
|
189
|
+
return engine.keyRegistry.lookup(name, values, rootNodeOf(ctx.node));
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/** `format-number(number, pattern, decimalFormat?)`. */
|
|
193
|
+
"format-number": (args, ctx) => {
|
|
194
|
+
const value = evaluator.toNumber(evaluate(args[0], ctx));
|
|
195
|
+
const pattern = asString(args[1], ctx);
|
|
196
|
+
const formatName =
|
|
197
|
+
args.length > 2 ? decimalFormatKey(asString(args[2], ctx), ctx) : "";
|
|
198
|
+
const format =
|
|
199
|
+
engine.decimalFormats[formatName] || DEFAULT_DECIMAL_FORMAT;
|
|
200
|
+
return formatNumber(value, pattern, format);
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/** `current()` - the XSLT current node, not the XPath context node. */
|
|
204
|
+
current: (args, ctx) => {
|
|
205
|
+
const currentNode = ctx.hostContext?.currentNode;
|
|
206
|
+
return currentNode ? [currentNode] : [ctx.node];
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/** `generate-id(node-set?)` - a stable id for the life of the transform. */
|
|
210
|
+
"generate-id": (args, ctx) => {
|
|
211
|
+
let node = ctx.node;
|
|
212
|
+
|
|
213
|
+
if (args.length > 0) {
|
|
214
|
+
const nodeSet = evaluate(args[0], ctx);
|
|
215
|
+
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return node ? engine.generateId(node) : "";
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/** `system-property(name)` - XSLT version and vendor information. */
|
|
222
|
+
"system-property": (args, ctx) => {
|
|
223
|
+
const name = asString(args[0], ctx);
|
|
224
|
+
return Object.hasOwn(SYSTEM_PROPERTIES, name)
|
|
225
|
+
? SYSTEM_PROPERTIES[name]
|
|
226
|
+
: "";
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `function-available(name)` - reflects the evaluator function table. A
|
|
231
|
+
* prefixed name is resolved through the stylesheet's namespace bindings,
|
|
232
|
+
* just as a call of that function would be. A function can report itself
|
|
233
|
+
* unavailable through an `isAvailable()` property (`dyn:evaluate()` does
|
|
234
|
+
* until the engine enables it).
|
|
235
|
+
*/
|
|
236
|
+
"function-available": (args, ctx) => {
|
|
237
|
+
const { prefix, localName } = splitQName(asString(args[0], ctx));
|
|
238
|
+
const fn = evaluator.resolveFunction(localName, prefix, ctx.namespaces);
|
|
239
|
+
return fn !== null && fn.isAvailable?.() !== false;
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/** `element-available(name)` - reflects the XSLT elements the engine runs. */
|
|
243
|
+
"element-available": (args, ctx) => {
|
|
244
|
+
const { prefix, localName } = splitQName(asString(args[0], ctx));
|
|
245
|
+
// An unprefixed name is in the default namespace (XSLT 1.0 section
|
|
246
|
+
// 15), which may be the XSLT namespace itself (libxslt bug-200)
|
|
247
|
+
const namespaceUri = prefix
|
|
248
|
+
? (ctx.namespaces[prefix] ?? (prefix === "xsl" ? XSLT_NAMESPACE : null))
|
|
249
|
+
: ctx.namespaces[""] || null;
|
|
250
|
+
if (!namespaceUri) return false;
|
|
251
|
+
|
|
252
|
+
if (namespaceUri === XSLT_NAMESPACE) {
|
|
253
|
+
return isXsltElementAvailable(localName);
|
|
254
|
+
}
|
|
255
|
+
// Extension elements with an implementation (registerExtensionElement)
|
|
256
|
+
return Boolean(
|
|
257
|
+
engine.extensionElements?.has(`{${namespaceUri}}${localName}`),
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `unparsed-entity-uri(name)` - always empty.
|
|
263
|
+
*
|
|
264
|
+
* Unparsed entity declarations are not exposed by the DOM, so this
|
|
265
|
+
* processor cannot resolve them; returning the empty string keeps
|
|
266
|
+
* stylesheets that call the function working.
|
|
267
|
+
*/
|
|
268
|
+
"unparsed-entity-uri": () => "",
|
|
269
|
+
};
|
|
270
|
+
}
|
package/src/xslt/index.js
CHANGED
|
@@ -3,4 +3,41 @@
|
|
|
3
3
|
* Based on W3C XSLT 1.0 Specification: http://www.w3.org/TR/1999/REC-xslt-19991116
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
XSLT_MAX_EXPRESSION_DEPTH,
|
|
8
|
+
XSLT_MAX_RESULT_SIZE,
|
|
9
|
+
XSLT_MAX_TEMPLATE_DEPTH,
|
|
10
|
+
XsltContext,
|
|
11
|
+
XsltEngine,
|
|
12
|
+
} from "./engine.js";
|
|
13
|
+
|
|
14
|
+
// XSLT vocabulary and function library (advanced usage)
|
|
15
|
+
export {
|
|
16
|
+
XSLT_ELEMENTS,
|
|
17
|
+
XSLT_NAMESPACE,
|
|
18
|
+
isXsltElementAvailable,
|
|
19
|
+
} from "./elements.js";
|
|
20
|
+
export { VENDOR, VENDOR_URL, createXsltFunctions } from "./functions.js";
|
|
21
|
+
export { KeyIndexRegistry } from "./keys.js";
|
|
22
|
+
export { DEFAULT_DECIMAL_FORMAT, formatNumber } from "./formatNumber.js";
|
|
23
|
+
export { countXsltNumber } from "./number.js";
|
|
24
|
+
export { formatXsltNumber, toRoman } from "./numberFormat.js";
|
|
25
|
+
export { WhitespaceFilter, stripWhitespaceNodes } from "./whitespace.js";
|
|
26
|
+
export {
|
|
27
|
+
NamespaceAliasMap,
|
|
28
|
+
getXsltAttribute,
|
|
29
|
+
lookupNamespaceUri,
|
|
30
|
+
shouldCopyAttribute,
|
|
31
|
+
} from "./literalResult.js";
|
|
32
|
+
export {
|
|
33
|
+
createResultDocument,
|
|
34
|
+
importResultFragment,
|
|
35
|
+
importResultNode,
|
|
36
|
+
} from "./resultTree.js";
|
|
37
|
+
export { isAbsoluteUri, resolveUri, stripFragment } from "./uri.js";
|
|
38
|
+
export {
|
|
39
|
+
serializeResult,
|
|
40
|
+
markRawText,
|
|
41
|
+
isRawText,
|
|
42
|
+
resolveOutputSettings,
|
|
43
|
+
} from "./serializer.js";
|
package/src/xslt/keys.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xsl:key` indexing for the XSLT `key()` function.
|
|
3
|
+
*
|
|
4
|
+
* Indexes are built lazily, once per (root node, key name) pair, and cached in
|
|
5
|
+
* a `WeakMap` so source documents stay garbage collectable. The root node is
|
|
6
|
+
* the document of the context node, or the DocumentFragment of a result tree
|
|
7
|
+
* fragment converted with `exsl:node-set()` (see `rootNodeOf` in xpath/axes). The registry is
|
|
8
|
+
* deliberately decoupled from the engine: pattern matching and `use` evaluation
|
|
9
|
+
* are injected as callbacks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} KeyDefinition
|
|
16
|
+
* @property {string} match - Pattern of the nodes to index
|
|
17
|
+
* @property {string} use - Expression computing the key values
|
|
18
|
+
* @property {Object<string, string>} [namespaces] - Prefixes in scope on the xsl:key element
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} KeyIndex
|
|
23
|
+
* @property {Map<string, Node[]>} buckets - Key value to nodes, in document order
|
|
24
|
+
* @property {Map<Node, number>} order - Document order position of every node
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazily built, per-tree indexes for all declared keys.
|
|
29
|
+
*/
|
|
30
|
+
export class KeyIndexRegistry {
|
|
31
|
+
/**
|
|
32
|
+
* @param {Object} options - Registry configuration
|
|
33
|
+
* @param {Object<string, (KeyDefinition|KeyDefinition[])>} options.keys - Declared keys by name; several xsl:key elements may share a name
|
|
34
|
+
* @param {(node: Node, pattern: string, definition: KeyDefinition) => boolean} options.matchesPattern - XSLT pattern matcher
|
|
35
|
+
* @param {(node: Node, expression: string, definition: KeyDefinition) => string[]} options.evaluateUse - `use` evaluator returning key values
|
|
36
|
+
*/
|
|
37
|
+
constructor({ keys, matchesPattern, evaluateUse }) {
|
|
38
|
+
this.keys = keys;
|
|
39
|
+
this.matchesPattern = matchesPattern;
|
|
40
|
+
this.evaluateUse = evaluateUse;
|
|
41
|
+
this.cache = new WeakMap();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Drop every cached index, for example after the key declarations changed.
|
|
46
|
+
*
|
|
47
|
+
* @returns {void}
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* registry.clear();
|
|
51
|
+
*/
|
|
52
|
+
clear() {
|
|
53
|
+
this.cache = new WeakMap();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Look up the nodes indexed under one or more key values.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} name - The key name
|
|
60
|
+
* @param {string|string[]} values - One key value, or several to union
|
|
61
|
+
* @param {Node} doc - Root of the tree to search (Document or DocumentFragment)
|
|
62
|
+
* @returns {Node[]} Matching nodes in document order, without duplicates
|
|
63
|
+
* @throws {Error} When the key name was never declared
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* registry.lookup('byId', 'a1', xmlDoc);
|
|
67
|
+
*/
|
|
68
|
+
lookup(name, values, doc) {
|
|
69
|
+
if (!Object.hasOwn(this.keys, name)) {
|
|
70
|
+
throw new Error(`Undefined key: ${name}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { buckets, order } = this.getIndex(name, doc);
|
|
74
|
+
const wanted = Array.isArray(values) ? values : [values];
|
|
75
|
+
if (wanted.length === 1) return [...(buckets.get(wanted[0]) || [])];
|
|
76
|
+
|
|
77
|
+
const found = new Set();
|
|
78
|
+
for (const value of wanted) {
|
|
79
|
+
for (const node of buckets.get(value) || []) found.add(node);
|
|
80
|
+
}
|
|
81
|
+
return [...found].sort((a, b) => order.get(a) - order.get(b));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get (building if needed) the index of one key for one document.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} name - The key name
|
|
88
|
+
* @param {Node} doc - Root of the tree being indexed
|
|
89
|
+
* @returns {KeyIndex} The index
|
|
90
|
+
*/
|
|
91
|
+
getIndex(name, doc) {
|
|
92
|
+
let byName = this.cache.get(doc);
|
|
93
|
+
if (!byName) {
|
|
94
|
+
byName = new Map();
|
|
95
|
+
this.cache.set(doc, byName);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let index = byName.get(name);
|
|
99
|
+
if (!index) {
|
|
100
|
+
index = this.buildIndex(name, doc);
|
|
101
|
+
byName.set(name, index);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return index;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build the index of one key for one document, merging every declaration
|
|
109
|
+
* of that key name.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} name - The key name
|
|
112
|
+
* @param {Node} doc - Root of the tree being indexed
|
|
113
|
+
* @returns {KeyIndex} The index
|
|
114
|
+
*/
|
|
115
|
+
buildIndex(name, doc) {
|
|
116
|
+
const definitions = [this.keys[name]].flat();
|
|
117
|
+
const buckets = new Map();
|
|
118
|
+
const order = new Map();
|
|
119
|
+
|
|
120
|
+
for (const node of documentOrderNodes(doc)) {
|
|
121
|
+
order.set(node, order.size);
|
|
122
|
+
|
|
123
|
+
for (const definition of definitions) {
|
|
124
|
+
if (!this.matchesPattern(node, definition.match, definition)) continue;
|
|
125
|
+
|
|
126
|
+
for (const value of this.evaluateUse(
|
|
127
|
+
node,
|
|
128
|
+
definition.use,
|
|
129
|
+
definition,
|
|
130
|
+
)) {
|
|
131
|
+
const bucket = buckets.get(value);
|
|
132
|
+
if (!bucket) buckets.set(value, [node]);
|
|
133
|
+
else if (bucket[bucket.length - 1] !== node) bucket.push(node);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { buckets, order };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Walk a document in document order, including attribute nodes.
|
|
144
|
+
*
|
|
145
|
+
* @param {Node} root - The document or subtree root
|
|
146
|
+
* @yields {Node} Every node of the subtree
|
|
147
|
+
*/
|
|
148
|
+
function* documentOrderNodes(root) {
|
|
149
|
+
const stack = [root];
|
|
150
|
+
|
|
151
|
+
while (stack.length > 0) {
|
|
152
|
+
const current = stack.pop();
|
|
153
|
+
yield current;
|
|
154
|
+
|
|
155
|
+
if (current.nodeType === 1 && current.attributes) {
|
|
156
|
+
for (const attribute of current.attributes) yield attribute;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const children = current.childNodes;
|
|
160
|
+
if (children) {
|
|
161
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|