@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.
Files changed (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +26 -8
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +518 -357
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +57 -0
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +184 -1736
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. 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
+ }