@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,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
+ }