@stll/folio-core 0.44.0 → 0.45.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.
- package/dist/ai-edits/__fixtures__/paragraphs.js +2 -2
- package/dist/ai-edits/headless.js +1 -0
- package/dist/compare/content-alignment.js +78 -53
- package/dist/compare/inline-atoms.js +34 -20
- package/dist/content-controls/mutateContentControls.js +4 -2
- package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
- package/dist/document-operations.js +14 -3
- package/dist/docx/borderParser.js +5 -5
- package/dist/docx/commentParser.js +47 -36
- package/dist/docx/commentThreadKey.d.ts +18 -0
- package/dist/docx/commentThreadKey.js +22 -0
- package/dist/docx/diagramPreview.js +87 -27
- package/dist/docx/groupDrawingParser.js +3 -3
- package/dist/docx/hyperlinkParser.js +2 -2
- package/dist/docx/imageParser.d.ts +9 -1
- package/dist/docx/imageParser.js +58 -12
- package/dist/docx/imageRawXml.d.ts +14 -1
- package/dist/docx/imageRawXml.js +30 -6
- package/dist/docx/mathToMathml.js +12 -14
- package/dist/docx/nonVisualDrawingProps.d.ts +34 -0
- package/dist/docx/nonVisualDrawingProps.js +46 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +3 -0
- package/dist/docx/parser.js +2 -2
- package/dist/docx/previewBudget.d.ts +64 -0
- package/dist/docx/previewBudget.js +88 -0
- package/dist/docx/revisionIdNormalization.js +17 -5
- package/dist/docx/rezip.js +17 -18
- package/dist/docx/sdtPropertiesPatch.js +24 -18
- package/dist/docx/sectionReferenceHistory.js +2 -2
- package/dist/docx/selectiveSave.js +6 -6
- package/dist/docx/serializer/blockSdtSerializer.js +38 -26
- package/dist/docx/serializer/borderSerializer.d.ts +1 -1
- package/dist/docx/serializer/borderSerializer.js +13 -12
- package/dist/docx/serializer/commentSerializer.d.ts +41 -16
- package/dist/docx/serializer/commentSerializer.js +82 -85
- package/dist/docx/serializer/fontTableSerializer.js +6 -6
- package/dist/docx/serializer/headerFooterSerializer.js +5 -5
- package/dist/docx/serializer/markupRangeAttributes.js +2 -2
- package/dist/docx/serializer/numberingSerializer.js +7 -6
- package/dist/docx/serializer/paragraphSerializer.js +19 -18
- package/dist/docx/serializer/partNamespaces.js +2 -2
- package/dist/docx/serializer/runSerializer.js +48 -28
- package/dist/docx/serializer/sectionPropertiesSerializer.js +11 -10
- package/dist/docx/serializer/settingsSerializer.js +4 -3
- package/dist/docx/serializer/stylesSerializer.js +6 -6
- package/dist/docx/serializer/tableSerializer.js +10 -9
- package/dist/docx/serializer/textFormattingSerializer.js +29 -28
- package/dist/docx/serializer/themeSerializer.js +6 -6
- package/dist/docx/serializer/trackedChangeAttributes.js +2 -2
- package/dist/docx/serializer/xmlUtils.d.ts +1 -2
- package/dist/docx/serializer/xmlUtils.js +1 -13
- package/dist/docx/server/boundedArchive.d.ts +12 -0
- package/dist/docx/server/boundedArchive.js +20 -1
- package/dist/docx/server/validateDocxConformance.js +22 -1
- package/dist/docx/shapeParser.js +7 -5
- package/dist/docx/textBoxParser.js +7 -2
- package/dist/docx/unzip.d.ts +23 -0
- package/dist/docx/unzip.js +32 -22
- package/dist/docx/verbatimCapture.js +1 -1
- package/dist/docx/vmlImageParser.js +3 -2
- package/dist/docx/vmlPreview.d.ts +1 -3
- package/dist/docx/vmlPreview.js +2 -30
- package/dist/docx/xmlParser.d.ts +16 -1
- package/dist/docx/xmlParser.js +56 -26
- package/dist/docx/xmlResourceLimits.d.ts +89 -9
- package/dist/docx/xmlResourceLimits.js +105 -24
- package/dist/internal/paragraphFormattingSerialization.js +3 -2
- package/dist/layout-painter/renderImage.js +4 -3
- package/dist/layout-painter/renderParagraph.js +4 -3
- package/dist/managers/autoSaveCodec.js +2 -8
- package/dist/markdown/images.js +1 -4
- package/dist/prosemirror/attrs/index.js +69 -0
- package/dist/prosemirror/commands/image.js +1 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +69 -29
- package/dist/prosemirror/conversion/toProseDoc.js +83 -19
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +2 -3
- package/dist/prosemirror/extensions/nodes/ImageExtension.js +4 -0
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +7 -2
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +8 -4
- package/dist/prosemirror/paragraphFormattingProvenance.d.ts +6 -3
- package/dist/prosemirror/paragraphFormattingProvenance.js +12 -3
- package/dist/prosemirror/schema/nodes.d.ts +50 -1
- package/dist/utils/base64.d.ts +36 -0
- package/dist/utils/base64.js +40 -0
- package/dist/utils/clipboard.js +2 -1
- package/dist/utils/units.d.ts +10 -1
- package/dist/utils/units.js +12 -1
- package/dist/utils/urlSecurity.d.ts +8 -2
- package/dist/utils/urlSecurity.js +21 -3
- package/package.json +2 -2
|
@@ -1,27 +1,107 @@
|
|
|
1
1
|
//#region src/docx/xmlResourceLimits.d.ts
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Shared bounds for XML parts parsed by Folio.
|
|
4
|
+
*
|
|
5
|
+
* `maxBytes` and the package expansion ceiling bound the *markup*; they do not
|
|
6
|
+
* bound what parsing that markup allocates. A parsed element retains far more
|
|
7
|
+
* than the bytes it was written as, so a part that satisfies a byte bound can
|
|
8
|
+
* still cost multiples of it in tree. Measured on this repository's corpus
|
|
9
|
+
* generators (Bun 1.4, arm64, heap retained after a forced GC):
|
|
10
|
+
*
|
|
11
|
+
* shape bytes/element tree heap / part bytes
|
|
12
|
+
* `<w:r/>` 49.5 B 8.3x
|
|
13
|
+
* `<w:r a=".." x4/>` 105-124 B 2.8-3.3x
|
|
14
|
+
* `<w:r><w:t>x</w:t></w:r>` 162-171 B 14.1-14.8x
|
|
15
|
+
*
|
|
16
|
+
* An element is therefore the unit a memory bound has to count, because the
|
|
17
|
+
* adversarial shape is the cheap one: `<w:r/>` is 7 bytes of markup and 49.5
|
|
18
|
+
* bytes of tree, so 128 MiB of markup buys ~19M elements and ~950 MB of tree
|
|
19
|
+
* inside a byte budget that never trips.
|
|
20
|
+
*
|
|
21
|
+
* Corpus distribution (5,314 readable packages, every XML and .rels part):
|
|
22
|
+
*
|
|
23
|
+
* metric p50 p90 p99 p99.9 max
|
|
24
|
+
* elements / part 16 227 1,590 24,719 596,668
|
|
25
|
+
* elements / package 917 3,230 29,106 71,975 602,212
|
|
26
|
+
* attributes / part 27 577 1,993 21,037 630,374
|
|
27
|
+
* attributes / package 1,783 4,711 25,896 91,946 639,110
|
|
28
|
+
* depth / part 3 9 15 22 15,005
|
|
29
|
+
* elements per byte 0.012 0.027 0.036 0.043 0.111
|
|
30
|
+
*
|
|
31
|
+
* The defaults below reject no corpus package that today's bounds accept. The
|
|
32
|
+
* package budget is the one that matters: it caps a whole package at ~2.5M
|
|
33
|
+
* elements and ~3M attributes, which is at most ~425 MB of tree at the densest
|
|
34
|
+
* measured shape and ~124 MB at the cheapest. Before it existed, only
|
|
35
|
+
* `word/document.xml`, `word/styles.xml` and `word/numbering.xml` were counted
|
|
36
|
+
* at all, so a package of many merely-large parts could reach the expansion
|
|
37
|
+
* ceiling of 250 MiB, ~37M elements and well past 1.8 GB of tree while passing
|
|
38
|
+
* every bound. Lower these to trade format reach for a smaller ceiling.
|
|
39
|
+
*/
|
|
3
40
|
declare const FOLIO_XML_RESOURCE_LIMITS: {
|
|
4
41
|
readonly maxBytes: number;
|
|
5
42
|
readonly maxDepth: 100;
|
|
6
|
-
|
|
43
|
+
/** 1.68x the corpus maximum (596,668). Unchanged; a shipped bound is not loosened. */
|
|
44
|
+
readonly maxElementsPerPart: 1000000;
|
|
45
|
+
/** 3.97x the corpus maximum (630,374). */
|
|
46
|
+
readonly maxAttributesPerPart: 2500000;
|
|
47
|
+
/** 4.15x the corpus maximum (602,212). */
|
|
48
|
+
readonly maxElementsPerPackage: 2500000;
|
|
49
|
+
/** 4.69x the corpus maximum (639,110). */
|
|
50
|
+
readonly maxAttributesPerPackage: 3000000;
|
|
7
51
|
};
|
|
8
52
|
type XmlResourceLimits = {
|
|
9
53
|
maxBytes: number;
|
|
10
54
|
maxDepth: number;
|
|
11
|
-
|
|
55
|
+
maxElementsPerPart: number;
|
|
56
|
+
maxAttributesPerPart: number;
|
|
57
|
+
maxElementsPerPackage: number;
|
|
58
|
+
maxAttributesPerPackage: number;
|
|
12
59
|
};
|
|
13
|
-
type XmlResourceLimitKind = "bytes" | "depth" | "
|
|
60
|
+
type XmlResourceLimitKind = "bytes" | "depth" | "elements" | "attributes" | "package-elements" | "package-attributes" | "syntax";
|
|
14
61
|
declare const XmlResourceLimitError_base: import("better-result").TaggedErrorClass<"XmlResourceLimitError">;
|
|
15
62
|
/** XML input exceeded a parser resource bound or could not be scanned safely. */
|
|
16
63
|
declare class XmlResourceLimitError extends XmlResourceLimitError_base<{
|
|
17
64
|
message: string;
|
|
18
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;
|
|
19
71
|
}> {}
|
|
20
72
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
+
type XmlPackageBudget = {
|
|
80
|
+
elements: number;
|
|
81
|
+
attributes: number;
|
|
82
|
+
};
|
|
83
|
+
declare const createXmlPackageBudget: () => XmlPackageBudget;
|
|
84
|
+
type XmlResourceScanOptions = {
|
|
85
|
+
xml: string;
|
|
86
|
+
limits?: XmlResourceLimits;
|
|
87
|
+
/** The package path scanned, carried on any refusal so a host can name it. */
|
|
88
|
+
partPath?: string;
|
|
89
|
+
/** Charged as the scan proceeds; omit for a part with no package context. */
|
|
90
|
+
budget?: XmlPackageBudget;
|
|
91
|
+
};
|
|
92
|
+
/** What the preflight counted, for callers that reconcile it against the tree. */
|
|
93
|
+
type XmlResourceScanResult = {
|
|
94
|
+
elements: number;
|
|
95
|
+
attributes: number;
|
|
96
|
+
maxDepth: number;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Bound XML bytes, element count, attribute count and nesting before building
|
|
100
|
+
* an object tree. The lexical scan is iterative, so deeply nested input cannot
|
|
101
|
+
* consume the JS call stack before the depth limit is enforced, and every
|
|
102
|
+
* bound is checked at the point it is crossed, so the work done before a
|
|
103
|
+
* refusal is proportional to the limit rather than to the input.
|
|
24
104
|
*/
|
|
25
|
-
declare const assertXmlResourceLimits: (xml
|
|
105
|
+
declare const assertXmlResourceLimits: ({ xml, limits, partPath, budget }: XmlResourceScanOptions) => XmlResourceScanResult;
|
|
26
106
|
//#endregion
|
|
27
|
-
export { FOLIO_XML_RESOURCE_LIMITS, XmlResourceLimitError, assertXmlResourceLimits };
|
|
107
|
+
export { FOLIO_XML_RESOURCE_LIMITS, XmlPackageBudget, XmlResourceLimitError, XmlResourceLimits, XmlResourceScanOptions, XmlResourceScanResult, assertXmlResourceLimits, createXmlPackageBudget };
|
|
@@ -1,12 +1,60 @@
|
|
|
1
1
|
import { TaggedError } from "better-result";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Shared bounds for XML parts parsed by Folio.
|
|
4
|
+
*
|
|
5
|
+
* `maxBytes` and the package expansion ceiling bound the *markup*; they do not
|
|
6
|
+
* bound what parsing that markup allocates. A parsed element retains far more
|
|
7
|
+
* than the bytes it was written as, so a part that satisfies a byte bound can
|
|
8
|
+
* still cost multiples of it in tree. Measured on this repository's corpus
|
|
9
|
+
* generators (Bun 1.4, arm64, heap retained after a forced GC):
|
|
10
|
+
*
|
|
11
|
+
* shape bytes/element tree heap / part bytes
|
|
12
|
+
* `<w:r/>` 49.5 B 8.3x
|
|
13
|
+
* `<w:r a=".." x4/>` 105-124 B 2.8-3.3x
|
|
14
|
+
* `<w:r><w:t>x</w:t></w:r>` 162-171 B 14.1-14.8x
|
|
15
|
+
*
|
|
16
|
+
* An element is therefore the unit a memory bound has to count, because the
|
|
17
|
+
* adversarial shape is the cheap one: `<w:r/>` is 7 bytes of markup and 49.5
|
|
18
|
+
* bytes of tree, so 128 MiB of markup buys ~19M elements and ~950 MB of tree
|
|
19
|
+
* inside a byte budget that never trips.
|
|
20
|
+
*
|
|
21
|
+
* Corpus distribution (5,314 readable packages, every XML and .rels part):
|
|
22
|
+
*
|
|
23
|
+
* metric p50 p90 p99 p99.9 max
|
|
24
|
+
* elements / part 16 227 1,590 24,719 596,668
|
|
25
|
+
* elements / package 917 3,230 29,106 71,975 602,212
|
|
26
|
+
* attributes / part 27 577 1,993 21,037 630,374
|
|
27
|
+
* attributes / package 1,783 4,711 25,896 91,946 639,110
|
|
28
|
+
* depth / part 3 9 15 22 15,005
|
|
29
|
+
* elements per byte 0.012 0.027 0.036 0.043 0.111
|
|
30
|
+
*
|
|
31
|
+
* The defaults below reject no corpus package that today's bounds accept. The
|
|
32
|
+
* package budget is the one that matters: it caps a whole package at ~2.5M
|
|
33
|
+
* elements and ~3M attributes, which is at most ~425 MB of tree at the densest
|
|
34
|
+
* measured shape and ~124 MB at the cheapest. Before it existed, only
|
|
35
|
+
* `word/document.xml`, `word/styles.xml` and `word/numbering.xml` were counted
|
|
36
|
+
* at all, so a package of many merely-large parts could reach the expansion
|
|
37
|
+
* ceiling of 250 MiB, ~37M elements and well past 1.8 GB of tree while passing
|
|
38
|
+
* every bound. Lower these to trade format reach for a smaller ceiling.
|
|
39
|
+
*/
|
|
3
40
|
const FOLIO_XML_RESOURCE_LIMITS = {
|
|
4
41
|
maxBytes: 128 * (1024 * 1024),
|
|
5
42
|
maxDepth: 100,
|
|
6
|
-
|
|
43
|
+
/** 1.68x the corpus maximum (596,668). Unchanged; a shipped bound is not loosened. */
|
|
44
|
+
maxElementsPerPart: 1e6,
|
|
45
|
+
/** 3.97x the corpus maximum (630,374). */
|
|
46
|
+
maxAttributesPerPart: 25e5,
|
|
47
|
+
/** 4.15x the corpus maximum (602,212). */
|
|
48
|
+
maxElementsPerPackage: 25e5,
|
|
49
|
+
/** 4.69x the corpus maximum (639,110). */
|
|
50
|
+
maxAttributesPerPackage: 3e6
|
|
7
51
|
};
|
|
8
52
|
/** XML input exceeded a parser resource bound or could not be scanned safely. */
|
|
9
53
|
var XmlResourceLimitError = class extends TaggedError("XmlResourceLimitError") {};
|
|
54
|
+
const createXmlPackageBudget = () => ({
|
|
55
|
+
elements: 0,
|
|
56
|
+
attributes: 0
|
|
57
|
+
});
|
|
10
58
|
const exceedsUtf8ByteLimit = (value, maxBytes) => {
|
|
11
59
|
let bytes = 0;
|
|
12
60
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -22,8 +70,9 @@ const exceedsUtf8ByteLimit = (value, maxBytes) => {
|
|
|
22
70
|
return false;
|
|
23
71
|
};
|
|
24
72
|
const isXmlWhitespace = (code) => code === 9 || code === 10 || code === 13 || code === 32;
|
|
25
|
-
const
|
|
73
|
+
const scanTag = (xml, start) => {
|
|
26
74
|
let quote = 0;
|
|
75
|
+
let attributes = 0;
|
|
27
76
|
for (let cursor = start; cursor < xml.length; cursor += 1) {
|
|
28
77
|
const code = xml.charCodeAt(cursor);
|
|
29
78
|
if (quote !== 0) {
|
|
@@ -34,29 +83,53 @@ const findTagClose = (xml, start) => {
|
|
|
34
83
|
quote = code;
|
|
35
84
|
continue;
|
|
36
85
|
}
|
|
37
|
-
if (code ===
|
|
86
|
+
if (code === 61) {
|
|
87
|
+
attributes += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (code === 62) return {
|
|
91
|
+
close: cursor,
|
|
92
|
+
attributes
|
|
93
|
+
};
|
|
38
94
|
}
|
|
39
|
-
return
|
|
95
|
+
return {
|
|
96
|
+
close: -1,
|
|
97
|
+
attributes
|
|
98
|
+
};
|
|
40
99
|
};
|
|
41
100
|
const throwSyntaxLimit = () => {
|
|
42
101
|
throw new XmlResourceLimitError({
|
|
43
102
|
message: "XML resource preflight could not safely scan malformed markup",
|
|
44
|
-
limit: "syntax"
|
|
103
|
+
limit: "syntax",
|
|
104
|
+
observed: 0,
|
|
105
|
+
allowed: 0
|
|
45
106
|
});
|
|
46
107
|
};
|
|
47
108
|
/**
|
|
48
|
-
* Bound XML bytes, element count, and nesting before building
|
|
49
|
-
* The lexical scan is iterative, so deeply nested input cannot
|
|
50
|
-
* call stack before the depth limit is enforced
|
|
109
|
+
* Bound XML bytes, element count, attribute count and nesting before building
|
|
110
|
+
* an object tree. The lexical scan is iterative, so deeply nested input cannot
|
|
111
|
+
* consume the JS call stack before the depth limit is enforced, and every
|
|
112
|
+
* bound is checked at the point it is crossed, so the work done before a
|
|
113
|
+
* refusal is proportional to the limit rather than to the input.
|
|
51
114
|
*/
|
|
52
|
-
const assertXmlResourceLimits = (xml, limits = FOLIO_XML_RESOURCE_LIMITS) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
115
|
+
const assertXmlResourceLimits = ({ xml, limits = FOLIO_XML_RESOURCE_LIMITS, partPath, budget }) => {
|
|
116
|
+
const refuse = (limit, message, observed, allowed) => {
|
|
117
|
+
throw new XmlResourceLimitError({
|
|
118
|
+
message: partPath === void 0 ? message : `${message} (part ${partPath})`,
|
|
119
|
+
limit,
|
|
120
|
+
...partPath === void 0 ? {} : { partPath },
|
|
121
|
+
observed,
|
|
122
|
+
allowed
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
if (exceedsUtf8ByteLimit(xml, limits.maxBytes)) refuse("bytes", `XML part exceeds ${String(limits.maxBytes)} bytes`, limits.maxBytes + 1, limits.maxBytes);
|
|
126
|
+
const budgetElements = budget?.elements ?? 0;
|
|
127
|
+
const budgetAttributes = budget?.attributes ?? 0;
|
|
57
128
|
let cursor = 0;
|
|
58
129
|
let depth = 0;
|
|
130
|
+
let maxDepth = 0;
|
|
59
131
|
let nodes = 0;
|
|
132
|
+
let attributes = 0;
|
|
60
133
|
while (cursor < xml.length) {
|
|
61
134
|
const open = xml.indexOf("<", cursor);
|
|
62
135
|
if (open === -1) break;
|
|
@@ -79,7 +152,7 @@ const assertXmlResourceLimits = (xml, limits = FOLIO_XML_RESOURCE_LIMITS) => {
|
|
|
79
152
|
continue;
|
|
80
153
|
}
|
|
81
154
|
if (xml.startsWith("<!", open)) throwSyntaxLimit();
|
|
82
|
-
const close =
|
|
155
|
+
const { close, attributes: tagAttributes } = scanTag(xml, open + 1);
|
|
83
156
|
if (close === -1) throwSyntaxLimit();
|
|
84
157
|
if (xml.charCodeAt(open + 1) === 47) {
|
|
85
158
|
if (depth === 0) throwSyntaxLimit();
|
|
@@ -88,21 +161,29 @@ const assertXmlResourceLimits = (xml, limits = FOLIO_XML_RESOURCE_LIMITS) => {
|
|
|
88
161
|
continue;
|
|
89
162
|
}
|
|
90
163
|
nodes += 1;
|
|
91
|
-
if (nodes > limits.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
164
|
+
if (nodes > limits.maxElementsPerPart) refuse("elements", `XML part contains more than ${String(limits.maxElementsPerPart)} elements`, nodes, limits.maxElementsPerPart);
|
|
165
|
+
if (budgetElements + nodes > limits.maxElementsPerPackage) refuse("package-elements", `DOCX package contains more than ${String(limits.maxElementsPerPackage)} XML elements`, budgetElements + nodes, limits.maxElementsPerPackage);
|
|
166
|
+
attributes += tagAttributes;
|
|
167
|
+
if (attributes > limits.maxAttributesPerPart) refuse("attributes", `XML part contains more than ${String(limits.maxAttributesPerPart)} attributes`, attributes, limits.maxAttributesPerPart);
|
|
168
|
+
if (budgetAttributes + attributes > limits.maxAttributesPerPackage) refuse("package-attributes", `DOCX package contains more than ${String(limits.maxAttributesPerPackage)} XML attributes`, budgetAttributes + attributes, limits.maxAttributesPerPackage);
|
|
95
169
|
let lastContent = close - 1;
|
|
96
170
|
while (lastContent > open && isXmlWhitespace(xml.charCodeAt(lastContent))) lastContent -= 1;
|
|
97
171
|
const elementDepth = depth + 1;
|
|
98
|
-
if (elementDepth > limits.maxDepth)
|
|
99
|
-
|
|
100
|
-
limit: "depth"
|
|
101
|
-
});
|
|
172
|
+
if (elementDepth > limits.maxDepth) refuse("depth", `XML part is nested deeper than ${String(limits.maxDepth)} elements`, elementDepth, limits.maxDepth);
|
|
173
|
+
if (elementDepth > maxDepth) maxDepth = elementDepth;
|
|
102
174
|
if (xml.charCodeAt(lastContent) !== 47) depth = elementDepth;
|
|
103
175
|
cursor = close + 1;
|
|
104
176
|
}
|
|
105
177
|
if (depth !== 0) throwSyntaxLimit();
|
|
178
|
+
if (budget) {
|
|
179
|
+
budget.elements += nodes;
|
|
180
|
+
budget.attributes += attributes;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
elements: nodes,
|
|
184
|
+
attributes,
|
|
185
|
+
maxDepth
|
|
186
|
+
};
|
|
106
187
|
};
|
|
107
188
|
//#endregion
|
|
108
|
-
export { FOLIO_XML_RESOURCE_LIMITS, XmlResourceLimitError, assertXmlResourceLimits };
|
|
189
|
+
export { FOLIO_XML_RESOURCE_LIMITS, XmlResourceLimitError, assertXmlResourceLimits, createXmlPackageBudget };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { serializeBorder } from "../docx/serializer/borderSerializer.js";
|
|
2
2
|
import { serializeShading, serializeTextFormatting } from "../docx/serializer/textFormattingSerializer.js";
|
|
3
|
-
import {
|
|
3
|
+
import { intAttr } from "../docx/serializer/xmlUtils.js";
|
|
4
4
|
import { sanitizeCapturedXmlElement } from "../docx/verbatimCapture.js";
|
|
5
5
|
import { NAMESPACES, OOXML_NAMESPACE_SCOPE } from "../docx/xmlParser.js";
|
|
6
|
+
import { escapeXmlAttribute } from "@stll/docx-core";
|
|
6
7
|
//#region src/internal/paragraphFormattingSerialization.ts
|
|
7
8
|
const modelParagraphNumberingReference = (reference) => {
|
|
8
9
|
if (!reference) return null;
|
|
@@ -140,7 +141,7 @@ const modelParagraphFormattingEmission = (input) => {
|
|
|
140
141
|
const { alignment, bidi, kinsoku, overflowPunctuation, spaceBefore, spaceAfter, lineSpacing, lineSpacingRule, snapToGrid, beforeAutospacing, afterAutospacing, spacingExplicit, indentLeft, indentRight, indentFirstLine, hangingIndent, borders, shading, tabs, keepNext, keepLines, widowControl, pageBreakBefore, contextualSpacing, numPr, numPrFromStyle, numberingChangeXml, outlineLevel, styleId, frame, suppressLineNumbers, suppressAutoHyphens, runProperties, runInWithNext } = input;
|
|
141
142
|
modelSpacingProvenance(spacingExplicit);
|
|
142
143
|
const propertiesXml = [
|
|
143
|
-
styleId ? `<w:pStyle w:val="${
|
|
144
|
+
styleId ? `<w:pStyle w:val="${escapeXmlAttribute(styleId)}"/>` : "",
|
|
144
145
|
serializeToggle("keepNext", keepNext),
|
|
145
146
|
serializeToggle("keepLines", keepLines),
|
|
146
147
|
serializeToggle("pageBreakBefore", pageBreakBefore),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
|
|
2
|
-
import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
|
|
2
|
+
import { anchorTargetAttrs, sanitizeExternalUrl } from "../utils/urlSecurity.js";
|
|
3
3
|
//#region src/layout-painter/renderImage.ts
|
|
4
4
|
/**
|
|
5
5
|
* CSS class names for image elements
|
|
@@ -145,8 +145,9 @@ function renderImageFragment(fragment, block, _measure, _context, options = {})
|
|
|
145
145
|
if (hlinkHref) {
|
|
146
146
|
const linkEl = doc.createElement("a");
|
|
147
147
|
linkEl.href = hlinkHref;
|
|
148
|
-
|
|
149
|
-
linkEl.
|
|
148
|
+
const { target, rel } = anchorTargetAttrs(void 0);
|
|
149
|
+
linkEl.target = target;
|
|
150
|
+
linkEl.rel = rel;
|
|
150
151
|
linkEl.style.display = "block";
|
|
151
152
|
linkEl.style.width = "100%";
|
|
152
153
|
linkEl.style.height = "100%";
|
|
@@ -16,7 +16,7 @@ import { resolvePhysicalParagraphInlineLayout } from "../utils/paragraphInlineLa
|
|
|
16
16
|
import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
|
|
17
17
|
import { sanitizeImageSrc } from "../utils/sanitizeImageSrc.js";
|
|
18
18
|
import { SCRIPT_CLASS, hasCjk, hasComplexScript, segmentByScript } from "../utils/scriptSegments.js";
|
|
19
|
-
import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
|
|
19
|
+
import { anchorTargetAttrs, sanitizeExternalUrl } from "../utils/urlSecurity.js";
|
|
20
20
|
import { borderStrokeToCss, resolveParagraphBorderHorizontalOutsets } from "./borderStroke.js";
|
|
21
21
|
import { planCursiveJoiners, withCursiveJoiners } from "./cursiveJoiners.js";
|
|
22
22
|
import { getAutomaticTextColorForBackground, setAuthoredBackgroundColor, setAuthoredTextColor } from "./documentColors.js";
|
|
@@ -357,8 +357,9 @@ function renderTextRun(run, doc, options) {
|
|
|
357
357
|
anchor.href = hyperlinkHref;
|
|
358
358
|
if (options?.hyperlinkDirection || DISPLAYED_URL_PATTERN.test(paintedText.trim())) anchor.dir = LEFT_TO_RIGHT_DIRECTION;
|
|
359
359
|
if (!isBookmarkTarget) {
|
|
360
|
-
|
|
361
|
-
anchor.
|
|
360
|
+
const { target, rel } = anchorTargetAttrs(void 0);
|
|
361
|
+
anchor.target = target;
|
|
362
|
+
anchor.rel = rel;
|
|
362
363
|
}
|
|
363
364
|
if (run.hyperlink.tooltip) anchor.title = run.hyperlink.tooltip;
|
|
364
365
|
appendPaintedText({
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { validateFolioDocumentModel } from "../docx/modelValidation.js";
|
|
2
|
+
import { bytesToBase64 } from "../utils/base64.js";
|
|
2
3
|
//#region src/managers/autoSaveCodec.ts
|
|
3
4
|
const AUTOSAVE_FORMAT_VERSION = 2;
|
|
4
|
-
const BASE64_CHUNK_BYTES = 32768;
|
|
5
|
-
const encodeBase64 = (buffer) => {
|
|
6
|
-
const bytes = new Uint8Array(buffer);
|
|
7
|
-
const chunks = [];
|
|
8
|
-
for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK_BYTES) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK_BYTES)));
|
|
9
|
-
return btoa(chunks.join(""));
|
|
10
|
-
};
|
|
11
5
|
const decodeBase64 = (value) => {
|
|
12
6
|
const binary = atob(value);
|
|
13
7
|
const bytes = new Uint8Array(binary.length);
|
|
@@ -23,7 +17,7 @@ const encodeValue = (value) => {
|
|
|
23
17
|
};
|
|
24
18
|
if (value instanceof ArrayBuffer) return {
|
|
25
19
|
type: "arrayBuffer",
|
|
26
|
-
value:
|
|
20
|
+
value: bytesToBase64(new Uint8Array(value))
|
|
27
21
|
};
|
|
28
22
|
if (value instanceof Map) return {
|
|
29
23
|
type: "map",
|
package/dist/markdown/images.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bytesToBase64 } from "../utils/base64.js";
|
|
1
2
|
//#region src/markdown/images.ts
|
|
2
3
|
const MIME_TO_EXT = {
|
|
3
4
|
"image/png": "png",
|
|
@@ -14,10 +15,6 @@ const MIME_TO_EXT = {
|
|
|
14
15
|
function extFor(mimeType, fallback) {
|
|
15
16
|
return MIME_TO_EXT[mimeType] ?? fallback;
|
|
16
17
|
}
|
|
17
|
-
function bytesToBase64(bytes) {
|
|
18
|
-
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
19
|
-
return btoa(new TextDecoder("latin1").decode(bytes));
|
|
20
|
-
}
|
|
21
18
|
function toUint8(data) {
|
|
22
19
|
if (data instanceof Uint8Array) return data;
|
|
23
20
|
return new Uint8Array(data);
|
|
@@ -53,6 +53,11 @@ const SHAPE_OUTLINE_CAPS = [
|
|
|
53
53
|
"round",
|
|
54
54
|
"square"
|
|
55
55
|
];
|
|
56
|
+
const SHAPE_OUTLINE_JOINS = [
|
|
57
|
+
"bevel",
|
|
58
|
+
"miter",
|
|
59
|
+
"round"
|
|
60
|
+
];
|
|
56
61
|
const SHAPE_LINE_END_TYPES = [
|
|
57
62
|
"none",
|
|
58
63
|
"triangle",
|
|
@@ -404,7 +409,11 @@ const readImageAttrs = (node) => {
|
|
|
404
409
|
optionalNumber(attrs, "paddingLeft", "image.attrs.paddingLeft", issues);
|
|
405
410
|
optionalImagePosition(attrs, "position", "image.attrs.position", issues);
|
|
406
411
|
optionalBoolean(attrs, "layoutInCell", "image.attrs.layoutInCell", issues);
|
|
412
|
+
optionalBoolean(attrs, "decorative", "image.attrs.decorative", issues);
|
|
413
|
+
optionalBoolean(attrs, "hidden", "image.attrs.hidden", issues);
|
|
414
|
+
optionalStringArray(attrs, "docPrExtensions", "image.attrs.docPrExtensions", issues);
|
|
407
415
|
optionalImageFrameLocks(attrs, "frameLocks", "image.attrs.frameLocks", issues);
|
|
416
|
+
optionalAuthoredEmu(attrs, "_docxAuthoredEmu", "image.attrs._docxAuthoredEmu", issues, IMAGE_AUTHORED_EMU_ATTRS);
|
|
408
417
|
optionalNumber(attrs, "borderWidth", "image.attrs.borderWidth", issues);
|
|
409
418
|
optionalString(attrs, "borderColor", "image.attrs.borderColor", issues);
|
|
410
419
|
optionalString(attrs, "borderStyle", "image.attrs.borderStyle", issues);
|
|
@@ -487,8 +496,12 @@ const readShapeAttrs = (node) => {
|
|
|
487
496
|
optionalString(attrs, "shapeType", "shape.attrs.shapeType", issues);
|
|
488
497
|
optionalString(attrs, "geometryAdjustments", "shape.attrs.geometryAdjustments", issues);
|
|
489
498
|
optionalString(attrs, "shapeId", "shape.attrs.shapeId", issues);
|
|
499
|
+
optionalString(attrs, "shapeName", "shape.attrs.shapeName", issues);
|
|
500
|
+
optionalString(attrs, "alt", "shape.attrs.alt", issues);
|
|
501
|
+
optionalString(attrs, "title", "shape.attrs.title", issues);
|
|
490
502
|
optionalNumber(attrs, "width", "shape.attrs.width", issues);
|
|
491
503
|
optionalNumber(attrs, "height", "shape.attrs.height", issues);
|
|
504
|
+
optionalAuthoredEmu(attrs, "_docxAuthoredEmu", "shape.attrs._docxAuthoredEmu", issues, SHAPE_AUTHORED_EMU_ATTRS);
|
|
492
505
|
optionalString(attrs, "fillColor", "shape.attrs.fillColor", issues);
|
|
493
506
|
optionalColorValue(attrs, "fillColorValue", "shape.attrs.fillColorValue", issues);
|
|
494
507
|
optionalOneOf(attrs, "fillType", "shape.attrs.fillType", issues, SHAPE_FILL_TYPES);
|
|
@@ -500,6 +513,7 @@ const readShapeAttrs = (node) => {
|
|
|
500
513
|
optionalColorValue(attrs, "outlineColorValue", "shape.attrs.outlineColorValue", issues);
|
|
501
514
|
optionalOneOf(attrs, "outlineStyle", "shape.attrs.outlineStyle", issues, OUTLINE_STYLE_ATTR_VALUES);
|
|
502
515
|
optionalOneOf(attrs, "outlineCap", "shape.attrs.outlineCap", issues, SHAPE_OUTLINE_CAPS);
|
|
516
|
+
optionalOneOf(attrs, "outlineJoin", "shape.attrs.outlineJoin", issues, SHAPE_OUTLINE_JOINS);
|
|
503
517
|
optionalShapeLineEnd(attrs, "outlineHeadEnd", "shape.attrs.outlineHeadEnd", issues);
|
|
504
518
|
optionalShapeLineEnd(attrs, "outlineTailEnd", "shape.attrs.outlineTailEnd", issues);
|
|
505
519
|
optionalString(attrs, "transform", "shape.attrs.transform", issues);
|
|
@@ -531,8 +545,12 @@ const readTextBoxAttrs = (node) => {
|
|
|
531
545
|
optionalWordArt(attrs, "wordArt", "textBox.attrs.wordArt", issues);
|
|
532
546
|
optionalOneOf(attrs, "textWrap", "textBox.attrs.textWrap", issues, TEXT_BOX_TEXT_WRAP_VALUES);
|
|
533
547
|
optionalString(attrs, "textBoxId", "textBox.attrs.textBoxId", issues);
|
|
548
|
+
optionalString(attrs, "textBoxName", "textBox.attrs.textBoxName", issues);
|
|
549
|
+
optionalString(attrs, "alt", "textBox.attrs.alt", issues);
|
|
550
|
+
optionalString(attrs, "title", "textBox.attrs.title", issues);
|
|
534
551
|
optionalString(attrs, "fillColor", "textBox.attrs.fillColor", issues);
|
|
535
552
|
optionalNumber(attrs, "outlineWidth", "textBox.attrs.outlineWidth", issues);
|
|
553
|
+
optionalAuthoredEmu(attrs, "_docxAuthoredEmu", "textBox.attrs._docxAuthoredEmu", issues, TEXT_BOX_AUTHORED_EMU_ATTRS);
|
|
536
554
|
optionalString(attrs, "outlineColor", "textBox.attrs.outlineColor", issues);
|
|
537
555
|
optionalOneOf(attrs, "outlineStyle", "textBox.attrs.outlineStyle", issues, OUTLINE_STYLE_ATTR_VALUES);
|
|
538
556
|
optionalTextBoxTransform(attrs, "transform", "textBox.attrs.transform", issues);
|
|
@@ -1784,6 +1802,57 @@ const validatePropertyChangeInfo = (value, path, issues) => {
|
|
|
1784
1802
|
optionalOneOf(value, "provenance", `${path}.provenance`, issues, TRACKED_CHANGE_PROVENANCE_VALUES);
|
|
1785
1803
|
optionalString(value, "suggestionId", `${path}.suggestionId`, issues);
|
|
1786
1804
|
};
|
|
1805
|
+
/**
|
|
1806
|
+
* The authored-EMU carrier: a sparse record of finite numbers keyed by the
|
|
1807
|
+
* pixel attribute each one was projected into. The keys are the node's own, so
|
|
1808
|
+
* they are checked against the list the caller passes rather than a shared one.
|
|
1809
|
+
*/
|
|
1810
|
+
const optionalAuthoredEmu = (attrs, key, path, issues, allowed) => {
|
|
1811
|
+
const value = attrs[key];
|
|
1812
|
+
if (value === void 0 || value === null) return;
|
|
1813
|
+
if (!isRecord(value)) {
|
|
1814
|
+
issues.push({
|
|
1815
|
+
path,
|
|
1816
|
+
message: "Expected an object."
|
|
1817
|
+
});
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
for (const name of Object.keys(value)) {
|
|
1821
|
+
if (!allowed.includes(name)) {
|
|
1822
|
+
issues.push({
|
|
1823
|
+
path: `${path}.${name}`,
|
|
1824
|
+
message: "Not a pixel attribute of this node."
|
|
1825
|
+
});
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
optionalNumber(value, name, `${path}.${name}`, issues);
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
const WRAP_DISTANCE_ATTRS = [
|
|
1832
|
+
"distTop",
|
|
1833
|
+
"distBottom",
|
|
1834
|
+
"distLeft",
|
|
1835
|
+
"distRight"
|
|
1836
|
+
];
|
|
1837
|
+
const TEXT_BOX_MARGIN_ATTRS = [
|
|
1838
|
+
"marginTop",
|
|
1839
|
+
"marginBottom",
|
|
1840
|
+
"marginLeft",
|
|
1841
|
+
"marginRight"
|
|
1842
|
+
];
|
|
1843
|
+
const IMAGE_AUTHORED_EMU_ATTRS = [
|
|
1844
|
+
"width",
|
|
1845
|
+
"height",
|
|
1846
|
+
"borderWidth",
|
|
1847
|
+
...WRAP_DISTANCE_ATTRS
|
|
1848
|
+
];
|
|
1849
|
+
const SHAPE_AUTHORED_EMU_ATTRS = [
|
|
1850
|
+
"width",
|
|
1851
|
+
"height",
|
|
1852
|
+
"outlineWidth",
|
|
1853
|
+
...WRAP_DISTANCE_ATTRS
|
|
1854
|
+
];
|
|
1855
|
+
const TEXT_BOX_AUTHORED_EMU_ATTRS = [...SHAPE_AUTHORED_EMU_ATTRS, ...TEXT_BOX_MARGIN_ATTRS];
|
|
1787
1856
|
const optionalImageFrameLocks = (attrs, key, path, issues) => {
|
|
1788
1857
|
const value = attrs[key];
|
|
1789
1858
|
if (value === void 0 || value === null) return;
|
|
@@ -179,6 +179,7 @@ const insertImageFromFile = async (view, file, onInserted) => {
|
|
|
179
179
|
if (!imageType) return;
|
|
180
180
|
const imageNode = imageType.create({
|
|
181
181
|
src: dataUrl,
|
|
182
|
+
docPrName: file.name,
|
|
182
183
|
alt: file.name,
|
|
183
184
|
width: constrained.width,
|
|
184
185
|
height: constrained.height,
|