@stll/folio-core 0.53.0 → 0.54.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 +415 -166
- package/dist/ai-edits/batch-claims.d.ts +21 -6
- package/dist/ai-edits/batch-claims.js +47 -16
- package/dist/ai-edits/headless.d.ts +20 -0
- package/dist/ai-edits/headless.js +140 -11
- package/dist/ai-edits/pending-suggestions.js +1 -1
- package/dist/ai-edits/read.d.ts +5 -3
- package/dist/ai-edits/read.js +97 -47
- package/dist/ai-edits/revisionStretches.d.ts +17 -0
- package/dist/ai-edits/revisionStretches.js +80 -0
- package/dist/ai-edits/snapshot.js +3 -2
- package/dist/ai-edits/table-cell-mutations.d.ts +3 -1
- package/dist/ai-edits/table-cell-mutations.js +2 -1
- package/dist/ai-edits/table-row-column-mutations.d.ts +6 -1
- package/dist/ai-edits/table-row-column-mutations.js +27 -10
- package/dist/compare/content-alignment.d.ts +5 -1
- package/dist/compare/content-alignment.js +39 -32
- package/dist/compare/inline-provenance.js +1 -1
- package/dist/controller/layoutPipeline.js +3 -2
- package/dist/docx/archiveInflation.d.ts +127 -0
- package/dist/docx/archiveInflation.js +181 -0
- package/dist/docx/metadataPrivacy.js +24 -1
- package/dist/docx/noteReferenceMark.d.ts +15 -0
- package/dist/docx/noteReferenceMark.js +48 -0
- package/dist/docx/paragraphParser.js +5 -3
- package/dist/docx/paragraphPropertySource.d.ts +1 -1
- package/dist/docx/paragraphPropertySource.js +4 -2
- package/dist/docx/parser.js +2 -2
- package/dist/docx/rezip.js +4 -2
- package/dist/docx/selectiveXmlPatch.d.ts +3 -1
- package/dist/docx/selectiveXmlPatch.js +81 -3
- package/dist/docx/server/boundedArchive.d.ts +8 -13
- package/dist/docx/server/boundedArchive.js +70 -59
- package/dist/docx/server/extractDocxText.d.ts +8 -2
- package/dist/docx/server/extractDocxText.js +2 -2
- package/dist/docx/server/validateDocxConformance.d.ts +1 -1
- package/dist/docx/server/validateDocxConformance.js +1 -0
- package/dist/docx/unzip.d.ts +19 -2
- package/dist/docx/unzip.js +135 -24
- package/dist/docx/verticalMergeProjection.d.ts +17 -0
- package/dist/docx/verticalMergeProjection.js +120 -0
- package/dist/fonts/embeddedFonts.js +1 -1
- package/dist/i18n/messages/catalogs.gen.d.ts +34 -34
- package/dist/i18n/messages/catalogs.gen.js +34 -34
- package/dist/i18n/messages/messages.gen.d.ts +2 -2
- package/dist/internal/wholeStoryRevisionResolution.js +95 -25
- package/dist/layout-engine/measure/measureParagraph.js +2 -2
- package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +12 -1
- package/dist/layout-engine/measure/paragraphMeasureShared.js +14 -1
- package/dist/markdown/escape.d.ts +2 -2
- package/dist/markdown/escape.js +4 -5
- package/dist/markdown/renderBlock.js +91 -25
- package/dist/markdown/renderRuns.d.ts +10 -1
- package/dist/markdown/renderRuns.js +320 -59
- package/dist/prosemirror/clearRunColor.d.ts +6 -0
- package/dist/prosemirror/clearRunColor.js +46 -0
- package/dist/prosemirror/commands/comments.js +62 -22
- package/dist/prosemirror/commands/formatPainter.js +1 -1
- package/dist/prosemirror/commands/hyperlink.js +2 -1
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +3 -2
- package/dist/prosemirror/commands/propertyChangeScope.js +28 -4
- package/dist/prosemirror/commands/resolveAllTableChanges.js +40 -41
- package/dist/prosemirror/commands/resolveParagraphProperties.js +33 -8
- package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +7 -1
- package/dist/prosemirror/commands/tableCellMergeResolution.js +39 -20
- package/dist/prosemirror/commands/tableMergeFoldDecisions.d.ts +61 -0
- package/dist/prosemirror/commands/tableMergeFoldDecisions.js +79 -0
- package/dist/prosemirror/containerFinalParagraph.d.ts +11 -5
- package/dist/prosemirror/containerFinalParagraph.js +7 -6
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
- package/dist/prosemirror/conversion/fromProseDoc.js +12 -590
- package/dist/prosemirror/conversion/toProseDoc.d.ts +1 -13
- package/dist/prosemirror/conversion/toProseDoc.js +28 -262
- package/dist/prosemirror/documentSchema.d.ts +8 -0
- package/dist/prosemirror/documentSchema.js +18 -0
- package/dist/prosemirror/extensions/StarterKit.js +2 -0
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +104 -63
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -1
- package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +24 -3
- package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.d.ts +20 -0
- package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +43 -0
- package/dist/prosemirror/extensions/features/ListExtension.js +10 -9
- package/dist/prosemirror/extensions/features/pastedHeadingStyles.js +1 -1
- package/dist/prosemirror/extensions/features/pastedHtmlLists.js +2 -4
- package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +14 -8
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +8 -4
- package/dist/prosemirror/extensions/marks/StrikeExtension.js +2 -2
- package/dist/prosemirror/extensions/marks/SubscriptExtension.js +2 -2
- package/dist/prosemirror/extensions/marks/SuperscriptExtension.js +2 -2
- package/dist/prosemirror/extensions/marks/TextColorExtension.js +4 -3
- package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -6
- package/dist/prosemirror/extensions/marks/markUtils.js +17 -8
- package/dist/prosemirror/extensions/marks/noteReferenceDeletion.d.ts +3 -1
- package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +12 -1
- package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +2 -2
- package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +5 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +104 -210
- package/dist/prosemirror/extensions/types.d.ts +7 -0
- package/dist/prosemirror/hyperlinkRemoval.d.ts +9 -0
- package/dist/prosemirror/hyperlinkRemoval.js +129 -0
- package/dist/prosemirror/index.d.ts +2 -1
- package/dist/prosemirror/index.js +2 -1
- package/dist/prosemirror/listNumbering.d.ts +3 -21
- package/dist/prosemirror/listNumbering.js +24 -39
- package/dist/prosemirror/listRendering.d.ts +29 -0
- package/dist/prosemirror/listRendering.js +26 -0
- package/dist/prosemirror/markupViewNotes.d.ts +8 -0
- package/dist/prosemirror/markupViewNotes.js +49 -0
- package/dist/prosemirror/markupViewProjection.d.ts +3 -1
- package/dist/prosemirror/markupViewProjection.js +7 -2
- package/dist/prosemirror/noteReferenceReview.d.ts +77 -0
- package/dist/prosemirror/noteReferenceReview.js +332 -0
- package/dist/prosemirror/paragraphIndentation.js +5 -2
- package/dist/prosemirror/paragraphMarkJoin.d.ts +6 -4
- package/dist/prosemirror/paragraphMarkJoin.js +21 -19
- package/dist/prosemirror/paragraphPropertyCarry.d.ts +64 -0
- package/dist/prosemirror/paragraphPropertyCarry.js +155 -0
- package/dist/prosemirror/plugins/documentStyleState.d.ts +26 -0
- package/dist/prosemirror/plugins/documentStyleState.js +32 -0
- package/dist/prosemirror/plugins/documentStyles.d.ts +2 -21
- package/dist/prosemirror/plugins/documentStyles.js +66 -29
- package/dist/prosemirror/plugins/index.d.ts +2 -1
- package/dist/prosemirror/plugins/index.js +2 -1
- package/dist/prosemirror/plugins/paragraphStyleResolution.d.ts +15 -0
- package/dist/prosemirror/plugins/paragraphStyleResolution.js +230 -0
- package/dist/prosemirror/plugins/suggestionMode.d.ts +12 -2
- package/dist/prosemirror/plugins/suggestionMode.js +387 -28
- package/dist/prosemirror/plugins/templateDirectives.d.ts +6 -0
- package/dist/prosemirror/plugins/templateDirectives.js +21 -7
- package/dist/prosemirror/plugins/templatePreviewValues.js +14 -21
- package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +22 -2
- package/dist/prosemirror/rebaseParagraphRunFormatting.js +39 -13
- package/dist/prosemirror/rebaseParagraphRuns.d.ts +36 -0
- package/dist/prosemirror/rebaseParagraphRuns.js +99 -0
- package/dist/prosemirror/runFormattingFromMarks.d.ts +18 -0
- package/dist/prosemirror/runFormattingFromMarks.js +567 -0
- package/dist/prosemirror/runFormattingReconciliation.js +10 -9
- package/dist/prosemirror/schema/nodes.d.ts +2 -0
- package/dist/prosemirror/styles/paragraphStyleCascade.d.ts +59 -0
- package/dist/prosemirror/styles/paragraphStyleCascade.js +145 -0
- package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +21 -2
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +38 -1
- package/dist/prosemirror/styles/tableStyleRegions.d.ts +31 -0
- package/dist/prosemirror/styles/tableStyleRegions.js +55 -0
- package/dist/prosemirror/tableCellPaste.d.ts +74 -0
- package/dist/prosemirror/tableCellPaste.js +525 -0
- package/dist/prosemirror/tableGridMutation.d.ts +53 -14
- package/dist/prosemirror/tableGridMutation.js +166 -18
- package/dist/prosemirror/tableRunIn.d.ts +51 -0
- package/dist/prosemirror/tableRunIn.js +165 -0
- package/dist/prosemirror/textInput.js +11 -2
- package/dist/prosemirror/trackedRevisionPath.d.ts +17 -0
- package/dist/prosemirror/trackedRevisionPath.js +32 -0
- package/dist/server.d.ts +2 -2
- package/package.json +6 -6
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import JSZip from "jszip";
|
|
2
|
+
//#region src/docx/archiveInflation.d.ts
|
|
3
|
+
declare module "jszip" {
|
|
4
|
+
interface JSZipObject {
|
|
5
|
+
/**
|
|
6
|
+
* Chunked read of the entry content, missing from the published typings.
|
|
7
|
+
* `nodeStream` is this stream wrapped in a Node.js `Readable`, which
|
|
8
|
+
* browsers and web workers cannot provide; the stream itself is
|
|
9
|
+
* platform-neutral.
|
|
10
|
+
*/
|
|
11
|
+
internalStream(type: "uint8array"): JSZip.JSZipStreamHelper<Uint8Array>;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Default ceiling on inflated bytes per compressed byte.
|
|
16
|
+
*
|
|
17
|
+
* Document parts in the repository's fixtures stay under 75:1, and parts over a
|
|
18
|
+
* mebibyte under 15:1. A DEFLATE stream tops out near 1032:1, which is the
|
|
19
|
+
* shape of an entry made of one repeated byte.
|
|
20
|
+
*/
|
|
21
|
+
declare const DOCX_MAX_COMPRESSION_RATIO = 200;
|
|
22
|
+
/**
|
|
23
|
+
* Inflated size below which the ratio cap does not apply.
|
|
24
|
+
*
|
|
25
|
+
* A small part can compress extremely well without costing anything to hold,
|
|
26
|
+
* so the ratio only has to bound entries (and packages) large enough to
|
|
27
|
+
* matter. The byte caps still apply below this size.
|
|
28
|
+
*/
|
|
29
|
+
declare const DOCX_COMPRESSION_RATIO_GRACE_BYTES: number;
|
|
30
|
+
/**
|
|
31
|
+
* Upper bound on the central-directory records in `bytes`, counted without
|
|
32
|
+
* parsing the archive and stopping once the count passes `stopAfter`.
|
|
33
|
+
*
|
|
34
|
+
* Every record JSZip reads starts with the record signature, so the number of
|
|
35
|
+
* signature occurrences cannot be lower than the number of entries JSZip would
|
|
36
|
+
* build, whatever the end-of-central-directory record claims.
|
|
37
|
+
*/
|
|
38
|
+
declare const countCentralDirectoryRecords: (bytes: Uint8Array, stopAfter: number) => number;
|
|
39
|
+
/** Sizes a loaded entry declares in the central directory. */
|
|
40
|
+
type ZipEntrySizes = {
|
|
41
|
+
readonly compressedBytes: number | null;
|
|
42
|
+
readonly uncompressedBytes: number | null;
|
|
43
|
+
};
|
|
44
|
+
/** Declared sizes of an entry JSZip loaded from an archive. */
|
|
45
|
+
declare const getZipEntrySizes: (entry: JSZip.JSZipObject) => ZipEntrySizes;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a loaded entry is stored rather than compressed. A stored entry
|
|
48
|
+
* reads back exactly the bytes the archive carries, so it cannot expand.
|
|
49
|
+
*/
|
|
50
|
+
declare const isStoredZipEntry: (entry: JSZip.JSZipObject) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the per-entry ratio cap applies to a part. Markup and text parts
|
|
53
|
+
* never compress anywhere near the cap. Binary parts can: an uncompressed
|
|
54
|
+
* bitmap of one colour compresses like a run of one byte. Those stay bounded
|
|
55
|
+
* by the byte caps and the package-wide ratio instead.
|
|
56
|
+
*/
|
|
57
|
+
declare const isRatioBoundedPart: (path: string) => boolean;
|
|
58
|
+
/** The per-entry ratio cap for `path`: `maxRatio`, or none for a binary part. */
|
|
59
|
+
declare const compressionRatioLimitFor: (path: string, maxRatio: number) => number;
|
|
60
|
+
type CompressionRatioCheck = {
|
|
61
|
+
inflatedBytes: number;
|
|
62
|
+
compressedBytes: number | null;
|
|
63
|
+
maxRatio: number;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Whether `inflatedBytes` produced from `compressedBytes` passes the ratio cap.
|
|
67
|
+
* Sizes under {@link DOCX_COMPRESSION_RATIO_GRACE_BYTES} never do, and an
|
|
68
|
+
* unknown compressed size is left to the byte caps.
|
|
69
|
+
*/
|
|
70
|
+
declare const exceedsCompressionRatio: ({ inflatedBytes, compressedBytes, maxRatio }: CompressionRatioCheck) => boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Inflated bytes charged across one package. `aborted` stops every inflation
|
|
73
|
+
* sharing the budget at its next chunk, once one of them has failed the
|
|
74
|
+
* package.
|
|
75
|
+
*/
|
|
76
|
+
type InflationBudget = {
|
|
77
|
+
readonly maxTotalBytes: number;
|
|
78
|
+
inflatedBytes: number;
|
|
79
|
+
aborted: boolean;
|
|
80
|
+
};
|
|
81
|
+
declare const createInflationBudget: (maxTotalBytes: number) => InflationBudget;
|
|
82
|
+
/**
|
|
83
|
+
* The bound an inflation stopped at.
|
|
84
|
+
*
|
|
85
|
+
* - `declared-size` — the entry produced more than its declared size.
|
|
86
|
+
* - `compression-ratio` — the entry passed the ratio cap for its compressed size.
|
|
87
|
+
* - `entry` — the entry passed the per-entry cap the caller set.
|
|
88
|
+
* - `total` — the package passed its cumulative budget.
|
|
89
|
+
* - `aborted` — another inflation sharing the budget already failed.
|
|
90
|
+
*/
|
|
91
|
+
type InflationLimit = "declared-size" | "compression-ratio" | "entry" | "total" | "aborted";
|
|
92
|
+
type InflateEntryResult = {
|
|
93
|
+
readonly ok: true;
|
|
94
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
95
|
+
} | {
|
|
96
|
+
readonly ok: false;
|
|
97
|
+
readonly limit: InflationLimit;
|
|
98
|
+
};
|
|
99
|
+
type InflateEntryOptions = {
|
|
100
|
+
entry: JSZip.JSZipObject;
|
|
101
|
+
/** Most bytes this entry may inflate to. */
|
|
102
|
+
maxEntryBytes: number;
|
|
103
|
+
/** Ratio cap while streaming; see {@link compressionRatioLimitFor}. */
|
|
104
|
+
maxCompressionRatio: number;
|
|
105
|
+
budget: InflationBudget;
|
|
106
|
+
/**
|
|
107
|
+
* `false` inflates the entry only to prove it stays within its bounds; the
|
|
108
|
+
* chunks are dropped and the result carries no bytes. A package part that
|
|
109
|
+
* is never read here is still inflated by a later save, so the bound has
|
|
110
|
+
* to be established for it too.
|
|
111
|
+
*/
|
|
112
|
+
retain?: boolean;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Inflate one entry chunk by chunk, checking every bound before a chunk is
|
|
116
|
+
* kept. Pausing the stream abandons the rest of the entry, so a limit bounds
|
|
117
|
+
* memory rather than reporting an overrun afterwards.
|
|
118
|
+
*
|
|
119
|
+
* The budget is charged as chunks arrive, which keeps concurrent inflations
|
|
120
|
+
* of one package honest, and refunded when this entry stops at a limit or
|
|
121
|
+
* fails, since none of its bytes are kept. Stream errors (a corrupt entry)
|
|
122
|
+
* reject; limits resolve with `ok: false` for the caller to map onto its own
|
|
123
|
+
* error or skip policy.
|
|
124
|
+
*/
|
|
125
|
+
declare const inflateEntryWithinLimits: ({ entry, maxEntryBytes, maxCompressionRatio, budget, retain }: InflateEntryOptions) => Promise<InflateEntryResult>;
|
|
126
|
+
//#endregion
|
|
127
|
+
export { DOCX_COMPRESSION_RATIO_GRACE_BYTES, DOCX_MAX_COMPRESSION_RATIO, InflateEntryOptions, InflateEntryResult, InflationBudget, InflationLimit, ZipEntrySizes, compressionRatioLimitFor, countCentralDirectoryRecords, createInflationBudget, exceedsCompressionRatio, getZipEntrySizes, inflateEntryWithinLimits, isRatioBoundedPart, isStoredZipEntry };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//#region src/docx/archiveInflation.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default ceiling on inflated bytes per compressed byte.
|
|
4
|
+
*
|
|
5
|
+
* Document parts in the repository's fixtures stay under 75:1, and parts over a
|
|
6
|
+
* mebibyte under 15:1. A DEFLATE stream tops out near 1032:1, which is the
|
|
7
|
+
* shape of an entry made of one repeated byte.
|
|
8
|
+
*/
|
|
9
|
+
const DOCX_MAX_COMPRESSION_RATIO = 200;
|
|
10
|
+
/**
|
|
11
|
+
* Inflated size below which the ratio cap does not apply.
|
|
12
|
+
*
|
|
13
|
+
* A small part can compress extremely well without costing anything to hold,
|
|
14
|
+
* so the ratio only has to bound entries (and packages) large enough to
|
|
15
|
+
* matter. The byte caps still apply below this size.
|
|
16
|
+
*/
|
|
17
|
+
const DOCX_COMPRESSION_RATIO_GRACE_BYTES = 4 * 1024 * 1024;
|
|
18
|
+
const CENTRAL_DIRECTORY_RECORD_SIGNATURE = [
|
|
19
|
+
80,
|
|
20
|
+
75,
|
|
21
|
+
1,
|
|
22
|
+
2
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Upper bound on the central-directory records in `bytes`, counted without
|
|
26
|
+
* parsing the archive and stopping once the count passes `stopAfter`.
|
|
27
|
+
*
|
|
28
|
+
* Every record JSZip reads starts with the record signature, so the number of
|
|
29
|
+
* signature occurrences cannot be lower than the number of entries JSZip would
|
|
30
|
+
* build, whatever the end-of-central-directory record claims.
|
|
31
|
+
*/
|
|
32
|
+
const countCentralDirectoryRecords = (bytes, stopAfter) => {
|
|
33
|
+
const [first, second, third, fourth] = CENTRAL_DIRECTORY_RECORD_SIGNATURE;
|
|
34
|
+
let count = 0;
|
|
35
|
+
let offset = bytes.indexOf(first);
|
|
36
|
+
while (offset !== -1 && offset + 3 < bytes.length) {
|
|
37
|
+
if (bytes[offset + 1] === second && bytes[offset + 2] === third && bytes[offset + 3] === fourth) {
|
|
38
|
+
count += 1;
|
|
39
|
+
if (count > stopAfter) return count;
|
|
40
|
+
}
|
|
41
|
+
offset = bytes.indexOf(first, offset + 1);
|
|
42
|
+
}
|
|
43
|
+
return count;
|
|
44
|
+
};
|
|
45
|
+
const finiteSize = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
46
|
+
/** Declared sizes of an entry JSZip loaded from an archive. */
|
|
47
|
+
const getZipEntrySizes = (entry) => {
|
|
48
|
+
const data = "_data" in entry ? entry._data : void 0;
|
|
49
|
+
if (typeof data !== "object" || data === null) return {
|
|
50
|
+
compressedBytes: null,
|
|
51
|
+
uncompressedBytes: null
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
compressedBytes: "compressedSize" in data ? finiteSize(data.compressedSize) : null,
|
|
55
|
+
uncompressedBytes: "uncompressedSize" in data ? finiteSize(data.uncompressedSize) : null
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const STORED_COMPRESSION_MAGIC = "\0\0";
|
|
59
|
+
/**
|
|
60
|
+
* Whether a loaded entry is stored rather than compressed. A stored entry
|
|
61
|
+
* reads back exactly the bytes the archive carries, so it cannot expand.
|
|
62
|
+
*/
|
|
63
|
+
const isStoredZipEntry = (entry) => {
|
|
64
|
+
const data = "_data" in entry ? entry._data : void 0;
|
|
65
|
+
if (typeof data !== "object" || data === null || !("compression" in data)) return false;
|
|
66
|
+
const { compression } = data;
|
|
67
|
+
return typeof compression === "object" && compression !== null && "magic" in compression && compression.magic === STORED_COMPRESSION_MAGIC;
|
|
68
|
+
};
|
|
69
|
+
const RATIO_BOUNDED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
70
|
+
"xml",
|
|
71
|
+
"rels",
|
|
72
|
+
"vml",
|
|
73
|
+
"txt",
|
|
74
|
+
"htm",
|
|
75
|
+
"html",
|
|
76
|
+
"mht",
|
|
77
|
+
"mhtml",
|
|
78
|
+
"rtf"
|
|
79
|
+
]);
|
|
80
|
+
/**
|
|
81
|
+
* Whether the per-entry ratio cap applies to a part. Markup and text parts
|
|
82
|
+
* never compress anywhere near the cap. Binary parts can: an uncompressed
|
|
83
|
+
* bitmap of one colour compresses like a run of one byte. Those stay bounded
|
|
84
|
+
* by the byte caps and the package-wide ratio instead.
|
|
85
|
+
*/
|
|
86
|
+
const isRatioBoundedPart = (path) => RATIO_BOUNDED_EXTENSIONS.has(path.slice(path.lastIndexOf(".") + 1).toLowerCase());
|
|
87
|
+
/** The per-entry ratio cap for `path`: `maxRatio`, or none for a binary part. */
|
|
88
|
+
const compressionRatioLimitFor = (path, maxRatio) => isRatioBoundedPart(path) ? maxRatio : Number.POSITIVE_INFINITY;
|
|
89
|
+
/**
|
|
90
|
+
* Whether `inflatedBytes` produced from `compressedBytes` passes the ratio cap.
|
|
91
|
+
* Sizes under {@link DOCX_COMPRESSION_RATIO_GRACE_BYTES} never do, and an
|
|
92
|
+
* unknown compressed size is left to the byte caps.
|
|
93
|
+
*/
|
|
94
|
+
const exceedsCompressionRatio = ({ inflatedBytes, compressedBytes, maxRatio }) => compressedBytes !== null && inflatedBytes > 4194304 && inflatedBytes > compressedBytes * maxRatio;
|
|
95
|
+
const createInflationBudget = (maxTotalBytes) => ({
|
|
96
|
+
maxTotalBytes,
|
|
97
|
+
inflatedBytes: 0,
|
|
98
|
+
aborted: false
|
|
99
|
+
});
|
|
100
|
+
const EMPTY_BYTES = /* @__PURE__ */ new Uint8Array(0);
|
|
101
|
+
const concatChunks = (chunks, totalBytes) => {
|
|
102
|
+
const merged = new Uint8Array(totalBytes);
|
|
103
|
+
let offset = 0;
|
|
104
|
+
for (const chunk of chunks) {
|
|
105
|
+
merged.set(chunk, offset);
|
|
106
|
+
offset += chunk.length;
|
|
107
|
+
}
|
|
108
|
+
return merged;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Inflate one entry chunk by chunk, checking every bound before a chunk is
|
|
112
|
+
* kept. Pausing the stream abandons the rest of the entry, so a limit bounds
|
|
113
|
+
* memory rather than reporting an overrun afterwards.
|
|
114
|
+
*
|
|
115
|
+
* The budget is charged as chunks arrive, which keeps concurrent inflations
|
|
116
|
+
* of one package honest, and refunded when this entry stops at a limit or
|
|
117
|
+
* fails, since none of its bytes are kept. Stream errors (a corrupt entry)
|
|
118
|
+
* reject; limits resolve with `ok: false` for the caller to map onto its own
|
|
119
|
+
* error or skip policy.
|
|
120
|
+
*/
|
|
121
|
+
const inflateEntryWithinLimits = async ({ entry, maxEntryBytes, maxCompressionRatio, budget, retain = true }) => {
|
|
122
|
+
const { compressedBytes, uncompressedBytes } = getZipEntrySizes(entry);
|
|
123
|
+
return await new Promise((resolve, reject) => {
|
|
124
|
+
const stream = entry.internalStream("uint8array");
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let entryBytes = 0;
|
|
127
|
+
let settled = false;
|
|
128
|
+
const settle = () => {
|
|
129
|
+
if (settled) return false;
|
|
130
|
+
settled = true;
|
|
131
|
+
return true;
|
|
132
|
+
};
|
|
133
|
+
const refund = () => {
|
|
134
|
+
budget.inflatedBytes -= entryBytes;
|
|
135
|
+
};
|
|
136
|
+
const stop = (limit) => {
|
|
137
|
+
if (!settle()) return;
|
|
138
|
+
stream.pause();
|
|
139
|
+
refund();
|
|
140
|
+
resolve({
|
|
141
|
+
ok: false,
|
|
142
|
+
limit
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
const limitFor = () => {
|
|
146
|
+
if (budget.aborted) return "aborted";
|
|
147
|
+
if (uncompressedBytes !== null && entryBytes > uncompressedBytes) return "declared-size";
|
|
148
|
+
if (exceedsCompressionRatio({
|
|
149
|
+
inflatedBytes: entryBytes,
|
|
150
|
+
compressedBytes,
|
|
151
|
+
maxRatio: maxCompressionRatio
|
|
152
|
+
})) return "compression-ratio";
|
|
153
|
+
if (entryBytes > maxEntryBytes) return "entry";
|
|
154
|
+
if (budget.inflatedBytes > budget.maxTotalBytes) return "total";
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
stream.on("data", (chunk) => {
|
|
158
|
+
if (settled) return;
|
|
159
|
+
entryBytes += chunk.length;
|
|
160
|
+
budget.inflatedBytes += chunk.length;
|
|
161
|
+
const limit = limitFor();
|
|
162
|
+
if (limit !== null) {
|
|
163
|
+
stop(limit);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (retain) chunks.push(chunk);
|
|
167
|
+
}).on("end", () => {
|
|
168
|
+
if (!settle()) return;
|
|
169
|
+
resolve({
|
|
170
|
+
ok: true,
|
|
171
|
+
bytes: retain ? concatChunks(chunks, entryBytes) : EMPTY_BYTES
|
|
172
|
+
});
|
|
173
|
+
}).on("error", (error) => {
|
|
174
|
+
if (!settle()) return;
|
|
175
|
+
refund();
|
|
176
|
+
reject(error);
|
|
177
|
+
}).resume();
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
//#endregion
|
|
181
|
+
export { DOCX_COMPRESSION_RATIO_GRACE_BYTES, DOCX_MAX_COMPRESSION_RATIO, compressionRatioLimitFor, countCentralDirectoryRecords, createInflationBudget, exceedsCompressionRatio, getZipEntrySizes, inflateEntryWithinLimits, isRatioBoundedPart, isStoredZipEntry };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countCentralDirectoryRecords, createInflationBudget, inflateEntryWithinLimits } from "./archiveInflation.js";
|
|
1
2
|
import { elementToXml, getLocalName, parseXmlDocument } from "./xmlParser.js";
|
|
2
3
|
import { TaggedError } from "better-result";
|
|
3
4
|
import JSZip from "jszip";
|
|
@@ -63,6 +64,10 @@ const loadPrivacyArchive = async (buffer) => {
|
|
|
63
64
|
message: "Document privacy input exceeded the compressed-size limit",
|
|
64
65
|
reason: "input-too-large"
|
|
65
66
|
});
|
|
67
|
+
if (countCentralDirectoryRecords(new Uint8Array(buffer), MAX_ARCHIVE_ENTRIES) > MAX_ARCHIVE_ENTRIES) throw new FolioDocumentPrivacyArchiveError({
|
|
68
|
+
message: "Document privacy input exceeded the package-entry limit",
|
|
69
|
+
reason: "too-many-entries"
|
|
70
|
+
});
|
|
66
71
|
let zip;
|
|
67
72
|
try {
|
|
68
73
|
zip = await JSZip.loadAsync(buffer);
|
|
@@ -109,6 +114,24 @@ const rewriteCorePropertiesPrivacy = (xml, transforms) => {
|
|
|
109
114
|
removedMetadataProperties: FOLIO_DOCUMENT_METADATA_PROPERTIES.filter((property) => removedPropertySet.has(property))
|
|
110
115
|
};
|
|
111
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
* Read the core-properties part within its size limit. The declared size was
|
|
119
|
+
* checked when the archive loaded, but only the inflation itself can show that
|
|
120
|
+
* the part is no larger than it claims.
|
|
121
|
+
*/
|
|
122
|
+
const readCoreProperties = async (entry) => {
|
|
123
|
+
const result = await inflateEntryWithinLimits({
|
|
124
|
+
entry,
|
|
125
|
+
maxEntryBytes: MAX_CORE_PROPERTIES_BYTES,
|
|
126
|
+
maxCompressionRatio: 200,
|
|
127
|
+
budget: createInflationBudget(MAX_CORE_PROPERTIES_BYTES)
|
|
128
|
+
});
|
|
129
|
+
if (!result.ok) throw new FolioDocumentPrivacyArchiveError({
|
|
130
|
+
message: "Document privacy core properties exceeded the part-size limit",
|
|
131
|
+
reason: "core-properties-too-large"
|
|
132
|
+
});
|
|
133
|
+
return new TextDecoder("utf-8", { ignoreBOM: true }).decode(result.bytes);
|
|
134
|
+
};
|
|
112
135
|
/** Rewrite selected package metadata fields without changing other package parts. */
|
|
113
136
|
const rewriteDocxMetadataPrivacy = async (buffer, { transforms }) => {
|
|
114
137
|
const appliedTransforms = resolveFolioDocumentPrivacyTransforms(transforms);
|
|
@@ -121,7 +144,7 @@ const rewriteDocxMetadataPrivacy = async (buffer, { transforms }) => {
|
|
|
121
144
|
removedMetadataProperties: []
|
|
122
145
|
}
|
|
123
146
|
};
|
|
124
|
-
const rewritten = rewriteCorePropertiesPrivacy(await coreProperties
|
|
147
|
+
const rewritten = rewriteCorePropertiesPrivacy(await readCoreProperties(coreProperties), appliedTransforms);
|
|
125
148
|
if (rewritten.removedMetadataProperties.length === 0) return {
|
|
126
149
|
buffer,
|
|
127
150
|
privacyReport: {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
//#region src/docx/noteReferenceMark.d.ts
|
|
3
|
+
type NoteKind = "footnote" | "endnote";
|
|
4
|
+
/** The run carrying a note's in-note reference mark, in the note reference style. */
|
|
5
|
+
declare const noteReferenceMarkRun: (kind: NoteKind) => document_d_exports.Run;
|
|
6
|
+
/**
|
|
7
|
+
* A note's content with its reference mark run at the start of its first
|
|
8
|
+
* paragraph, added when the note has none.
|
|
9
|
+
*/
|
|
10
|
+
declare const withNoteReferenceMark: (kind: NoteKind, content: readonly document_d_exports.BlockContent[]) => document_d_exports.BlockContent[];
|
|
11
|
+
/** A new note, as an editor adds one: its content led by its own reference mark. */
|
|
12
|
+
declare function createNote(kind: "footnote", id: number, content: readonly document_d_exports.BlockContent[]): document_d_exports.Footnote;
|
|
13
|
+
declare function createNote(kind: "endnote", id: number, content: readonly document_d_exports.BlockContent[]): document_d_exports.Endnote;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { createNote, noteReferenceMarkRun, withNoteReferenceMark };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cloneParagraphWithPropertySource } from "./paragraphPropertySource.js";
|
|
2
|
+
//#region src/docx/noteReferenceMark.ts
|
|
3
|
+
const MARK_ELEMENT = /<(?:[\w.-]+:)?(?:footnote|endnote)Ref[\s/>]/u;
|
|
4
|
+
/** The run carrying a note's in-note reference mark, in the note reference style. */
|
|
5
|
+
const noteReferenceMarkRun = (kind) => ({
|
|
6
|
+
type: "run",
|
|
7
|
+
formatting: { styleId: kind === "footnote" ? "FootnoteReference" : "EndnoteReference" },
|
|
8
|
+
content: [{
|
|
9
|
+
type: "preservedXml",
|
|
10
|
+
xml: `<w:${kind}Ref/>`,
|
|
11
|
+
text: ""
|
|
12
|
+
}]
|
|
13
|
+
});
|
|
14
|
+
const holdsReferenceMark = (value) => {
|
|
15
|
+
if (Array.isArray(value)) return value.some(holdsReferenceMark);
|
|
16
|
+
if (typeof value !== "object" || value === null) return false;
|
|
17
|
+
const record = value;
|
|
18
|
+
if (record["type"] === "preservedXml" && typeof record["xml"] === "string") return MARK_ELEMENT.test(record["xml"]);
|
|
19
|
+
return Object.values(record).some(holdsReferenceMark);
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A note's content with its reference mark run at the start of its first
|
|
23
|
+
* paragraph, added when the note has none.
|
|
24
|
+
*/
|
|
25
|
+
const withNoteReferenceMark = (kind, content) => {
|
|
26
|
+
if (holdsReferenceMark(content)) return [...content];
|
|
27
|
+
const index = content.findIndex((block) => block.type === "paragraph");
|
|
28
|
+
const mark = noteReferenceMarkRun(kind);
|
|
29
|
+
if (index === -1) return [{
|
|
30
|
+
type: "paragraph",
|
|
31
|
+
content: [mark]
|
|
32
|
+
}, ...content];
|
|
33
|
+
const paragraph = content[index];
|
|
34
|
+
return content.map((block, position) => position === index ? cloneParagraphWithPropertySource(paragraph, { content: [mark, ...paragraph.content] }) : block);
|
|
35
|
+
};
|
|
36
|
+
function createNote(kind, id, content) {
|
|
37
|
+
return kind === "footnote" ? {
|
|
38
|
+
type: "footnote",
|
|
39
|
+
id,
|
|
40
|
+
content: withNoteReferenceMark(kind, content)
|
|
41
|
+
} : {
|
|
42
|
+
type: "endnote",
|
|
43
|
+
id,
|
|
44
|
+
content: withNoteReferenceMark(kind, content)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { createNote, noteReferenceMarkRun, withNoteReferenceMark };
|
|
@@ -2,6 +2,7 @@ import { isValidHexId } from "../utils/hexId.js";
|
|
|
2
2
|
import { attributeRemainder } from "./attributeRemainder.js";
|
|
3
3
|
import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
|
|
4
4
|
import { CAPTURE, dispatchChildrenWithContext, ownedElsewhere, transitionalNamespaceOf, withPreservedChildren } from "./containerChildren.js";
|
|
5
|
+
import { resolveDefaultParagraphStyle } from "./defaultParagraphStyle.js";
|
|
5
6
|
import { parseFieldType } from "./fieldParser.js";
|
|
6
7
|
import { fieldStateOf, parseFieldState } from "./fieldState.js";
|
|
7
8
|
import { HYPERLINK_CHILD_HANDLERS, parseHyperlink as parseHyperlink$1, parseHyperlinkShell } from "./hyperlinkParser.js";
|
|
@@ -1241,7 +1242,8 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
|
|
|
1241
1242
|
}
|
|
1242
1243
|
if (implicitChildLevelAdvances > 0) listRendering.implicitChildLevelAdvances = implicitChildLevelAdvances;
|
|
1243
1244
|
paragraph.listRendering = listRendering;
|
|
1244
|
-
const
|
|
1245
|
+
const styleInd = styleChainInd(paragraph.formatting?.styleId ?? (styles ? resolveDefaultParagraphStyle(styles.values())?.styleId : void 0), styles);
|
|
1246
|
+
const chainInd = numPrFromStyle ? styleInd : {
|
|
1245
1247
|
left: false,
|
|
1246
1248
|
firstLine: false
|
|
1247
1249
|
};
|
|
@@ -1250,8 +1252,8 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
|
|
|
1250
1252
|
const directInd = pPr ? findChild(pPr, "w", "ind") : null;
|
|
1251
1253
|
const hasDirectLeft = hasAttributeAnySpelling(directInd, "CT_Ind @left");
|
|
1252
1254
|
const hasDirectFirstLineOrHanging = parseNumericAttribute(directInd, "w", "firstLine") !== void 0 || parseNumericAttribute(directInd, "w", "hanging") !== void 0;
|
|
1253
|
-
if (!hasDirectLeft && !chainInd.left && level.pPr.indentLeft !== void 0) paragraph.formatting.indentLeft = level.pPr.indentLeft;
|
|
1254
|
-
if (!hasDirectFirstLineOrHanging && !chainInd.firstLine && numberingLevelHasMarkerSlot(level)) {
|
|
1255
|
+
if (!hasDirectLeft && !chainInd.left && level.pPr.indentLeft !== void 0 && (level.pPr.indentLeft !== 0 || styleInd.left)) paragraph.formatting.indentLeft = level.pPr.indentLeft;
|
|
1256
|
+
if (!hasDirectFirstLineOrHanging && !chainInd.firstLine && numberingLevelHasMarkerSlot(level) && (level.pPr.indentFirstLine !== 0 || styleInd.firstLine)) {
|
|
1255
1257
|
if (level.pPr.indentFirstLine !== void 0) paragraph.formatting.indentFirstLine = level.pPr.indentFirstLine;
|
|
1256
1258
|
if (level.pPr.hangingIndent !== void 0) paragraph.formatting.hangingIndent = level.pPr.hangingIndent;
|
|
1257
1259
|
}
|
|
@@ -136,4 +136,4 @@ declare const linkParagraphPropertySourceCandidate: (target: document_d_exports.
|
|
|
136
136
|
declare const getParagraphPropertySourceCandidate: (paragraph: document_d_exports.Paragraph) => document_d_exports.Paragraph | undefined;
|
|
137
137
|
declare const getParagraphPropertySourceTransferId: (paragraph: document_d_exports.Paragraph) => string | undefined;
|
|
138
138
|
//#endregion
|
|
139
|
-
export { DecodedTableCellParagraphSourcePayload, PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES, PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, ParagraphPropertySourceValidationCode, ParagraphPropertySourceValidationError, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR, TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS, TableCellParagraphPropertySourceBinding, TableCellParagraphSourcePayloadErrorClassification, assignDocumentParagraphPropertySourceContract, assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, cloneTableCellsWithParagraphPropertyCaptures, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphPropertyCapture, copyParagraphPropertySource, createProseParagraphWithPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getExplicitParagraphPropertySourceTransfers, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, isParagraphPropertySourceToken, joinProseParagraphsWithRightPropertySource, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, markParagraphPropertySourceTransfers, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceMatchesEmission, paragraphPropertySourceTokenMatchesContract, proseParagraphAttrsWithoutPropertySource, recreateProseNodeWithDetachedParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, restoreTableCellsWithParagraphPropertySources, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource, transportTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings };
|
|
139
|
+
export { DecodedTableCellParagraphSourcePayload, PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES, PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, ParagraphPropertySourceTransfer, ParagraphPropertySourceValidationCode, ParagraphPropertySourceValidationError, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR, TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS, TableCellParagraphPropertySourceBinding, TableCellParagraphSourcePayloadErrorClassification, assignDocumentParagraphPropertySourceContract, assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, cloneTableCellsWithParagraphPropertyCaptures, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphPropertyCapture, copyParagraphPropertySource, createProseParagraphWithPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getExplicitParagraphPropertySourceTransfers, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, isParagraphPropertySourceToken, joinProseParagraphsWithRightPropertySource, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, markParagraphPropertySourceTransfers, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceMatchesEmission, paragraphPropertySourceTokenMatchesContract, proseParagraphAttrsWithoutPropertySource, recreateProseNodeWithDetachedParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, restoreTableCellsWithParagraphPropertySources, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource, transportTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings };
|
|
@@ -542,7 +542,7 @@ const tableCellParagraphPropertySourceBindingForTransport = (paragraph, inspecti
|
|
|
542
542
|
};
|
|
543
543
|
/** Prepare opaque continuation cells for ProseMirror and Yjs transport. */
|
|
544
544
|
const transportTableCellsWithParagraphPropertySources = (cells) => {
|
|
545
|
-
const cloned = structuredClone(
|
|
545
|
+
const cloned = cells.map((cell) => structuredClone(cell));
|
|
546
546
|
const sources = paragraphsInTableCells(cells);
|
|
547
547
|
const targets = paragraphsInTableCells(cloned);
|
|
548
548
|
if (sources.length !== targets.length) panic("The cloned table cells changed paragraph graph ownership.");
|
|
@@ -679,7 +679,9 @@ const proseParagraphAttrsWithoutPropertySource = (attrs) => {
|
|
|
679
679
|
/** Replace paragraph markup without losing its private parser-owner link. */
|
|
680
680
|
const setProseParagraphMarkupWithPropertySource = ({ attrs, ownership, pos, transaction }) => {
|
|
681
681
|
const source = transaction.doc.nodeAt(pos);
|
|
682
|
-
|
|
682
|
+
if (source) {
|
|
683
|
+
for (const [name, value] of Object.entries(attrs)) if (source.attrs[name] !== value) transaction.setNodeAttribute(pos, name, value);
|
|
684
|
+
} else transaction.setNodeMarkup(pos, void 0, attrs);
|
|
683
685
|
const target = transaction.doc.nodeAt(pos);
|
|
684
686
|
if (!source || !target) return;
|
|
685
687
|
if (ownership === "preserve") {
|
package/dist/docx/parser.js
CHANGED
|
@@ -738,7 +738,7 @@ function fullParseDocx(buffer, onProgress) {
|
|
|
738
738
|
* Faster than full parse when you only need variables
|
|
739
739
|
*/
|
|
740
740
|
async function getDocxVariables(buffer) {
|
|
741
|
-
const raw = await unzipDocx(buffer);
|
|
741
|
+
const raw = await unzipDocx(buffer, {}, { verifyUnreadEntries: false });
|
|
742
742
|
if (!raw.documentXml) return [];
|
|
743
743
|
return extractAllTemplateVariables(parseDocumentBody(raw.documentXml).content);
|
|
744
744
|
}
|
|
@@ -746,7 +746,7 @@ async function getDocxVariables(buffer) {
|
|
|
746
746
|
* Get document summary without full parsing
|
|
747
747
|
*/
|
|
748
748
|
async function getDocxSummary(buffer) {
|
|
749
|
-
const raw = await unzipDocx(buffer);
|
|
749
|
+
const raw = await unzipDocx(buffer, {}, { verifyUnreadEntries: false });
|
|
750
750
|
const variables = raw.documentXml ? extractAllTemplateVariables(parseDocumentBody(raw.documentXml).content) : [];
|
|
751
751
|
return {
|
|
752
752
|
hasDocument: raw.documentXml !== null,
|
package/dist/docx/rezip.js
CHANGED
|
@@ -1339,7 +1339,8 @@ async function serializeHeadersFootersToZip(doc, zip, compressionLevel) {
|
|
|
1339
1339
|
}
|
|
1340
1340
|
async function serializeNotesToZip({ doc, originalZip, newZip, compressionLevel, changedNoteParaIds }) {
|
|
1341
1341
|
const footnotes = doc.package.footnotes ?? [];
|
|
1342
|
-
|
|
1342
|
+
const originalFootnotes = doc.package.footnotes === void 0 ? null : findNotePartEntry(originalZip, "word/footnotes.xml");
|
|
1343
|
+
if (footnotes.length > 0 || originalFootnotes) if (originalFootnotes) await patchNotePartIntoZip({
|
|
1343
1344
|
conventionalLowerPath: "word/footnotes.xml",
|
|
1344
1345
|
currentXml: serializeFootnotes(footnotes),
|
|
1345
1346
|
replacementXml: serializeNewFootnotesPart(footnotes),
|
|
@@ -1359,7 +1360,8 @@ async function serializeNotesToZip({ doc, originalZip, newZip, compressionLevel,
|
|
|
1359
1360
|
compressionLevel
|
|
1360
1361
|
});
|
|
1361
1362
|
const endnotes = doc.package.endnotes ?? [];
|
|
1362
|
-
|
|
1363
|
+
const originalEndnotes = doc.package.endnotes === void 0 ? null : findNotePartEntry(originalZip, "word/endnotes.xml");
|
|
1364
|
+
if (endnotes.length > 0 || originalEndnotes) if (originalEndnotes) await patchNotePartIntoZip({
|
|
1363
1365
|
conventionalLowerPath: "word/endnotes.xml",
|
|
1364
1366
|
currentXml: serializeEndnotes(endnotes),
|
|
1365
1367
|
replacementXml: serializeNewEndnotesPart(endnotes),
|
|
@@ -165,7 +165,9 @@ type XmlSplice = {
|
|
|
165
165
|
* the rest of the part byte-for-byte, so it can write half a comment range:
|
|
166
166
|
* invalid OOXML that anchors the comment to nothing. Answers null when it
|
|
167
167
|
* would, leaving the caller to rewrite a wider region — ultimately the whole
|
|
168
|
-
* part from the model, which is balanced with itself.
|
|
168
|
+
* part from the model, which is balanced with itself. Overlapping source
|
|
169
|
+
* regions are refused too: replacing an inner region moves the outer region's
|
|
170
|
+
* end, so its original offsets cannot be applied to the resulting string.
|
|
169
171
|
*/
|
|
170
172
|
declare const spliceXml: (xml: string, splices: readonly XmlSplice[]) => string | null;
|
|
171
173
|
type NoteElementName = "footnote" | "endnote";
|
|
@@ -426,7 +426,11 @@ const routeChangedParagraphs = (originalXml, serializedXml, changedIds) => {
|
|
|
426
426
|
});
|
|
427
427
|
}
|
|
428
428
|
const candidate = spliceXml(originalXml, splices);
|
|
429
|
-
if (candidate === null
|
|
429
|
+
if (candidate === null) return {
|
|
430
|
+
type: "refused",
|
|
431
|
+
reason: "unsafe-paragraph-splices"
|
|
432
|
+
};
|
|
433
|
+
if (!splicesAsCanonical(candidate, PARAGRAPH_SCAN_NAMES)) return {
|
|
430
434
|
type: "refused",
|
|
431
435
|
reason: "replacement-namespace-conflict"
|
|
432
436
|
};
|
|
@@ -496,11 +500,18 @@ function buildPatchedNoteXml(originalXml, serializedXml, changedIds) {
|
|
|
496
500
|
* the rest of the part byte-for-byte, so it can write half a comment range:
|
|
497
501
|
* invalid OOXML that anchors the comment to nothing. Answers null when it
|
|
498
502
|
* would, leaving the caller to rewrite a wider region — ultimately the whole
|
|
499
|
-
* part from the model, which is balanced with itself.
|
|
503
|
+
* part from the model, which is balanced with itself. Overlapping source
|
|
504
|
+
* regions are refused too: replacing an inner region moves the outer region's
|
|
505
|
+
* end, so its original offsets cannot be applied to the resulting string.
|
|
500
506
|
*/
|
|
501
507
|
const spliceXml = (xml, splices) => {
|
|
502
508
|
let result = xml;
|
|
503
|
-
|
|
509
|
+
let unpatchedEnd = xml.length;
|
|
510
|
+
for (const { start, end, newXml } of [...splices].toSorted((a, b) => b.start - a.start)) {
|
|
511
|
+
if (end > unpatchedEnd) return null;
|
|
512
|
+
result = result.slice(0, start) + newXml + result.slice(end);
|
|
513
|
+
unpatchedEnd = start;
|
|
514
|
+
}
|
|
504
515
|
return patchBreaksCommentRangeBalance(xml, result) ? null : result;
|
|
505
516
|
};
|
|
506
517
|
/**
|
|
@@ -732,9 +743,25 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
|
|
|
732
743
|
const serializedParaIds = collectParaIds(serializedXml);
|
|
733
744
|
const effectiveChangedParaIds = changedParaIds ?? collectChangedNoteParaIds(baselineXml, serializedXml);
|
|
734
745
|
const unroutedChangedParaIds = new Set([...effectiveChangedParaIds].filter((paraId) => serializedParaIds.has(paraId)));
|
|
746
|
+
/** Notes the model has and the part does not: new notes, appended to the part. */
|
|
747
|
+
const addedNotes = [];
|
|
735
748
|
for (const [id, currentSyntaxEntries] of currentElements) {
|
|
736
749
|
const originalSyntaxEntries = originalElements.get(id);
|
|
737
750
|
const replacementSyntaxEntries = replacementElements.get(id);
|
|
751
|
+
if (originalSyntaxEntries === void 0) {
|
|
752
|
+
const replacementSyntax = currentSyntaxEntries.length === 1 && replacementSyntaxEntries?.length === 1 ? replacementSyntaxEntries[0] : void 0;
|
|
753
|
+
const replacementNote = replacementSyntax ? extractNoteElement(replacementXml, replacementSyntax, id) : null;
|
|
754
|
+
if (!replacementSyntax || !replacementNote) return {
|
|
755
|
+
type: "refused",
|
|
756
|
+
reason: "unroutable-paragraph"
|
|
757
|
+
};
|
|
758
|
+
for (const paraId of collectParaIds(replacementNote).keys()) unroutedChangedParaIds.delete(paraId);
|
|
759
|
+
addedNotes.push({
|
|
760
|
+
note: replacementNote,
|
|
761
|
+
syntax: replacementSyntax
|
|
762
|
+
});
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
738
765
|
if (currentSyntaxEntries.length !== 1 || originalSyntaxEntries?.length !== 1 || replacementSyntaxEntries?.length !== 1) return {
|
|
739
766
|
type: "refused",
|
|
740
767
|
reason: "unroutable-paragraph"
|
|
@@ -806,10 +833,36 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
|
|
|
806
833
|
});
|
|
807
834
|
}
|
|
808
835
|
}
|
|
836
|
+
for (const [id, baselineSyntaxEntries] of baselineElements) {
|
|
837
|
+
if (currentElements.has(id) || baselineSyntaxEntries.length !== 1) continue;
|
|
838
|
+
const originalSyntaxEntries = originalElements.get(id);
|
|
839
|
+
const originalSyntax = originalSyntaxEntries?.length === 1 ? originalSyntaxEntries[0] : void 0;
|
|
840
|
+
const originalOffsets = originalSyntax && findNoteElement(originalXml, originalSyntax, id);
|
|
841
|
+
if (!originalOffsets) return {
|
|
842
|
+
type: "refused",
|
|
843
|
+
reason: "unroutable-paragraph"
|
|
844
|
+
};
|
|
845
|
+
const removal = {
|
|
846
|
+
start: originalOffsets.start,
|
|
847
|
+
end: originalOffsets.end,
|
|
848
|
+
newXml: ""
|
|
849
|
+
};
|
|
850
|
+
paragraphSplices.push(removal);
|
|
851
|
+
noteSplices.push(removal);
|
|
852
|
+
}
|
|
809
853
|
if (unroutedChangedParaIds.size > 0) return {
|
|
810
854
|
type: "refused",
|
|
811
855
|
reason: "unroutable-paragraph"
|
|
812
856
|
};
|
|
857
|
+
if (addedNotes.length > 0) {
|
|
858
|
+
const appended = appendNotesSplice(originalXml, originalElements, addedNotes, { sourceXmlnsDeclarations: replacementXmlnsDeclarations });
|
|
859
|
+
if (!appended) return {
|
|
860
|
+
type: "refused",
|
|
861
|
+
reason: "unroutable-paragraph"
|
|
862
|
+
};
|
|
863
|
+
paragraphSplices.push(appended);
|
|
864
|
+
noteSplices.push(appended);
|
|
865
|
+
}
|
|
813
866
|
const patched = spliceXml(originalXml, paragraphSplices);
|
|
814
867
|
if (patched !== null) return {
|
|
815
868
|
type: "patched",
|
|
@@ -825,6 +878,31 @@ function buildPatchedNotePartXml({ originalXml, baselineXml, serializedXml, repl
|
|
|
825
878
|
};
|
|
826
879
|
}
|
|
827
880
|
/**
|
|
881
|
+
* Insert new notes after the part's last note, spelled with that note's
|
|
882
|
+
* prefixes. Null when the part holds no note to follow.
|
|
883
|
+
*/
|
|
884
|
+
const appendNotesSplice = (originalXml, originalElements, addedNotes, { sourceXmlnsDeclarations }) => {
|
|
885
|
+
let last = null;
|
|
886
|
+
for (const [id, entries] of originalElements) for (const syntax of entries) {
|
|
887
|
+
const offsets = findNoteElement(originalXml, syntax, id);
|
|
888
|
+
if (offsets && (!last || offsets.end > last.end)) last = {
|
|
889
|
+
end: offsets.end,
|
|
890
|
+
syntax
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (!last) return null;
|
|
894
|
+
const target = last.syntax;
|
|
895
|
+
return {
|
|
896
|
+
start: last.end,
|
|
897
|
+
end: last.end,
|
|
898
|
+
newXml: addedNotes.map(({ note, syntax }) => rewriteWordprocessingPrefixes(note, {
|
|
899
|
+
source: syntax,
|
|
900
|
+
target,
|
|
901
|
+
sourceXmlnsDeclarations
|
|
902
|
+
})).join("")
|
|
903
|
+
};
|
|
904
|
+
};
|
|
905
|
+
/**
|
|
828
906
|
* The full range of the first `<openLiteral …>…</closeTag>` element, or null.
|
|
829
907
|
* Used to locate an unkeyed sub-element (a level's `mc:AlternateContent`).
|
|
830
908
|
*/
|