@stll/folio-core 0.65.0 → 0.66.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 (43) hide show
  1. package/dist/ai-edits/apply.js +31 -9
  2. package/dist/docx/attributeRemainder.js +1 -1
  3. package/dist/docx/documentSectionFacts.js +1 -1
  4. package/dist/docx/ensureParaIds.js +2 -2
  5. package/dist/docx/importedIdCollisions.js +1 -1
  6. package/dist/docx/mathToMathml.js +1 -1
  7. package/dist/docx/numericIdNormalization.js +2 -2
  8. package/dist/docx/parser.js +1 -1
  9. package/dist/docx/revisionIdNormalization.js +2 -2
  10. package/dist/docx/rezip.js +1 -1
  11. package/dist/docx/sdtProperties.js +1 -2
  12. package/dist/docx/selectedXmlIdentityScan.d.ts +1 -1
  13. package/dist/docx/selectedXmlIdentityScan.js +1 -1
  14. package/dist/docx/selectiveXmlPatch.js +2 -2
  15. package/dist/docx/server/boundedArchive.d.ts +1 -1
  16. package/dist/docx/server/boundedArchive.js +1 -1
  17. package/dist/docx/server/validateDocxConformance.js +1 -1
  18. package/dist/docx/shapeAlternateContent.js +1 -1
  19. package/dist/docx/storyBlockReplay.js +1 -1
  20. package/dist/docx/unzip.js +1 -1
  21. package/dist/docx/verbatimCapture.js +1 -1
  22. package/dist/docx/xmlParser.d.ts +5 -36
  23. package/dist/docx/xmlParser.js +14 -78
  24. package/dist/docx/xmlSafety.js +5 -5
  25. package/dist/layout-painter/renderPage.js +4 -2
  26. package/dist/markdown/countParagraphList.d.ts +18 -0
  27. package/dist/markdown/countParagraphList.js +90 -0
  28. package/dist/markdown/renderParagraph.js +2 -62
  29. package/dist/markdown/renderRuns.js +4 -1
  30. package/dist/prosemirror/conversion/fromProseDoc.js +5 -3
  31. package/dist/prosemirror/conversion/toProseDoc.js +4 -1
  32. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -1
  33. package/dist/prosemirror/replacedAnnotations.d.ts +6 -0
  34. package/dist/prosemirror/replacedAnnotations.js +30 -8
  35. package/dist/prosemirror/schema/nodes.d.ts +7 -0
  36. package/dist/prosemirror/textBoxAnchorAttrs.js +9 -1
  37. package/package.json +2 -3
  38. package/dist/docx/streamingXmlParser.d.ts +0 -60
  39. package/dist/docx/streamingXmlParser.js +0 -406
  40. package/dist/docx/xmlNamespaceContext.d.ts +0 -6
  41. package/dist/docx/xmlNamespaceContext.js +0 -53
  42. package/dist/docx/xmlResourceLimits.d.ts +0 -107
  43. package/dist/docx/xmlResourceLimits.js +0 -194
