@stll/folio-core 0.32.2 → 0.33.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/__fixtures__/paragraphs.d.ts +17 -0
- package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
- package/dist/ai-edits/apply.d.ts +49 -3
- package/dist/ai-edits/apply.js +652 -86
- package/dist/ai-edits/blockRange.d.ts +7 -3
- package/dist/ai-edits/blockRange.js +14 -6
- package/dist/ai-edits/headless.d.ts +39 -5
- package/dist/ai-edits/headless.js +76 -17
- package/dist/ai-edits/index.d.ts +6 -6
- package/dist/ai-edits/index.js +3 -3
- package/dist/ai-edits/read.d.ts +4 -0
- package/dist/ai-edits/read.js +30 -6
- package/dist/ai-edits/scoped-reading.js +4 -3
- package/dist/ai-edits/snapshot.d.ts +42 -2
- package/dist/ai-edits/snapshot.js +127 -62
- package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
- package/dist/ai-edits/table-row-column-mutations.js +98 -6
- package/dist/ai-edits/table-targets.d.ts +12 -3
- package/dist/ai-edits/table-targets.js +15 -5
- package/dist/ai-edits/types.d.ts +219 -15
- package/dist/ai-edits/word-diff.d.ts +54 -11
- package/dist/ai-edits/word-diff.js +221 -61
- package/dist/compare/__fixtures__/body-sequence.d.ts +56 -0
- package/dist/compare/__fixtures__/body-sequence.js +136 -0
- package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
- package/dist/compare/__fixtures__/nested-table.js +50 -0
- package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
- package/dist/compare/__fixtures__/numbered-list.js +85 -0
- package/dist/compare/column-alignment.d.ts +27 -0
- package/dist/compare/column-alignment.js +162 -0
- package/dist/compare/compare.d.ts +107 -0
- package/dist/compare/compare.js +412 -0
- package/dist/compare/formatting.d.ts +29 -0
- package/dist/compare/formatting.js +84 -0
- package/dist/compare/plan.d.ts +22 -0
- package/dist/compare/plan.js +1141 -0
- package/dist/compare/reproducible-package.d.ts +21 -0
- package/dist/compare/reproducible-package.js +47 -0
- package/dist/compare/scenario.d.ts +77 -0
- package/dist/compare/scenario.js +263 -0
- package/dist/compare/types.d.ts +297 -0
- package/dist/compare/types.js +42 -0
- package/dist/compare/verification.d.ts +83 -0
- package/dist/compare/verification.js +189 -0
- package/dist/compat/eigenpal.d.ts +9 -6
- package/dist/compat/eigenpal.js +6 -3
- package/dist/controller/layoutPipeline.d.ts +13 -0
- package/dist/controller/layoutPipeline.js +15 -1
- package/dist/display-list/build/buildContext.d.ts +39 -0
- package/dist/display-list/build/buildContext.js +31 -0
- package/dist/display-list/build/buildDisplayList.d.ts +79 -0
- package/dist/display-list/build/buildDisplayList.js +283 -0
- package/dist/display-list/build/colors.d.ts +20 -0
- package/dist/display-list/build/colors.js +136 -0
- package/dist/display-list/build/floatingImages.d.ts +24 -0
- package/dist/display-list/build/floatingImages.js +41 -0
- package/dist/display-list/build/fontTable.d.ts +31 -0
- package/dist/display-list/build/fontTable.js +97 -0
- package/dist/display-list/build/furniture.d.ts +86 -0
- package/dist/display-list/build/furniture.js +98 -0
- package/dist/display-list/build/glyphs.d.ts +55 -0
- package/dist/display-list/build/glyphs.js +103 -0
- package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
- package/dist/display-list/build/headerFooterPrimitives.js +40 -0
- package/dist/display-list/build/imagePrimitives.d.ts +35 -0
- package/dist/display-list/build/imagePrimitives.js +264 -0
- package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
- package/dist/display-list/build/pageBorderPrimitives.js +111 -0
- package/dist/display-list/build/pageFurniture.d.ts +35 -0
- package/dist/display-list/build/pageFurniture.js +141 -0
- package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
- package/dist/display-list/build/paragraphPrimitives.js +819 -0
- package/dist/display-list/build/regions.d.ts +46 -0
- package/dist/display-list/build/regions.js +56 -0
- package/dist/display-list/build/storyPrimitives.d.ts +29 -0
- package/dist/display-list/build/storyPrimitives.js +279 -0
- package/dist/display-list/build/strokes.d.ts +19 -0
- package/dist/display-list/build/strokes.js +78 -0
- package/dist/display-list/build/tablePrimitives.d.ts +38 -0
- package/dist/display-list/build/tablePrimitives.js +409 -0
- package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
- package/dist/display-list/build/textBoxPrimitives.js +129 -0
- package/dist/display-list/build/textDecorations.d.ts +29 -0
- package/dist/display-list/build/textDecorations.js +29 -0
- package/dist/display-list/build/unsupported.d.ts +57 -0
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
- package/dist/display-list/build/watermarkPrimitives.js +128 -0
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
- package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
- package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
- package/dist/display-list/editor/displayListPagePainter.js +63 -0
- package/dist/display-list/editor/pageRenderer.d.ts +24 -0
- package/dist/display-list/editor/pageRenderer.js +36 -0
- package/dist/display-list/primitives.d.ts +95 -0
- package/dist/display-list/primitives.js +127 -0
- package/dist/display-list/types.d.ts +472 -0
- package/dist/display-list/types.js +0 -0
- package/dist/document-operations.d.ts +39 -8
- package/dist/document-operations.js +169 -16
- package/dist/document-stories.d.ts +16 -0
- package/dist/document-stories.js +48 -4
- package/dist/docx/hyperlinkParser.d.ts +9 -1
- package/dist/docx/hyperlinkParser.js +19 -13
- package/dist/docx/packageParts.d.ts +35 -0
- package/dist/docx/packageParts.js +120 -0
- package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
- package/dist/docx/paraIdRangeNormalization.js +64 -0
- package/dist/docx/paragraphParser.js +101 -26
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/renderedPageBreakNormalization.js +3 -1
- package/dist/docx/revisionIdNormalization.d.ts +27 -0
- package/dist/docx/revisionIdNormalization.js +121 -0
- package/dist/docx/rezip.d.ts +6 -0
- package/dist/docx/rezip.js +51 -19
- package/dist/docx/selectiveSave.js +2 -2
- package/dist/docx/serializer/commentSerializer.js +6 -1
- package/dist/docx/serializer/paragraphSerializer.js +73 -20
- package/dist/docx/serializer/runSerializer.js +11 -10
- package/dist/docx/serializer/tableSerializer.js +26 -10
- package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
- package/dist/docx/server/createBilingualDocx.js +3 -2
- package/dist/docx/streamingXmlParser.d.ts +13 -1
- package/dist/docx/streamingXmlParser.js +49 -11
- package/dist/docx/unzip.d.ts +1 -2
- package/dist/docx/unzip.js +25 -6
- package/dist/docx/xmlParser.d.ts +3 -1
- package/dist/docx/xmlParser.js +33 -27
- package/dist/export-pdf.d.ts +43 -0
- package/dist/export-pdf.js +98 -0
- package/dist/fonts/headlessMeasure.d.ts +76 -0
- package/dist/fonts/headlessMeasure.js +0 -0
- package/dist/fonts/sfnt/parse.d.ts +51 -0
- package/dist/fonts/sfnt/parse.js +525 -0
- package/dist/fonts/sfnt/subset.d.ts +20 -0
- package/dist/fonts/sfnt/subset.js +350 -0
- package/dist/fonts/sfnt/tables.d.ts +88 -0
- package/dist/fonts/sfnt/tables.js +131 -0
- package/dist/fonts/sfnt/woff.d.ts +14 -0
- package/dist/fonts/sfnt/woff.js +163 -0
- package/dist/generated/text_shaper.js +324 -0
- package/dist/generated/text_shaper_bg.wasm +0 -0
- package/dist/headless-layout.d.ts +75 -0
- package/dist/headless-layout.js +350 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.js +6 -3
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
- package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
- package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
- package/dist/layout-engine/measure/advanceComposition.js +148 -0
- package/dist/layout-engine/measure/cache.d.ts +2 -0
- package/dist/layout-engine/measure/measureContainer.js +13 -0
- package/dist/layout-engine/measure/measureParagraph.js +1 -2
- package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
- package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
- package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
- package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
- package/dist/layout-engine/tableIndentCompatibility.js +15 -0
- package/dist/layout-engine/types.d.ts +8 -0
- package/dist/layout-painter/renderPage.d.ts +29 -1
- package/dist/layout-painter/renderPage.js +47 -19
- package/dist/layout-painter/renderParagraph.d.ts +21 -2
- package/dist/layout-painter/renderParagraph.js +1 -1
- package/dist/layout-painter/renderTable.js +37 -4
- package/dist/managers/editorShortcuts.d.ts +33 -1
- package/dist/managers/editorShortcuts.js +20 -1
- package/dist/markdown/renderRuns.js +7 -2
- package/dist/markdown/renderTable.js +5 -1
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/pdf/contentStream.d.ts +73 -0
- package/dist/pdf/contentStream.js +143 -0
- package/dist/pdf/fonts.d.ts +96 -0
- package/dist/pdf/fonts.js +593 -0
- package/dist/pdf/images.d.ts +28 -0
- package/dist/pdf/images.js +439 -0
- package/dist/pdf/objects.d.ts +93 -0
- package/dist/pdf/objects.js +237 -0
- package/dist/pdf/pageSpace.d.ts +43 -0
- package/dist/pdf/pageSpace.js +57 -0
- package/dist/pdf/paint.d.ts +32 -0
- package/dist/pdf/paint.js +442 -0
- package/dist/pdf/writePdf.d.ts +56 -0
- package/dist/pdf/writePdf.js +345 -0
- package/dist/prosemirror/commands/comments.js +96 -6
- package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
- package/dist/prosemirror/conversion/toProseDoc.js +41 -4
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
- package/dist/prosemirror/plugins/suggestionMode.js +1 -0
- package/dist/prosemirror/revisionCarriers.js +19 -2
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
- package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
- package/dist/prosemirror/validation.js +55 -27
- package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
- package/dist/prosemirror/zeroWidthAnchors.js +23 -0
- package/dist/redline.js +12 -79
- package/dist/server.d.ts +3 -3
- package/dist/server.js +2 -2
- package/dist/shaping/placeRun.d.ts +51 -0
- package/dist/shaping/placeRun.js +84 -0
- package/dist/shaping/shaper.d.ts +92 -0
- package/dist/shaping/shaper.js +102 -0
- package/dist/types/block-id.d.ts +18 -2
- package/dist/types/block-id.js +49 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/utils/clipboard.js +4 -3
- package/dist/utils/fontResolver.js +183 -0
- package/dist/utils/htmlComments.d.ts +17 -0
- package/dist/utils/htmlComments.js +22 -0
- package/dist/utils/scriptSegments.d.ts +12 -1
- package/dist/utils/scriptSegments.js +17 -1
- package/dist/version-comparison.d.ts +1 -1
- package/package.json +7 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { deterministicHexId } from "../utils/hexId.js";
|
|
2
|
+
//#region src/docx/paraIdRangeNormalization.ts
|
|
3
|
+
/**
|
|
4
|
+
* Keep every paragraph id a package carries inside the range the schema gives
|
|
5
|
+
* it.
|
|
6
|
+
*
|
|
7
|
+
* `w14:paraId`, `w14:textId` and the comment-part ids that reference a
|
|
8
|
+
* paragraph are `ST_LongHexNumber` with a maximum: the value has to be below
|
|
9
|
+
* `0x80000000`, so the ids are 31-bit. Producers exist that write eight hex
|
|
10
|
+
* digits without that bound, and folio preserves the ids a document arrives
|
|
11
|
+
* with — so a package can carry an out-of-range id in, and a save that copies
|
|
12
|
+
* it through hands a consumer a package it will refuse.
|
|
13
|
+
*
|
|
14
|
+
* {@link paraIdInRange} is the one mapping, and it is a pure function of the
|
|
15
|
+
* value alone. That is what lets the parser and the save agree without
|
|
16
|
+
* consulting each other: a paragraph's id in the model is the id the file gets,
|
|
17
|
+
* so bringing an id into range does not make a document's own identity move
|
|
18
|
+
* under it between reading and writing. The package pass below is the same
|
|
19
|
+
* mapping applied to every attribute that carries such an id, so a paragraph
|
|
20
|
+
* and every reference to it move together.
|
|
21
|
+
*/
|
|
22
|
+
/** Exclusive upper bound on a paragraph id: the values are 31-bit. */
|
|
23
|
+
const MAX_PARA_ID_EXCLUSIVE = 2147483648;
|
|
24
|
+
/**
|
|
25
|
+
* Every attribute that carries a paragraph id or the text-revision marker
|
|
26
|
+
* written beside one: the paragraph's own `w14:paraId` / `w14:textId` (the
|
|
27
|
+
* parser accepts a `w:` prefix too), the comment part's `w15:paraId` and the
|
|
28
|
+
* `w15:paraIdParent` that links a reply to its thread, and the durable-comment
|
|
29
|
+
* part's `w16cid:paraId`.
|
|
30
|
+
*/
|
|
31
|
+
const PARA_ID_ATTRIBUTE = /\b(w|w14|w15|w16cid):(paraId|paraIdParent|textId)=(?<quote>["'])([0-9A-Fa-f]{8})\k<quote>/gu;
|
|
32
|
+
/** A candidate part is one that mentions any of those attributes at all. */
|
|
33
|
+
const PARA_ID_CANDIDATE = /\b(?:w|w14|w15|w16cid):(?:paraId|paraIdParent|textId)=/u;
|
|
34
|
+
/**
|
|
35
|
+
* `value` when it already fits, and a value derived from it when it does not.
|
|
36
|
+
*
|
|
37
|
+
* The replacement is derived from the id being replaced and from nothing else.
|
|
38
|
+
* A package-aware search for a free id would read better here and would be
|
|
39
|
+
* wrong: the parser has no package to search, and an id that means one
|
|
40
|
+
* paragraph while reading and another while writing is worse than the
|
|
41
|
+
* vanishing chance of two rewritten ids landing on one value, which is a
|
|
42
|
+
* duplicate rather than a package a consumer refuses.
|
|
43
|
+
*/
|
|
44
|
+
const paraIdInRange = (value) => Number.parseInt(value, 16) < MAX_PARA_ID_EXCLUSIVE ? value : deterministicHexId(value);
|
|
45
|
+
/**
|
|
46
|
+
* Rewrite out-of-range paragraph ids across a whole package.
|
|
47
|
+
*
|
|
48
|
+
* Returns the parts unchanged when every id already fits, so a save of a
|
|
49
|
+
* document that never carried one is byte-identical.
|
|
50
|
+
*/
|
|
51
|
+
const normalizeParaIdRangeInXmlParts = (parts) => {
|
|
52
|
+
const normalized = new Map(parts);
|
|
53
|
+
for (const [path, xml] of parts) {
|
|
54
|
+
if (!PARA_ID_CANDIDATE.test(xml)) continue;
|
|
55
|
+
const rewritten = xml.replaceAll(PARA_ID_ATTRIBUTE, (whole, prefix, name, quote, value) => {
|
|
56
|
+
const replacement = paraIdInRange(value);
|
|
57
|
+
return replacement === value ? whole : `${prefix}:${name}=${quote}${replacement}${quote}`;
|
|
58
|
+
});
|
|
59
|
+
if (rewritten !== xml) normalized.set(path, rewritten);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { normalizeParaIdRangeInXmlParts, paraIdInRange };
|
|
@@ -2,16 +2,17 @@ import { isValidHexColor } from "../utils/colorResolver.js";
|
|
|
2
2
|
import { isValidHexId } from "../utils/hexId.js";
|
|
3
3
|
import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
|
|
4
4
|
import { parseFieldType } from "./fieldParser.js";
|
|
5
|
-
import { parseHyperlink as parseHyperlink$1 } from "./hyperlinkParser.js";
|
|
5
|
+
import { parseHyperlink as parseHyperlink$1, parseHyperlinkChild } from "./hyperlinkParser.js";
|
|
6
6
|
import { markerFormattingFromLevel } from "./numberingParser.js";
|
|
7
|
+
import { paraIdInRange } from "./paraIdRangeNormalization.js";
|
|
7
8
|
import { BorderStyleSchema, FrameWrapSchema, FrameXAlignSchema, FrameYAlignSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, TabLeaderSchema, TabStopAlignmentSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
|
|
8
9
|
import { consolidateParagraphContent } from "./runConsolidator.js";
|
|
9
10
|
import { parseRun, parseRunProperties } from "./runParser.js";
|
|
10
11
|
import { parseSdtProperties } from "./sdtProperties.js";
|
|
11
12
|
import { parseSectionProperties } from "./sectionParser.js";
|
|
12
|
-
import { elementToXml, findChild, findChildren, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
|
|
13
|
+
import { WORDPROCESSINGML_NAMESPACE_URIS, elementToXml, findChild, findChildByNamespaceUri, findChildren, getAttribute, getChildElements, getLocalName, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute } from "./xmlParser.js";
|
|
13
14
|
import { panic } from "better-result";
|
|
14
|
-
import { normalizeRevisionId } from "@stll/docx-core/model";
|
|
15
|
+
import { PARAGRAPH_MARK_CHANGE_KINDS, normalizeRevisionId } from "@stll/docx-core/model";
|
|
15
16
|
//#region src/docx/paragraphParser.ts
|
|
16
17
|
/**
|
|
17
18
|
* Extract plain text from a math element (recursive text content extraction)
|
|
@@ -528,19 +529,16 @@ function parseParagraphMarkChange(pPr) {
|
|
|
528
529
|
if (!pPr) return;
|
|
529
530
|
const rPr = findChild(pPr, "w", "rPr");
|
|
530
531
|
if (!rPr) return;
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
kind: "del",
|
|
539
|
-
info: parseTrackedChangeInfo(del)
|
|
540
|
-
};
|
|
532
|
+
for (const kind of PARAGRAPH_MARK_CHANGE_KINDS) {
|
|
533
|
+
const element = findChildByNamespaceUri(rPr, WORDPROCESSINGML_NAMESPACE_URIS, kind);
|
|
534
|
+
if (element) return {
|
|
535
|
+
kind,
|
|
536
|
+
info: parseTrackedChangeInfo(element)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
541
539
|
}
|
|
542
540
|
function isTrackedChangeWrapperChild(content) {
|
|
543
|
-
return content.type === "run" || content.type === "hyperlink";
|
|
541
|
+
return content.type === "run" || content.type === "hyperlink" || content.type === "bookmarkStart" || content.type === "bookmarkEnd" || content.type === "simpleField" || content.type === "complexField" || content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo";
|
|
544
542
|
}
|
|
545
543
|
function isInlineSdtContent(content) {
|
|
546
544
|
return content.type === "run" || content.type === "hyperlink" || content.type === "simpleField" || content.type === "complexField" || content.type === "inlineSdt" || content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo" || content.type === "mathEquation";
|
|
@@ -649,6 +647,80 @@ function pushInlineSdtSegments({ contents, properties, parsedContent }) {
|
|
|
649
647
|
function parseHyperlink(node, rels, styles, theme, media, rootXmlns = {}) {
|
|
650
648
|
return parseHyperlink$1(node, rels, styles, theme, media, rootXmlns);
|
|
651
649
|
}
|
|
650
|
+
/** The revision wrapper a `w:hyperlink` child is, when it is one. */
|
|
651
|
+
const hyperlinkRevisionWrapperType = (node) => {
|
|
652
|
+
switch (getLocalName(node.name)) {
|
|
653
|
+
case "ins": return "insertion";
|
|
654
|
+
case "del": return "deletion";
|
|
655
|
+
case "moveFrom": return "moveFrom";
|
|
656
|
+
case "moveTo": return "moveTo";
|
|
657
|
+
default: return;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
const isHyperlinkChildContent = (content) => content.type === "run" || content.type === "bookmarkStart" || content.type === "bookmarkEnd";
|
|
661
|
+
/**
|
|
662
|
+
* A `w:hyperlink` as paragraph content, with any revision wrapper it holds
|
|
663
|
+
* hoisted around it.
|
|
664
|
+
*
|
|
665
|
+
* OOXML nests `w:ins`/`w:del` INSIDE `w:hyperlink`; the model nests the
|
|
666
|
+
* hyperlink inside the revision, because a revision is the unit a redline
|
|
667
|
+
* reads and a link that is half deleted is two links to it. This is the exact
|
|
668
|
+
* inverse of what the serializer writes, so a package survives the round trip.
|
|
669
|
+
*/
|
|
670
|
+
function parseHyperlinkParagraphContents(node, rels, styles, theme, media, rootXmlns) {
|
|
671
|
+
const children = getChildElements(node);
|
|
672
|
+
if (!children.some((child) => hyperlinkRevisionWrapperType(child) !== void 0)) return [parseHyperlink(node, rels, styles, theme, media, rootXmlns)];
|
|
673
|
+
const inScopeXmlns = mergeXmlnsDeclarations(rootXmlns, node);
|
|
674
|
+
const shell = parseHyperlink(node, rels, styles, theme, media, rootXmlns);
|
|
675
|
+
const linkOver = (linkChildren) => ({
|
|
676
|
+
...shell,
|
|
677
|
+
children: [...linkChildren]
|
|
678
|
+
});
|
|
679
|
+
const contents = [];
|
|
680
|
+
let plain = [];
|
|
681
|
+
const flushPlain = () => {
|
|
682
|
+
if (plain.length > 0) {
|
|
683
|
+
contents.push(linkOver(plain));
|
|
684
|
+
plain = [];
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
for (const child of children) {
|
|
688
|
+
const wrapperType = hyperlinkRevisionWrapperType(child);
|
|
689
|
+
if (wrapperType === void 0) {
|
|
690
|
+
const parsed = parseHyperlinkChild(child, styles, theme, rels, media, inScopeXmlns);
|
|
691
|
+
if (parsed) plain.push(parsed);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
flushPlain();
|
|
695
|
+
const wrapped = parseParagraphContents(child, styles, theme, null, rels, media, wrapperType === "deletion" || wrapperType === "moveFrom" ? "deletion" : "default", inScopeXmlns);
|
|
696
|
+
const content = [];
|
|
697
|
+
let linked = [];
|
|
698
|
+
const flushLinked = () => {
|
|
699
|
+
if (linked.length > 0) {
|
|
700
|
+
content.push(linkOver(linked));
|
|
701
|
+
linked = [];
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
for (const item of wrapped) {
|
|
705
|
+
if (isHyperlinkChildContent(item)) {
|
|
706
|
+
linked.push(item);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
flushLinked();
|
|
710
|
+
if (isTrackedChangeWrapperChild(item)) content.push(item);
|
|
711
|
+
}
|
|
712
|
+
flushLinked();
|
|
713
|
+
pushTrackedChangeWrapper({
|
|
714
|
+
contents,
|
|
715
|
+
type: wrapperType,
|
|
716
|
+
info: parseTrackedChangeInfo(child),
|
|
717
|
+
content,
|
|
718
|
+
preserveEmpty: true
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
flushPlain();
|
|
722
|
+
return contents;
|
|
723
|
+
}
|
|
652
724
|
/**
|
|
653
725
|
* Parse bookmark start (w:bookmarkStart)
|
|
654
726
|
* Delegates to bookmarkParser module.
|
|
@@ -801,7 +873,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
|
|
|
801
873
|
break;
|
|
802
874
|
}
|
|
803
875
|
case "hyperlink":
|
|
804
|
-
contents.push(
|
|
876
|
+
contents.push(...parseHyperlinkParagraphContents(child, rels, styles, theme, media, inScopeXmlns));
|
|
805
877
|
break;
|
|
806
878
|
case "bookmarkStart":
|
|
807
879
|
contents.push(parseBookmarkStart(child));
|
|
@@ -964,9 +1036,9 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
|
|
|
964
1036
|
content: []
|
|
965
1037
|
};
|
|
966
1038
|
const paraId = getAttribute(node, "w14", "paraId") ?? getAttribute(node, "w", "paraId");
|
|
967
|
-
if (paraId && isValidHexId(paraId)) paragraph.paraId = paraId;
|
|
1039
|
+
if (paraId && isValidHexId(paraId)) paragraph.paraId = paraIdInRange(paraId);
|
|
968
1040
|
const textId = getAttribute(node, "w14", "textId") ?? getAttribute(node, "w", "textId");
|
|
969
|
-
if (textId && isValidHexId(textId)) paragraph.textId = textId;
|
|
1041
|
+
if (textId && isValidHexId(textId)) paragraph.textId = paraIdInRange(textId);
|
|
970
1042
|
if (!options?.inHeaderFooter && paragraphStartsWithRenderedPageBreak(node)) paragraph.renderedPageBreakBefore = true;
|
|
971
1043
|
const pPr = findChild(node, "w", "pPr");
|
|
972
1044
|
if (pPr) {
|
|
@@ -980,16 +1052,19 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
|
|
|
980
1052
|
if (sectPr) paragraph.sectionProperties = parseSectionProperties(sectPr);
|
|
981
1053
|
}
|
|
982
1054
|
paragraph.content = consolidateParagraphContent(parseParagraphContents(node, styles, theme, numbering, rels, media, "default", options?.rootXmlns ?? {}));
|
|
983
|
-
|
|
1055
|
+
const paragraphFormatting = paragraph.formatting;
|
|
1056
|
+
const directNumPr = paragraphFormatting?.numPr;
|
|
1057
|
+
const styleNumPr = paragraphFormatting?.styleId && styles ? styles.get(paragraphFormatting.styleId)?.pPr?.numPr : void 0;
|
|
1058
|
+
let effectiveNumPr = directNumPr;
|
|
984
1059
|
let numPrFromStyle = false;
|
|
985
|
-
if (
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1060
|
+
if (paragraphFormatting && styleNumPr && directNumPr?.numId === void 0) {
|
|
1061
|
+
effectiveNumPr = {
|
|
1062
|
+
...styleNumPr,
|
|
1063
|
+
...directNumPr
|
|
1064
|
+
};
|
|
1065
|
+
numPrFromStyle = true;
|
|
1066
|
+
paragraphFormatting.numPr = effectiveNumPr;
|
|
1067
|
+
paragraphFormatting.numPrFromStyle = styleNumPr;
|
|
993
1068
|
}
|
|
994
1069
|
if (effectiveNumPr && numbering) {
|
|
995
1070
|
const { numId, ilvl = 0 } = effectiveNumPr;
|
|
@@ -37,7 +37,9 @@ const scanHyperlink = (hyperlink, scan) => {
|
|
|
37
37
|
};
|
|
38
38
|
const scanInlineContent = (content, scan) => {
|
|
39
39
|
for (const child of content) {
|
|
40
|
-
|
|
40
|
+
let result;
|
|
41
|
+
if (child.type === "run") result = scanRun(child, scan);
|
|
42
|
+
else if (child.type === "hyperlink") result = scanHyperlink(child, scan);
|
|
41
43
|
if (result !== void 0) return result;
|
|
42
44
|
}
|
|
43
45
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/docx/revisionIdNormalization.d.ts
|
|
2
|
+
declare const RevisionIdCollisionError_base: import("better-result").TaggedErrorClass<"RevisionIdCollisionError">;
|
|
3
|
+
/**
|
|
4
|
+
* Two revision elements in one package claimed the same `w:id`.
|
|
5
|
+
*
|
|
6
|
+
* `w:id` on a revision element is unique across the package, so a collision is
|
|
7
|
+
* a package a consumer may reject rather than a cosmetic detail. The
|
|
8
|
+
* normalization below hands every id it emits to one choke point, which throws
|
|
9
|
+
* this rather than letting the duplicate reach the ZIP.
|
|
10
|
+
*/
|
|
11
|
+
declare class RevisionIdCollisionError extends RevisionIdCollisionError_base<{
|
|
12
|
+
message: string;
|
|
13
|
+
revisionId: number;
|
|
14
|
+
part: string;
|
|
15
|
+
}> {}
|
|
16
|
+
declare const REVISION_ELEMENT_NAMES: Set<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Keep physical tracked-change element ids unique across a package.
|
|
19
|
+
*
|
|
20
|
+
* Live editor marks and operation receipts keep their logical revision IDs.
|
|
21
|
+
* Saving assigns fresh IDs only where one logical change was split into
|
|
22
|
+
* multiple physical OOXML wrappers; reopening therefore exposes the physical
|
|
23
|
+
* wrapper IDs that the file format requires.
|
|
24
|
+
*/
|
|
25
|
+
declare const normalizeRevisionIdsInXmlParts: (parts: ReadonlyMap<string, string>) => Map<string, string>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { REVISION_ELEMENT_NAMES, RevisionIdCollisionError, normalizeRevisionIdsInXmlParts };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { rewriteStreamingXmlDecimalAttributes } from "./streamingXmlParser.js";
|
|
2
|
+
import { WORDPROCESSINGML_NAMESPACE_URIS, findAttributeByNamespaceUri, getLocalName, getNamespaceUri } from "./xmlParser.js";
|
|
3
|
+
import { XmlResourceLimitError, assertXmlResourceLimits } from "./xmlResourceLimits.js";
|
|
4
|
+
import { TaggedError } from "better-result";
|
|
5
|
+
//#region src/docx/revisionIdNormalization.ts
|
|
6
|
+
/**
|
|
7
|
+
* Two revision elements in one package claimed the same `w:id`.
|
|
8
|
+
*
|
|
9
|
+
* `w:id` on a revision element is unique across the package, so a collision is
|
|
10
|
+
* a package a consumer may reject rather than a cosmetic detail. The
|
|
11
|
+
* normalization below hands every id it emits to one choke point, which throws
|
|
12
|
+
* this rather than letting the duplicate reach the ZIP.
|
|
13
|
+
*/
|
|
14
|
+
var RevisionIdCollisionError = class extends TaggedError("RevisionIdCollisionError") {};
|
|
15
|
+
const REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
|
|
16
|
+
"cellDel",
|
|
17
|
+
"cellIns",
|
|
18
|
+
"cellMerge",
|
|
19
|
+
"del",
|
|
20
|
+
"ins",
|
|
21
|
+
"moveFrom",
|
|
22
|
+
"moveTo",
|
|
23
|
+
"numberingChange",
|
|
24
|
+
"pPrChange",
|
|
25
|
+
"rPrChange",
|
|
26
|
+
"sectPrChange",
|
|
27
|
+
"tblGridChange",
|
|
28
|
+
"tblPrChange",
|
|
29
|
+
"tblPrExChange",
|
|
30
|
+
"tcPrChange",
|
|
31
|
+
"trPrChange"
|
|
32
|
+
]);
|
|
33
|
+
const REVISION_ELEMENT_CANDIDATE = new RegExp(`<(?:[^\\s<>/:]+:)?(?:${[...REVISION_ELEMENT_NAMES].join("|")})(?:[\\s/>])`, "u");
|
|
34
|
+
const revisionAttribute = (element) => {
|
|
35
|
+
if (!element.name || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "") || !REVISION_ELEMENT_NAMES.has(getLocalName(element.name))) return null;
|
|
36
|
+
const attribute = findAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "id");
|
|
37
|
+
if (!attribute) return null;
|
|
38
|
+
const id = Number(attribute.value);
|
|
39
|
+
return Number.isSafeInteger(id) && id >= 0 ? {
|
|
40
|
+
name: attribute.name,
|
|
41
|
+
id
|
|
42
|
+
} : null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Keep physical tracked-change element ids unique across a package.
|
|
46
|
+
*
|
|
47
|
+
* Live editor marks and operation receipts keep their logical revision IDs.
|
|
48
|
+
* Saving assigns fresh IDs only where one logical change was split into
|
|
49
|
+
* multiple physical OOXML wrappers; reopening therefore exposes the physical
|
|
50
|
+
* wrapper IDs that the file format requires.
|
|
51
|
+
*/
|
|
52
|
+
const normalizeRevisionIdsInXmlParts = (parts) => {
|
|
53
|
+
const candidates = [...parts].filter(([, xml]) => REVISION_ELEMENT_CANDIDATE.test(xml));
|
|
54
|
+
const occurrencesByPath = /* @__PURE__ */ new Map();
|
|
55
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
56
|
+
for (const [path, xml] of candidates) {
|
|
57
|
+
assertXmlResourceLimits(xml);
|
|
58
|
+
const ids = [];
|
|
59
|
+
if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
|
|
60
|
+
const attribute = revisionAttribute(element);
|
|
61
|
+
if (attribute) {
|
|
62
|
+
ids.push(attribute.id);
|
|
63
|
+
reserved.add(attribute.id);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}).status === "unsupported") throw new XmlResourceLimitError({
|
|
67
|
+
message: `Revision-id normalization could not safely scan ${path}`,
|
|
68
|
+
limit: "syntax"
|
|
69
|
+
});
|
|
70
|
+
occurrencesByPath.set(path, ids);
|
|
71
|
+
}
|
|
72
|
+
const repeatedPaths = /* @__PURE__ */ new Set();
|
|
73
|
+
const firstSeen = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const [path, ids] of occurrencesByPath) for (const id of ids) if (firstSeen.has(id)) repeatedPaths.add(path);
|
|
75
|
+
else firstSeen.add(id);
|
|
76
|
+
let nextId = 0;
|
|
77
|
+
const allocate = () => {
|
|
78
|
+
while (reserved.has(nextId)) nextId += 1;
|
|
79
|
+
const allocated = nextId;
|
|
80
|
+
reserved.add(allocated);
|
|
81
|
+
nextId += 1;
|
|
82
|
+
return allocated;
|
|
83
|
+
};
|
|
84
|
+
const seen = /* @__PURE__ */ new Set();
|
|
85
|
+
const normalized = new Map(parts);
|
|
86
|
+
for (const [path, xml] of candidates) {
|
|
87
|
+
const ids = occurrencesByPath.get(path);
|
|
88
|
+
if (!ids) continue;
|
|
89
|
+
const claim = (id) => {
|
|
90
|
+
if (seen.has(id)) throw new RevisionIdCollisionError({
|
|
91
|
+
message: `Revision id ${String(id)} is claimed twice in ${path}`,
|
|
92
|
+
revisionId: id,
|
|
93
|
+
part: path
|
|
94
|
+
});
|
|
95
|
+
seen.add(id);
|
|
96
|
+
};
|
|
97
|
+
if (!repeatedPaths.has(path) && ids.every((id) => !seen.has(id))) {
|
|
98
|
+
for (const id of ids) claim(id);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const rewritten = rewriteStreamingXmlDecimalAttributes(xml, (element) => {
|
|
102
|
+
const attribute = revisionAttribute(element);
|
|
103
|
+
if (!attribute) return null;
|
|
104
|
+
if (!seen.has(attribute.id)) {
|
|
105
|
+
claim(attribute.id);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const replacement = allocate();
|
|
109
|
+
claim(replacement);
|
|
110
|
+
return /* @__PURE__ */ new Map([[attribute.name, String(replacement)]]);
|
|
111
|
+
});
|
|
112
|
+
if (rewritten.status === "unsupported") throw new XmlResourceLimitError({
|
|
113
|
+
message: `Revision-id normalization could not safely rewrite ${path}`,
|
|
114
|
+
limit: "syntax"
|
|
115
|
+
});
|
|
116
|
+
normalized.set(path, rewritten.value);
|
|
117
|
+
}
|
|
118
|
+
return normalized;
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
export { REVISION_ELEMENT_NAMES, RevisionIdCollisionError, normalizeRevisionIdsInXmlParts };
|
package/dist/docx/rezip.d.ts
CHANGED
|
@@ -105,6 +105,12 @@ declare function updateMultipleFiles(originalBuffer: ArrayBuffer, updates: Map<s
|
|
|
105
105
|
/**
|
|
106
106
|
* Apply file updates to an already-loaded JSZip instance and generate the output.
|
|
107
107
|
* Use this when the zip is already loaded to avoid a redundant decompression pass.
|
|
108
|
+
*
|
|
109
|
+
* This is the selective save's exit, so it owes the package the same id passes
|
|
110
|
+
* {@link generateDocxZip} runs: a save that rewrites only the changed
|
|
111
|
+
* paragraphs still has to see the parts it left alone, both to know which
|
|
112
|
+
* revision ids are free and because an out-of-range paragraph id can sit in a
|
|
113
|
+
* part it never touched.
|
|
108
114
|
*/
|
|
109
115
|
declare function applyUpdatesToZip(zip: JSZip, updates: Map<string, string | ArrayBuffer>, options?: RepackOptions): Promise<ArrayBuffer>;
|
|
110
116
|
/**
|
package/dist/docx/rezip.js
CHANGED
|
@@ -5,7 +5,10 @@ import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
|
|
|
5
5
|
import { assertValidFolioDocumentModel } from "./modelValidation.js";
|
|
6
6
|
import { isNewDataUrlDrawing } from "./newImage.js";
|
|
7
7
|
import { parseNumbering } from "./numberingParser.js";
|
|
8
|
+
import { isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries } from "./packageParts.js";
|
|
9
|
+
import { normalizeParaIdRangeInXmlParts } from "./paraIdRangeNormalization.js";
|
|
8
10
|
import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
|
|
11
|
+
import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
|
|
9
12
|
import { appendNumberingDefs, buildPatchedNotePartXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
|
|
10
13
|
import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
11
14
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
@@ -17,7 +20,6 @@ import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
|
|
|
17
20
|
import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
|
|
18
21
|
import { serializeThemeXml } from "./serializer/themeSerializer.js";
|
|
19
22
|
import { escapeXml } from "./serializer/xmlUtils.js";
|
|
20
|
-
import { isPreservableDocxEntry } from "./unzip.js";
|
|
21
23
|
import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
|
|
22
24
|
import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
|
|
23
25
|
import { panic } from "better-result";
|
|
@@ -405,20 +407,47 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
|
|
|
405
407
|
});
|
|
406
408
|
}
|
|
407
409
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Bring the ids a package addresses itself by inside the bounds the format
|
|
412
|
+
* gives them.
|
|
413
|
+
*
|
|
414
|
+
* Both bounds belong to the package rather than to a part, and neither is
|
|
415
|
+
* something one serializer can see on its own. A revision `w:id` is unique
|
|
416
|
+
* across the package, and one logical revision serializes as several physical
|
|
417
|
+
* wrappers — a word diff cut around unchanged words, a revision split around a
|
|
418
|
+
* hyperlink. A paragraph id is 31-bit, and a paragraph is referenced by id
|
|
419
|
+
* from parts other than the one it lives in. So the passes run at the exits,
|
|
420
|
+
* over every `word/*.xml` part including the ones a save left untouched.
|
|
421
|
+
*/
|
|
422
|
+
const normalizePackageIdsInZip = async (zip, compressionLevel) => {
|
|
423
|
+
const xmlParts = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.startsWith("word/") && path.endsWith(".xml")) xmlParts.set(path, await file.async("text"));
|
|
425
|
+
const normalizedParts = normalizeParaIdRangeInXmlParts(normalizeRevisionIdsInXmlParts(xmlParts));
|
|
426
|
+
for (const [path, xml] of normalizedParts) if (xml !== xmlParts.get(path)) zip.file(path, xml, {
|
|
427
|
+
compression: "DEFLATE",
|
|
428
|
+
compressionOptions: { level: compressionLevel }
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* The single exit for a repacked package. Reconciliation runs here rather than
|
|
433
|
+
* at each caller so no save path can emit a package whose relationships or
|
|
434
|
+
* content types name a part it does not hold.
|
|
435
|
+
*/
|
|
436
|
+
const generateDocxZip = async (zip, compressionLevel) => {
|
|
437
|
+
await reconcilePackageReferences(zip, compressionLevel);
|
|
438
|
+
await normalizePackageIdsInZip(zip, compressionLevel);
|
|
439
|
+
return zip.generateAsync({
|
|
440
|
+
type: "arraybuffer",
|
|
441
|
+
compression: "DEFLATE",
|
|
442
|
+
compressionOptions: { level: compressionLevel }
|
|
443
|
+
});
|
|
416
444
|
};
|
|
445
|
+
const parsedZipSources = /* @__PURE__ */ new WeakMap();
|
|
417
446
|
const loadParsedZipSource = async (document, buffer) => {
|
|
418
447
|
const cached = parsedZipSources.get(document);
|
|
419
448
|
if (cached?.buffer === buffer) return cached;
|
|
420
449
|
const zip = await JSZip.loadAsync(buffer);
|
|
421
|
-
|
|
450
|
+
removeUnsafeEntries(zip);
|
|
422
451
|
const [documentXml, corePropertiesXml] = await Promise.all([zip.file("word/document.xml")?.async("text"), zip.file("docProps/core.xml")?.async("text")]);
|
|
423
452
|
const source = {
|
|
424
453
|
buffer,
|
|
@@ -486,7 +515,7 @@ async function repackDocx(doc, options = {}) {
|
|
|
486
515
|
const exportDocument = withoutOrphanCommentRanges(doc);
|
|
487
516
|
const originalZip = await JSZip.loadAsync(doc.originalBuffer);
|
|
488
517
|
const [originalDocumentXml, originalCorePropertiesXml] = await Promise.all([originalZip.file("word/document.xml")?.async("text"), originalZip.file("docProps/core.xml")?.async("text")]);
|
|
489
|
-
|
|
518
|
+
removeUnsafeEntries(originalZip);
|
|
490
519
|
const newZip = cloneDocxZip(originalZip);
|
|
491
520
|
return finishRepack({
|
|
492
521
|
document: exportDocument,
|
|
@@ -517,7 +546,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
|
|
|
517
546
|
newZip.folder(path.replace(/\/$/u, ""));
|
|
518
547
|
continue;
|
|
519
548
|
}
|
|
520
|
-
if (
|
|
549
|
+
if (isUnsafePackagePath(path)) continue;
|
|
521
550
|
const content = await file.async("arraybuffer");
|
|
522
551
|
newZip.file(path, content, {
|
|
523
552
|
compression: "DEFLATE",
|
|
@@ -559,11 +588,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
|
|
|
559
588
|
compressionOptions: { level: compressionLevel }
|
|
560
589
|
});
|
|
561
590
|
}
|
|
562
|
-
return
|
|
563
|
-
type: "arraybuffer",
|
|
564
|
-
compression: "DEFLATE",
|
|
565
|
-
compressionOptions: { level: compressionLevel }
|
|
566
|
-
});
|
|
591
|
+
return generateDocxZip(newZip, compressionLevel);
|
|
567
592
|
}
|
|
568
593
|
const COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
|
|
569
594
|
const COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
|
|
@@ -749,14 +774,21 @@ async function updateMultipleFiles(originalBuffer, updates, options = {}) {
|
|
|
749
774
|
/**
|
|
750
775
|
* Apply file updates to an already-loaded JSZip instance and generate the output.
|
|
751
776
|
* Use this when the zip is already loaded to avoid a redundant decompression pass.
|
|
777
|
+
*
|
|
778
|
+
* This is the selective save's exit, so it owes the package the same id passes
|
|
779
|
+
* {@link generateDocxZip} runs: a save that rewrites only the changed
|
|
780
|
+
* paragraphs still has to see the parts it left alone, both to know which
|
|
781
|
+
* revision ids are free and because an out-of-range paragraph id can sit in a
|
|
782
|
+
* part it never touched.
|
|
752
783
|
*/
|
|
753
|
-
function applyUpdatesToZip(zip, updates, options = {}) {
|
|
784
|
+
async function applyUpdatesToZip(zip, updates, options = {}) {
|
|
754
785
|
const { compressionLevel = 6 } = options;
|
|
755
786
|
for (const [path, content] of updates) zip.file(path, content, {
|
|
756
787
|
compression: "DEFLATE",
|
|
757
788
|
compressionOptions: { level: compressionLevel }
|
|
758
789
|
});
|
|
759
|
-
|
|
790
|
+
await normalizePackageIdsInZip(zip, compressionLevel);
|
|
791
|
+
return await zip.generateAsync({
|
|
760
792
|
type: "arraybuffer",
|
|
761
793
|
compression: "DEFLATE",
|
|
762
794
|
compressionOptions: { level: compressionLevel }
|
|
@@ -4,6 +4,7 @@ import { hasUnsynthesizedReplyRanges } from "./commentReplyMarkers.js";
|
|
|
4
4
|
import { validateFolioDocumentModel } from "./modelValidation.js";
|
|
5
5
|
import { isNewDataUrlDrawing } from "./newImage.js";
|
|
6
6
|
import { parseNumbering } from "./numberingParser.js";
|
|
7
|
+
import { isUnsafePackagePath } from "./packageParts.js";
|
|
7
8
|
import { RELATIONSHIP_TYPES } from "./relsParser.js";
|
|
8
9
|
import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
|
|
9
10
|
import "./selectiveSaveFlags.js";
|
|
@@ -12,7 +13,6 @@ import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExten
|
|
|
12
13
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
13
14
|
import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
|
|
14
15
|
import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
|
|
15
|
-
import { isPreservableDocxEntry } from "./unzip.js";
|
|
16
16
|
//#region src/docx/selectiveSave.ts
|
|
17
17
|
/**
|
|
18
18
|
* Check if document content has new images (data: URL without rId) or
|
|
@@ -212,7 +212,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
212
212
|
const headerFooterUpdates = collectHeaderFooterUpdates(doc);
|
|
213
213
|
try {
|
|
214
214
|
const zip = await (await import("jszip")).default.loadAsync(originalBuffer);
|
|
215
|
-
for (const [path, file] of Object.entries(zip.files)) if (!file.dir &&
|
|
215
|
+
for (const [path, file] of Object.entries(zip.files)) if (!file.dir && isUnsafePackagePath(path)) return null;
|
|
216
216
|
const updates = /* @__PURE__ */ new Map();
|
|
217
217
|
if (changedParaIds.size > 0) {
|
|
218
218
|
const docXmlFile = zip.file("word/document.xml");
|
|
@@ -36,7 +36,12 @@ function serializeComment(comment) {
|
|
|
36
36
|
xml += "</w:comment>";
|
|
37
37
|
return xml;
|
|
38
38
|
}
|
|
39
|
-
const
|
|
39
|
+
const COMMENT_EXTENSION_NAMESPACES = {
|
|
40
|
+
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
|
|
41
|
+
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
|
|
42
|
+
};
|
|
43
|
+
const COMMENTS_HEADER = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
44
|
+
<w:comments xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ${Object.entries(COMMENT_EXTENSION_NAMESPACES).map(([prefix, namespace]) => `xmlns:${prefix}="${namespace}"`).join(" ")} xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="${Object.keys(COMMENT_EXTENSION_NAMESPACES).join(" ")}">`;
|
|
40
45
|
/**
|
|
41
46
|
* Serialize comments array to comments.xml content. Returns a valid empty
|
|
42
47
|
* `<w:comments/>` document for an empty array so callers can overwrite an
|