@tradik/xslt-processor 1.0.3 → 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.
Files changed (47) hide show
  1. package/README.md +290 -45
  2. package/bin/lib/options.js +114 -0
  3. package/bin/lib/paths.js +186 -0
  4. package/bin/lib/transform.js +115 -0
  5. package/bin/xslt.js +68 -162
  6. package/dist/xslt-processor.browser.js +2074 -159
  7. package/dist/xslt-processor.browser.js.map +4 -4
  8. package/dist/xslt-processor.browser.min.js +6 -2
  9. package/dist/xslt-processor.browser.min.js.map +4 -4
  10. package/dist/xslt-processor.cjs +2078 -158
  11. package/dist/xslt-processor.cjs.map +4 -4
  12. package/dist/xslt-processor.d.cts +299 -0
  13. package/dist/xslt-processor.d.ts +92 -4
  14. package/dist/xslt-processor.js +2073 -157
  15. package/dist/xslt-processor.js.map +4 -4
  16. package/package.json +26 -15
  17. package/src/XSLTProcessor.js +177 -8
  18. package/src/index.js +11 -5
  19. package/src/xpath/evaluator.js +48 -7
  20. package/src/xslt/elements.js +57 -0
  21. package/src/xslt/engine.js +471 -179
  22. package/src/xslt/formatNumber.js +220 -0
  23. package/src/xslt/functions.js +191 -0
  24. package/src/xslt/index.js +31 -0
  25. package/src/xslt/keys.js +141 -0
  26. package/src/xslt/literalResult.js +167 -0
  27. package/src/xslt/number.js +178 -0
  28. package/src/xslt/numberFormat.js +155 -0
  29. package/src/xslt/resultTree.js +74 -0
  30. package/src/xslt/serializer/baseWriter.js +283 -0
  31. package/src/xslt/serializer/constants.js +78 -0
  32. package/src/xslt/serializer/escape.js +98 -0
  33. package/src/xslt/serializer/htmlSerializer.js +141 -0
  34. package/src/xslt/serializer/indent.js +51 -0
  35. package/src/xslt/serializer/namespaces.js +68 -0
  36. package/src/xslt/serializer/rawText.js +41 -0
  37. package/src/xslt/serializer/settings.js +103 -0
  38. package/src/xslt/serializer/textSerializer.js +29 -0
  39. package/src/xslt/serializer/xmlSerializer.js +127 -0
  40. package/src/xslt/serializer.js +57 -0
  41. package/src/xslt/templatePriority.js +45 -0
  42. package/src/xslt/uri.js +68 -0
  43. package/src/xslt/whitespace.js +184 -0
  44. package/src/XSLTProcessor.test.js +0 -930
  45. package/src/xpath/evaluator.test.js +0 -1852
  46. package/src/xpath/tokenizer.test.js +0 -224
  47. 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 = { "&": "&amp;", "<": "&lt;" };
9
+
10
+ const HTML_TEXT_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
11
+
12
+ const XML_ATTRIBUTE_ESCAPES = {
13
+ "&": "&amp;",
14
+ "<": "&lt;",
15
+ ">": "&gt;",
16
+ '"': "&quot;",
17
+ "\t": "&#9;",
18
+ "\n": "&#10;",
19
+ "\r": "&#13;",
20
+ };
21
+
22
+ const HTML_ATTRIBUTE_ESCAPES = {
23
+ "&": "&amp;",
24
+ "<": "&lt;",
25
+ ">": "&gt;",
26
+ '"': "&quot;",
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
+ "]]&gt;",
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
+ }