@tradik/xslt-processor 1.1.1 → 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 (122) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +102 -757
  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 +17 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +3 -3
  9. package/bin/lib/transform.js +124 -33
  10. package/bin/xslt.js +26 -27
  11. package/dist/xslt-processor.browser.js +8784 -2720
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -6
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +8789 -2723
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +380 -21
  18. package/dist/xslt-processor.d.ts +380 -21
  19. package/dist/xslt-processor.js +8770 -2722
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +51 -11
  22. package/src/XSLTProcessor.js +343 -66
  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 +16 -4
  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 +475 -355
  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 +1 -1
  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 +176 -2020
  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 +22 -9
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +94 -15
  89. package/src/xslt/index.js +7 -1
  90. package/src/xslt/keys.js +51 -28
  91. package/src/xslt/literalResult.js +63 -7
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +171 -78
  94. package/src/xslt/numberFormat.js +124 -26
  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 +143 -6
  102. package/src/xslt/serializer/baseWriter.js +173 -66
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +14 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +49 -12
  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 +123 -25
  111. package/src/xslt/serializer/settings.js +89 -13
  112. package/src/xslt/serializer/textSerializer.js +58 -10
  113. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  114. package/src/xslt/serializer/xmlSerializer.js +113 -13
  115. package/src/xslt/serializer.js +50 -17
  116. package/src/xslt/sort.js +151 -0
  117. package/src/xslt/spaceNameTests.js +115 -0
  118. package/src/xslt/stylesheetChecks.js +206 -0
  119. package/src/xslt/stylesheetNamespaces.js +266 -0
  120. package/src/xslt/variables.js +152 -0
  121. package/src/xslt/whitespace.js +43 -27
  122. package/LICENSE +0 -29
@@ -4,15 +4,20 @@
4
4
  * Walks a result tree and turns it into markup. Everything that differs
5
5
  * between the xml, xhtml and html output methods of XSLT 1.0 section 16 is
6
6
  * delegated to the dialect hooks implemented by the concrete writers.
7
+ *
8
+ * The walk is iterative: the elements whose children are being written are
9
+ * frames on an explicit stack (see frames.js), so a result tree nested
10
+ * deeper than the JavaScript call stack allows (tens of thousands of
11
+ * levels, as deep template recursion builds) is written like any other.
7
12
  */
8
13
 
9
- import {
10
- INDENT_UNIT,
11
- NODE_TYPE,
12
- TEXT_MODE,
13
- XMLNS_NAMESPACE,
14
- } from "./constants.js";
14
+ import { NODE_TYPE, TEXT_MODE, XMLNS_NAMESPACE } from "./constants.js";
15
15
  import { wrapCdata } from "./escape.js";
