@stll/folio-core 0.65.0 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-edits/apply.js +31 -9
- package/dist/docx/attributeRemainder.js +1 -1
- package/dist/docx/documentSectionFacts.js +1 -1
- package/dist/docx/ensureParaIds.js +2 -2
- package/dist/docx/importedIdCollisions.js +1 -1
- package/dist/docx/mathToMathml.js +1 -1
- package/dist/docx/numericIdNormalization.js +2 -2
- package/dist/docx/parser.js +1 -1
- package/dist/docx/revisionIdNormalization.js +2 -2
- package/dist/docx/rezip.js +1 -1
- package/dist/docx/sdtProperties.js +1 -2
- package/dist/docx/selectedXmlIdentityScan.d.ts +1 -1
- package/dist/docx/selectedXmlIdentityScan.js +1 -1
- package/dist/docx/selectiveXmlPatch.js +2 -2
- package/dist/docx/server/boundedArchive.d.ts +1 -1
- package/dist/docx/server/boundedArchive.js +1 -1
- package/dist/docx/server/validateDocxConformance.js +1 -1
- package/dist/docx/shapeAlternateContent.js +1 -1
- package/dist/docx/storyBlockReplay.js +1 -1
- package/dist/docx/unzip.js +1 -1
- package/dist/docx/verbatimCapture.js +1 -1
- package/dist/docx/xmlParser.d.ts +5 -36
- package/dist/docx/xmlParser.js +14 -78
- package/dist/docx/xmlSafety.js +5 -5
- package/dist/fonts/bundledFonts.d.ts +54 -0
- package/dist/fonts/bundledFonts.js +45 -0
- package/dist/fonts/fontsourceFaces.d.ts +9 -14
- package/dist/fonts/fontsourceFaces.js +10 -22
- package/dist/layout-painter/renderPage.js +4 -2
- package/dist/markdown/countParagraphList.d.ts +18 -0
- package/dist/markdown/countParagraphList.js +90 -0
- package/dist/markdown/renderParagraph.js +2 -62
- package/dist/markdown/renderRuns.js +4 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +5 -3
- package/dist/prosemirror/conversion/toProseDoc.js +4 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -1
- package/dist/prosemirror/replacedAnnotations.d.ts +6 -0
- package/dist/prosemirror/replacedAnnotations.js +30 -8
- package/dist/prosemirror/schema/nodes.d.ts +7 -0
- package/dist/prosemirror/textBoxAnchorAttrs.js +9 -1
- package/dist/utils/fontLoader.d.ts +1 -1
- package/dist/utils/fontLoader.js +3 -11
- package/package.json +5 -4
- package/dist/docx/streamingXmlParser.d.ts +0 -60
- package/dist/docx/streamingXmlParser.js +0 -406
- package/dist/docx/xmlNamespaceContext.d.ts +0 -6
- package/dist/docx/xmlNamespaceContext.js +0 -53
- package/dist/docx/xmlResourceLimits.d.ts +0 -107
- package/dist/docx/xmlResourceLimits.js +0 -194
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { XmlElement, XmlNamespaceScope } from "./xmlParser.js";
|
|
2
|
-
type ParseXmlResult = {
|
|
3
|
-
status: "parsed";
|
|
4
|
-
value: XmlElement;
|
|
5
|
-
} | {
|
|
6
|
-
status: "unsupported";
|
|
7
|
-
};
|
|
8
|
-
type XmlSourceRange = {
|
|
9
|
-
readonly start: number;
|
|
10
|
-
readonly end: number;
|
|
11
|
-
};
|
|
12
|
-
/** Exact element source ranges, available only from the opt-in source-range reader. */
|
|
13
|
-
export declare const getXmlSourceRange: (element: XmlElement) => XmlSourceRange | undefined;
|
|
14
|
-
/**
|
|
15
|
-
* Parse ordinary OOXML into Folio's existing element representation in one
|
|
16
|
-
* pass. Unsupported or malformed constructs return a sentinel so callers can
|
|
17
|
-
* retain the general-purpose parser as a compatibility fallback.
|
|
18
|
-
*/
|
|
19
|
-
type AttributeValueSpan = {
|
|
20
|
-
start: number;
|
|
21
|
-
end: number;
|
|
22
|
-
};
|
|
23
|
-
type OpenTagVisitor = (element: XmlElement, attributeValueSpans: ReadonlyMap<string, AttributeValueSpan>) => ReadonlyMap<string, string> | null;
|
|
24
|
-
type OpenTagScanVisitor = (...args: Parameters<OpenTagVisitor>) => null;
|
|
25
|
-
type ElementScanOptions = {
|
|
26
|
-
element: XmlElement;
|
|
27
|
-
attributeValueSpans: ReadonlyMap<string, AttributeValueSpan>;
|
|
28
|
-
nameEnd: number;
|
|
29
|
-
parent: XmlElement | undefined;
|
|
30
|
-
};
|
|
31
|
-
type ElementScanVisitor = (options: ElementScanOptions) => void;
|
|
32
|
-
/**
|
|
33
|
-
* @param inheritedNamespaceScope the bindings in scope around `xml`, for a
|
|
34
|
-
* fragment captured out of a part whose root declared them.
|
|
35
|
-
*/
|
|
36
|
-
export declare const parseStreamingXml: (xml: string, inheritedNamespaceScope?: XmlNamespaceScope) => ParseXmlResult;
|
|
37
|
-
/** Retain source spans for every element without changing the parsed namespace tree. */
|
|
38
|
-
export declare const parseStreamingXmlWithSourceRanges: (xml: string, inheritedNamespaceScope?: XmlNamespaceScope) => ParseXmlResult;
|
|
39
|
-
/** Retain the body tree while collecting identities for import repair. */
|
|
40
|
-
export declare const parseStreamingXmlWithIdentityVisitor: (xml: string, visitOpenTag: OpenTagScanVisitor) => ParseXmlResult;
|
|
41
|
-
/** Rewrite selected decimal attribute values while preserving every other source byte. */
|
|
42
|
-
export declare const rewriteStreamingXmlDecimalAttributes: (xml: string, visitOpenTag: OpenTagVisitor) => {
|
|
43
|
-
status: "rewritten";
|
|
44
|
-
value: string;
|
|
45
|
-
} | {
|
|
46
|
-
status: "unsupported";
|
|
47
|
-
};
|
|
48
|
-
/** Visit identity attributes and namespace bindings without building an XML tree. */
|
|
49
|
-
export declare const scanStreamingXmlNumericIdAttributes: (xml: string, visitOpenTag: OpenTagScanVisitor) => {
|
|
50
|
-
status: "unsupported";
|
|
51
|
-
} | {
|
|
52
|
-
status: "scanned";
|
|
53
|
-
};
|
|
54
|
-
/** Visit namespace-resolved start tags and source spans without building a tree. */
|
|
55
|
-
export declare const scanStreamingXmlElements: (xml: string, visitElement: ElementScanVisitor) => {
|
|
56
|
-
status: "unsupported";
|
|
57
|
-
} | {
|
|
58
|
-
status: "scanned";
|
|
59
|
-
};
|
|
60
|
-
export {};
|
|
@@ -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 {};
|