@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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xsl:number (XSLT 1.0 section 7.7) and the number formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Methods installed on XsltEngine.prototype (`this` is the engine).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { countXsltNumber, isMemoizable } from "../number.js";
|
|
8
|
+
import { formatXsltNumber, toRoman } from "../numberFormat.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The numbers an xsl:number instruction formats: its rounded `value`, else
|
|
12
|
+
* the position of the current node counted by level, count and from.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} engine - The engine
|
|
15
|
+
* @param {Element} node - The xsl:number instruction
|
|
16
|
+
* @param {XsltContext} context - The current context
|
|
17
|
+
* @returns {number[]} The numbers
|
|
18
|
+
*/
|
|
19
|
+
function numberValues(engine, node, context) {
|
|
20
|
+
const value = node.getAttribute("value");
|
|
21
|
+
if (value) {
|
|
22
|
+
const number = Math.round(
|
|
23
|
+
engine.xpathEvaluator.toNumber(engine.evaluateXPath(value, context)),
|
|
24
|
+
);
|
|
25
|
+
// An error that libxslt reports and recovers from by numbering 0
|
|
26
|
+
if (number < 0) {
|
|
27
|
+
engine.warnOnce("xsl:number: negative value, 0 is used");
|
|
28
|
+
}
|
|
29
|
+
return [number];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const count = node.getAttribute("count");
|
|
33
|
+
const from = node.getAttribute("from");
|
|
34
|
+
return countXsltNumber(
|
|
35
|
+
context.currentNode,
|
|
36
|
+
{ level: node.getAttribute("level") || "single", count, from },
|
|
37
|
+
(candidate, pattern) => engine.matchesPattern(candidate, pattern, context),
|
|
38
|
+
isMemoizable(count, from) ? engine.numberMemo(node) : null,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const numberingMethods = {
|
|
43
|
+
/**
|
|
44
|
+
* Instantiate `xsl:number` (XSLT 1.0 section 7.7). The formatting
|
|
45
|
+
* attributes format, grouping-separator and grouping-size are attribute
|
|
46
|
+
* value templates; lang and letter-value have no effect.
|
|
47
|
+
*
|
|
48
|
+
* @param {Element} node - The xsl:number instruction
|
|
49
|
+
* @param {XsltContext} context - The current context
|
|
50
|
+
* @param {Node} output - The result node receiving the number
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
53
|
+
xslNumber(node, context, output) {
|
|
54
|
+
const format = this.optionalAvt(node, "format", context) || "1";
|
|
55
|
+
const grouping = {
|
|
56
|
+
separator: this.optionalAvt(node, "grouping-separator", context),
|
|
57
|
+
size: Number(this.optionalAvt(node, "grouping-size", context)),
|
|
58
|
+
};
|
|
59
|
+
const numbers = numberValues(this, node, context);
|
|
60
|
+
|
|
61
|
+
const text = context.outputDocument.createTextNode(
|
|
62
|
+
formatXsltNumber(numbers, format, grouping),
|
|
63
|
+
);
|
|
64
|
+
output.appendChild(text);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The memo of an xsl:number instruction for the current transformation
|
|
69
|
+
* (see number.js), so numbering a long list stays linear.
|
|
70
|
+
*
|
|
71
|
+
* @param {Element} node - The xsl:number instruction
|
|
72
|
+
* @returns {Map} The instruction's memo
|
|
73
|
+
*/
|
|
74
|
+
numberMemo(node) {
|
|
75
|
+
this.numberMemos ??= new WeakMap();
|
|
76
|
+
let memo = this.numberMemos.get(node);
|
|
77
|
+
if (!memo) {
|
|
78
|
+
memo = new Map();
|
|
79
|
+
this.numberMemos.set(node, memo);
|
|
80
|
+
}
|
|
81
|
+
return memo;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Format a single number with an `xsl:number` format token.
|
|
86
|
+
*
|
|
87
|
+
* @param {number} number - The number to format
|
|
88
|
+
* @param {string} format - The format token, e.g. `1`, `01`, `a`, `I`
|
|
89
|
+
* @returns {string} The formatted number
|
|
90
|
+
*/
|
|
91
|
+
formatNumber(number, format) {
|
|
92
|
+
return formatXsltNumber([number], format);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convert a number to an upper case Roman numeral.
|
|
97
|
+
*
|
|
98
|
+
* @param {number} num - The number to convert
|
|
99
|
+
* @returns {string} The Roman numeral
|
|
100
|
+
*/
|
|
101
|
+
toRoman(num) {
|
|
102
|
+
return toRoman(num);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The xsl:output declaration and the engine's output settings
|
|
3
|
+
* (XSLT 1.0 section 16).
|
|
4
|
+
*
|
|
5
|
+
* Methods installed on XsltEngine.prototype (`this` is the engine).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cdataSectionNames } from "../outputNames.js";
|
|
9
|
+
import { inScopeNamespaces } from "../stylesheetNamespaces.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The xsl:output attributes copied as they are, with their setting names.
|
|
13
|
+
* A later non-empty attribute replaces the setting.
|
|
14
|
+
*/
|
|
15
|
+
const OUTPUT_ATTRIBUTES = Object.freeze([
|
|
16
|
+
["method", "method"],
|
|
17
|
+
["version", "version"],
|
|
18
|
+
["encoding", "encoding"],
|
|
19
|
+
["standalone", "standalone"],
|
|
20
|
+
["indent", "indent"],
|
|
21
|
+
["omit-xml-declaration", "omitXmlDeclaration"],
|
|
22
|
+
["doctype-public", "doctypePublic"],
|
|
23
|
+
["doctype-system", "doctypeSystem"],
|
|
24
|
+
["media-type", "mediaType"],
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The output settings of a stylesheet without xsl:output.
|
|
29
|
+
*
|
|
30
|
+
* A null method means "not declared": the serializer then picks html or xml
|
|
31
|
+
* from the result tree (XSLT 1.0 section 16). An undefined indent is "no"
|
|
32
|
+
* too, except for the line breaks libxslt writes between comments and the
|
|
33
|
+
* document element (see serializer/settings.js).
|
|
34
|
+
*
|
|
35
|
+
* @returns {object} New default output settings
|
|
36
|
+
*/
|
|
37
|
+
export function createOutputSettings() {
|
|
38
|
+
return {
|
|
39
|
+
method: null,
|
|
40
|
+
version: "1.0",
|
|
41
|
+
encoding: "UTF-8",
|
|
42
|
+
standalone: null,
|
|
43
|
+
indent: undefined,
|
|
44
|
+
omitXmlDeclaration: "no",
|
|
45
|
+
doctypePublic: null,
|
|
46
|
+
doctypeSystem: null,
|
|
47
|
+
mediaType: null,
|
|
48
|
+
cdataSectionElements: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const outputDeclarationMethods = {
|
|
53
|
+
/**
|
|
54
|
+
* Merge an xsl:output element into the output settings (XSLT 1.0
|
|
55
|
+
* section 16): a later attribute wins, except cdata-section-elements whose
|
|
56
|
+
* expanded names are united.
|
|
57
|
+
*
|
|
58
|
+
* @param {Element} node - The xsl:output element
|
|
59
|
+
* @returns {void}
|
|
60
|
+
*/
|
|
61
|
+
processOutput(node) {
|
|
62
|
+
for (const [attribute, setting] of OUTPUT_ATTRIBUTES) {
|
|
63
|
+
const value = node.getAttribute(attribute);
|
|
64
|
+
if (value) this.outputSettings[setting] = value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cdataElements = node.getAttribute("cdata-section-elements");
|
|
68
|
+
if (cdataElements) {
|
|
69
|
+
this.outputSettings.cdataSectionElements = cdataSectionNames(
|
|
70
|
+
cdataElements,
|
|
71
|
+
inScopeNamespaces(node),
|
|
72
|
+
this.outputSettings.cdataSectionElements,
|
|
73
|
+
(message) => this.warnOnce(message),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instantiation of sequence constructors: the dispatch of each child of a
|
|
3
|
+
* template body to its instruction, stylesheet text, unknown XSLT elements,
|
|
4
|
+
* xsl:fallback and extension elements.
|
|
5
|
+
*
|
|
6
|
+
* Methods installed on XsltEngine.prototype (`this` is the engine).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isTextContinuation } from "../../xpath/axes.js";
|
|
10
|
+
import { isXmlWhitespace } from "../whitespace.js";
|
|
11
|
+
import { isExtensionElement } from "../stylesheetNamespaces.js";
|
|
12
|
+
import { xsltLocalName } from "../stylesheetChecks.js";
|
|
13
|
+
import { fallbackChildren } from "../forwardsCompatible.js";
|
|
14
|
+
import { SequenceFrame } from "./workStack.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Engine method instantiating each XSLT element that may occur in a sequence
|
|
18
|
+
* constructor; null marks elements that produce nothing there.
|
|
19
|
+
*/
|
|
20
|
+
const INSTRUCTION_METHODS = Object.freeze({
|
|
21
|
+
"apply-templates": "xslApplyTemplates",
|
|
22
|
+
"apply-imports": "xslApplyImports",
|
|
23
|
+
"call-template": "xslCallTemplate",
|
|
24
|
+
"value-of": "xslValueOf",
|
|
25
|
+
text: "xslText",
|
|
26
|
+
element: "xslElement",
|
|
27
|
+
attribute: "xslAttribute",
|
|
28
|
+
if: "xslIf",
|
|
29
|
+
choose: "xslChoose",
|
|
30
|
+
"for-each": "xslForEach",
|
|
31
|
+
copy: "xslCopy",
|
|
32
|
+
"copy-of": "xslCopyOf",
|
|
33
|
+
variable: "xslVariable",
|
|
34
|
+
comment: "xslComment",
|
|
35
|
+
"processing-instruction": "xslProcessingInstruction",
|
|
36
|
+
number: "xslNumber",
|
|
37
|
+
message: "xslMessage",
|
|
38
|
+
// Handled by their parent instruction, or at template start
|
|
39
|
+
param: null,
|
|
40
|
+
sort: null,
|
|
41
|
+
"with-param": null,
|
|
42
|
+
// Used for forward compatibility
|
|
43
|
+
fallback: null,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const sequenceConstructorMethods = {
|
|
47
|
+
/**
|
|
48
|
+
* Instantiate the children of a stylesheet element (a sequence
|
|
49
|
+
* constructor) now, for instructions that use the result at once.
|
|
50
|
+
*
|
|
51
|
+
* @param {Element|object} node - The parent stylesheet element
|
|
52
|
+
* @param {XsltContext} context - The current context
|
|
53
|
+
* @param {Node} output - The result node receiving the output
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
processChildren(node, context, output) {
|
|
57
|
+
this.runFrame(new SequenceFrame(this, node, context, output));
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Instantiate the children of a stylesheet element once the current
|
|
62
|
+
* instruction returns (see workStack.js), keeping the JavaScript stack flat
|
|
63
|
+
* in deep recursion. For content that comes last in an instruction.
|
|
64
|
+
*
|
|
65
|
+
* @param {Element|object} node - The parent stylesheet element
|
|
66
|
+
* @param {XsltContext} context - The current context
|
|
67
|
+
* @param {Node} output - The result node receiving the output
|
|
68
|
+
* @param {(() => void)|null} [then] - Called once the children are done
|
|
69
|
+
* @returns {void}
|
|
70
|
+
*/
|
|
71
|
+
scheduleChildren(node, context, output, then = null) {
|
|
72
|
+
this.continueWith(new SequenceFrame(this, node, context, output, then));
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether an element has an xsl:variable child. Cached per element.
|
|
77
|
+
*
|
|
78
|
+
* @param {Element|object} node - A stylesheet element
|
|
79
|
+
* @returns {boolean} True when a child declares a variable
|
|
80
|
+
*/
|
|
81
|
+
declaresVariables(node) {
|
|
82
|
+
this.variableDeclarations ??= new WeakMap();
|
|
83
|
+
let declares = this.variableDeclarations.get(node);
|
|
84
|
+
if (declares === undefined) {
|
|
85
|
+
declares = false;
|
|
86
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
87
|
+
if (this.isXsltElement(child, "variable")) declares = true;
|
|
88
|
+
}
|
|
89
|
+
this.variableDeclarations.set(node, declares);
|
|
90
|
+
}
|
|
91
|
+
return declares;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Instantiate a stylesheet text node. Adjacent text and CDATA nodes form
|
|
96
|
+
* one text node, which is dropped when it only holds XML whitespace (unless
|
|
97
|
+
* xml:space="preserve" is in scope, XSLT 1.0 section 3.4).
|
|
98
|
+
*
|
|
99
|
+
* @param {Text} node - A text or CDATA node of the stylesheet
|
|
100
|
+
* @param {XsltContext} context - The current context
|
|
101
|
+
* @param {Node} output - The result node receiving the output
|
|
102
|
+
* @returns {void}
|
|
103
|
+
*/
|
|
104
|
+
processText(node, context, output) {
|
|
105
|
+
if (isTextContinuation(node)) return;
|
|
106
|
+
const text = this.xpathEvaluator.getStringValue(node);
|
|
107
|
+
if (text && (!isXmlWhitespace(text) || this.shouldPreserveSpace(node))) {
|
|
108
|
+
output.appendChild(context.outputDocument.createTextNode(text));
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether xml:space="preserve" is in scope on a stylesheet text node.
|
|
114
|
+
*
|
|
115
|
+
* @param {Node} node - The text node
|
|
116
|
+
* @returns {boolean} True when the nearest xml:space says preserve
|
|
117
|
+
*/
|
|
118
|
+
shouldPreserveSpace(node) {
|
|
119
|
+
let current = node.parentNode;
|
|
120
|
+
while (current && current.nodeType === 1) {
|
|
121
|
+
const space = current.getAttribute("xml:space");
|
|
122
|
+
if (space === "preserve") return true;
|
|
123
|
+
if (space === "default") return false;
|
|
124
|
+
current = current.parentNode;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Name of the engine method instantiating a stylesheet element: the
|
|
131
|
+
* handler of an XSLT instruction, processLiteralResultElement, or
|
|
132
|
+
* instantiateUnknown for an XSLT element the engine does not implement.
|
|
133
|
+
* Returns null for elements that are not instructions (xsl:param,
|
|
134
|
+
* xsl:sort, xsl:with-param, xsl:fallback).
|
|
135
|
+
*
|
|
136
|
+
* @param {Element} node - A stylesheet element in a sequence constructor
|
|
137
|
+
* @returns {string|null} The method name
|
|
138
|
+
*/
|
|
139
|
+
instructionMethod(node) {
|
|
140
|
+
if (!this.isXsltNamespace(node)) {
|
|
141
|
+
return isExtensionElement(node)
|
|
142
|
+
? "instantiateExtension"
|
|
143
|
+
: "processLiteralResultElement";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
|
|
147
|
+
if (Object.hasOwn(INSTRUCTION_METHODS, localName)) {
|
|
148
|
+
return INSTRUCTION_METHODS[localName];
|
|
149
|
+
}
|
|
150
|
+
return "instantiateUnknown";
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Instantiate an XSLT element the engine does not implement (XSLT 1.0
|
|
155
|
+
* section 15): its xsl:fallback children are instantiated in order; without
|
|
156
|
+
* any, the error is reported and nothing is produced.
|
|
157
|
+
*
|
|
158
|
+
* @param {Element} node - The unknown XSLT element
|
|
159
|
+
* @param {XsltContext} context - The current context
|
|
160
|
+
* @param {Node} output - The result node receiving the output
|
|
161
|
+
* @returns {void}
|
|
162
|
+
*/
|
|
163
|
+
instantiateUnknown(node, context, output) {
|
|
164
|
+
if (!this.instantiateFallbacks(node, context, output)) {
|
|
165
|
+
console.warn(`Unknown XSLT element: ${xsltLocalName(node)}`);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Instantiate the xsl:fallback children of an element in order.
|
|
171
|
+
*
|
|
172
|
+
* @param {Element} node - An unknown XSLT element or extension element
|
|
173
|
+
* @param {XsltContext} context - The current context
|
|
174
|
+
* @param {Node} output - The result node receiving the output
|
|
175
|
+
* @returns {boolean} Whether the element had any xsl:fallback child
|
|
176
|
+
*/
|
|
177
|
+
instantiateFallbacks(node, context, output) {
|
|
178
|
+
const fallbacks = fallbackChildren(node);
|
|
179
|
+
for (const fallback of fallbacks) {
|
|
180
|
+
this.processChildren(fallback, context, output);
|
|
181
|
+
}
|
|
182
|
+
return fallbacks.length > 0;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Register the implementation of an extension element (XSLT 1.0 section
|
|
187
|
+
* 14.1). It is called as `handler(node, context, output, engine)` where
|
|
188
|
+
* an element of that name is instantiated in a namespace declared with
|
|
189
|
+
* `extension-element-prefixes`.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} namespaceUri - The extension namespace
|
|
192
|
+
* @param {string} localName - The element's local name
|
|
193
|
+
* @param {(node: Element, context: XsltContext, output: Node, engine: XsltEngine) => void} handler - The implementation
|
|
194
|
+
* @returns {XsltEngine} This engine, to allow chaining
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* engine.registerExtensionElement("urn:my", "log", (node) => console.log(node.textContent));
|
|
198
|
+
*/
|
|
199
|
+
registerExtensionElement(namespaceUri, localName, handler) {
|
|
200
|
+
this.extensionElements.set(`{${namespaceUri}}${localName}`, handler);
|
|
201
|
+
return this;
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Instantiate an extension element: its registered implementation, else
|
|
206
|
+
* its xsl:fallback children (XSLT 1.0 sections 14.1 and 15); without
|
|
207
|
+
* either, the error is reported once and nothing is produced, as libxslt
|
|
208
|
+
* does. An extension element is never copied to the result.
|
|
209
|
+
*
|
|
210
|
+
* @param {Element} node - The extension element
|
|
211
|
+
* @param {XsltContext} context - The current context
|
|
212
|
+
* @param {Node} output - The result node receiving the output
|
|
213
|
+
* @returns {void}
|
|
214
|
+
*/
|
|
215
|
+
instantiateExtension(node, context, output) {
|
|
216
|
+
const name = `{${node.namespaceURI}}${node.localName}`;
|
|
217
|
+
const handler = this.extensionElements.get(name);
|
|
218
|
+
if (handler) {
|
|
219
|
+
handler(node, context, output, this);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!this.instantiateFallbacks(node, context, output)) {
|
|
223
|
+
this.warnOnce(
|
|
224
|
+
`extension element ${node.nodeName} (${name}) is not supported and has no xsl:fallback`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stylesheet loading: the main stylesheet and the xsl:import and
|
|
3
|
+
* xsl:include modules (the top-level elements are in topLevel.js).
|
|
4
|
+
*
|
|
5
|
+
* Methods installed on XsltEngine.prototype (`this` is the engine).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolveUri } from "../uri.js";
|
|
9
|
+
import { parseXml, resolveDomParser } from "../domParsing.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether an element is an xsl:stylesheet or xsl:transform element.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} engine - The engine
|
|
15
|
+
* @param {Element} root - The document element of a stylesheet module
|
|
16
|
+
* @returns {boolean} True for a stylesheet element
|
|
17
|
+
*/
|
|
18
|
+
function isStylesheetElement(engine, root) {
|
|
19
|
+
return (
|
|
20
|
+
engine.isXsltElement(root, "stylesheet") ||
|
|
21
|
+
engine.isXsltElement(root, "transform")
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const stylesheetLoadingMethods = {
|
|
26
|
+
/**
|
|
27
|
+
* Set the loader used by xsl:import and xsl:include.
|
|
28
|
+
*
|
|
29
|
+
* @param {((href: string, baseUri?: string) => (Document|string))|null} loader - The loader, or null to remove it
|
|
30
|
+
* @returns {XsltEngine} This engine, to allow chaining
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* engine.setStylesheetLoader((href) => readFileSync(href, 'utf8'));
|
|
34
|
+
*/
|
|
35
|
+
setStylesheetLoader(loader) {
|
|
36
|
+
this.stylesheetLoader = loader ?? null;
|
|
37
|
+
return this;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a relative URI against a base URI
|
|
42
|
+
*
|
|
43
|
+
* @param {string} href - The URI to resolve
|
|
44
|
+
* @param {string} [baseUri] - The base URI
|
|
45
|
+
* @returns {string} The resolved URI
|
|
46
|
+
*/
|
|
47
|
+
resolveUri(href, baseUri) {
|
|
48
|
+
return resolveUri(href, baseUri);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load an external stylesheet document with the stylesheet loader.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} href - The referenced URI
|
|
55
|
+
* @param {string} [baseUri] - Base URI of the referencing stylesheet
|
|
56
|
+
* @returns {{document: (Document|string), uri: string}} What the loader
|
|
57
|
+
* returned (a string is parsed by the caller) and the resolved URI
|
|
58
|
+
* @throws {Error} When no stylesheet loader is configured
|
|
59
|
+
*/
|
|
60
|
+
loadStylesheet(href, baseUri) {
|
|
61
|
+
if (!this.stylesheetLoader) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Cannot load stylesheet "${href}": no stylesheetLoader configured. ` +
|
|
64
|
+
"Use engine.setStylesheetLoader(fn) to provide a loader function.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolvedUri = this.resolveUri(href, baseUri);
|
|
69
|
+
const result = this.stylesheetLoader(resolvedUri, baseUri);
|
|
70
|
+
return { document: result, uri: resolvedUri };
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse an XML string returned by a stylesheet or document loader, with
|
|
75
|
+
* the `domParser` option, else the global DOMParser, else the DOMParser of
|
|
76
|
+
* the stylesheet's window (see domParsing.js).
|
|
77
|
+
*
|
|
78
|
+
* @param {string} xmlString - The markup
|
|
79
|
+
* @returns {Document} The parsed document
|
|
80
|
+
* @throws {Error} When no parser is available or the markup is malformed
|
|
81
|
+
*/
|
|
82
|
+
parseXmlString(xmlString) {
|
|
83
|
+
return parseXml(
|
|
84
|
+
xmlString,
|
|
85
|
+
resolveDomParser(this.domParser, this.stylesheetDoc),
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Import and compile an XSLT stylesheet
|
|
91
|
+
* @param {Document|Element} stylesheetNode - The stylesheet document or root element
|
|
92
|
+
* @param {string} [stylesheetUri] - Optional URI of the stylesheet for resolving imports
|
|
93
|
+
*/
|
|
94
|
+
importStylesheet(stylesheetNode, stylesheetUri) {
|
|
95
|
+
const isMainStylesheet = this.stylesheetDoc === null;
|
|
96
|
+
|
|
97
|
+
if (isMainStylesheet) {
|
|
98
|
+
this.stylesheetDoc = stylesheetNode.ownerDocument || stylesheetNode;
|
|
99
|
+
if (stylesheetUri) this.baseUri = stylesheetUri;
|
|
100
|
+
if (this.baseUri) this.stylesheetStack.push(this.baseUri);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const root = stylesheetNode.documentElement || stylesheetNode;
|
|
104
|
+
|
|
105
|
+
if (!isStylesheetElement(this, root)) {
|
|
106
|
+
// A literal result element as document element: simplified stylesheet
|
|
107
|
+
if (root.getAttribute && root.getAttribute("xsl:version")) {
|
|
108
|
+
this.processLiteralResultStylesheet(root);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(
|
|
112
|
+
"Invalid XSLT stylesheet: root element must be xsl:stylesheet or xsl:transform",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.processTopLevelElements(root, stylesheetUri || this.baseUri);
|
|
117
|
+
|
|
118
|
+
// Increment import precedence after processing this stylesheet
|
|
119
|
+
if (isMainStylesheet) {
|
|
120
|
+
this.currentImportPrecedence++;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Process an xsl:include element: the included stylesheet is merged at the
|
|
126
|
+
* import precedence of the including stylesheet.
|
|
127
|
+
*
|
|
128
|
+
* @param {Element} node - The xsl:include element
|
|
129
|
+
* @param {string} baseUri - URI of the including stylesheet
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
processInclude(node, baseUri) {
|
|
133
|
+
const savedPrecedence = this.currentImportPrecedence;
|
|
134
|
+
this.loadStylesheetModule(node, baseUri, "include");
|
|
135
|
+
this.currentImportPrecedence = savedPrecedence;
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Process an xsl:import element: the imported stylesheet gets a lower import
|
|
140
|
+
* precedence than everything processed after it.
|
|
141
|
+
*
|
|
142
|
+
* @param {Element} node - The xsl:import element
|
|
143
|
+
* @param {string} baseUri - URI of the importing stylesheet
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
146
|
+
processImport(node, baseUri) {
|
|
147
|
+
this.loadStylesheetModule(node, baseUri, "import");
|
|
148
|
+
this.currentImportPrecedence++;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Load and process the stylesheet referenced by xsl:import or xsl:include.
|
|
153
|
+
*
|
|
154
|
+
* Only a stylesheet that (directly or indirectly) references itself is an
|
|
155
|
+
* error; the same stylesheet may be reached through several branches of the
|
|
156
|
+
* import tree ("diamond" imports), as in libxslt.
|
|
157
|
+
*
|
|
158
|
+
* @param {Element} node - The xsl:import or xsl:include element
|
|
159
|
+
* @param {string} baseUri - URI of the referencing stylesheet
|
|
160
|
+
* @param {"import"|"include"} kind - The referencing instruction
|
|
161
|
+
* @returns {void}
|
|
162
|
+
* @throws {Error} When href is missing, loading fails or a cycle is found
|
|
163
|
+
*/
|
|
164
|
+
loadStylesheetModule(node, baseUri, kind) {
|
|
165
|
+
const href = node.getAttribute("href");
|
|
166
|
+
if (!href) {
|
|
167
|
+
throw new Error(`xsl:${kind} requires an href attribute`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolvedUri = this.resolveUri(href, baseUri);
|
|
171
|
+
if (this.stylesheetStack.includes(resolvedUri)) {
|
|
172
|
+
throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.stylesheetStack.push(resolvedUri);
|
|
176
|
+
try {
|
|
177
|
+
const { document: loaded } = this.loadStylesheet(href, baseUri);
|
|
178
|
+
const doc =
|
|
179
|
+
typeof loaded === "string" ? this.parseXmlString(loaded) : loaded;
|
|
180
|
+
this.processIncludedStylesheet(doc, resolvedUri);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Failed to ${kind} stylesheet "${href}": ${error.message}`,
|
|
184
|
+
{ cause: error },
|
|
185
|
+
);
|
|
186
|
+
} finally {
|
|
187
|
+
this.stylesheetStack.pop();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Process an included/imported stylesheet document.
|
|
193
|
+
*
|
|
194
|
+
* @param {Document|Element} stylesheetDoc - The loaded stylesheet module
|
|
195
|
+
* @param {string} stylesheetUri - Its resolved URI
|
|
196
|
+
* @returns {void}
|
|
197
|
+
* @throws {Error} When the document is not an XSLT stylesheet
|
|
198
|
+
*/
|
|
199
|
+
processIncludedStylesheet(stylesheetDoc, stylesheetUri) {
|
|
200
|
+
const root = stylesheetDoc.documentElement || stylesheetDoc;
|
|
201
|
+
if (!isStylesheetElement(this, root)) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"Included/imported document is not a valid XSLT stylesheet",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
this.processTopLevelElements(root, stylesheetUri);
|
|
207
|
+
},
|
|
208
|
+
};
|