@tradik/xslt-processor 1.0.2 → 1.1.1
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/README.md +292 -47
- package/bin/lib/options.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +115 -0
- package/bin/xslt.js +68 -162
- package/dist/xslt-processor.browser.js +2073 -163
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +6 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +2077 -162
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +299 -0
- package/dist/xslt-processor.d.ts +92 -4
- package/dist/xslt-processor.js +2072 -161
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +27 -16
- package/src/XSLTProcessor.js +177 -8
- package/src/index.js +11 -5
- package/src/xpath/evaluator.js +48 -7
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine.js +474 -185
- package/src/xslt/formatNumber.js +220 -0
- package/src/xslt/functions.js +191 -0
- package/src/xslt/index.js +31 -0
- package/src/xslt/keys.js +141 -0
- package/src/xslt/literalResult.js +167 -0
- package/src/xslt/number.js +178 -0
- package/src/xslt/numberFormat.js +155 -0
- package/src/xslt/resultTree.js +74 -0
- package/src/xslt/serializer/baseWriter.js +283 -0
- package/src/xslt/serializer/constants.js +78 -0
- package/src/xslt/serializer/escape.js +98 -0
- package/src/xslt/serializer/htmlSerializer.js +141 -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 +103 -0
- package/src/xslt/serializer/textSerializer.js +29 -0
- package/src/xslt/serializer/xmlSerializer.js +127 -0
- package/src/xslt/serializer.js +57 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/whitespace.js +184 -0
- 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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Tree Writer
|
|
3
|
+
*
|
|
4
|
+
* Walks a result tree and turns it into markup. Everything that differs
|
|
5
|
+
* between the xml, xhtml and html output methods of XSLT 1.0 section 16 is
|
|
6
|
+
* delegated to the dialect hooks implemented by the concrete writers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
INDENT_UNIT,
|
|
11
|
+
NODE_TYPE,
|
|
12
|
+
TEXT_MODE,
|
|
13
|
+
XMLNS_NAMESPACE,
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
import { wrapCdata } from "./escape.js";
|
|
16
|
+
import {
|
|
17
|
+
collectNamespaceDeclarations,
|
|
18
|
+
createNamespaceScope,
|
|
19
|
+
} from "./namespaces.js";
|
|
20
|
+
import { getIndentableChildren } from "./indent.js";
|
|
21
|
+
import { isRawText } from "./rawText.js";
|
|
22
|
+
import { findRootElement } from "./settings.js";
|
|
23
|
+
|
|
24
|
+
export class BaseWriter {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} settings - Normalized output settings
|
|
27
|
+
* @param {{xhtml?: boolean}} [options] - Dialect options
|
|
28
|
+
*/
|
|
29
|
+
constructor(settings, options = {}) {
|
|
30
|
+
this.settings = settings;
|
|
31
|
+
this.xhtml = options.xhtml === true;
|
|
32
|
+
this.parts = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Serialize a result tree node.
|
|
37
|
+
*
|
|
38
|
+
* @param {Node} node - Document, fragment or element to serialize
|
|
39
|
+
* @returns {string} Serialized output
|
|
40
|
+
*/
|
|
41
|
+
serialize(node) {
|
|
42
|
+
this.parts = [];
|
|
43
|
+
this.writeProlog(node);
|
|
44
|
+
this.writeNode(node, createNamespaceScope(), 0, TEXT_MODE.ESCAPE);
|
|
45
|
+
return this.parts.join("");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Write the XML declaration and the document type declaration.
|
|
50
|
+
*
|
|
51
|
+
* @param {Node} node - Result tree root
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
writeProlog(node) {
|
|
55
|
+
if (this.emitsXmlDeclaration) {
|
|
56
|
+
const { version, encoding, standalone } = this.settings;
|
|
57
|
+
const standalonePart = standalone ? ` standalone="${standalone}"` : "";
|
|
58
|
+
this.parts.push(
|
|
59
|
+
`<?xml version="${version}" encoding="${encoding}"${standalonePart}?>\n`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const doctype = this.doctypeMarkup(findRootElement(node));
|
|
64
|
+
if (doctype) {
|
|
65
|
+
this.parts.push(`${doctype}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Write any result tree node.
|
|
71
|
+
*
|
|
72
|
+
* @param {Node} node - Node to write
|
|
73
|
+
* @param {Map<string, string>} scope - Namespace scope in effect
|
|
74
|
+
* @param {number} depth - Current indentation depth
|
|
75
|
+
* @param {string} textMode - {@link TEXT_MODE} for character data children
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
writeNode(node, scope, depth, textMode) {
|
|
79
|
+
switch (node.nodeType) {
|
|
80
|
+
case NODE_TYPE.ELEMENT:
|
|
81
|
+
this.writeElement(node, scope, depth);
|
|
82
|
+
break;
|
|
83
|
+
case NODE_TYPE.TEXT:
|
|
84
|
+
case NODE_TYPE.CDATA_SECTION:
|
|
85
|
+
this.writeText(node, textMode);
|
|
86
|
+
break;
|
|
87
|
+
case NODE_TYPE.COMMENT:
|
|
88
|
+
this.parts.push(`<!--${node.nodeValue}-->`);
|
|
89
|
+
break;
|
|
90
|
+
case NODE_TYPE.PROCESSING_INSTRUCTION:
|
|
91
|
+
this.writeProcessingInstruction(node);
|
|
92
|
+
break;
|
|
93
|
+
case NODE_TYPE.DOCUMENT:
|
|
94
|
+
case NODE_TYPE.DOCUMENT_FRAGMENT:
|
|
95
|
+
this.writeChildNodes(node, scope, depth, textMode);
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Write every child of a node without adding whitespace.
|
|
104
|
+
*
|
|
105
|
+
* @param {Node} node - Parent node
|
|
106
|
+
* @param {Map<string, string>} scope - Namespace scope in effect
|
|
107
|
+
* @param {number} depth - Current indentation depth
|
|
108
|
+
* @param {string} textMode - {@link TEXT_MODE} for character data children
|
|
109
|
+
* @returns {void}
|
|
110
|
+
*/
|
|
111
|
+
writeChildNodes(node, scope, depth, textMode) {
|
|
112
|
+
for (const child of node.childNodes) {
|
|
113
|
+
this.writeNode(child, scope, depth, textMode);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Write an element with its namespaces, attributes and children.
|
|
119
|
+
*
|
|
120
|
+
* @param {Element} element - Element to write
|
|
121
|
+
* @param {Map<string, string>} scope - Namespace scope inherited from the parent
|
|
122
|
+
* @param {number} depth - Current indentation depth
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
writeElement(element, scope, depth) {
|
|
126
|
+
const namespaces = this.emitsNamespaces
|
|
127
|
+
? collectNamespaceDeclarations(element, scope)
|
|
128
|
+
: { declarations: [], scope };
|
|
129
|
+
const name = element.nodeName;
|
|
130
|
+
|
|
131
|
+
this.parts.push(
|
|
132
|
+
`<${name}${this.namespaceMarkup(namespaces.declarations)}` +
|
|
133
|
+
this.attributesMarkup(element),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!element.firstChild) {
|
|
137
|
+
this.parts.push(this.emptyElementMarkup(element, name));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.parts.push(">");
|
|
142
|
+
this.writeElementChildren(element, namespaces.scope, depth);
|
|
143
|
+
this.parts.push(`</${name}>`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Write the children of an element, indenting element-only content.
|
|
148
|
+
*
|
|
149
|
+
* @param {Element} element - Parent element
|
|
150
|
+
* @param {Map<string, string>} scope - Namespace scope in effect
|
|
151
|
+
* @param {number} depth - Depth of the parent element
|
|
152
|
+
* @returns {void}
|
|
153
|
+
*/
|
|
154
|
+
writeElementChildren(element, scope, depth) {
|
|
155
|
+
const textMode = this.childTextMode(element);
|
|
156
|
+
const indentable = this.indentableChildren(element, textMode);
|
|
157
|
+
|
|
158
|
+
if (!indentable) {
|
|
159
|
+
this.writeChildNodes(element, scope, depth, textMode);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const childIndent = `\n${INDENT_UNIT.repeat(depth + 1)}`;
|
|
164
|
+
for (const child of indentable) {
|
|
165
|
+
this.parts.push(childIndent);
|
|
166
|
+
this.writeNode(child, scope, depth + 1, textMode);
|
|
167
|
+
}
|
|
168
|
+
this.parts.push(`\n${INDENT_UNIT.repeat(depth)}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Determine the children to indent inside an element.
|
|
173
|
+
*
|
|
174
|
+
* @param {Element} element - Parent element
|
|
175
|
+
* @param {string} textMode - {@link TEXT_MODE} for character data children
|
|
176
|
+
* @returns {Node[]|null} Children to indent, or null when indenting is off
|
|
177
|
+
*/
|
|
178
|
+
indentableChildren(element, textMode) {
|
|
179
|
+
if (!this.settings.indent || textMode !== TEXT_MODE.ESCAPE) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (!this.allowsIndentInside(element)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return getIndentableChildren(element);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build the namespace declaration markup of an element.
|
|
190
|
+
*
|
|
191
|
+
* @param {Array<{prefix: string, uri: string}>} declarations - Declarations
|
|
192
|
+
* @returns {string} Attribute markup, starting with a space when non-empty
|
|
193
|
+
*/
|
|
194
|
+
namespaceMarkup(declarations) {
|
|
195
|
+
return declarations
|
|
196
|
+
.map(({ prefix, uri }) => {
|
|
197
|
+
const name = prefix ? `xmlns:${prefix}` : "xmlns";
|
|
198
|
+
return ` ${name}="${this.escapeAttribute(uri)}"`;
|
|
199
|
+
})
|
|
200
|
+
.join("");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Build the attribute markup of an element, skipping namespace declarations.
|
|
205
|
+
*
|
|
206
|
+
* @param {Element} element - Element being written
|
|
207
|
+
* @returns {string} Attribute markup, starting with a space when non-empty
|
|
208
|
+
*/
|
|
209
|
+
attributesMarkup(element) {
|
|
210
|
+
let markup = "";
|
|
211
|
+
for (const attribute of Array.from(element.attributes || [])) {
|
|
212
|
+
if (attribute.namespaceURI !== XMLNS_NAMESPACE) {
|
|
213
|
+
markup += this.attributeMarkup(attribute);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return markup;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Build the markup of a single attribute.
|
|
221
|
+
*
|
|
222
|
+
* @param {Attr} attribute - Attribute to write
|
|
223
|
+
* @returns {string} Attribute markup, starting with a space
|
|
224
|
+
*/
|
|
225
|
+
attributeMarkup(attribute) {
|
|
226
|
+
return ` ${attribute.name}="${this.escapeAttribute(attribute.value)}"`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Write a character data node.
|
|
231
|
+
*
|
|
232
|
+
* Nodes produced with `disable-output-escaping="yes"` are written verbatim.
|
|
233
|
+
*
|
|
234
|
+
* @param {Node} node - Text or CDATA section node
|
|
235
|
+
* @param {string} textMode - {@link TEXT_MODE} requested by the parent
|
|
236
|
+
* @returns {void}
|
|
237
|
+
*/
|
|
238
|
+
writeText(node, textMode) {
|
|
239
|
+
const value = node.nodeValue || "";
|
|
240
|
+
|
|
241
|
+
if (isRawText(node)) {
|
|
242
|
+
this.parts.push(value);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const mode = this.resolveTextMode(node, textMode);
|
|
247
|
+
if (mode === TEXT_MODE.CDATA) {
|
|
248
|
+
this.parts.push(wrapCdata(value));
|
|
249
|
+
} else if (mode === TEXT_MODE.RAW) {
|
|
250
|
+
this.parts.push(value);
|
|
251
|
+
} else {
|
|
252
|
+
this.parts.push(this.escapeText(value));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Resolve the effective text mode of a character data node.
|
|
258
|
+
*
|
|
259
|
+
* @param {Node} node - Text or CDATA section node
|
|
260
|
+
* @param {string} textMode - {@link TEXT_MODE} requested by the parent
|
|
261
|
+
* @returns {string} A {@link TEXT_MODE} value
|
|
262
|
+
*/
|
|
263
|
+
resolveTextMode(node, textMode) {
|
|
264
|
+
if (textMode !== TEXT_MODE.ESCAPE) {
|
|
265
|
+
return textMode;
|
|
266
|
+
}
|
|
267
|
+
return node.nodeType === NODE_TYPE.CDATA_SECTION
|
|
268
|
+
? this.cdataNodeMode
|
|
269
|
+
: TEXT_MODE.ESCAPE;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Write a processing instruction node.
|
|
274
|
+
*
|
|
275
|
+
* @param {ProcessingInstruction} node - Node to write
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
writeProcessingInstruction(node) {
|
|
279
|
+
const data = node.nodeValue || "";
|
|
280
|
+
const separator = data ? " " : "";
|
|
281
|
+
this.parts.push(`<?${node.target}${separator}${data}${this.piTerminator}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializer Constants
|
|
3
|
+
*
|
|
4
|
+
* Shared node type codes, namespace URIs and HTML element tables used by the
|
|
5
|
+
* XSLT 1.0 output serializers (XSLT 1.0 section 16).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* DOM node type codes used by the serializers.
|
|
10
|
+
*/
|
|
11
|
+
export const NODE_TYPE = {
|
|
12
|
+
ELEMENT: 1,
|
|
13
|
+
TEXT: 3,
|
|
14
|
+
CDATA_SECTION: 4,
|
|
15
|
+
PROCESSING_INSTRUCTION: 7,
|
|
16
|
+
COMMENT: 8,
|
|
17
|
+
DOCUMENT: 9,
|
|
18
|
+
DOCUMENT_FRAGMENT: 11,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Namespace URI reserved for namespace declaration attributes.
|
|
23
|
+
*/
|
|
24
|
+
export const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Namespace URI bound to the reserved `xml` prefix.
|
|
28
|
+
*/
|
|
29
|
+
export const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How the text children of an element have to be written out.
|
|
33
|
+
*/
|
|
34
|
+
export const TEXT_MODE = {
|
|
35
|
+
ESCAPE: "escape",
|
|
36
|
+
CDATA: "cdata",
|
|
37
|
+
RAW: "raw",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* HTML elements that never have an end tag.
|
|
42
|
+
*/
|
|
43
|
+
export const VOID_ELEMENTS = new Set([
|
|
44
|
+
"area",
|
|
45
|
+
"base",
|
|
46
|
+
"br",
|
|
47
|
+
"col",
|
|
48
|
+
"embed",
|
|
49
|
+
"hr",
|
|
50
|
+
"img",
|
|
51
|
+
"input",
|
|
52
|
+
"link",
|
|
53
|
+
"meta",
|
|
54
|
+
"param",
|
|
55
|
+
"source",
|
|
56
|
+
"track",
|
|
57
|
+
"wbr",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* HTML elements whose character data must not be escaped.
|
|
62
|
+
*/
|
|
63
|
+
export const RAW_TEXT_ELEMENTS = new Set(["script", "style"]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* HTML elements whose content must never be re-indented.
|
|
67
|
+
*/
|
|
68
|
+
export const PRESERVE_SPACE_ELEMENTS = new Set([
|
|
69
|
+
"pre",
|
|
70
|
+
"script",
|
|
71
|
+
"style",
|
|
72
|
+
"textarea",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Indentation unit used when `indent="yes"` is requested.
|
|
77
|
+
*/
|
|
78
|
+
export const INDENT_UNIT = " ";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Escaping Helpers
|
|
3
|
+
*
|
|
4
|
+
* Character escaping rules for the xml, xhtml and html output methods
|
|
5
|
+
* (XSLT 1.0 section 16).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const XML_TEXT_ESCAPES = { "&": "&", "<": "<" };
|
|
9
|
+
|
|
10
|
+
const HTML_TEXT_ESCAPES = { "&": "&", "<": "<", ">": ">" };
|
|
11
|
+
|
|
12
|
+
const XML_ATTRIBUTE_ESCAPES = {
|
|
13
|
+
"&": "&",
|
|
14
|
+
"<": "<",
|
|
15
|
+
">": ">",
|
|
16
|
+
'"': """,
|
|
17
|
+
"\t": "	",
|
|
18
|
+
"\n": " ",
|
|
19
|
+
"\r": " ",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const HTML_ATTRIBUTE_ESCAPES = {
|
|
23
|
+
"&": "&",
|
|
24
|
+
"<": "<",
|
|
25
|
+
">": ">",
|
|
26
|
+
'"': """,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Replace every character matched by a pattern using a lookup table.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} value - Text to escape
|
|
33
|
+
* @param {RegExp} pattern - Global pattern selecting the characters to replace
|
|
34
|
+
* @param {Record<string, string>} escapes - Character to replacement mapping
|
|
35
|
+
* @returns {string} Escaped text
|
|
36
|
+
*/
|
|
37
|
+
function escapeWith(value, pattern, escapes) {
|
|
38
|
+
return String(value).replaceAll(pattern, (character) => escapes[character]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Escape character data for the xml output method.
|
|
43
|
+
*
|
|
44
|
+
* `>` is only escaped where it would close a CDATA section, matching the
|
|
45
|
+
* "minimal escaping" rule of XSLT 1.0 section 16.1.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} value - Text content
|
|
48
|
+
* @returns {string} Escaped text
|
|
49
|
+
*/
|
|
50
|
+
export function escapeXmlText(value) {
|
|
51
|
+
return escapeWith(value, /[&<]/g, XML_TEXT_ESCAPES).replaceAll(
|
|
52
|
+
"]]>",
|
|
53
|
+
"]]>",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Escape an attribute value for the xml output method.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} value - Attribute value
|
|
61
|
+
* @returns {string} Escaped value
|
|
62
|
+
*/
|
|
63
|
+
export function escapeXmlAttribute(value) {
|
|
64
|
+
return escapeWith(value, /[&<>"\t\n\r]/g, XML_ATTRIBUTE_ESCAPES);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Escape character data for the html output method.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} value - Text content
|
|
71
|
+
* @returns {string} Escaped text
|
|
72
|
+
*/
|
|
73
|
+
export function escapeHtmlText(value) {
|
|
74
|
+
return escapeWith(value, /[&<>]/g, HTML_TEXT_ESCAPES);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Escape an attribute value for the html output method.
|
|
79
|
+
*
|
|
80
|
+
* URI attributes keep their reserved characters; only markup significant
|
|
81
|
+
* characters are escaped (XSLT 1.0 section 16.2).
|
|
82
|
+
*
|
|
83
|
+
* @param {string} value - Attribute value
|
|
84
|
+
* @returns {string} Escaped value
|
|
85
|
+
*/
|
|
86
|
+
export function escapeHtmlAttribute(value) {
|
|
87
|
+
return escapeWith(value, /[&<>"]/g, HTML_ATTRIBUTE_ESCAPES);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wrap text in a CDATA section, splitting it around any `]]>` terminator.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} value - Text content
|
|
94
|
+
* @returns {string} One or more CDATA sections
|
|
95
|
+
*/
|
|
96
|
+
export function wrapCdata(value) {
|
|
97
|
+
return `<![CDATA[${String(value).replaceAll("]]>", "]]]]><![CDATA[>")}]]>`;
|
|
98
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Output Serializer
|
|
3
|
+
*
|
|
4
|
+
* Implements the `html` output method of XSLT 1.0 section 16.2 on top of the
|
|
5
|
+
* XML writer: no XML declaration, no namespace declarations, void elements
|
|
6
|
+
* without a closing slash, minimized boolean attributes and unescaped
|
|
7
|
+
* script/style content.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
PRESERVE_SPACE_ELEMENTS,
|
|
12
|
+
RAW_TEXT_ELEMENTS,
|
|
13
|
+
TEXT_MODE,
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
import { escapeHtmlAttribute, escapeHtmlText } from "./escape.js";
|
|
16
|
+
import { XmlWriter } from "./xmlSerializer.js";
|
|
17
|
+
|
|
18
|
+
export class HtmlWriter extends XmlWriter {
|
|
19
|
+
/**
|
|
20
|
+
* The html output method never writes an XML declaration.
|
|
21
|
+
* @returns {boolean} Always false
|
|
22
|
+
*/
|
|
23
|
+
get emitsXmlDeclaration() {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The html output method never writes namespace declarations.
|
|
29
|
+
* @returns {boolean} Always false
|
|
30
|
+
*/
|
|
31
|
+
get emitsNamespaces() {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* HTML processing instructions are terminated by `>` alone.
|
|
37
|
+
* @returns {string} The HTML processing instruction terminator
|
|
38
|
+
*/
|
|
39
|
+
get piTerminator() {
|
|
40
|
+
return ">";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* HTML has no CDATA sections, so such nodes are escaped as text.
|
|
45
|
+
* @returns {string} A {@link TEXT_MODE} value
|
|
46
|
+
*/
|
|
47
|
+
get cdataNodeMode() {
|
|
48
|
+
return TEXT_MODE.ESCAPE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the document type declaration for the html output method.
|
|
53
|
+
*
|
|
54
|
+
* @param {Element|null} rootElement - Result document element
|
|
55
|
+
* @returns {string} Doctype markup, or an empty string when not applicable
|
|
56
|
+
*/
|
|
57
|
+
doctypeMarkup(rootElement) {
|
|
58
|
+
const { doctypePublic, doctypeSystem } = this.settings;
|
|
59
|
+
if (!doctypePublic && !doctypeSystem) {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const name = rootElement ? rootElement.nodeName : "html";
|
|
64
|
+
if (doctypePublic && doctypeSystem) {
|
|
65
|
+
return `<!DOCTYPE ${name} PUBLIC "${doctypePublic}" "${doctypeSystem}">`;
|
|
66
|
+
}
|
|
67
|
+
if (doctypePublic) {
|
|
68
|
+
return `<!DOCTYPE ${name} PUBLIC "${doctypePublic}">`;
|
|
69
|
+
}
|
|
70
|
+
return `<!DOCTYPE ${name} SYSTEM "${doctypeSystem}">`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Script and style content is written verbatim.
|
|
75
|
+
*
|
|
76
|
+
* @param {Element} element - Parent element
|
|
77
|
+
* @returns {string} A {@link TEXT_MODE} value
|
|
78
|
+
*/
|
|
79
|
+
childTextMode(element) {
|
|
80
|
+
return RAW_TEXT_ELEMENTS.has(String(element.localName).toLowerCase())
|
|
81
|
+
? TEXT_MODE.RAW
|
|
82
|
+
: TEXT_MODE.ESCAPE;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Content of `pre`, `script`, `style` and `textarea` is never re-indented.
|
|
87
|
+
*
|
|
88
|
+
* @param {Element} element - Element being inspected
|
|
89
|
+
* @returns {boolean} True when the content may be indented
|
|
90
|
+
*/
|
|
91
|
+
allowsIndentInside(element) {
|
|
92
|
+
return !PRESERVE_SPACE_ELEMENTS.has(
|
|
93
|
+
String(element.localName).toLowerCase(),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Void elements have no end tag; every other element gets one.
|
|
99
|
+
*
|
|
100
|
+
* @param {Element} element - Empty element
|
|
101
|
+
* @param {string} name - Element name as written
|
|
102
|
+
* @returns {string} Markup terminating the start tag
|
|
103
|
+
*/
|
|
104
|
+
emptyElementMarkup(element, name) {
|
|
105
|
+
return this.isVoidElement(element) ? ">" : `></${name}>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Boolean attributes are minimized to their name alone.
|
|
110
|
+
*
|
|
111
|
+
* @param {Attr} attribute - Attribute to write
|
|
112
|
+
* @returns {string} Attribute markup, starting with a space
|
|
113
|
+
*/
|
|
114
|
+
attributeMarkup(attribute) {
|
|
115
|
+
const { name, value } = attribute;
|
|
116
|
+
if (String(value).toLowerCase() === name.toLowerCase()) {
|
|
117
|
+
return ` ${name}`;
|
|
118
|
+
}
|
|
119
|
+
return ` ${name}="${this.escapeAttribute(value)}"`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Escape character data for HTML.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} value - Text content
|
|
126
|
+
* @returns {string} Escaped text
|
|
127
|
+
*/
|
|
128
|
+
escapeText(value) {
|
|
129
|
+
return escapeHtmlText(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Escape an attribute value for HTML.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} value - Attribute value
|
|
136
|
+
* @returns {string} Escaped value
|
|
137
|
+
*/
|
|
138
|
+
escapeAttribute(value) {
|
|
139
|
+
return escapeHtmlAttribute(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indentation Rules
|
|
3
|
+
*
|
|
4
|
+
* Decides which elements may be pretty printed when `indent="yes"` is set.
|
|
5
|
+
* Only element-only content is indented; mixed content is left untouched so
|
|
6
|
+
* that the transformation result stays character-for-character faithful.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NODE_TYPE } from "./constants.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Test whether a character data node holds only whitespace.
|
|
13
|
+
*
|
|
14
|
+
* @param {Node} node - Text node to test
|
|
15
|
+
* @returns {boolean} True when the node contains no non-whitespace character
|
|
16
|
+
*/
|
|
17
|
+
export function isWhitespaceOnlyText(node) {
|
|
18
|
+
return !/\S/.test(node.nodeValue || "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Collect the children to write when indenting an element.
|
|
23
|
+
*
|
|
24
|
+
* @param {Element} element - Element whose children are inspected
|
|
25
|
+
* @returns {Node[]|null} Children to indent, or null when the element must be
|
|
26
|
+
* serialized without any added whitespace
|
|
27
|
+
*/
|
|
28
|
+
export function getIndentableChildren(element) {
|
|
29
|
+
const indentable = [];
|
|
30
|
+
|
|
31
|
+
for (const child of element.childNodes) {
|
|
32
|
+
if (child.nodeType === NODE_TYPE.TEXT) {
|
|
33
|
+
if (isWhitespaceOnlyText(child)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
child.nodeType !== NODE_TYPE.ELEMENT &&
|
|
41
|
+
child.nodeType !== NODE_TYPE.COMMENT &&
|
|
42
|
+
child.nodeType !== NODE_TYPE.PROCESSING_INSTRUCTION
|
|
43
|
+
) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
indentable.push(child);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return indentable.length > 0 ? indentable : null;
|
|
51
|
+
}
|