@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level elements of a stylesheet module: their dispatch to the
|
|
3
|
+
* declaration handlers, simplified stylesheets and the stylesheet-wide
|
|
4
|
+
* namespace table.
|
|
5
|
+
*
|
|
6
|
+
* Methods installed on XsltEngine.prototype (`this` is the engine).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { XSLT_NAMESPACE } from "../elements.js";
|
|
10
|
+
import { inScopeNamespaces } from "../stylesheetNamespaces.js";
|
|
11
|
+
import {
|
|
12
|
+
checkLocalBindings,
|
|
13
|
+
checkNumberPatterns,
|
|
14
|
+
checkTopLevelText,
|
|
15
|
+
} from "../stylesheetChecks.js";
|
|
16
|
+
import { isForwardsCompatible } from "../forwardsCompatible.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Engine method handling each top-level XSLT element (xsl:import is handled
|
|
20
|
+
* first, separately, because imports must precede everything else).
|
|
21
|
+
*/
|
|
22
|
+
const TOP_LEVEL_HANDLERS = Object.freeze({
|
|
23
|
+
template: "registerTemplate",
|
|
24
|
+
output: "processOutput",
|
|
25
|
+
variable: "processGlobalVariable",
|
|
26
|
+
param: "processGlobalParam",
|
|
27
|
+
key: "processKey",
|
|
28
|
+
"decimal-format": "processDecimalFormat",
|
|
29
|
+
"namespace-alias": "processNamespaceAlias",
|
|
30
|
+
"attribute-set": "processAttributeSet",
|
|
31
|
+
"strip-space": "processStripSpace",
|
|
32
|
+
"preserve-space": "processPreserveSpace",
|
|
33
|
+
include: "processInclude",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const topLevelMethods = {
|
|
37
|
+
/**
|
|
38
|
+
* Process the top-level elements of a stylesheet module.
|
|
39
|
+
*
|
|
40
|
+
* xsl:import elements are processed first, so imported declarations get a
|
|
41
|
+
* lower import precedence than the ones of the importing stylesheet.
|
|
42
|
+
*
|
|
43
|
+
* @param {Element} root - The xsl:stylesheet element
|
|
44
|
+
* @param {string} stylesheetUri - URI used to resolve imports and includes
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
processTopLevelElements(root, stylesheetUri) {
|
|
48
|
+
checkTopLevelText(root);
|
|
49
|
+
this.collectNamespaces(root);
|
|
50
|
+
|
|
51
|
+
const imports = [];
|
|
52
|
+
const otherElements = [];
|
|
53
|
+
for (const child of root.childNodes) {
|
|
54
|
+
if (child.nodeType !== 1) continue;
|
|
55
|
+
if (this.isXsltElement(child, "import")) imports.push(child);
|
|
56
|
+
else otherElements.push(child);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const importNode of imports) {
|
|
60
|
+
this.processImport(importNode, stylesheetUri);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const child of otherElements) {
|
|
64
|
+
if (!this.isXsltNamespace(child)) continue;
|
|
65
|
+
const method = TOP_LEVEL_HANDLERS[child.localName];
|
|
66
|
+
if (method) this[method](child, stylesheetUri);
|
|
67
|
+
else this.unknownTopLevelElement(child);
|
|
68
|
+
}
|
|
69
|
+
checkNumberPatterns(this.patternMatcher, root);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Ignore an unknown top-level XSLT element: silently in forwards-compatible
|
|
74
|
+
* mode (XSLT 1.0 section 2.5), with a warning otherwise.
|
|
75
|
+
*
|
|
76
|
+
* @param {Element} node - The unknown element
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
unknownTopLevelElement(node) {
|
|
80
|
+
if (isForwardsCompatible(node)) return;
|
|
81
|
+
this.warnOnce(`unknown top-level element xsl:${node.localName} is ignored`);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register a simplified stylesheet (XSLT 1.0 section 2.3): the literal result
|
|
86
|
+
* root element is the body of a template rule matching "/", so the root
|
|
87
|
+
* element itself is instantiated, not only its children.
|
|
88
|
+
*
|
|
89
|
+
* @param {Element} root - The literal result root element
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
processLiteralResultStylesheet(root) {
|
|
93
|
+
this.collectNamespaces(root);
|
|
94
|
+
checkNumberPatterns(this.patternMatcher, root);
|
|
95
|
+
checkLocalBindings(root, (message) => this.warnOnce(message));
|
|
96
|
+
this.templates.push({
|
|
97
|
+
match: "/",
|
|
98
|
+
name: null,
|
|
99
|
+
mode: null,
|
|
100
|
+
priority: 0.5,
|
|
101
|
+
importPrecedence: this.currentImportPrecedence,
|
|
102
|
+
namespaces: inScopeNamespaces(root),
|
|
103
|
+
node: { firstChild: root, childNodes: [root] },
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Record the namespace declarations of a stylesheet document element in
|
|
109
|
+
* the stylesheet-wide fallback table. Instructions resolve prefixes against
|
|
110
|
+
* their own in-scope namespaces; this table only serves contexts without a
|
|
111
|
+
* stylesheet element. The first binding of a prefix wins, so a module
|
|
112
|
+
* loaded later cannot rebind the main stylesheet's prefixes.
|
|
113
|
+
*
|
|
114
|
+
* @param {Element} node - The stylesheet element
|
|
115
|
+
* @returns {void}
|
|
116
|
+
*/
|
|
117
|
+
collectNamespaces(node) {
|
|
118
|
+
if (!node.attributes) return;
|
|
119
|
+
|
|
120
|
+
for (const attr of node.attributes) {
|
|
121
|
+
let prefix = null;
|
|
122
|
+
if (attr.name.startsWith("xmlns:")) prefix = attr.name.substring(6);
|
|
123
|
+
else if (attr.name === "xmlns") prefix = "";
|
|
124
|
+
|
|
125
|
+
if (prefix !== null && attr.value !== XSLT_NAMESPACE) {
|
|
126
|
+
this.namespaces[prefix] ??= attr.value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transformation entry points: building the result tree and shaping it as
|
|
3
|
+
* a fragment, a document or a string.
|
|
4
|
+
*
|
|
5
|
+
* Methods installed on XsltEngine.prototype (`this` is the engine).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { WhitespaceFilter, stripWhitespaceNodes } from "../whitespace.js";
|
|
9
|
+
import {
|
|
10
|
+
createResultDocument,
|
|
11
|
+
importResultFragment,
|
|
12
|
+
isHtmlDocument,
|
|
13
|
+
parseHtmlFragment,
|
|
14
|
+
wrapTextResult,
|
|
15
|
+
} from "../resultTree.js";
|
|
16
|
+
import { resolveOutputSettings, serializeResult } from "../serializer.js";
|
|
17
|
+
import { fillXmlDocument, parseHtmlDocument } from "../resultDocument.js";
|
|
18
|
+
import { XsltContext } from "./context.js";
|
|
19
|
+
import {
|
|
20
|
+
DocumentOrderIndex,
|
|
21
|
+
hasNativePositionComparison,
|
|
22
|
+
} from "../../xpath/documentOrder.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Turn a JavaScript stack overflow into a clear transformation error; any
|
|
26
|
+
* other error is returned unchanged.
|
|
27
|
+
*
|
|
28
|
+
* @param {Error} error - The error thrown by a transformation
|
|
29
|
+
* @returns {Error} The error to report
|
|
30
|
+
*/
|
|
31
|
+
function recursionError(error) {
|
|
32
|
+
const isStackOverflow =
|
|
33
|
+
(error instanceof RangeError && /call stack/i.test(error.message)) ||
|
|
34
|
+
// Firefox reports "InternalError: too much recursion"
|
|
35
|
+
(error?.name === "InternalError" && /recursion/i.test(error.message));
|
|
36
|
+
if (!isStackOverflow) return error;
|
|
37
|
+
|
|
38
|
+
return new Error(
|
|
39
|
+
"Template recursion too deep: the transformation exceeded the JavaScript " +
|
|
40
|
+
"call stack (infinite recursion, or recursion deeper than the runtime allows)",
|
|
41
|
+
{ cause: error },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const transformationMethods = {
|
|
46
|
+
/**
|
|
47
|
+
* Transform a source node into a fragment owned by `ownerDocument`, built
|
|
48
|
+
* with the XML DOM (names and namespaces as in the result tree).
|
|
49
|
+
*
|
|
50
|
+
* @param {Node} sourceNode - Source document or element
|
|
51
|
+
* @param {Document} [ownerDocument] - Output document, default the global one
|
|
52
|
+
* @returns {DocumentFragment} The result
|
|
53
|
+
*/
|
|
54
|
+
transform(sourceNode, ownerDocument) {
|
|
55
|
+
const doc = this.outputDocumentOf(ownerDocument);
|
|
56
|
+
return importResultFragment(this.buildResultTree(sourceNode, doc), doc);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Transform a source node into a fragment of `ownerDocument` as Chrome's
|
|
61
|
+
* `transformToFragment` does: into an HTML document, the output of the
|
|
62
|
+
* html method (declared or detected) is serialized and parsed as HTML, so
|
|
63
|
+
* it holds HTMLElements, and other output keeps its nodes except that
|
|
64
|
+
* elements in no namespace become XHTML elements (as in Chrome and
|
|
65
|
+
* Firefox); into an XML document the result nodes are kept.
|
|
66
|
+
*
|
|
67
|
+
* @param {Node} sourceNode - Source document or element
|
|
68
|
+
* @param {Document} ownerDocument - Output document
|
|
69
|
+
* @returns {DocumentFragment} The result
|
|
70
|
+
*/
|
|
71
|
+
transformToFragment(sourceNode, ownerDocument) {
|
|
72
|
+
const doc = this.outputDocumentOf(ownerDocument);
|
|
73
|
+
const fragment = this.buildResultTree(sourceNode, doc);
|
|
74
|
+
const settings = resolveOutputSettings(this.outputSettings, fragment);
|
|
75
|
+
if (isHtmlDocument(doc) && settings.method === "html") {
|
|
76
|
+
return parseHtmlFragment(
|
|
77
|
+
serializeResult(fragment, this.outputSettings),
|
|
78
|
+
doc,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return importResultFragment(fragment, doc, { htmlElements: true });
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The document that owns a transformation result.
|
|
86
|
+
*
|
|
87
|
+
* @param {Document} [ownerDocument] - Requested owner
|
|
88
|
+
* @returns {Document} The owner, else the global document
|
|
89
|
+
* @throws {Error} When there is no document at all
|
|
90
|
+
*/
|
|
91
|
+
outputDocumentOf(ownerDocument) {
|
|
92
|
+
const doc =
|
|
93
|
+
ownerDocument || (typeof document !== "undefined" ? document : null);
|
|
94
|
+
|
|
95
|
+
if (!doc) {
|
|
96
|
+
throw new Error("No output document available");
|
|
97
|
+
}
|
|
98
|
+
return doc;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run the transformation and return the result tree, built in a neutral
|
|
103
|
+
* XML document: creating nodes directly in an HTML owner document would
|
|
104
|
+
* lower case names and force the XHTML namespace on every element.
|
|
105
|
+
*
|
|
106
|
+
* The templates are applied to the document node (not the document
|
|
107
|
+
* element), so the "/" template has the document as context node and
|
|
108
|
+
* paths such as "RootElement/child" work.
|
|
109
|
+
*
|
|
110
|
+
* @param {Node} sourceNode - Source document or element
|
|
111
|
+
* @param {Document} doc - Document providing the DOM implementation
|
|
112
|
+
* @returns {DocumentFragment} The result tree
|
|
113
|
+
*/
|
|
114
|
+
buildResultTree(sourceNode, doc) {
|
|
115
|
+
const resultDocument = createResultDocument(doc);
|
|
116
|
+
const source = this.initialNode(this.prepareSource(sourceNode, doc));
|
|
117
|
+
|
|
118
|
+
const context = new XsltContext({
|
|
119
|
+
currentNode: source,
|
|
120
|
+
currentNodeList: [source],
|
|
121
|
+
position: 1,
|
|
122
|
+
outputDocument: resultDocument,
|
|
123
|
+
stylesheet: this.stylesheetDoc,
|
|
124
|
+
namespaces: this.namespaces,
|
|
125
|
+
templates: this.templates,
|
|
126
|
+
keys: this.keys,
|
|
127
|
+
decimalFormats: this.decimalFormats,
|
|
128
|
+
outputMethod: this.outputSettings.method,
|
|
129
|
+
xpathEvaluator: this.xpathEvaluator,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.rootContext = context;
|
|
133
|
+
// The source tree may have changed since the previous transformation
|
|
134
|
+
this.keyRegistry.clear();
|
|
135
|
+
this.patternMatcher.reset();
|
|
136
|
+
this.xpathEvaluator.resetNamespaceNodes();
|
|
137
|
+
// Document order from positions numbered once, unless the DOM compares
|
|
138
|
+
// positions natively (see documentOrder.js)
|
|
139
|
+
this.xpathEvaluator.resetDocumentOrder(
|
|
140
|
+
hasNativePositionComparison(source) ? null : new DocumentOrderIndex(),
|
|
141
|
+
);
|
|
142
|
+
this.numberMemos = new WeakMap();
|
|
143
|
+
|
|
144
|
+
const fragment = resultDocument.createDocumentFragment();
|
|
145
|
+
try {
|
|
146
|
+
context.globals = this.createGlobals(context);
|
|
147
|
+
context.globals.evaluateAll();
|
|
148
|
+
this.applyTemplates([source], null, context, fragment);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw recursionError(error);
|
|
151
|
+
}
|
|
152
|
+
return fragment;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Choose the node the transformation starts from.
|
|
157
|
+
*
|
|
158
|
+
* A document element is transformed through its document, so that the "/"
|
|
159
|
+
* template rule applies as for a whole document (as browsers do); any other
|
|
160
|
+
* node is transformed as is.
|
|
161
|
+
*
|
|
162
|
+
* @param {Node} source - The (prepared) source node
|
|
163
|
+
* @returns {Node} The initial context node
|
|
164
|
+
*/
|
|
165
|
+
initialNode(source) {
|
|
166
|
+
const owner = source.ownerDocument;
|
|
167
|
+
return source.nodeType === 1 && owner?.documentElement === source
|
|
168
|
+
? owner
|
|
169
|
+
: source;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Apply `xsl:strip-space` to the source tree.
|
|
174
|
+
*
|
|
175
|
+
* Stripping produces a copy so the caller's document is never modified; when
|
|
176
|
+
* no `xsl:strip-space` is declared the original node is used unchanged.
|
|
177
|
+
*
|
|
178
|
+
* @param {Node} sourceNode - The source document or element
|
|
179
|
+
* @param {Document} ownerDocument - Document providing the DOM implementation
|
|
180
|
+
* @returns {Node} The source to transform
|
|
181
|
+
*/
|
|
182
|
+
prepareSource(sourceNode, ownerDocument) {
|
|
183
|
+
const filter = new WhitespaceFilter(this.stripSpace, this.preserveSpace);
|
|
184
|
+
if (!filter.isActive()) return sourceNode;
|
|
185
|
+
|
|
186
|
+
return stripWhitespaceNodes(
|
|
187
|
+
sourceNode,
|
|
188
|
+
filter,
|
|
189
|
+
createResultDocument(ownerDocument),
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Transform to a complete document, shaped as Chrome's XSLTProcessor
|
|
195
|
+
* returns it (see resultDocument.js): with `method="text"` an XHTML page
|
|
196
|
+
* holding the text in a `pre` element (see wrapTextResult), with the html
|
|
197
|
+
* method (declared or detected) an HTML document parsed from the html
|
|
198
|
+
* output, otherwise an XML document of the result nodes.
|
|
199
|
+
*
|
|
200
|
+
* @param {Node} sourceNode - Source document or element to transform
|
|
201
|
+
* @returns {Document} The result document
|
|
202
|
+
*/
|
|
203
|
+
transformToDocument(sourceNode) {
|
|
204
|
+
const doc = this.createDocument(sourceNode);
|
|
205
|
+
const fragment = this.transform(sourceNode, doc);
|
|
206
|
+
|
|
207
|
+
if (this.outputSettings.method === "text") {
|
|
208
|
+
return wrapTextResult(doc, fragment.textContent);
|
|
209
|
+
}
|
|
210
|
+
const settings = resolveOutputSettings(this.outputSettings, fragment);
|
|
211
|
+
if (settings.method === "html") {
|
|
212
|
+
const markup = serializeResult(fragment, this.outputSettings);
|
|
213
|
+
const htmlDoc = parseHtmlDocument(markup, doc);
|
|
214
|
+
if (htmlDoc) return htmlDoc;
|
|
215
|
+
}
|
|
216
|
+
return fillXmlDocument(doc, fragment, settings);
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Transform a source document and serialize the result to a string.
|
|
221
|
+
*
|
|
222
|
+
* Non-W3C convenience method: the result tree is serialized honoring the
|
|
223
|
+
* `xsl:output` settings of the stylesheet (XSLT 1.0 section 16).
|
|
224
|
+
*
|
|
225
|
+
* @param {Node} sourceNode - Source document or element to transform
|
|
226
|
+
* @returns {string} The serialized transformation result
|
|
227
|
+
*/
|
|
228
|
+
transformToString(sourceNode) {
|
|
229
|
+
const fragment = this.buildResultTree(
|
|
230
|
+
sourceNode,
|
|
231
|
+
this.createDocument(sourceNode),
|
|
232
|
+
);
|
|
233
|
+
return serializeResult(fragment, this.outputSettings);
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create an empty XML document to hold a transformation result.
|
|
238
|
+
*
|
|
239
|
+
* Uses the global `document` when running in a browser and otherwise falls
|
|
240
|
+
* back to the DOM implementation owning `referenceNode` (e.g. a jsdom or
|
|
241
|
+
* xmldom document in Node.js).
|
|
242
|
+
*
|
|
243
|
+
* @param {Node} [referenceNode] - Any node whose DOM implementation can be reused
|
|
244
|
+
* @returns {Document} A new empty document
|
|
245
|
+
* @throws {Error} When no DOM implementation is available
|
|
246
|
+
*/
|
|
247
|
+
createDocument(referenceNode) {
|
|
248
|
+
if (typeof document !== "undefined") {
|
|
249
|
+
return document.implementation.createDocument(null, null, null);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const ownerDocument =
|
|
253
|
+
referenceNode &&
|
|
254
|
+
(referenceNode.nodeType === 9
|
|
255
|
+
? referenceNode
|
|
256
|
+
: referenceNode.ownerDocument);
|
|
257
|
+
if (ownerDocument?.implementation) {
|
|
258
|
+
return ownerDocument.implementation.createDocument(null, null, null);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw new Error("Document creation not available in this environment");
|
|
262
|
+
},
|
|
263
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit work stack for template instantiation.
|
|
3
|
+
*
|
|
4
|
+
* Recursive stylesheets nest template invocations thousands of levels deep
|
|
5
|
+
* (libxslt allows 3000). Instantiating each level with JavaScript recursion
|
|
6
|
+
* costs several native frames per level and overflows the call stack of
|
|
7
|
+
* browsers and Node long before that, so the engine keeps the pending work
|
|
8
|
+
* in frames on an explicit stack instead:
|
|
9
|
+
*
|
|
10
|
+
* - a SequenceFrame walks the children of a stylesheet element (a sequence
|
|
11
|
+
* constructor: a template body, the content of xsl:if, a literal result
|
|
12
|
+
* element, ...);
|
|
13
|
+
* - a LoopFrame visits the items of xsl:apply-templates or xsl:for-each.
|
|
14
|
+
*
|
|
15
|
+
* An instruction whose content comes last (xsl:if, xsl:choose,
|
|
16
|
+
* xsl:call-template, literal result elements, ...) schedules that content
|
|
17
|
+
* with {@link workStackMethods.continueWith} and returns; the driver loop
|
|
18
|
+
* runs it before the next sibling of the instruction. The native stack then
|
|
19
|
+
* stays flat however deep the templates nest. Instructions that need the
|
|
20
|
+
* result at once (xsl:attribute, xsl:with-param content, ...) run a nested
|
|
21
|
+
* driver with {@link workStackMethods.runFrame}.
|
|
22
|
+
*
|
|
23
|
+
* Methods installed on XsltEngine.prototype (`this` is the engine).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { inScopeNamespaces } from "../stylesheetNamespaces.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Deepest nesting of template instantiations in a transformation, the
|
|
30
|
+
* default of libxslt's `xsltMaxDepth`. Deeper recursion is reported as a
|
|
31
|
+
* potential infinite recursion. Override it with the `maxTemplateDepth`
|
|
32
|
+
* engine option.
|
|
33
|
+
*/
|
|
34
|
+
export const XSLT_MAX_TEMPLATE_DEPTH = 3000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Pending instantiation of the children of a stylesheet element.
|
|
38
|
+
*/
|
|
39
|
+
export class SequenceFrame {
|
|
40
|
+
/**
|
|
41
|
+
* A body declaring variables gets its own copy of the local bindings: a
|
|
42
|
+
* variable is visible to its following siblings and their descendants only.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} engine - The engine
|
|
45
|
+
* @param {Element|object} node - The parent stylesheet element
|
|
46
|
+
* @param {XsltContext} context - The current context
|
|
47
|
+
* @param {Node} output - The result node receiving the output
|
|
48
|
+
* @param {(() => void)|null} [onDone] - Called once the children are done
|
|
49
|
+
*/
|
|
50
|
+
constructor(engine, node, context, output, onDone = null) {
|
|
51
|
+
this.scope = engine.declaresVariables(node) ? context.clone() : context;
|
|
52
|
+
this.saved = this.scope.namespaces;
|
|
53
|
+
this.next = node.firstChild;
|
|
54
|
+
this.output = output;
|
|
55
|
+
// Not named `then`: an object with a `then` method is treated as a promise
|
|
56
|
+
this.onDone = onDone;
|
|
57
|
+
// Whether the frame is a template instantiation (counted in the depth)
|
|
58
|
+
this.template = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Instantiate children until one schedules further frames or none is left.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} engine - The engine
|
|
65
|
+
* @param {object[]} frames - The work stack
|
|
66
|
+
* @returns {boolean} False when every child is done
|
|
67
|
+
*/
|
|
68
|
+
advance(engine, frames) {
|
|
69
|
+
const height = frames.length;
|
|
70
|
+
const scope = this.scope;
|
|
71
|
+
for (let child = this.next; child; child = this.next) {
|
|
72
|
+
this.next = child.nextSibling;
|
|
73
|
+
const type = child.nodeType;
|
|
74
|
+
if (type === 1) {
|
|
75
|
+
// Prefixes resolve against the namespaces in scope on the element
|
|
76
|
+
scope.namespaces = inScopeNamespaces(child);
|
|
77
|
+
const method = engine.instructionMethod(child);
|
|
78
|
+
if (method) engine[method](child, scope, this.output);
|
|
79
|
+
} else if (type === 3 || type === 4) {
|
|
80
|
+
engine.processText(child, scope, this.output);
|
|
81
|
+
}
|
|
82
|
+
if (frames.length !== height) return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Leave the frame, completed or abandoned by an error.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} engine - The engine
|
|
91
|
+
* @returns {void}
|
|
92
|
+
*/
|
|
93
|
+
release(engine) {
|
|
94
|
+
this.scope.namespaces = this.saved;
|
|
95
|
+
if (this.template) engine.templateDepth--;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Complete the frame.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} engine - The engine
|
|
102
|
+
* @returns {void}
|
|
103
|
+
*/
|
|
104
|
+
finish(engine) {
|
|
105
|
+
this.release(engine);
|
|
106
|
+
if (this.onDone) this.onDone();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Pending visits of the items of a list (the nodes of xsl:apply-templates
|
|
112
|
+
* or xsl:for-each), one at a time.
|
|
113
|
+
*/
|
|
114
|
+
export class LoopFrame {
|
|
115
|
+
/**
|
|
116
|
+
* @param {number} count - Number of items
|
|
117
|
+
* @param {(index: number) => void} visit - Instantiates one item; it may
|
|
118
|
+
* schedule frames, which run before the next item
|
|
119
|
+
*/
|
|
120
|
+
constructor(count, visit) {
|
|
121
|
+
this.count = count;
|
|
122
|
+
this.index = 0;
|
|
123
|
+
this.visit = visit;
|
|
124
|
+
this.template = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Visit items until one schedules further frames or none is left.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} _engine - The engine
|
|
131
|
+
* @param {object[]} frames - The work stack
|
|
132
|
+
* @returns {boolean} False when every item is done
|
|
133
|
+
*/
|
|
134
|
+
advance(_engine, frames) {
|
|
135
|
+
const height = frames.length;
|
|
136
|
+
while (this.index < this.count) {
|
|
137
|
+
this.visit(this.index++);
|
|
138
|
+
if (frames.length !== height) return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Leave the frame, completed or abandoned by an error.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} engine - The engine
|
|
147
|
+
* @returns {void}
|
|
148
|
+
*/
|
|
149
|
+
release(engine) {
|
|
150
|
+
if (this.template) engine.templateDepth--;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Complete the frame.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} engine - The engine
|
|
157
|
+
* @returns {void}
|
|
158
|
+
*/
|
|
159
|
+
finish(engine) {
|
|
160
|
+
this.release(engine);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const workStackMethods = {
|
|
165
|
+
/**
|
|
166
|
+
* Run a frame, and the frames it schedules, to completion.
|
|
167
|
+
*
|
|
168
|
+
* The outermost call owns the work stack of the transformation; nested
|
|
169
|
+
* calls (content whose result is needed at once) run on top of it.
|
|
170
|
+
*
|
|
171
|
+
* @param {SequenceFrame|LoopFrame} frame - The frame
|
|
172
|
+
* @returns {void}
|
|
173
|
+
*/
|
|
174
|
+
runFrame(frame) {
|
|
175
|
+
const outermost = this.frames === null;
|
|
176
|
+
if (outermost) {
|
|
177
|
+
this.frames = [];
|
|
178
|
+
this.templateDepth = 0;
|
|
179
|
+
}
|
|
180
|
+
const frames = this.frames;
|
|
181
|
+
const base = frames.length;
|
|
182
|
+
try {
|
|
183
|
+
this.pushFrame(frame);
|
|
184
|
+
while (frames.length > base) {
|
|
185
|
+
const top = frames[frames.length - 1];
|
|
186
|
+
if (!top.advance(this, frames)) {
|
|
187
|
+
frames.pop();
|
|
188
|
+
top.finish(this);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.unwindFrames(base);
|
|
193
|
+
throw error;
|
|
194
|
+
} finally {
|
|
195
|
+
if (outermost) this.frames = null;
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Schedule a frame to run once the current instruction returns, before
|
|
201
|
+
* its next sibling; without a running work stack, run it now.
|
|
202
|
+
*
|
|
203
|
+
* The instruction must not use the output of the frame afterwards.
|
|
204
|
+
*
|
|
205
|
+
* @param {SequenceFrame|LoopFrame} frame - The frame
|
|
206
|
+
* @returns {void}
|
|
207
|
+
*/
|
|
208
|
+
continueWith(frame) {
|
|
209
|
+
if (this.frames === null) this.runFrame(frame);
|
|
210
|
+
else this.pushFrame(frame);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Push a frame on the work stack, counting template instantiations.
|
|
215
|
+
*
|
|
216
|
+
* @param {SequenceFrame|LoopFrame} frame - The frame
|
|
217
|
+
* @returns {void}
|
|
218
|
+
* @throws {Error} When the frame would nest templates deeper than
|
|
219
|
+
* `maxTemplateDepth`
|
|
220
|
+
*/
|
|
221
|
+
pushFrame(frame) {
|
|
222
|
+
if (frame.template) {
|
|
223
|
+
if (this.templateDepth >= this.maxTemplateDepth) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Template recursion too deep: more than ${this.maxTemplateDepth} ` +
|
|
226
|
+
"nested template invocations, a potential infinite recursion " +
|
|
227
|
+
"(raise the maxTemplateDepth option to allow deeper recursion)",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
this.templateDepth++;
|
|
231
|
+
}
|
|
232
|
+
this.frames.push(frame);
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Drop the frames above a height after an error.
|
|
237
|
+
*
|
|
238
|
+
* @param {number} base - The height to return to
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
241
|
+
unwindFrames(base) {
|
|
242
|
+
const frames = this.frames;
|
|
243
|
+
while (frames.length > base) frames.pop().release(this);
|
|
244
|
+
},
|
|
245
|
+
};
|