@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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Settings Resolution
|
|
3
|
+
*
|
|
4
|
+
* Normalizes the `xsl:output` settings collected by the XSLT engine into the
|
|
5
|
+
* shape the serializers consume (XSLT 1.0 section 16).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NODE_TYPE } from "./constants.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Test whether an `xsl:output` yes/no attribute is enabled.
|
|
12
|
+
*
|
|
13
|
+
* @param {string|boolean|undefined} value - Raw attribute value
|
|
14
|
+
* @returns {boolean} True when the value means "yes"
|
|
15
|
+
*/
|
|
16
|
+
function isYes(value) {
|
|
17
|
+
return value === true || String(value).toLowerCase() === "yes";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Lookup key of an expanded name, in Clark notation: `{uri}local`, or the
|
|
22
|
+
* bare local name for a name in no namespace.
|
|
23
|
+
*
|
|
24
|
+
* @param {string|null|undefined} namespaceUri - Namespace URI, empty for none
|
|
25
|
+
* @param {string} localName - Local name
|
|
26
|
+
* @returns {string} The key
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* expandedNameKey("urn:p", "c"); // "{urn:p}c"
|
|
30
|
+
* expandedNameKey(null, "c"); // "c"
|
|
31
|
+
*/
|
|
32
|
+
export function expandedNameKey(namespaceUri, localName) {
|
|
33
|
+
return namespaceUri ? `{${namespaceUri}}${localName}` : localName;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} CdataNames
|
|
38
|
+
* @property {Set<string>} expanded - Expanded name keys ({@link expandedNameKey})
|
|
39
|
+
* @property {Array<{prefix: string, localName: string}>} qnames - Prefixed
|
|
40
|
+
* names whose prefix is resolved against the result element
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalize a `cdata-section-elements` value.
|
|
45
|
+
*
|
|
46
|
+
* Entries resolved by the engine, `{namespaceUri, localName}`, are exact
|
|
47
|
+
* expanded names. A plain string QName carries no namespace bindings: an
|
|
48
|
+
* unprefixed name is taken to be in no namespace, and a prefixed one is kept
|
|
49
|
+
* aside to be resolved with the in-scope namespaces of each result element.
|
|
50
|
+
*
|
|
51
|
+
* @param {string|Array<string|{namespaceUri: ?string, localName: string}>|undefined} value -
|
|
52
|
+
* Whitespace separated QNames, or an array of QNames and expanded names
|
|
53
|
+
* @returns {CdataNames} The names
|
|
54
|
+
*/
|
|
55
|
+
function toCdataNames(value) {
|
|
56
|
+
const entries =
|
|
57
|
+
typeof value === "string" ? value.split(/\s+/) : [value ?? []].flat();
|
|
58
|
+
const expanded = new Set();
|
|
59
|
+
const qnames = [];
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (typeof entry !== "string") {
|
|
63
|
+
expanded.add(expandedNameKey(entry.namespaceUri, entry.localName));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const colon = entry.indexOf(":");
|
|
67
|
+
if (colon === -1) {
|
|
68
|
+
if (entry) expanded.add(entry);
|
|
69
|
+
} else {
|
|
70
|
+
qnames.push({
|
|
71
|
+
prefix: entry.slice(0, colon),
|
|
72
|
+
localName: entry.slice(colon + 1),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { expanded, qnames };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find the first element node of a result tree.
|
|
82
|
+
*
|
|
83
|
+
* @param {Node|null} node - Document, fragment or element
|
|
84
|
+
* @returns {Element|null} The result document element, when there is one
|
|
85
|
+
*/
|
|
86
|
+
export function findRootElement(node) {
|
|
87
|
+
if (!node) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (node.nodeType === NODE_TYPE.ELEMENT) {
|
|
91
|
+
return node;
|
|
92
|
+
}
|
|
93
|
+
for (const child of node.childNodes || []) {
|
|
94
|
+
if (child.nodeType === NODE_TYPE.ELEMENT) {
|
|
95
|
+
return child;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Text made only of XML whitespace (#x20 #x9 #xD #xA). */
|
|
102
|
+
const XML_WHITESPACE_ONLY = /^[ \t\r\n]*$/;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether text other than XML whitespace precedes the first element child.
|
|
106
|
+
*
|
|
107
|
+
* @param {Node} node - Document or fragment that has an element child
|
|
108
|
+
* @returns {boolean} True when a non-whitespace text node comes first
|
|
109
|
+
*/
|
|
110
|
+
function hasLeadingText(node) {
|
|
111
|
+
for (
|
|
112
|
+
let child = node.firstChild;
|
|
113
|
+
child.nodeType !== NODE_TYPE.ELEMENT;
|
|
114
|
+
child = child.nextSibling
|
|
115
|
+
) {
|
|
116
|
+
const isText =
|
|
117
|
+
child.nodeType === NODE_TYPE.TEXT ||
|
|
118
|
+
child.nodeType === NODE_TYPE.CDATA_SECTION;
|
|
119
|
+
if (isText && !XML_WHITESPACE_ONLY.test(child.nodeValue)) return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Derive the default output method from the result tree.
|
|
126
|
+
*
|
|
127
|
+
* XSLT 1.0 section 16 defaults to `html` when the document element is `html`
|
|
128
|
+
* in no namespace and no text other than whitespace precedes it, and to
|
|
129
|
+
* `xml` otherwise.
|
|
130
|
+
*
|
|
131
|
+
* @param {Node|null} node - Result tree root
|
|
132
|
+
* @returns {string} Either "html" or "xml"
|
|
133
|
+
*/
|
|
134
|
+
export function detectOutputMethod(node) {
|
|
135
|
+
const root = findRootElement(node);
|
|
136
|
+
const isHtmlRoot =
|
|
137
|
+
root &&
|
|
138
|
+
!root.namespaceURI &&
|
|
139
|
+
root.localName.toLowerCase() === "html" &&
|
|
140
|
+
(root === node || !hasLeadingText(node));
|
|
141
|
+
return isHtmlRoot ? "html" : "xml";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Normalize an `xsl:output` settings object.
|
|
146
|
+
*
|
|
147
|
+
* An absent, empty or "auto" method triggers the XSLT 1.0 default method
|
|
148
|
+
* detection based on the result tree.
|
|
149
|
+
*
|
|
150
|
+
* @param {object|null} outputSettings - Raw settings from the XSLT engine
|
|
151
|
+
* @param {Node|null} node - Result tree used for default method detection
|
|
152
|
+
* @returns {object} Normalized settings consumed by the serializers
|
|
153
|
+
*/
|
|
154
|
+
export function resolveOutputSettings(outputSettings, node) {
|
|
155
|
+
const raw = outputSettings || {};
|
|
156
|
+
const declared = typeof raw.method === "string" ? raw.method.trim() : "";
|
|
157
|
+
const method =
|
|
158
|
+
declared && declared !== "auto"
|
|
159
|
+
? declared.toLowerCase()
|
|
160
|
+
: detectOutputMethod(node);
|
|
161
|
+
const cdata = toCdataNames(raw.cdataSectionElements);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
method,
|
|
165
|
+
version: raw.version || "1.0",
|
|
166
|
+
encoding: raw.encoding || "UTF-8",
|
|
167
|
+
standalone: raw.standalone || null,
|
|
168
|
+
indent: isYes(raw.indent),
|
|
169
|
+
// libxslt writes a line break after a top-level comment followed by
|
|
170
|
+
// another node unless indent="no" is declared (xsltSaveResultTo)
|
|
171
|
+
topLevelLineBreaks: raw.indent == null || isYes(raw.indent),
|
|
172
|
+
omitXmlDeclaration: isYes(raw.omitXmlDeclaration),
|
|
173
|
+
doctypePublic: raw.doctypePublic || null,
|
|
174
|
+
doctypeSystem: raw.doctypeSystem || null,
|
|
175
|
+
mediaType: raw.mediaType || null,
|
|
176
|
+
cdataSectionElements: cdata.expanded,
|
|
177
|
+
cdataSectionQNames: cdata.qnames,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Output Serializer
|
|
3
|
+
*
|
|
4
|
+
* Implements the `text` output method of XSLT 1.0 section 16.3: the result is
|
|
5
|
+
* the concatenation of every descendant character data node, unescaped.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NODE_TYPE } from "./constants.js";
|
|
9
|
+
import { ChunkBuffer } from "./chunks.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a node is character data (text or CDATA section).
|
|
13
|
+
*
|
|
14
|
+
* @param {Node} node - Node to test
|
|
15
|
+
* @returns {boolean} True for character data
|
|
16
|
+
*/
|
|
17
|
+
function isCharacterData(node) {
|
|
18
|
+
return (
|
|
19
|
+
node.nodeType === NODE_TYPE.TEXT ||
|
|
20
|
+
node.nodeType === NODE_TYPE.CDATA_SECTION
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The character data nodes under `root` in document order, found without
|
|
26
|
+
* recursion (firstChild/nextSibling walk), so deep trees cannot overflow the
|
|
27
|
+
* stack.
|
|
28
|
+
*
|
|
29
|
+
* @param {Node} root - Document, fragment, element or character data node
|
|
30
|
+
* @yields {Node} Character data nodes
|
|
31
|
+
* @returns {Generator<Node, void, void>} The nodes
|
|
32
|
+
*/
|
|
33
|
+
function* characterDataNodes(root) {
|
|
34
|
+
let node = root;
|
|
35
|
+
while (node) {
|
|
36
|
+
if (isCharacterData(node)) yield node;
|
|
37
|
+
if (node.firstChild) {
|
|
38
|
+
node = node.firstChild;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
while (node !== root && !node.nextSibling) node = node.parentNode;
|
|
42
|
+
node = node === root ? null : node.nextSibling;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Serialize a result tree with the text output method, in chunks.
|
|
48
|
+
*
|
|
49
|
+
* @param {Node} node - Document, fragment, element or character data node
|
|
50
|
+
* @param {number} [chunkSize] - Chunk size in UTF-16 code units; Infinity
|
|
51
|
+
* yields the whole text as one chunk
|
|
52
|
+
* @yields {string} Non-empty chunks of at most `chunkSize` code units
|
|
53
|
+
* @returns {Generator<string, void, void>} The chunks, in order
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* [...textChunks(fragment, 16384)].join("");
|
|
57
|
+
*/
|
|
58
|
+
export function* textChunks(node, chunkSize) {
|
|
59
|
+
const buffer = new ChunkBuffer(chunkSize);
|
|
60
|
+
for (const text of characterDataNodes(node)) {
|
|
61
|
+
buffer.write(text.nodeValue || "");
|
|
62
|
+
if (buffer.full) yield* buffer.take();
|
|
63
|
+
}
|
|
64
|
+
yield* buffer.take(true);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Serialize a result tree with the text output method.
|
|
69
|
+
*
|
|
70
|
+
* @param {Node} node - Document, fragment, element or character data node
|
|
71
|
+
* @returns {string} Concatenated character data
|
|
72
|
+
*/
|
|
73
|
+
export function serializeText(node) {
|
|
74
|
+
let text = "";
|
|
75
|
+
for (const chunk of textChunks(node, Infinity)) text += chunk;
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XHTML 1.0 documents in xml output, as libxml2 (and so Chrome's
|
|
3
|
+
* XSLTProcessor) writes them.
|
|
4
|
+
*
|
|
5
|
+
* When the xml output declares one of the XHTML 1.0 document types
|
|
6
|
+
* (`doctype-public` or `doctype-system` of XHTML 1.0 Strict, Transitional
|
|
7
|
+
* or Frameset), libxml2 switches to its XHTML serializer, which follows the
|
|
8
|
+
* compatibility guidelines of XHTML 1.0 appendix C:
|
|
9
|
+
* - an `html` element in no namespace without namespace declarations gets
|
|
10
|
+
* `xmlns="http://www.w3.org/1999/xhtml"` (A.3.1.1);
|
|
11
|
+
* - a `head` child of the `html` document element without a Content-Type
|
|
12
|
+
* `meta` gets `<meta http-equiv="Content-Type" content="text/html;
|
|
13
|
+
* charset=..." />` as its first child (C.9);
|
|
14
|
+
* - elements in no namespace follow the empty element rules of XHTML
|
|
15
|
+
* elements (C.2, C.3).
|
|
16
|
+
*
|
|
17
|
+
* @module xslt/serializer/xhtmlDocument
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { NODE_TYPE, XHTML_NAMESPACE } from "./constants.js";
|
|
21
|
+
|
|
22
|
+
/** Public identifiers of the XHTML 1.0 document types. */
|
|
23
|
+
const XHTML1_PUBLIC_IDS = new Set([
|
|
24
|
+
"-//W3C//DTD XHTML 1.0 Strict//EN",
|
|
25
|
+
"-//W3C//DTD XHTML 1.0 Transitional//EN",
|
|
26
|
+
"-//W3C//DTD XHTML 1.0 Frameset//EN",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/** System identifiers of the XHTML 1.0 document types. */
|
|
30
|
+
const XHTML1_SYSTEM_IDS = new Set([
|
|
31
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd",
|
|
32
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd",
|
|
33
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the output settings declare an XHTML 1.0 document type.
|
|
38
|
+
*
|
|
39
|
+
* @param {{doctypePublic?: string|null, doctypeSystem?: string|null}} settings -
|
|
40
|
+
* Normalized output settings
|
|
41
|
+
* @returns {boolean} True for the XHTML 1.0 public or system identifiers
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* isXhtml1Doctype({ doctypePublic: "-//W3C//DTD XHTML 1.0 Strict//EN" }); // true
|
|
45
|
+
*/
|
|
46
|
+
export function isXhtml1Doctype(settings) {
|
|
47
|
+
return (
|
|
48
|
+
XHTML1_PUBLIC_IDS.has(settings.doctypePublic) ||
|
|
49
|
+
XHTML1_SYSTEM_IDS.has(settings.doctypeSystem)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The namespace declaration libxml2 adds to an `html` element in no
|
|
55
|
+
* namespace that declares no namespace itself.
|
|
56
|
+
*
|
|
57
|
+
* @param {Element} element - Element being written
|
|
58
|
+
* @param {Array<object>} declarations - Declarations written on it
|
|
59
|
+
* @returns {string} ` xmlns="http://www.w3.org/1999/xhtml"`, or ""
|
|
60
|
+
*/
|
|
61
|
+
export function xhtmlRootNamespace(element, declarations) {
|
|
62
|
+
const needed =
|
|
63
|
+
element.localName === "html" &&
|
|
64
|
+
!element.namespaceURI &&
|
|
65
|
+
declarations.length === 0;
|
|
66
|
+
return needed ? ` xmlns="${XHTML_NAMESPACE}"` : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether a `head` element has a `meta` child with
|
|
71
|
+
* `http-equiv="Content-Type"` (compared case-insensitively).
|
|
72
|
+
*
|
|
73
|
+
* @param {Element} head - The head element
|
|
74
|
+
* @returns {boolean} True when libxml2 adds no meta element
|
|
75
|
+
*/
|
|
76
|
+
function hasContentTypeMeta(head) {
|
|
77
|
+
for (const child of head.childNodes) {
|
|
78
|
+
if (child.nodeType !== NODE_TYPE.ELEMENT || child.localName !== "meta") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const value = child.getAttribute("http-equiv");
|
|
82
|
+
if (value?.toLowerCase() === "content-type") return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The Content-Type `meta` element libxml2 writes as the first child of the
|
|
89
|
+
* `head` child of an `html` document element.
|
|
90
|
+
*
|
|
91
|
+
* @param {Element} element - Element being written
|
|
92
|
+
* @param {string} encoding - The output encoding, as declared
|
|
93
|
+
* @returns {string} The meta element markup, or ""
|
|
94
|
+
*/
|
|
95
|
+
export function xhtmlHeadMeta(element, encoding) {
|
|
96
|
+
const parent = element.parentNode;
|
|
97
|
+
const isHead =
|
|
98
|
+
element.localName === "head" &&
|
|
99
|
+
parent?.localName === "html" &&
|
|
100
|
+
parent.parentNode?.nodeType !== NODE_TYPE.ELEMENT;
|
|
101
|
+
if (!isHead || hasContentTypeMeta(element)) return "";
|
|
102
|
+
return `<meta http-equiv="Content-Type" content="text/html; charset=${encoding}" />`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML Output Serializer
|
|
3
|
+
*
|
|
4
|
+
* Implements the `xml` output method of XSLT 1.0 section 16.1 and, with the
|
|
5
|
+
* `xhtml` option, the XHTML empty element convention. It is also the base of
|
|
6
|
+
* the html serializer, which overrides the dialect hooks defined here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TEXT_MODE, VOID_ELEMENTS, XHTML_NAMESPACE } from "./constants.js";
|
|
10
|
+
import { escapeXmlAttribute, escapeXmlText } from "./escape.js";
|
|
11
|
+
import { characterReference } from "./encoding.js";
|
|
12
|
+
import { BaseWriter } from "./baseWriter.js";
|
|
13
|
+
import { expandedNameKey } from "./settings.js";
|
|
14
|
+
import {
|
|
15
|
+
isXhtml1Doctype,
|
|
16
|
+
xhtmlHeadMeta,
|
|
17
|
+
xhtmlRootNamespace,
|
|
18
|
+
} from "./xhtmlDocument.js";
|
|
19
|
+
|
|
20
|
+
export class XmlWriter extends BaseWriter {
|
|
21
|
+
/**
|
|
22
|
+
* Whether an XML declaration has to be written.
|
|
23
|
+
* @returns {boolean} True when the declaration is not omitted
|
|
24
|
+
*/
|
|
25
|
+
get emitsXmlDeclaration() {
|
|
26
|
+
return !this.settings.omitXmlDeclaration;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the output is an XHTML 1.0 document (see xhtmlDocument.js).
|
|
31
|
+
* @returns {boolean} True when an XHTML 1.0 doctype is declared
|
|
32
|
+
*/
|
|
33
|
+
get isXhtmlDocument() {
|
|
34
|
+
return isXhtml1Doctype(this.settings);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Namespace declarations of an element, plus the XHTML namespace libxml2
|
|
39
|
+
* adds to an `html` element in no namespace of an XHTML 1.0 document.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array<{prefix: string, uri: string}>} declarations - Declarations
|
|
42
|
+
* @param {Element} element - The element they are written on
|
|
43
|
+
* @returns {string} Attribute markup, starting with a space when non-empty
|
|
44
|
+
*/
|
|
45
|
+
namespaceMarkup(declarations, element) {
|
|
46
|
+
const markup = super.namespaceMarkup(declarations);
|
|
47
|
+
return this.isXhtmlDocument
|
|
48
|
+
? markup + xhtmlRootNamespace(element, declarations)
|
|
49
|
+
: markup;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Terminator of a processing instruction.
|
|
54
|
+
* @returns {string} The XML processing instruction terminator
|
|
55
|
+
*/
|
|
56
|
+
get piTerminator() {
|
|
57
|
+
return "?>";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How a source CDATA section node has to be written.
|
|
62
|
+
* @returns {string} A {@link TEXT_MODE} value
|
|
63
|
+
*/
|
|
64
|
+
get cdataNodeMode() {
|
|
65
|
+
return TEXT_MODE.CDATA;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether a line break follows a top-level comment that another node
|
|
70
|
+
* follows, as libxslt (xsltSaveResultTo) writes unless `indent="no"` is
|
|
71
|
+
* declared.
|
|
72
|
+
* @returns {boolean} True for xml output without `indent="no"`
|
|
73
|
+
*/
|
|
74
|
+
get topLevelLineBreaks() {
|
|
75
|
+
return this.settings.topLevelLineBreaks === true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the document type declaration for the xml output method.
|
|
80
|
+
*
|
|
81
|
+
* @param {Element|null} rootElement - Result document element
|
|
82
|
+
* @returns {string} Doctype markup, or an empty string when not applicable
|
|
83
|
+
*/
|
|
84
|
+
doctypeMarkup(rootElement) {
|
|
85
|
+
const { doctypePublic, doctypeSystem } = this.settings;
|
|
86
|
+
if (!rootElement || !doctypeSystem) {
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const name = rootElement.nodeName;
|
|
91
|
+
return doctypePublic
|
|
92
|
+
? `<!DOCTYPE ${name} PUBLIC "${doctypePublic}" "${doctypeSystem}">`
|
|
93
|
+
: `<!DOCTYPE ${name} SYSTEM "${doctypeSystem}">`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Determine how the character data children of an element are written:
|
|
98
|
+
* as CDATA sections when the expanded name of the element is listed in
|
|
99
|
+
* `cdata-section-elements` (XSLT 1.0 section 16.1).
|
|
100
|
+
*
|
|
101
|
+
* @param {Element} element - Parent element
|
|
102
|
+
* @returns {string} A {@link TEXT_MODE} value
|
|
103
|
+
*/
|
|
104
|
+
childTextMode(element) {
|
|
105
|
+
return this.isCdataSectionElement(element)
|
|
106
|
+
? TEXT_MODE.CDATA
|
|
107
|
+
: TEXT_MODE.ESCAPE;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether an element is listed in `cdata-section-elements`. Prefixed names
|
|
112
|
+
* the engine passed unresolved are resolved with the element's in-scope
|
|
113
|
+
* namespaces.
|
|
114
|
+
*
|
|
115
|
+
* @param {Element} element - Element to test
|
|
116
|
+
* @returns {boolean} True when its text is written as CDATA sections
|
|
117
|
+
*/
|
|
118
|
+
isCdataSectionElement(element) {
|
|
119
|
+
const { cdataSectionElements, cdataSectionQNames = [] } = this.settings;
|
|
120
|
+
const namespaceUri = element.namespaceURI || null;
|
|
121
|
+
const { localName } = element;
|
|
122
|
+
if (cdataSectionElements.has(expandedNameKey(namespaceUri, localName))) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return cdataSectionQNames.some(
|
|
126
|
+
(qname) =>
|
|
127
|
+
qname.localName === localName &&
|
|
128
|
+
namespaceUri !== null &&
|
|
129
|
+
element.lookupNamespaceURI(qname.prefix) === namespaceUri,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Markup the serializer itself adds as the first child of an element: the
|
|
135
|
+
* Content-Type meta of an XHTML 1.0 document head, as libxml2 writes it.
|
|
136
|
+
*
|
|
137
|
+
* @param {Element} element - Element being written
|
|
138
|
+
* @returns {string} The meta element, or an empty string
|
|
139
|
+
*/
|
|
140
|
+
leadingChildMarkup(element) {
|
|
141
|
+
return this.isXhtmlDocument
|
|
142
|
+
? xhtmlHeadMeta(element, this.settings.encoding)
|
|
143
|
+
: "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether the content of an element may be re-indented.
|
|
148
|
+
*
|
|
149
|
+
* @param {Element} _element - Element being inspected
|
|
150
|
+
* @returns {boolean} Always true for XML output
|
|
151
|
+
*/
|
|
152
|
+
allowsIndentInside(_element) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build the markup closing an element that has no children.
|
|
158
|
+
*
|
|
159
|
+
* XHTML elements follow the XHTML compatibility guidelines, as libxml2
|
|
160
|
+
* (Chrome) and the DOM serializer (Firefox) do: void elements become
|
|
161
|
+
* `<br />` and every other empty element gets an explicit end tag, because
|
|
162
|
+
* `<script/>` or `<div/>` break when XHTML reaches an HTML parser. Other
|
|
163
|
+
* elements use the XML empty-element tag.
|
|
164
|
+
*
|
|
165
|
+
* @param {Element} element - Empty element
|
|
166
|
+
* @param {string} name - Element name as written
|
|
167
|
+
* @returns {string} Markup terminating the start tag
|
|
168
|
+
*/
|
|
169
|
+
emptyElementMarkup(element, name) {
|
|
170
|
+
if (!this.followsXhtmlConventions(element)) return "/>";
|
|
171
|
+
return this.isVoidElement(element) ? " />" : `></${name}>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Whether an element is written with the XHTML empty-element conventions:
|
|
176
|
+
* elements in the XHTML namespace, and namespace-less elements when the
|
|
177
|
+
* output method is `xhtml` or the document type is XHTML 1.0.
|
|
178
|
+
*
|
|
179
|
+
* @param {Element} element - Element to test
|
|
180
|
+
* @returns {boolean} True for XHTML elements
|
|
181
|
+
*/
|
|
182
|
+
followsXhtmlConventions(element) {
|
|
183
|
+
const namespace = element.namespaceURI || null;
|
|
184
|
+
if (namespace === XHTML_NAMESPACE) return true;
|
|
185
|
+
return namespace === null && (this.xhtml || this.isXhtmlDocument);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Test whether an element is an HTML void element.
|
|
190
|
+
*
|
|
191
|
+
* @param {Element} element - Element to test
|
|
192
|
+
* @returns {boolean} True for void elements such as `br`
|
|
193
|
+
*/
|
|
194
|
+
isVoidElement(element) {
|
|
195
|
+
return VOID_ELEMENTS.has(String(element.localName).toLowerCase());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Escape character data.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} value - Text content
|
|
202
|
+
* @returns {string} Escaped text
|
|
203
|
+
*/
|
|
204
|
+
escapeText(value) {
|
|
205
|
+
return this.encodeReferences(escapeXmlText(value));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Escape an attribute value.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} value - Attribute value
|
|
212
|
+
* @returns {string} Escaped value
|
|
213
|
+
*/
|
|
214
|
+
escapeAttribute(value) {
|
|
215
|
+
return this.encodeReferences(escapeXmlAttribute(value));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Reference to a character the output encoding cannot represent.
|
|
220
|
+
*
|
|
221
|
+
* @param {number} codePoint - The code point
|
|
222
|
+
* @returns {string} A numeric character reference
|
|
223
|
+
*/
|
|
224
|
+
characterReference(codePoint) {
|
|
225
|
+
return characterReference(codePoint);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSLT Output Serializer
|
|
3
|
+
*
|
|
4
|
+
* Serializes a result tree to a string honoring the `xsl:output` settings of
|
|
5
|
+
* the stylesheet (XSLT 1.0 section 16): output method, indentation, XML
|
|
6
|
+
* declaration, document type declaration, CDATA sections and
|
|
7
|
+
* `disable-output-escaping`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { serializeResult } from './serializer.js';
|
|
11
|
+
*
|
|
12
|
+
* serializeResult(resultDocument, { method: 'xml', indent: 'yes' });
|
|
13
|
+
* // '<?xml version="1.0" encoding="UTF-8"?>\n<BAR>\n <QUX/>\n</BAR>'
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { resolveOutputSettings } from "./serializer/settings.js";
|
|
17
|
+
import { XmlWriter } from "./serializer/xmlSerializer.js";
|
|
18
|
+
import { HtmlWriter } from "./serializer/htmlSerializer.js";
|
|
19
|
+
import { textChunks } from "./serializer/textSerializer.js";
|
|
20
|
+
import { toChunkSize } from "./serializer/chunks.js";
|
|
21
|
+
|
|
22
|
+
export { markRawText, isRawText, rawTextNodes } from "./serializer/rawText.js";
|
|
23
|
+
export {
|
|
24
|
+
resolveOutputSettings,
|
|
25
|
+
detectOutputMethod,
|
|
26
|
+
findRootElement,
|
|
27
|
+
} from "./serializer/settings.js";
|
|
28
|
+
export { XmlWriter } from "./serializer/xmlSerializer.js";
|
|
29
|
+
export { HtmlWriter } from "./serializer/htmlSerializer.js";
|
|
30
|
+
export { serializeText, textChunks } from "./serializer/textSerializer.js";
|
|
31
|
+
export {
|
|
32
|
+
ChunkBuffer,
|
|
33
|
+
DEFAULT_CHUNK_SIZE,
|
|
34
|
+
toChunkSize,
|
|
35
|
+
} from "./serializer/chunks.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Serialize a transformation result incrementally, honoring the same
|
|
39
|
+
* `xsl:output` settings as {@link serializeResult}. The result tree must be
|
|
40
|
+
* complete (XSLT 1.0 builds it in memory); what is bounded is the output
|
|
41
|
+
* text: chunks are yielded as soon as they are full, so a consumer can write
|
|
42
|
+
* them out while the rest is being serialized.
|
|
43
|
+
*
|
|
44
|
+
* @param {Node|null} node - Result document, fragment or element
|
|
45
|
+
* @param {object} [outputSettings] - `xsl:output` settings (see serializeResult)
|
|
46
|
+
* @param {{chunkSize?: number}} [options] - `chunkSize` in UTF-16 code units,
|
|
47
|
+
* {@link DEFAULT_CHUNK_SIZE} (16 KiB) by default, Infinity for one chunk
|
|
48
|
+
* @yields {string} Non-empty chunks of at most `chunkSize` code units (one
|
|
49
|
+
* more when a surrogate pair straddles the boundary)
|
|
50
|
+
* @returns {Iterator<string>} The chunks; joined, they equal
|
|
51
|
+
* `serializeResult(node, outputSettings)`
|
|
52
|
+
* @throws {RangeError} When `chunkSize` is not a positive integer or Infinity
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* for (const chunk of serializeChunks(resultDocument, { method: "xml" })) {
|
|
56
|
+
* stream.write(chunk);
|
|
57
|
+
* }
|
|
58
|
+
*/
|
|
59
|
+
export function serializeChunks(node, outputSettings = {}, options = {}) {
|
|
60
|
+
const chunkSize = toChunkSize(options.chunkSize);
|
|
61
|
+
if (!node) return [][Symbol.iterator]();
|
|
62
|
+
|
|
63
|
+
const settings = resolveOutputSettings(outputSettings, node);
|
|
64
|
+
|
|
65
|
+
if (settings.method === "text") {
|
|
66
|
+
return textChunks(node, chunkSize);
|
|
67
|
+
}
|
|
68
|
+
const writer =
|
|
69
|
+
settings.method === "html"
|
|
70
|
+
? new HtmlWriter(settings)
|
|
71
|
+
: new XmlWriter(settings, { xhtml: settings.method === "xhtml" });
|
|
72
|
+
return writer.chunks(node, chunkSize);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Serialize a transformation result to a string.
|
|
77
|
+
*
|
|
78
|
+
* @param {Node|null} node - Result document, fragment or element
|
|
79
|
+
* @param {object} [outputSettings] - `xsl:output` settings, as collected by the
|
|
80
|
+
* XSLT engine (`method`, `version`, `encoding`, `standalone`, `indent`,
|
|
81
|
+
* `omitXmlDeclaration`, `doctypePublic`, `doctypeSystem`, `mediaType`,
|
|
82
|
+
* `cdataSectionElements`)
|
|
83
|
+
* @returns {string} The serialized result, or an empty string for a null node
|
|
84
|
+
*/
|
|
85
|
+
export function serializeResult(node, outputSettings = {}) {
|
|
86
|
+
const chunks = serializeChunks(node, outputSettings, { chunkSize: Infinity });
|
|
87
|
+
let output = "";
|
|
88
|
+
for (const chunk of chunks) output += chunk;
|
|
89
|
+
return output;
|
|
90
|
+
}
|