@@ -1,406 +0,0 @@
1
- import { EMPTY_NAMESPACE_SCOPE, attachXmlNamespaceContext } from "./xmlNamespaceContext.js";
2
- import { FOLIO_XML_RESOURCE_LIMITS } from "./xmlResourceLimits.js";
3
- import { isOoxmlNumericIdAttributeName } from "@stll/docx-core";
4
- // src/docx/streamingXmlParser.ts
5
- const sourceRanges = /* @__PURE__ */ new WeakMap();
6
- /** Exact element source ranges, available only from the opt-in source-range reader. */
7
- const getXmlSourceRange = (element) => sourceRanges.get(element);
8
- const BUILT_IN_ENTITIES = {
9
- amp: "&",
10
- apos: "'",
11
- gt: ">",
12
- lt: "<",
13
- quot: "\""
14
- };
15
- const parseStreamingXmlInternal = (options) => {
16
- const { xml, inheritedNamespaceScope = EMPTY_NAMESPACE_SCOPE } = options;
17
- const visitOpenTag = options.visitOpenTag;
18
- let spanMode = "none";
19
- if (visitOpenTag !== void 0) spanMode = options.mode === "tree" ? "identities" : "all";
20
- if (options.visitElement !== void 0) spanMode = "all";
21
- const openTagOptions = {
22
- xml,
23
- start: 0,
24
- close: 0,
25
- spanMode,
26
- retainAttribute: options.mode === "attributes" ? options.retainAttribute : void 0
27
- };
28
- const root = { elements: [] };
29
- const stack = [];
30
- const replacements = [];
31
- let cursor = 0;
32
- let mergeAdjacentText = false;
33
- const acceptText = (text, merge) => {
34
- if (options.mode === "tree") return appendText(text, stack, merge);
35
- const decoded = decodeXmlEntities(normalizeLineEndings(text));
36
- return decoded !== null && (stack.length > 0 || decoded.trim().length === 0);
37
- };
38
- while (cursor < xml.length) {
39
- const open = xml.indexOf("<", cursor);
40
- if (open === -1) {
41
- if (!acceptText(xml.slice(cursor), mergeAdjacentText)) return { status: "unsupported" };
42
- break;
43
- }
44
- if (open > cursor && !acceptText(xml.slice(cursor, open), mergeAdjacentText)) return { status: "unsupported" };
45
- if (open > cursor) mergeAdjacentText = true;
46
- if (xml.startsWith("<!--", open)) {
47
- const close = xml.indexOf("-->", open + 4);
48
- if (close === -1) return { status: "unsupported" };
49
- cursor = close + 3;
50
- continue;
51
- }
52
- if (xml.startsWith("<![CDATA[", open)) {
53
- const close = xml.indexOf("]]>", open + 9);
54
- const text = normalizeLineEndings(xml.slice(open + 9, close));
55
- if (close === -1 || !(options.mode === "tree" ? appendRawText(text, stack) : stack.length > 0 || text.trim().length === 0)) return { status: "unsupported" };
56
- cursor = close + 3;
57
- mergeAdjacentText = false;
58
- continue;
59
- }
60
- if (xml.startsWith("<?", open)) {
61
- const close = xml.indexOf("?>", open + 2);
62
- if (close === -1) return { status: "unsupported" };
63
- cursor = close + 2;
64
- mergeAdjacentText = false;
65
- continue;
66
- }
67
- if (xml.startsWith("<!", open)) return { status: "unsupported" };
68
- const close = findTagClose(xml, open + 1);
69
- if (close === -1) return { status: "unsupported" };
70
- if (xml.charCodeAt(open + 1) === 47) {
71
- const name = xml.slice(open + 2, close).trim();
72
- const frame = stack.pop();
73
- if (!frame || frame.name !== name) return { status: "unsupported" };
74
- if (frame.start !== void 0) sourceRanges.set(frame.element, {
75
- start: frame.start,
76
- end: close + 1
77
- });
78
- cursor = close + 1;
79
- mergeAdjacentText = false;
80
- continue;
81
- }
82
- openTagOptions.start = open + 1;
83
- openTagOptions.close = close;
84
- const parsedTag = parseOpenTag(openTagOptions);
85
- if (parsedTag.status === "unsupported") return parsedTag;
86
- const parent = stack.at(-1)?.element;
87
- const scope = parent?.namespaceScope ?? inheritedNamespaceScope;
88
- if (options.mode === "attributes" && options.retainAttribute !== void 0 && parsedTag.element.attributes === void 0) parsedTag.element.namespaceScope = scope;
89
- else attachXmlNamespaceContext(parsedTag.element, scope);
90
- options.visitElement?.({
91
- element: parsedTag.element,
92
- attributeValueSpans: parsedTag.attributeValueSpans ?? /* @__PURE__ */ new Map(),
93
- nameEnd: parsedTag.nameEnd,
94
- parent
95
- });
96
- const rewritten = (options.mode === "tree" ? parsedTag.attributeValueSpans !== void 0 : options.retainAttribute === void 0 || parsedTag.element.attributes !== void 0) ? visitOpenTag?.(parsedTag.element, parsedTag.attributeValueSpans ?? /* @__PURE__ */ new Map()) : null;
97
- if (rewritten) for (const [attributeName, value] of rewritten) {
98
- const span = parsedTag.attributeValueSpans?.get(attributeName);
99
- if (!span) return { status: "unsupported" };
100
- replacements.push({
101
- ...span,
102
- value
103
- });
104
- }
105
- if (options.mode === "tree") appendElement(parent ?? root, parsedTag.element);
106
- if (!parsedTag.selfClosing) {
107
- if (stack.length >= FOLIO_XML_RESOURCE_LIMITS.maxDepth) return { status: "unsupported" };
108
- stack.push(options.mode === "tree" && options.sourceRanges === "tracked" ? {
109
- element: parsedTag.element,
110
- name: parsedTag.name,
111
- start: open
112
- } : {
113
- element: parsedTag.element,
114
- name: parsedTag.name
115
- });
116
- } else if (options.mode === "tree" && options.sourceRanges === "tracked") sourceRanges.set(parsedTag.element, {
117
- start: open,
118
- end: close + 1
119
- });
120
- cursor = close + 1;
121
- mergeAdjacentText = false;
122
- }
123
- if (stack.length > 0) return { status: "unsupported" };
124
- return {
125
- status: "parsed",
126
- value: root,
127
- replacements
128
- };
129
- };
130
- /**
131
- * @param inheritedNamespaceScope the bindings in scope around `xml`, for a
132
- * fragment captured out of a part whose root declared them.
133
- */
134
- const parseStreamingXml = (xml, inheritedNamespaceScope = EMPTY_NAMESPACE_SCOPE) => {
135
- const parsed = parseStreamingXmlInternal({
136
- xml,
137
- mode: "tree",
138
- inheritedNamespaceScope
139
- });
140
- return parsed.status === "parsed" ? {
141
- status: "parsed",
142
- value: parsed.value
143
- } : { status: "unsupported" };
144
- };
145
- /** Retain source spans for every element without changing the parsed namespace tree. */
146
- const parseStreamingXmlWithSourceRanges = (xml, inheritedNamespaceScope = EMPTY_NAMESPACE_SCOPE) => {
147
- const parsed = parseStreamingXmlInternal({
148
- xml,
149
- mode: "tree",
150
- inheritedNamespaceScope,
151
- sourceRanges: "tracked"
152
- });
153
- return parsed.status === "parsed" ? {
154
- status: "parsed",
155
- value: parsed.value
156
- } : { status: "unsupported" };
157
- };
158
- /** Retain the body tree while collecting identities for import repair. */
159
- const parseStreamingXmlWithIdentityVisitor = (xml, visitOpenTag) => {
160
- const parsed = parseStreamingXmlInternal({
161
- xml,
162
- mode: "tree",
163
- visitOpenTag
164
- });
165
- return parsed.status === "parsed" ? {
166
- status: "parsed",
167
- value: parsed.value
168
- } : parsed;
169
- };
170
- /** Rewrite selected decimal attribute values while preserving every other source byte. */
171
- const rewriteStreamingXmlDecimalAttributes = (xml, visitOpenTag) => {
172
- const parsed = parseStreamingXmlInternal({
173
- xml,
174
- mode: "attributes",
175
- visitOpenTag
176
- });
177
- if (parsed.status === "unsupported") return parsed;
178
- const chunks = [];
179
- let cursor = 0;
180
- for (const replacement of parsed.replacements.toSorted((left, right) => left.start - right.start)) {
181
- if (!/^\d+$/u.test(replacement.value)) return { status: "unsupported" };
182
- chunks.push(xml.slice(cursor, replacement.start), replacement.value);
183
- cursor = replacement.end;
184
- }
185
- chunks.push(xml.slice(cursor));
186
- return {
187
- status: "rewritten",
188
- value: chunks.join("")
189
- };
190
- };
191
- /** Visit identity attributes and namespace bindings without building an XML tree. */
192
- const scanStreamingXmlNumericIdAttributes = (xml, visitOpenTag) => {
193
- const parsed = parseStreamingXmlInternal({
194
- xml,
195
- mode: "attributes",
196
- visitOpenTag,
197
- retainAttribute: ({ attributeName, elementName }) => {
198
- if (attributeName === "xmlns" || attributeName.startsWith("xmlns:")) return true;
199
- return isOoxmlNumericIdAttributeName({
200
- elementName,
201
- attributeName
202
- });
203
- }
204
- });
205
- return parsed.status === "parsed" ? { status: "scanned" } : parsed;
206
- };
207
- /** Visit namespace-resolved start tags and source spans without building a tree. */
208
- const scanStreamingXmlElements = (xml, visitElement) => {
209
- const parsed = parseStreamingXmlInternal({
210
- xml,
211
- mode: "attributes",
212
- visitOpenTag: () => null,
213
- visitElement
214
- });
215
- return parsed.status === "parsed" ? { status: "scanned" } : parsed;
216
- };
217
- const parseOpenTag = ({ xml, start, close, spanMode, retainAttribute }) => {
218
- let cursor = skipWhitespace(xml, start, close);
219
- const nameStart = cursor;
220
- cursor = scanName(xml, cursor, close);
221
- if (cursor === nameStart) return { status: "unsupported" };
222
- const name = xml.slice(nameStart, cursor);
223
- const nameEnd = cursor;
224
- if (isUnsafePropertyName(name)) return { status: "unsupported" };
225
- let attributes;
226
- let attributeValueSpans;
227
- let selfClosing = false;
228
- while (cursor < close) {
229
- cursor = skipWhitespace(xml, cursor, close);
230
- if (cursor >= close) break;
231
- if (xml.charCodeAt(cursor) === 47) {
232
- selfClosing = true;
233
- cursor = skipWhitespace(xml, cursor + 1, close);
234
- if (cursor !== close) return { status: "unsupported" };
235
- break;
236
- }
237
- const attributeNameStart = cursor;
238
- cursor = scanName(xml, cursor, close);
239
- if (cursor === attributeNameStart) return { status: "unsupported" };
240
- const attributeName = xml.slice(attributeNameStart, cursor);
241
- if (isUnsafePropertyName(attributeName)) return { status: "unsupported" };
242
- cursor = skipWhitespace(xml, cursor, close);
243
- if (xml.charCodeAt(cursor) !== 61) return { status: "unsupported" };
244
- cursor = skipWhitespace(xml, cursor + 1, close);
245
- const quote = xml.charCodeAt(cursor);
246
- if (quote !== 34 && quote !== 39) return { status: "unsupported" };
247
- const valueStart = cursor + 1;
248
- cursor = xml.indexOf(quote === 34 ? "\"" : "'", valueStart);
249
- if (cursor === -1 || cursor > close) return { status: "unsupported" };
250
- const decoded = decodeXmlEntities(normalizeLineEndings(xml.slice(valueStart, cursor)));
251
- if (decoded === null) return { status: "unsupported" };
252
- if (retainAttribute === void 0 || retainAttribute({
253
- attributeName,
254
- elementName: name
255
- })) {
256
- attributes ??= {};
257
- attributes[attributeName] = decoded;
258
- if (spanMode === "all" || spanMode === "identities" && isOoxmlNumericIdAttributeName({
259
- elementName: name,
260
- attributeName
261
- })) {
262
- attributeValueSpans ??= /* @__PURE__ */ new Map();
263
- attributeValueSpans.set(attributeName, {
264
- start: valueStart,
265
- end: cursor
266
- });
267
- }
268
- }
269
- cursor += 1;
270
- }
271
- const element = {
272
- type: "element",
273
- name
274
- };
275
- if (attributes) element.attributes = attributes;
276
- return {
277
- status: "parsed",
278
- element,
279
- name,
280
- nameEnd,
281
- selfClosing,
282
- attributeValueSpans
283
- };
284
- };
285
- const appendElement = (parent, child) => {
286
- parent.elements ??= [];
287
- parent.elements.push(child);
288
- };
289
- const appendText = (text, stack, mergeAdjacentText) => {
290
- if (text.length === 0) return true;
291
- const decoded = decodeXmlEntities(normalizeLineEndings(text));
292
- if (decoded === null) return false;
293
- const parent = stack.at(-1)?.element;
294
- const previous = mergeAdjacentText ? parent?.elements?.at(-1) : void 0;
295
- if (previous?.type === "text" && typeof previous.text === "string") {
296
- previous.text += decoded;
297
- return true;
298
- }
299
- return appendRawText(decoded, stack);
300
- };
301
- const appendRawText = (text, stack) => {
302
- const parent = stack.at(-1)?.element;
303
- if (!parent) return text.trim().length === 0;
304
- appendElement(parent, {
305
- type: "text",
306
- text
307
- });
308
- return true;
309
- };
310
- const findTagClose = (xml, start) => {
311
- let quote = 0;
312
- for (let cursor = start; cursor < xml.length; cursor += 1) {
313
- const code = xml.charCodeAt(cursor);
314
- if (quote !== 0) {
315
- if (code === quote) quote = 0;
316
- continue;
317
- }
318
- if (code === 34 || code === 39) {
319
- quote = code;
320
- continue;
321
- }
322
- if (code === 62) return cursor;
323
- }
324
- return -1;
325
- };
326
- const skipWhitespace = (xml, start, limit) => {
327
- let cursor = start;
328
- while (cursor < limit) {
329
- const code = xml.charCodeAt(cursor);
330
- if (code !== 9 && code !== 10 && code !== 13 && code !== 32) break;
331
- cursor += 1;
332
- }
333
- return cursor;
334
- };
335
- const scanName = (xml, start, limit) => {
336
- let cursor = start;
337
- while (cursor < limit) {
338
- const code = xml.charCodeAt(cursor);
339
- if (code === 9 || code === 10 || code === 13 || code === 32 || code === 47 || code === 61 || code === 62) break;
340
- cursor += 1;
341
- }
342
- return cursor;
343
- };
344
- const decodeXmlEntities = (value) => {
345
- const firstEntity = value.indexOf("&");
346
- if (firstEntity === -1) return value;
347
- const parts = [];
348
- let cursor = 0;
349
- let entityStart = firstEntity;
350
- while (entityStart !== -1) {
351
- parts.push(value.slice(cursor, entityStart));
352
- const entityEnd = value.indexOf(";", entityStart + 1);
353
- if (entityEnd === -1) return null;
354
- const entity = value.slice(entityStart + 1, entityEnd);
355
- const decoded = decodeXmlEntity(entity);
356
- if (decoded === null) return null;
357
- parts.push(decoded);
358
- cursor = entityEnd + 1;
359
- entityStart = value.indexOf("&", cursor);
360
- }
361
- parts.push(value.slice(cursor));
362
- return parts.join("");
363
- };
364
- const normalizeLineEndings = (value) => {
365
- if (!value.includes("\r")) return value;
366
- return value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
367
- };
368
- const decodeXmlEntity = (entity) => {
369
- switch (entity) {
370
- case "amp": return BUILT_IN_ENTITIES.amp;
371
- case "apos": return BUILT_IN_ENTITIES.apos;
372
- case "gt": return BUILT_IN_ENTITIES.gt;
373
- case "lt": return BUILT_IN_ENTITIES.lt;
374
- case "quot": return BUILT_IN_ENTITIES.quot;
375
- }
376
- if (entity.charCodeAt(0) !== 35) return null;
377
- const hexadecimal = entity.charCodeAt(1) === 120 || entity.charCodeAt(1) === 88;
378
- const digits = entity.slice(hexadecimal ? 2 : 1);
379
- if (digits.length === 0 || !hasOnlyDigits(digits, hexadecimal)) return null;
380
- const codePoint = Number.parseInt(digits, hexadecimal ? 16 : 10);
381
- if (!Number.isInteger(codePoint) || codePoint <= 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343) return null;
382
- return String.fromCodePoint(codePoint);
383
- };
384
- const hasOnlyDigits = (value, hexadecimal) => {
385
- for (let index = 0; index < value.length; index += 1) {
386
- const code = value.charCodeAt(index);
387
- if (!(code >= 48 && code <= 57) && !(hexadecimal && code >= 97 && code <= 102) && !(hexadecimal && code >= 65 && code <= 70)) return false;
388
- }
389
- return true;
390
- };
391
- const isUnsafePropertyName = (name) => {
392
- switch (name) {
393
- case "__defineGetter__":
394
- case "__defineSetter__":
395
- case "__lookupGetter__":
396
- case "__lookupSetter__":
397
- case "__proto__":
398
- case "constructor":
399
- case "hasOwnProperty":
400
- case "prototype":
401
- case "toString":
402
- case "valueOf": return true;
403
- default: return false;
404
- }
405
- };
406
- export { getXmlSourceRange, parseStreamingXml, parseStreamingXmlWithIdentityVisitor, parseStreamingXmlWithSourceRanges, rewriteStreamingXmlDecimalAttributes, scanStreamingXmlElements, scanStreamingXmlNumericIdAttributes };
@@ -1,6 +0,0 @@
1
- import { XmlElement, XmlNamespaceScope } from "./xmlParser.js";
2
- export declare const EMPTY_NAMESPACE_SCOPE: XmlNamespaceScope;
3
- export declare const XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
4
- export declare const resolveNamespaceUri: (scope: XmlNamespaceScope | undefined, prefix: string) => string | undefined;
5
- /** Attach the element's resolved namespace metadata from its in-scope declarations. */
6
- export declare const attachXmlNamespaceContext: (element: XmlElement, inheritedNamespaceScope?: XmlNamespaceScope) => XmlNamespaceScope;
@@ -1,53 +0,0 @@
1
- // src/docx/xmlNamespaceContext.ts
2
- const EMPTY_NAMESPACE_SCOPE = { bindings: /* @__PURE__ */ new Map() };
3
- /**
4
- * Reserved by the XML Namespaces recommendation (§3): every document binds
5
- * this prefix to this URI whether or not it declares `xmlns:xml`, and no
6
- * document may bind it to anything else. A source that omits the declaration
7
- * — the overwhelming majority — must still resolve `xml:space`, `xml:lang`
8
- * and `xml:id` to it, or a reader mistakes the attribute for an unprefixed
9
- * one and a save drops its prefix.
10
- */
11
- const XML_RESERVED_PREFIX = "xml";
12
- const XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
13
- const resolveNamespaceUri = (scope, prefix) => {
14
- let current = scope;
15
- while (current) {
16
- const value = current.bindings.get(prefix);
17
- if (value !== void 0) return value;
18
- current = current.parent;
19
- }
20
- return prefix === XML_RESERVED_PREFIX ? XML_NAMESPACE_URI : void 0;
21
- };
22
- /** Attach the element's resolved namespace metadata from its in-scope declarations. */
23
- const attachXmlNamespaceContext = (element, inheritedNamespaceScope = EMPTY_NAMESPACE_SCOPE) => {
24
- let localBindings = null;
25
- if (element.attributes) for (const [attribute, value] of Object.entries(element.attributes)) {
26
- if (typeof value !== "string" || attribute !== "xmlns" && !attribute.startsWith("xmlns:")) continue;
27
- localBindings ??= /* @__PURE__ */ new Map();
28
- const prefix = attribute === "xmlns" ? "" : attribute.slice(6);
29
- localBindings.set(prefix, value);
30
- }
31
- const namespaceScope = localBindings === null ? inheritedNamespaceScope : {
32
- bindings: localBindings,
33
- parent: inheritedNamespaceScope
34
- };
35
- Object.defineProperty(element, "namespaceScope", {
36
- configurable: false,
37
- enumerable: false,
38
- value: namespaceScope,
39
- writable: false
40
- });
41
- const name = element.name ?? "";
42
- const colonIndex = name.indexOf(":");
43
- const prefix = colonIndex === -1 ? "" : name.slice(0, colonIndex);
44
- const namespaceUri = resolveNamespaceUri(namespaceScope, prefix);
45
- if (namespaceUri !== void 0) Object.defineProperty(element, "namespaceUri", {
46
- configurable: false,
47
- enumerable: false,
48
- value: namespaceUri,
49
- writable: false
50
- });
51
- return namespaceScope;
52
- };
53
- export { EMPTY_NAMESPACE_SCOPE, XML_NAMESPACE_URI, attachXmlNamespaceContext, resolveNamespaceUri };
@@ -1,107 +0,0 @@
1
- import { TaggedErrorClass } from "better-result";
2
- declare const XmlResourceLimitErrorBase: TaggedErrorClass<"XmlResourceLimitError">;
3
- /**
4
- * Shared bounds for XML parts parsed by Folio.
5
- *
6
- * `maxBytes` and the package expansion ceiling bound the *markup*; they do not
7
- * bound what parsing that markup allocates. A parsed element retains far more
8
- * than the bytes it was written as, so a part that satisfies a byte bound can
9
- * still cost multiples of it in tree. Measured on this repository's corpus
10
- * generators (Bun 1.4, arm64, heap retained after a forced GC):
11
- *
12
- * shape bytes/element tree heap / part bytes
13
- * `<w:r/>` 49.5 B 8.3x
14
- * `<w:r a=".." x4/>` 105-124 B 2.8-3.3x
15
- * `<w:r><w:t>x</w:t></w:r>` 162-171 B 14.1-14.8x
16
- *
17
- * An element is therefore the unit a memory bound has to count, because the
18
- * adversarial shape is the cheap one: `<w:r/>` is 7 bytes of markup and 49.5
19
- * bytes of tree, so 128 MiB of markup buys ~19M elements and ~950 MB of tree
20
- * inside a byte budget that never trips.
21
- *
22
- * Corpus distribution (5,314 readable packages, every XML and .rels part):
23
- *
24
- * metric p50 p90 p99 p99.9 max
25
- * elements / part 16 227 1,590 24,719 596,668
26
- * elements / package 917 3,230 29,106 71,975 602,212
27
- * attributes / part 27 577 1,993 21,037 630,374
28
- * attributes / package 1,783 4,711 25,896 91,946 639,110
29
- * depth / part 3 9 15 22 15,005
30
- * elements per byte 0.012 0.027 0.036 0.043 0.111
31
- *
32
- * The defaults below reject no corpus package that today's bounds accept. The
33
- * package budget is the one that matters: it caps a whole package at ~2.5M
34
- * elements and ~3M attributes, which is at most ~425 MB of tree at the densest
35
- * measured shape and ~124 MB at the cheapest. Before it existed, only
36
- * `word/document.xml`, `word/styles.xml` and `word/numbering.xml` were counted
37
- * at all, so a package of many merely-large parts could reach the expansion
38
- * ceiling of 250 MiB, ~37M elements and well past 1.8 GB of tree while passing
39
- * every bound. Lower these to trade format reach for a smaller ceiling.
40
- */
41
- export declare const FOLIO_XML_RESOURCE_LIMITS: {
42
- readonly maxBytes: number;
43
- readonly maxDepth: 100;
44
- /** 1.68x the corpus maximum (596,668). Unchanged; a shipped bound is not loosened. */
45
- readonly maxElementsPerPart: 1000000;
46
- /** 3.97x the corpus maximum (630,374). */
47
- readonly maxAttributesPerPart: 2500000;
48
- /** 4.15x the corpus maximum (602,212). */
49
- readonly maxElementsPerPackage: 2500000;
50
- /** 4.69x the corpus maximum (639,110). */
51
- readonly maxAttributesPerPackage: 3000000;
52
- };
53
- export type XmlResourceLimits = {
54
- maxBytes: number;
55
- maxDepth: number;
56
- maxElementsPerPart: number;
57
- maxAttributesPerPart: number;
58
- maxElementsPerPackage: number;
59
- maxAttributesPerPackage: number;
60
- };
61
- type XmlResourceLimitKind = "bytes" | "depth" | "elements" | "attributes" | "package-elements" | "package-attributes" | "syntax";
62
- /** XML input exceeded a parser resource bound or could not be scanned safely. */
63
- export declare class XmlResourceLimitError extends XmlResourceLimitErrorBase<{
64
- message: string;
65
- limit: XmlResourceLimitKind;
66
- /** The package part being scanned, when the caller named one. */
67
- partPath?: string;
68
- /** The count reached at the point of refusal, not the count of the whole input. */
69
- observed: number;
70
- allowed: number;
71
- }> {}
72
- /**
73
- * What a package has spent so far, shared by every part in one package.
74
- *
75
- * Per-part bounds alone do not bound a package: a package may hold hundreds of
76
- * parts, each individually modest. The budget is the accumulator the readers
77
- * carry across parts so the ceiling is the package's, not each part's.
78
- */
79
- export type XmlPackageBudget = {
80
- elements: number;
81
- attributes: number;
82
- };
83
- export declare const createXmlPackageBudget: () => XmlPackageBudget;
84
- export declare const exceedsUtf8ByteLimit: (value: string, maxBytes: number) => boolean;
85
- export type XmlResourceScanOptions = {
86
- xml: string;
87
- limits?: XmlResourceLimits;
88
- /** The package path scanned, carried on any refusal so a host can name it. */
89
- partPath?: string;
90
- /** Charged as the scan proceeds; omit for a part with no package context. */
91
- budget?: XmlPackageBudget;
92
- };
93
- /** What the preflight counted, for callers that reconcile it against the tree. */
94
- export type XmlResourceScanResult = {
95
- elements: number;
96
- attributes: number;
97
- maxDepth: number;
98
- };
99
- /**
100
- * Bound XML bytes, element count, attribute count and nesting before building
101
- * an object tree. The lexical scan is iterative, so deeply nested input cannot
102
- * consume the JS call stack before the depth limit is enforced, and every
103
- * bound is checked at the point it is crossed, so the work done before a
104
- * refusal is proportional to the limit rather than to the input.
105
- */
106
- export declare const assertXmlResourceLimits: ({ xml, limits, partPath, budget }: XmlResourceScanOptions) => XmlResourceScanResult;
107
- export {};