16
+ import {
17
+ getOutputEncoding,
18
+ replaceUnencodable,
19
+ splitUnencodable,
20
+ } from "./encoding.js";
16
21
  import {
17
22
  collectNamespaceDeclarations,
18
23
  createNamespaceScope,
@@ -20,6 +25,22 @@ import {
20
25
  import { getIndentableChildren } from "./indent.js";
21
26
  import { isRawText } from "./rawText.js";
22
27
  import { findRootElement } from "./settings.js";
28
+ import { ChunkBuffer } from "./chunks.js";
29
+ import { ContentFrame, IndentFrame, TopLevelFrame } from "./frames.js";
30
+
31
+ /**
32
+ * Make comment text well-formed: XSLT 1.0 section 7.4 recovery inserts a
33
+ * space after any `-` followed by another `-` or ending the comment.
34
+ *
35
+ * @param {string} text - The comment text
36
+ * @returns {string} Text without `--` and without a trailing `-`
37
+ *
38
+ * @example
39
+ * safeCommentText("a--b-"); // "a- -b- "
40
+ */
41
+ export function safeCommentText(text) {
42
+ return text.replace(/-(?=-|$)/g, "- ");
43
+ }
23
44
 
24
45
  export class BaseWriter {
25
46
  /**
@@ -29,7 +50,38 @@ export class BaseWriter {
29
50
  constructor(settings, options = {}) {
30
51
  this.settings = settings;
31
52
  this.xhtml = options.xhtml === true;
32
- this.parts = [];
53
+ this.buffer = new ChunkBuffer();
54
+ this.encoding = getOutputEncoding(settings.encoding);
55
+ this.reference = (codePoint) => this.characterReference(codePoint);
56
+ }
57
+
58
+ /**
59
+ * Replace the characters the output encoding cannot represent with
60
+ * references (XSLT 1.0 section 16.1). Only escaped character data and
61
+ * attribute values go through here: comments, processing instructions and
62
+ * unescaped text cannot hold references and are written unchanged.
63
+ *
64
+ * @param {string} text - Escaped text or attribute value
65
+ * @returns {string} Text holding only representable characters
66
+ */
67
+ encodeReferences(text) {
68
+ return replaceUnencodable(text, this.encoding, this.reference);
69
+ }
70
+
71
+ /**
72
+ * Write text as CDATA sections. A character the output encoding cannot
73
+ * represent ends the section and is written as a reference between two
74
+ * sections, since a CDATA section cannot hold references.
75
+ *
76
+ * @param {string} value - Text content
77
+ * @returns {string} CDATA sections and references
78
+ */
79
+ cdataMarkup(value) {
80
+ return splitUnencodable(value, this.encoding)
81
+ .map(({ text, representable }) =>
82
+ representable ? wrapCdata(text) : this.reference(text.codePointAt(0)),
83
+ )
84
+ .join("");
33
85
  }
34
86
 
35
87
  /**
@@ -39,10 +91,65 @@ export class BaseWriter {
39
91
  * @returns {string} Serialized output
40
92
  */
41
93
  serialize(node) {
42
- this.parts = [];
94
+ let output = "";
95
+ for (const chunk of this.chunks(node, Infinity)) output += chunk;
96
+ return output;
97
+ }
98
+
99
+ /**
100
+ * Serialize a result tree node incrementally: a chunk is yielded as soon
101
+ * as `chunkSize` code units are written, so the first bytes are available
102
+ * before the whole tree has been written and the output is never held as
103
+ * one string.
104
+ *
105
+ * @param {Node} node - Document, fragment or element to serialize
106
+ * @param {number} [chunkSize] - Chunk size in UTF-16 code units (see
107
+ * chunks.js); Infinity yields the whole output as one chunk
108
+ * @yields {string} Non-empty chunks of at most `chunkSize` code units (one
109
+ * more when a surrogate pair straddles the boundary)
110
+ * @returns {Generator<string, void, void>} The chunks, in order
111
+ */
112
+ *chunks(node, chunkSize) {
113
+ this.buffer = new ChunkBuffer(chunkSize);
43
114
  this.writeProlog(node);
44
- this.writeNode(node, createNamespaceScope(), 0, TEXT_MODE.ESCAPE);
45
- return this.parts.join("");
115
+
116
+ const open = [];
117
+ const root = this.openNode(
118
+ node,
119
+ createNamespaceScope(),
120
+ 0,
121
+ TEXT_MODE.ESCAPE,
122
+ );
123
+ if (root) open.push(root);
124
+
125
+ while (open.length > 0) {
126
+ const frame = open[open.length - 1];
127
+ const child = frame.nextChild(this);
128
+ if (child) {
129
+ const opened = this.openNode(
130
+ child,
131
+ frame.scope,
132
+ frame.depth,
133
+ frame.textMode,
134
+ );
135
+ if (opened) open.push(opened);
136
+ } else {
137
+ open.pop();
138
+ this.write(frame.end);
139
+ }
140
+ if (this.buffer.full) yield* this.buffer.take();
141
+ }
142
+ yield* this.buffer.take(true);
143
+ }
144
+
145
+ /**
146
+ * Append markup to the output.
147
+ *
148
+ * @param {string} text - Markup
149
+ * @returns {void}
150
+ */
151
+ write(text) {
152
+ this.buffer.write(text);
46
153
  }
47
154
 
48
155
  /**
@@ -55,117 +162,116 @@ export class BaseWriter {
55
162
  if (this.emitsXmlDeclaration) {
56
163
  const { version, encoding, standalone } = this.settings;
57
164
  const standalonePart = standalone ? ` standalone="${standalone}"` : "";
58
- this.parts.push(
165
+ this.write(
59
166
  `<?xml version="${version}" encoding="${encoding}"${standalonePart}?>\n`,
60
167
  );
61
168
  }
62
169
 
63
170
  const doctype = this.doctypeMarkup(findRootElement(node));
64
171
  if (doctype) {
65
- this.parts.push(`${doctype}\n`);
172
+ this.write(`${doctype}\n`);
66
173
  }
67
174
  }
68
175
 
69
176
  /**
70
- * Write any result tree node.
177
+ * Start writing any result tree node. Leaves (character data, comments,
178
+ * processing instructions) are written at once; for an element with
179
+ * children or a result root, the start is written and the frame of its
180
+ * children is returned for the walk in {@link BaseWriter#chunks}.
71
181
  *
72
182
  * @param {Node} node - Node to write
73
183
  * @param {Map<string, string>} scope - Namespace scope in effect
74
184
  * @param {number} depth - Current indentation depth
75
185
  * @param {string} textMode - {@link TEXT_MODE} for character data children
76
- * @returns {void}
186
+ * @returns {object|null} The frame of the children (see frames.js), or
187
+ * null when the node is written completely
77
188
  */
78
- writeNode(node, scope, depth, textMode) {
189
+ openNode(node, scope, depth, textMode) {
79
190
  switch (node.nodeType) {
80
191
  case NODE_TYPE.ELEMENT:
81
- this.writeElement(node, scope, depth);
82
- break;
192
+ return this.openElement(node, scope, depth);
83
193
  case NODE_TYPE.TEXT:
84
194
  case NODE_TYPE.CDATA_SECTION:
85
195
  this.writeText(node, textMode);
86
196
  break;
87
197
  case NODE_TYPE.COMMENT:
88
- this.parts.push(`<!--${node.nodeValue}-->`);
198
+ this.write(`<!--${safeCommentText(node.nodeValue)}-->`);
89
199
  break;
90
200
  case NODE_TYPE.PROCESSING_INSTRUCTION:
91
201
  this.writeProcessingInstruction(node);
92
202
  break;
93
203
  case NODE_TYPE.DOCUMENT:
94
204
  case NODE_TYPE.DOCUMENT_FRAGMENT:
95
- this.writeChildNodes(node, scope, depth, textMode);
96
- break;
205
+ return new TopLevelFrame(
206
+ node,
207
+ scope,
208
+ depth,
209
+ textMode,
210
+ this.topLevelLineBreaks === true,
211
+ );
97
212
  default:
98
213
  break;
99
214
  }
215
+ return null;
100
216
  }
101
217
 
102
218
  /**
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.
219
+ * Write the start tag of an element with its namespaces and attributes,
220
+ * or the whole element when it is empty.
119
221
  *
120
222
  * @param {Element} element - Element to write
121
223
  * @param {Map<string, string>} scope - Namespace scope inherited from the parent
122
224
  * @param {number} depth - Current indentation depth
123
- * @returns {void}
225
+ * @returns {object|null} The frame of its children, null when empty
124
226
  */
125
- writeElement(element, scope, depth) {
126
- const namespaces = this.emitsNamespaces
127
- ? collectNamespaceDeclarations(element, scope)
128
- : { declarations: [], scope };
227
+ openElement(element, scope, depth) {
228
+ const namespaces = collectNamespaceDeclarations(element, scope);
129
229
  const name = element.nodeName;
130
230
 
131
- this.parts.push(
132
- `<${name}${this.namespaceMarkup(namespaces.declarations)}` +
231
+ this.write(
232
+ `<${name}${this.namespaceMarkup(namespaces.declarations, element)}` +
133
233
  this.attributesMarkup(element),
134
234
  );
135
235
 
136
- if (!element.firstChild) {
137
- this.parts.push(this.emptyElementMarkup(element, name));
138
- return;
236
+ const leading = this.leadingChildMarkup(element);
237
+ if (!element.firstChild && !leading) {
238
+ this.write(this.emptyElementMarkup(element, name));
239
+ return null;
139
240
  }
140
241
 
141
- this.parts.push(">");
142
- this.writeElementChildren(element, namespaces.scope, depth);
143
- this.parts.push(`</${name}>`);
242
+ this.write(">");
243
+ return this.childrenFrame(
244
+ element,
245
+ namespaces.scope,
246
+ depth,
247
+ leading,
248
+ `</${name}>`,
249
+ );
144
250
  }
145
251
 
146
252
  /**
147
- * Write the children of an element, indenting element-only content.
253
+ * Frame of the children of an element, indenting element-only content.
148
254
  *
149
255
  * @param {Element} element - Parent element
150
256
  * @param {Map<string, string>} scope - Namespace scope in effect
151
257
  * @param {number} depth - Depth of the parent element
152
- * @returns {void}
258
+ * @param {string} leading - Markup the serializer adds before the
259
+ * children, indented like a child (see leadingChildMarkup)
260
+ * @param {string} endTag - End tag of the element
261
+ * @returns {ContentFrame|IndentFrame} The frame
153
262
  */
154
- writeElementChildren(element, scope, depth) {
263
+ childrenFrame(element, scope, depth, leading, endTag) {
155
264
  const textMode = this.childTextMode(element);
156
265
  const indentable = this.indentableChildren(element, textMode);
157
266
 
158
267
  if (!indentable) {
159
- this.writeChildNodes(element, scope, depth, textMode);
160
- return;
268
+ this.write(leading);
269
+ return new ContentFrame(element, scope, depth, textMode, endTag);
161
270
  }
162
271
 
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)}`);
272
+ const frame = new IndentFrame(indentable, scope, depth, textMode, endTag);
273
+ if (leading) this.write(frame.lineStart + leading);
274
+ return frame;
169
275
  }
170
276
 
171
277
  /**
@@ -189,9 +295,10 @@ export class BaseWriter {
189
295
  * Build the namespace declaration markup of an element.
190
296
  *
191
297
  * @param {Array<{prefix: string, uri: string}>} declarations - Declarations
298
+ * @param {Element} [_element] - The element they are written on
192
299
  * @returns {string} Attribute markup, starting with a space when non-empty
193
300
  */
194
- namespaceMarkup(declarations) {
301
+ namespaceMarkup(declarations, _element) {
195
302
  return declarations
196
303
  .map(({ prefix, uri }) => {
197
304
  const name = prefix ? `xmlns:${prefix}` : "xmlns";
@@ -239,17 +346,17 @@ export class BaseWriter {
239
346
  const value = node.nodeValue || "";
240
347
 
241
348
  if (isRawText(node)) {
242
- this.parts.push(value);
349
+ this.write(value);
243
350
  return;
244
351
  }
245
352
 
246
353
  const mode = this.resolveTextMode(node, textMode);
247
354
  if (mode === TEXT_MODE.CDATA) {
248
- this.parts.push(wrapCdata(value));
355
+ this.write(this.cdataMarkup(value));
249
356
  } else if (mode === TEXT_MODE.RAW) {
250
- this.parts.push(value);
357
+ this.write(value);
251
358
  } else {
252
- this.parts.push(this.escapeText(value));
359
+ this.write(this.escapeText(value));
253
360
  }
254
361
  }
255
362
 
@@ -278,6 +385,6 @@ export class BaseWriter {
278
385
  writeProcessingInstruction(node) {
279
386
  const data = node.nodeValue || "";
280
387
  const separator = data ? " " : "";
281
- this.parts.push(`<?${node.target}${separator}${data}${this.piTerminator}`);
388
+ this.write(`<?${node.target}${separator}${data}${this.piTerminator}`);
282
389
  }
283
390
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Output Chunking
3
+ *
4
+ * The serializers write markup into a {@link ChunkBuffer}, which hands it out
5
+ * in chunks of a bounded size: the streaming API (`serializeChunks`,
6
+ * `transformToStream`) yields each chunk as soon as it is full, while
7
+ * `serializeResult` uses an unbounded buffer and joins everything once. One
8
+ * writer implementation serves both.
9
+ *
10
+ * @module xslt/serializer/chunks
11
+ */
12
+
13
+ /** Default chunk size, in UTF-16 code units (16 KiB of ASCII). */
14
+ export const DEFAULT_CHUNK_SIZE = 16384;
15
+
16
+ /**
17
+ * Validate a chunk size.
18
+ *
19
+ * @param {number} [size] - Requested size, {@link DEFAULT_CHUNK_SIZE} when absent
20
+ * @returns {number} A positive integer, or Infinity (one chunk)
21
+ * @throws {RangeError} For anything else
22
+ *
23
+ * @example
24
+ * toChunkSize(undefined); // 16384
25
+ */
26
+ export function toChunkSize(size) {
27
+ if (size === undefined) return DEFAULT_CHUNK_SIZE;
28
+ if (size === Infinity || (Number.isInteger(size) && size > 0)) return size;
29
+ throw new RangeError(
30
+ `chunkSize must be a positive integer or Infinity, got ${size}`,
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Whether a UTF-16 code unit is a high (leading) surrogate.
36
+ *
37
+ * @param {number} unit - The code unit
38
+ * @returns {boolean} True for U+D800..U+DBFF
39
+ */
40
+ function isHighSurrogate(unit) {
41
+ return unit >= 0xd800 && unit <= 0xdbff;
42
+ }
43
+
44
+ /**
45
+ * End of the next chunk of `text` starting at `start`, moved so that a
46
+ * surrogate pair is never split (a chunk encoded on its own, e.g. with
47
+ * TextEncoder, would otherwise turn each half into U+FFFD).
48
+ *
49
+ * @param {string} text - The buffered text
50
+ * @param {number} start - Start of the chunk
51
+ * @param {number} size - Chunk size
52
+ * @returns {number} End of the chunk (exclusive)
53
+ */
54
+ function chunkEnd(text, start, size) {
55
+ const end = start + size;
56
+ if (!isHighSurrogate(text.charCodeAt(end - 1))) return end;
57
+ return end - 1 > start ? end - 1 : end + 1;
58
+ }
59
+
60
+ export class ChunkBuffer {
61
+ /**
62
+ * @param {number} [chunkSize] - Chunk size in code units; Infinity keeps
63
+ * everything for one final chunk
64
+ */
65
+ constructor(chunkSize = Infinity) {
66
+ this.chunkSize = toChunkSize(chunkSize);
67
+ this.parts = [];
68
+ this.length = 0;
69
+ }
70
+
71
+ /**
72
+ * Append text.
73
+ *
74
+ * @param {string} text - Markup or character data
75
+ * @returns {void}
76
+ */
77
+ write(text) {
78
+ this.parts.push(text);
79
+ this.length += text.length;
80
+ }
81
+
82
+ /**
83
+ * Whether at least one full chunk is buffered.
84
+ *
85
+ * @returns {boolean} True when {@link ChunkBuffer#take} would yield
86
+ */
87
+ get full() {
88
+ return this.length >= this.chunkSize;
89
+ }
90
+
91
+ /**
92
+ * Yield the buffered text as chunks of `chunkSize` code units (one more
93
+ * when a surrogate pair straddles the boundary). The last partial chunk is
94
+ * kept for later unless `final` is set.
95
+ *
96
+ * @param {boolean} [final] - Also yield the partial last chunk
97
+ * @yields {string} Non-empty chunks
98
+ * @returns {Generator<string, void, void>} The chunks
99
+ *
100
+ * @example
101
+ * const buffer = new ChunkBuffer(2);
102
+ * buffer.write("abcde");
103
+ * [...buffer.take(true)]; // ["ab", "cd", "e"]
104
+ */
105
+ *take(final = false) {
106
+ const text = this.parts.length === 1 ? this.parts[0] : this.parts.join("");
107
+ this.parts = [];
108
+ this.length = 0;
109
+ let start = 0;
110
+ while (text.length - start >= this.chunkSize) {
111
+ const end = chunkEnd(text, start, this.chunkSize);
112
+ yield text.slice(start, end);
113
+ start = end;
114
+ }
115
+ const rest = text.slice(start);
116
+ if (!rest) return;
117
+ if (final) yield rest;
118
+ else this.write(rest);
119
+ }
120
+ }
@@ -37,6 +37,9 @@ export const TEXT_MODE = {
37
37
  RAW: "raw",
38
38
  };
39
39
 
40
+ /** Namespace of XHTML elements. */
41
+ export const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
42
+
40
43
  /**
41
44
  * HTML elements that never have an end tag.
42
45
  */
@@ -57,6 +60,17 @@ export const VOID_ELEMENTS = new Set([
57
60
  "wbr",
58
61
  ]);
59
62
 
63
+ /**
64
+ * HTML attributes holding URIs, which the html output method %-escapes
65
+ * (XSLT 1.0 section 16.2): the attributes libxml2 (and so Chrome) escapes,
66
+ * on any element. `name` is a URI attribute on `a` only (see
67
+ * {@link URI_ATTRIBUTES_OF_A}).
68
+ */
69
+ export const URI_ATTRIBUTES = new Set(["action", "href", "src"]);
70
+
71
+ /** Further URI attributes of the `a` element (libxml2). */
72
+ export const URI_ATTRIBUTES_OF_A = new Set(["name"]);
73
+
60
74
  /**
61
75
  * HTML elements whose character data must not be escaped.
62
76
  */