@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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSLT 1.0 pattern matching (section 5.2).
|
|
3
|
+
*
|
|
4
|
+
* A pattern is a union of location path patterns. Each alternative is an
|
|
5
|
+
* absolute or relative location path that only uses the child and attribute
|
|
6
|
+
* axes and the `/` and `//` separators, optionally anchored on an `id()` or
|
|
7
|
+
* `key()` call. Patterns are parsed once with the XPath parser and the
|
|
8
|
+
* compiled form is cached per pattern string.
|
|
9
|
+
*
|
|
10
|
+
* Matching runs right to left: the node is tested against the last step, then
|
|
11
|
+
* its parent (for `/`) or any ancestor (for `//`) against the previous step.
|
|
12
|
+
* This costs O(depth) per node instead of evaluating the pattern as an
|
|
13
|
+
* expression from the parent, which is O(siblings) and made
|
|
14
|
+
* `xsl:apply-templates` quadratic in the number of children.
|
|
15
|
+
*
|
|
16
|
+
* Predicates follow the "position in the context of the parent" rule: a
|
|
17
|
+
* predicate is evaluated with the node's position among its siblings that
|
|
18
|
+
* pass the step's node test and the preceding predicates. That sibling list is
|
|
19
|
+
* only built when the predicate uses `position()`/`last()` or evaluates to a
|
|
20
|
+
* number, and it is cached per parent until {@link PatternMatcher#reset}.
|
|
21
|
+
*
|
|
22
|
+
* @module xslt/patterns
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { childAxis } from "../xpath/axes.js";
|
|
26
|
+
import { ROOT, compilePattern } from "./patternCompiler.js";
|
|
27
|
+
import {
|
|
28
|
+
MatchScope,
|
|
29
|
+
isNamespaceDeclaration,
|
|
30
|
+
isRoot,
|
|
31
|
+
parentOf,
|
|
32
|
+
rootOf,
|
|
33
|
+
} from "./matchScope.js";
|
|
34
|
+
|
|
35
|
+
export { compilePattern };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Matches nodes against XSLT patterns with an XPath evaluator.
|
|
39
|
+
*/
|
|
40
|
+
export class PatternMatcher {
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('../xpath/evaluator.js').XPathEvaluator} evaluator - Evaluator used for node tests, predicates and anchors
|
|
43
|
+
*/
|
|
44
|
+
constructor(evaluator) {
|
|
45
|
+
this.evaluator = evaluator;
|
|
46
|
+
this.compiled = new Map();
|
|
47
|
+
this.positions = new WeakMap();
|
|
48
|
+
this.anchors = new WeakMap();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Forget cached sibling positions and anchor node-sets, e.g. before a new
|
|
53
|
+
* transformation (the source tree may have been modified in between).
|
|
54
|
+
*
|
|
55
|
+
* @returns {void}
|
|
56
|
+
*/
|
|
57
|
+
reset() {
|
|
58
|
+
this.positions = new WeakMap();
|
|
59
|
+
this.anchors = new WeakMap();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compile a pattern, reusing the cached form.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} pattern - The XSLT pattern
|
|
66
|
+
* @returns {object[]} The compiled alternatives
|
|
67
|
+
* @throws {Error} When the string is not a valid pattern (XSLT 1.0
|
|
68
|
+
* section 5.2); the message names the pattern
|
|
69
|
+
*/
|
|
70
|
+
compile(pattern) {
|
|
71
|
+
let compiled = this.compiled.get(pattern);
|
|
72
|
+
if (!compiled) {
|
|
73
|
+
try {
|
|
74
|
+
compiled = compilePattern(pattern);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`Invalid pattern "${pattern}": ${error.message}`, {
|
|
77
|
+
cause: error,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
this.compiled.set(pattern, compiled);
|
|
81
|
+
}
|
|
82
|
+
return compiled;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Test whether a node matches a pattern.
|
|
87
|
+
*
|
|
88
|
+
* @param {Node} node - The candidate node
|
|
89
|
+
* @param {string} pattern - The XSLT pattern
|
|
90
|
+
* @param {object|null} [host] - XSLT context supplying variables and namespaces
|
|
91
|
+
* @param {Object<string, string>} [namespaces] - Prefix bindings overriding the host's
|
|
92
|
+
* @returns {boolean} True when the node matches any alternative
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* matcher.matches(titleElement, 'chapter/title', xsltContext); // true
|
|
96
|
+
*/
|
|
97
|
+
matches(node, pattern, host = null, namespaces = undefined) {
|
|
98
|
+
const scope = new MatchScope(host, namespaces);
|
|
99
|
+
for (const alternative of this.compile(pattern)) {
|
|
100
|
+
if (this.matchesAlternative(node, alternative, scope)) return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {Node} node - Candidate node
|
|
107
|
+
* @param {object} alternative - Compiled alternative
|
|
108
|
+
* @param {MatchScope} scope - Match state
|
|
109
|
+
* @returns {boolean} Whether the node matches the alternative
|
|
110
|
+
*/
|
|
111
|
+
matchesAlternative(node, alternative, scope) {
|
|
112
|
+
const { anchor, steps } = alternative;
|
|
113
|
+
if (steps.length > 0) {
|
|
114
|
+
return this.matchesStep(node, alternative, steps.length - 1, scope);
|
|
115
|
+
}
|
|
116
|
+
if (anchor === ROOT) return isRoot(node);
|
|
117
|
+
return this.anchorNodes(anchor, node, scope).has(node);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Match a node against step `index`, then the rest of the path leftwards.
|
|
122
|
+
*
|
|
123
|
+
* @param {Node} node - Candidate node
|
|
124
|
+
* @param {object} alternative - Compiled alternative
|
|
125
|
+
* @param {number} index - Index of the step to test
|
|
126
|
+
* @param {MatchScope} scope - Match state
|
|
127
|
+
* @returns {boolean} Whether the node matches steps 0..index
|
|
128
|
+
*/
|
|
129
|
+
matchesStep(node, alternative, index, scope) {
|
|
130
|
+
const step = alternative.steps[index];
|
|
131
|
+
if (!this.testStep(node, step, scope)) return false;
|
|
132
|
+
|
|
133
|
+
const parent = parentOf(node);
|
|
134
|
+
if (step.separator === null) return true;
|
|
135
|
+
|
|
136
|
+
let accepts;
|
|
137
|
+
if (index > 0) {
|
|
138
|
+
accepts = (candidate) =>
|
|
139
|
+
this.matchesStep(candidate, alternative, index - 1, scope);
|
|
140
|
+
} else if (alternative.anchor === ROOT) {
|
|
141
|
+
accepts = isRoot;
|
|
142
|
+
} else {
|
|
143
|
+
const anchors = this.anchorNodes(alternative.anchor, node, scope);
|
|
144
|
+
accepts = (candidate) => anchors.has(candidate);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (step.separator === "/") return parent !== null && accepts(parent);
|
|
148
|
+
for (let ancestor = parent; ancestor; ancestor = parentOf(ancestor)) {
|
|
149
|
+
if (accepts(ancestor)) return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Test a node against the axis, node test and predicates of one step.
|
|
156
|
+
*
|
|
157
|
+
* @param {Node} node - Candidate node
|
|
158
|
+
* @param {object} step - Compiled step
|
|
159
|
+
* @param {MatchScope} scope - Match state
|
|
160
|
+
* @returns {boolean} Whether the node satisfies the step
|
|
161
|
+
*/
|
|
162
|
+
testStep(node, step, scope) {
|
|
163
|
+
const type = node.nodeType;
|
|
164
|
+
if (step.axis === "attribute") {
|
|
165
|
+
if (type !== 2 || isNamespaceDeclaration(node)) return false;
|
|
166
|
+
} else if (type === 2 || type === 9 || type === 11 || type === 13) {
|
|
167
|
+
// Patterns only use the child and attribute axes: roots, attributes
|
|
168
|
+
// and namespace nodes are never children
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (
|
|
172
|
+
!this.evaluator.matchNodeTest(
|
|
173
|
+
step.nodeTest,
|
|
174
|
+
node,
|
|
175
|
+
scope.testContext(node),
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (let k = 0; k < step.predicates.length; k++) {
|
|
182
|
+
if (!this.testPredicate(node, step, k, scope)) return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Evaluate predicate `k` of a step for a node.
|
|
189
|
+
*
|
|
190
|
+
* @param {Node} node - Candidate node
|
|
191
|
+
* @param {object} step - Compiled step
|
|
192
|
+
* @param {number} k - Predicate index
|
|
193
|
+
* @param {MatchScope} scope - Match state
|
|
194
|
+
* @returns {boolean} Whether the predicate holds
|
|
195
|
+
*/
|
|
196
|
+
testPredicate(node, step, k, scope) {
|
|
197
|
+
const predicate = step.predicates[k];
|
|
198
|
+
let position = 1;
|
|
199
|
+
let size = 1;
|
|
200
|
+
|
|
201
|
+
if (predicate.positional) {
|
|
202
|
+
({ position, size } = this.siblingPosition(node, step, k, scope));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const value = this.evaluator.evaluate(
|
|
206
|
+
predicate.expr,
|
|
207
|
+
scope.context(node, position, size),
|
|
208
|
+
);
|
|
209
|
+
if (typeof value !== "number") return this.evaluator.toBoolean(value);
|
|
210
|
+
if (!predicate.positional) {
|
|
211
|
+
position = this.siblingPosition(node, step, k, scope).position;
|
|
212
|
+
}
|
|
213
|
+
return value === position;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Position and size of a node among its siblings that pass the step's node
|
|
218
|
+
* test and its predicates before `k`. Cached per parent and predicate.
|
|
219
|
+
*
|
|
220
|
+
* @param {Node} node - Candidate node
|
|
221
|
+
* @param {object} step - Compiled step
|
|
222
|
+
* @param {number} k - Predicate index
|
|
223
|
+
* @param {MatchScope} scope - Match state
|
|
224
|
+
* @returns {{position: number, size: number}} Context position and size
|
|
225
|
+
*/
|
|
226
|
+
siblingPosition(node, step, k, scope) {
|
|
227
|
+
const parent = parentOf(node);
|
|
228
|
+
if (!parent) return { position: 1, size: 1 };
|
|
229
|
+
|
|
230
|
+
let byPredicate = this.positions.get(parent);
|
|
231
|
+
if (!byPredicate) {
|
|
232
|
+
byPredicate = new Map();
|
|
233
|
+
this.positions.set(parent, byPredicate);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const predicate = step.predicates[k];
|
|
237
|
+
let entry = byPredicate.get(predicate);
|
|
238
|
+
if (!entry) {
|
|
239
|
+
const nodes = this.candidateSiblings(parent, step, k, scope);
|
|
240
|
+
const index = new Map();
|
|
241
|
+
nodes.forEach((sibling, i) => index.set(sibling, i + 1));
|
|
242
|
+
entry = { index, size: nodes.length };
|
|
243
|
+
byPredicate.set(predicate, entry);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { position: entry.index.get(node) ?? 0, size: entry.size };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Siblings selected by a step before predicate `k` is applied.
|
|
251
|
+
*
|
|
252
|
+
* @param {Node} parent - The common parent
|
|
253
|
+
* @param {object} step - Compiled step
|
|
254
|
+
* @param {number} k - Predicate index
|
|
255
|
+
* @param {MatchScope} scope - Match state
|
|
256
|
+
* @returns {Node[]} The candidate siblings in document order
|
|
257
|
+
*/
|
|
258
|
+
candidateSiblings(parent, step, k, scope) {
|
|
259
|
+
const all =
|
|
260
|
+
step.axis === "attribute"
|
|
261
|
+
? Array.from(parent.attributes || [])
|
|
262
|
+
: childAxis(parent);
|
|
263
|
+
let nodes = all.filter(
|
|
264
|
+
(sibling) =>
|
|
265
|
+
(sibling.nodeType === 2) === (step.axis === "attribute") &&
|
|
266
|
+
!(sibling.nodeType === 2 && isNamespaceDeclaration(sibling)) &&
|
|
267
|
+
this.evaluator.matchNodeTest(
|
|
268
|
+
step.nodeTest,
|
|
269
|
+
sibling,
|
|
270
|
+
scope.testContext(sibling),
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
const base = scope.context(parent, 1, 1);
|
|
274
|
+
for (let i = 0; i < k; i++) {
|
|
275
|
+
nodes = this.evaluator.filterByPredicate(nodes, step.predicates[i], base);
|
|
276
|
+
}
|
|
277
|
+
return nodes;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The nodes selected by the `id()`/`key()` anchor of a pattern relative to
|
|
282
|
+
* a node. Anchors only have literal arguments (see patternCompiler.js), so
|
|
283
|
+
* the set only depends on the node's tree and the prefixes in scope: it is
|
|
284
|
+
* computed once per root until {@link PatternMatcher#reset}, instead of
|
|
285
|
+
* once per candidate node.
|
|
286
|
+
*
|
|
287
|
+
* @param {object} anchor - Function call AST
|
|
288
|
+
* @param {Node} node - Node providing the document
|
|
289
|
+
* @param {MatchScope} scope - Match state
|
|
290
|
+
* @returns {Set<Node>} The anchor nodes
|
|
291
|
+
*/
|
|
292
|
+
anchorNodes(anchor, node, scope) {
|
|
293
|
+
const root = rootOf(node);
|
|
294
|
+
let byAnchor = this.anchors.get(root);
|
|
295
|
+
if (!byAnchor) {
|
|
296
|
+
byAnchor = new Map();
|
|
297
|
+
this.anchors.set(root, byAnchor);
|
|
298
|
+
}
|
|
299
|
+
let byScope = byAnchor.get(anchor);
|
|
300
|
+
if (!byScope) {
|
|
301
|
+
byScope = new Map();
|
|
302
|
+
byAnchor.set(anchor, byScope);
|
|
303
|
+
}
|
|
304
|
+
let nodes = byScope.get(scope.namespaces);
|
|
305
|
+
if (!nodes) {
|
|
306
|
+
nodes = this.evaluateAnchor(anchor, node, scope);
|
|
307
|
+
byScope.set(scope.namespaces, nodes);
|
|
308
|
+
}
|
|
309
|
+
return nodes;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Evaluate the `id()`/`key()` anchor of a pattern relative to a node.
|
|
314
|
+
*
|
|
315
|
+
* @param {object} anchor - Function call AST
|
|
316
|
+
* @param {Node} node - Node providing the document
|
|
317
|
+
* @param {MatchScope} scope - Match state
|
|
318
|
+
* @returns {Set<Node>} The anchor nodes
|
|
319
|
+
*/
|
|
320
|
+
evaluateAnchor(anchor, node, scope) {
|
|
321
|
+
const result = this.evaluator.evaluate(anchor, scope.context(node, 1, 1));
|
|
322
|
+
return new Set(Array.isArray(result) ? result : []);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML names (XML 1.0 fifth edition, section 2.3) and qualified names
|
|
3
|
+
* (Namespaces in XML 1.0, section 4).
|
|
4
|
+
*
|
|
5
|
+
* Used to validate names computed at run time by `xsl:element` and
|
|
6
|
+
* `xsl:attribute` (XSLT 1.0 sections 7.1.2 and 7.1.3) before they reach the
|
|
7
|
+
* DOM, so an invalid name is reported instead of producing malformed output.
|
|
8
|
+
*
|
|
9
|
+
* @module xslt/qname
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
/** NameStartChar code point ranges, without ":" (production [4]). */
|
|
15
|
+
const NAME_START_RANGES = [
|
|
16
|
+
[0x41, 0x5a], // A-Z
|
|
17
|
+
[0x5f, 0x5f], // _
|
|
18
|
+
[0x61, 0x7a], // a-z
|
|
19
|
+
[0xc0, 0xd6],
|
|
20
|
+
[0xd8, 0xf6],
|
|
21
|
+
[0xf8, 0x2ff],
|
|
22
|
+
[0x370, 0x37d],
|
|
23
|
+
[0x37f, 0x1fff],
|
|
24
|
+
[0x200c, 0x200d],
|
|
25
|
+
[0x2070, 0x218f],
|
|
26
|
+
[0x2c00, 0x2fef],
|
|
27
|
+
[0x3001, 0xd7ff],
|
|
28
|
+
[0xf900, 0xfdcf],
|
|
29
|
+
[0xfdf0, 0xfffd],
|
|
30
|
+
[0x10000, 0xeffff],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Extra NameChar code point ranges (production [4a]). */
|
|
34
|
+
const NAME_CHAR_RANGES = [
|
|
35
|
+
[0x2d, 0x2e], // - .
|
|
36
|
+
[0x30, 0x39], // 0-9
|
|
37
|
+
[0xb7, 0xb7],
|
|
38
|
+
[0x300, 0x36f],
|
|
39
|
+
[0x203f, 0x2040],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a code point lies in one of the ranges.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} code - A Unicode code point
|
|
46
|
+
* @param {number[][]} ranges - Inclusive `[first, last]` ranges
|
|
47
|
+
* @returns {boolean} True when the code point is in a range
|
|
48
|
+
*/
|
|
49
|
+
function inRanges(code, ranges) {
|
|
50
|
+
return ranges.some(([first, last]) => code >= first && code <= last);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether a string is an NCName (a name without a colon).
|
|
55
|
+
*
|
|
56
|
+
* @param {string} name - The candidate name
|
|
57
|
+
* @returns {boolean} True for a valid NCName
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* isNcName("item-1"); // true
|
|
61
|
+
* isNcName("1item"); // false
|
|
62
|
+
*/
|
|
63
|
+
export function isNcName(name) {
|
|
64
|
+
let first = true;
|
|
65
|
+
for (const char of name) {
|
|
66
|
+
const code = char.codePointAt(0);
|
|
67
|
+
const valid =
|
|
68
|
+
inRanges(code, NAME_START_RANGES) ||
|
|
69
|
+
(!first && inRanges(code, NAME_CHAR_RANGES));
|
|
70
|
+
if (!valid) return false;
|
|
71
|
+
first = false;
|
|
72
|
+
}
|
|
73
|
+
return !first;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a string is a QName: an NCName, optionally prefixed by another
|
|
78
|
+
* NCName and a single colon.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} name - The candidate name
|
|
81
|
+
* @returns {boolean} True for a valid QName
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* isQName("xl:href"); // true
|
|
85
|
+
* isQName("a:b:c"); // false
|
|
86
|
+
*/
|
|
87
|
+
export function isQName(name) {
|
|
88
|
+
const parts = name.split(":");
|
|
89
|
+
return parts.length <= 2 && parts.every(isNcName);
|
|
90
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document returned by `transformToDocument`, built like Chrome's
|
|
3
|
+
* `XSLTProcessor` builds it from the serialized output:
|
|
4
|
+
*
|
|
5
|
+
* - xml output: the result tree nodes, minus whitespace-only text at the
|
|
6
|
+
* document level (the XML parser drops it, and a document cannot hold
|
|
7
|
+
* text), with a document type node when `xsl:output` declares
|
|
8
|
+
* `doctype-public` or `doctype-system` (as libxslt creates one);
|
|
9
|
+
* - html output: an HTML document parsed from the serialized html output,
|
|
10
|
+
* so it has `html`, `head` and `body` elements that are HTMLElements;
|
|
11
|
+
* - text output: see wrapTextResult in resultTree.js.
|
|
12
|
+
*
|
|
13
|
+
* @module xslt/resultDocument
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
import { findRootElement } from "./serializer/settings.js";
|
|
19
|
+
import { appendDoctype } from "./resultTree.js";
|
|
20
|
+
import { findParseError } from "./domParsing.js";
|
|
21
|
+
|
|
22
|
+
/** Text made only of XML whitespace (#x20 #x9 #xD #xA). */
|
|
23
|
+
const WHITESPACE_ONLY = /^[ \t\r\n]*$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a result node is whitespace-only character data.
|
|
27
|
+
*
|
|
28
|
+
* @param {Node} node - A child of the result fragment
|
|
29
|
+
* @returns {boolean} True for text or CDATA holding only whitespace
|
|
30
|
+
*/
|
|
31
|
+
function isWhitespaceText(node) {
|
|
32
|
+
return (
|
|
33
|
+
(node.nodeType === 3 || node.nodeType === 4) &&
|
|
34
|
+
WHITESPACE_ONLY.test(node.nodeValue)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Move an xml result fragment into an empty document.
|
|
40
|
+
*
|
|
41
|
+
* @param {Document} doc - An empty XML document
|
|
42
|
+
* @param {DocumentFragment} fragment - The result, owned by `doc`
|
|
43
|
+
* @param {{doctypePublic?: string|null, doctypeSystem?: string|null}} settings -
|
|
44
|
+
* The xsl:output settings
|
|
45
|
+
* @returns {Document} The same document, filled in
|
|
46
|
+
* @throws {Error} When the result cannot be a document (text or several
|
|
47
|
+
* elements at the top level), as the DOM rejects it
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* fillXmlDocument(doc, fragment, { doctypeSystem: "doc.dtd" }).doctype.name;
|
|
51
|
+
*/
|
|
52
|
+
export function fillXmlDocument(doc, fragment, settings) {
|
|
53
|
+
const { doctypePublic, doctypeSystem } = settings;
|
|
54
|
+
const root = findRootElement(fragment);
|
|
55
|
+
if (root && (doctypePublic || doctypeSystem)) {
|
|
56
|
+
appendDoctype(doc, root.nodeName, doctypePublic ?? "", doctypeSystem ?? "");
|
|
57
|
+
}
|
|
58
|
+
for (const child of Array.from(fragment.childNodes)) {
|
|
59
|
+
if (!isWhitespaceText(child)) doc.appendChild(child);
|
|
60
|
+
}
|
|
61
|
+
return doc;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse serialized html output into an HTML document, as Chrome does for
|
|
66
|
+
* `transformToDocument` with the html output method. The DOMParser of the
|
|
67
|
+
* host (global, or the window of the source document) is used; without one,
|
|
68
|
+
* an HTML document of the source's DOM implementation is filled through
|
|
69
|
+
* `innerHTML` (without a doctype node).
|
|
70
|
+
*
|
|
71
|
+
* xmldom parses `text/html` too, into a document without the HTML
|
|
72
|
+
* accessors (`body`, `head`, `title`); DOMs that can do neither, and markup
|
|
73
|
+
* the HTML parser rejects, leave the XML result in place.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} markup - The serialized html output
|
|
76
|
+
* @param {Document} referenceDoc - A document of the DOM implementation to use
|
|
77
|
+
* @returns {Document|null} The HTML document, or null when the DOM cannot
|
|
78
|
+
* create HTML documents (the caller then keeps the XML result)
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* parseHtmlDocument("<html><body><p>x</p></body></html>", xmlDoc).body;
|
|
82
|
+
*/
|
|
83
|
+
export function parseHtmlDocument(markup, referenceDoc) {
|
|
84
|
+
const Parser =
|
|
85
|
+
globalThis.DOMParser ?? referenceDoc.defaultView?.DOMParser ?? null;
|
|
86
|
+
if (Parser) {
|
|
87
|
+
const doc = new Parser().parseFromString(markup, "text/html");
|
|
88
|
+
return findParseError(doc) ? null : doc;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const implementation = referenceDoc.implementation;
|
|
92
|
+
if (typeof implementation?.createHTMLDocument !== "function") return null;
|
|
93
|
+
const doc = implementation.createHTMLDocument("");
|
|
94
|
+
if (!("innerHTML" in doc.documentElement)) return null;
|
|
95
|
+
if (doc.doctype) doc.removeChild(doc.doctype);
|
|
96
|
+
doc.documentElement.innerHTML = markup;
|
|
97
|
+
return doc;
|
|
98
|
+
}
|