@stll/folio-core 0.32.2 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-edits/__fixtures__/paragraphs.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 +649 -85
- 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 +33 -0
- package/dist/compare/__fixtures__/body-sequence.js +67 -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 +406 -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 +1059 -0
- package/dist/compare/reproducible-package.d.ts +17 -0
- package/dist/compare/reproducible-package.js +30 -0
- package/dist/compare/scenario.d.ts +77 -0
- package/dist/compare/scenario.js +263 -0
- package/dist/compare/types.d.ts +282 -0
- package/dist/compare/types.js +32 -0
- package/dist/compare/verification.d.ts +55 -0
- package/dist/compare/verification.js +146 -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/packageParts.d.ts +35 -0
- package/dist/docx/packageParts.js +120 -0
- package/dist/docx/paragraphParser.js +22 -22
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/renderedPageBreakNormalization.js +3 -1
- package/dist/docx/revisionIdNormalization.d.ts +13 -0
- package/dist/docx/revisionIdNormalization.js +103 -0
- package/dist/docx/rezip.js +26 -17
- package/dist/docx/selectiveSave.js +2 -2
- package/dist/docx/serializer/commentSerializer.js +6 -1
- package/dist/docx/serializer/paragraphSerializer.js +33 -4
- package/dist/docx/serializer/runSerializer.js +11 -10
- package/dist/docx/serializer/tableSerializer.js +2 -1
- 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 +91 -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,103 @@
|
|
|
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
|
+
//#region src/docx/revisionIdNormalization.ts
|
|
5
|
+
const REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
|
|
6
|
+
"cellDel",
|
|
7
|
+
"cellIns",
|
|
8
|
+
"cellMerge",
|
|
9
|
+
"del",
|
|
10
|
+
"ins",
|
|
11
|
+
"moveFrom",
|
|
12
|
+
"moveTo",
|
|
13
|
+
"numberingChange",
|
|
14
|
+
"pPrChange",
|
|
15
|
+
"rPrChange",
|
|
16
|
+
"sectPrChange",
|
|
17
|
+
"tblGridChange",
|
|
18
|
+
"tblPrChange",
|
|
19
|
+
"tblPrExChange",
|
|
20
|
+
"tcPrChange",
|
|
21
|
+
"trPrChange"
|
|
22
|
+
]);
|
|
23
|
+
const REVISION_ELEMENT_CANDIDATE = new RegExp(`<(?:[^\\s<>/:]+:)?(?:${[...REVISION_ELEMENT_NAMES].join("|")})(?:[\\s/>])`, "u");
|
|
24
|
+
const revisionAttribute = (element) => {
|
|
25
|
+
if (!element.name || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "") || !REVISION_ELEMENT_NAMES.has(getLocalName(element.name))) return null;
|
|
26
|
+
const attribute = findAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "id");
|
|
27
|
+
if (!attribute) return null;
|
|
28
|
+
const id = Number(attribute.value);
|
|
29
|
+
return Number.isSafeInteger(id) && id >= 0 ? {
|
|
30
|
+
name: attribute.name,
|
|
31
|
+
id
|
|
32
|
+
} : null;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Keep physical tracked-change element ids unique across a package.
|
|
36
|
+
*
|
|
37
|
+
* Live editor marks and operation receipts keep their logical revision IDs.
|
|
38
|
+
* Saving assigns fresh IDs only where one logical change was split into
|
|
39
|
+
* multiple physical OOXML wrappers; reopening therefore exposes the physical
|
|
40
|
+
* wrapper IDs that the file format requires.
|
|
41
|
+
*/
|
|
42
|
+
const normalizeRevisionIdsInXmlParts = (parts) => {
|
|
43
|
+
const candidates = [...parts].filter(([, xml]) => REVISION_ELEMENT_CANDIDATE.test(xml));
|
|
44
|
+
const occurrencesByPath = /* @__PURE__ */ new Map();
|
|
45
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
46
|
+
for (const [path, xml] of candidates) {
|
|
47
|
+
assertXmlResourceLimits(xml);
|
|
48
|
+
const ids = [];
|
|
49
|
+
if (rewriteStreamingXmlDecimalAttributes(xml, (element) => {
|
|
50
|
+
const attribute = revisionAttribute(element);
|
|
51
|
+
if (attribute) {
|
|
52
|
+
ids.push(attribute.id);
|
|
53
|
+
reserved.add(attribute.id);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}).status === "unsupported") throw new XmlResourceLimitError({
|
|
57
|
+
message: `Revision-id normalization could not safely scan ${path}`,
|
|
58
|
+
limit: "syntax"
|
|
59
|
+
});
|
|
60
|
+
occurrencesByPath.set(path, ids);
|
|
61
|
+
}
|
|
62
|
+
const repeatedPaths = /* @__PURE__ */ new Set();
|
|
63
|
+
const firstSeen = /* @__PURE__ */ new Set();
|
|
64
|
+
for (const [path, ids] of occurrencesByPath) for (const id of ids) if (firstSeen.has(id)) repeatedPaths.add(path);
|
|
65
|
+
else firstSeen.add(id);
|
|
66
|
+
let nextId = 0;
|
|
67
|
+
const allocate = () => {
|
|
68
|
+
while (reserved.has(nextId)) nextId += 1;
|
|
69
|
+
const allocated = nextId;
|
|
70
|
+
reserved.add(allocated);
|
|
71
|
+
nextId += 1;
|
|
72
|
+
return allocated;
|
|
73
|
+
};
|
|
74
|
+
const seen = /* @__PURE__ */ new Set();
|
|
75
|
+
const normalized = new Map(parts);
|
|
76
|
+
for (const [path, xml] of candidates) {
|
|
77
|
+
const ids = occurrencesByPath.get(path);
|
|
78
|
+
if (!ids) continue;
|
|
79
|
+
if (!repeatedPaths.has(path) && ids.every((id) => !seen.has(id))) {
|
|
80
|
+
for (const id of ids) seen.add(id);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const rewritten = rewriteStreamingXmlDecimalAttributes(xml, (element) => {
|
|
84
|
+
const attribute = revisionAttribute(element);
|
|
85
|
+
if (!attribute) return null;
|
|
86
|
+
if (!seen.has(attribute.id)) {
|
|
87
|
+
seen.add(attribute.id);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const replacement = allocate();
|
|
91
|
+
seen.add(replacement);
|
|
92
|
+
return /* @__PURE__ */ new Map([[attribute.name, String(replacement)]]);
|
|
93
|
+
});
|
|
94
|
+
if (rewritten.status === "unsupported") throw new XmlResourceLimitError({
|
|
95
|
+
message: `Revision-id normalization could not safely rewrite ${path}`,
|
|
96
|
+
limit: "syntax"
|
|
97
|
+
});
|
|
98
|
+
normalized.set(path, rewritten.value);
|
|
99
|
+
}
|
|
100
|
+
return normalized;
|
|
101
|
+
};
|
|
102
|
+
//#endregion
|
|
103
|
+
export { REVISION_ELEMENT_NAMES, normalizeRevisionIdsInXmlParts };
|
package/dist/docx/rezip.js
CHANGED
|
@@ -5,7 +5,9 @@ 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";
|
|
8
9
|
import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
|
|
10
|
+
import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
|
|
9
11
|
import { appendNumberingDefs, buildPatchedNotePartXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
|
|
10
12
|
import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
11
13
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
@@ -17,7 +19,6 @@ import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
|
|
|
17
19
|
import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerializer.js";
|
|
18
20
|
import { serializeThemeXml } from "./serializer/themeSerializer.js";
|
|
19
21
|
import { escapeXml } from "./serializer/xmlUtils.js";
|
|
20
|
-
import { isPreservableDocxEntry } from "./unzip.js";
|
|
21
22
|
import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
|
|
22
23
|
import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
|
|
23
24
|
import { panic } from "better-result";
|
|
@@ -405,20 +406,32 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
|
|
|
405
406
|
});
|
|
406
407
|
}
|
|
407
408
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
409
|
+
/**
|
|
410
|
+
* The single exit for a repacked package. Reconciliation runs here rather than
|
|
411
|
+
* at each caller so no save path can emit a package whose relationships or
|
|
412
|
+
* content types name a part it does not hold.
|
|
413
|
+
*/
|
|
414
|
+
const generateDocxZip = async (zip, compressionLevel) => {
|
|
415
|
+
await reconcilePackageReferences(zip, compressionLevel);
|
|
416
|
+
const xmlParts = /* @__PURE__ */ new Map();
|
|
417
|
+
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"));
|
|
418
|
+
const normalizedParts = normalizeRevisionIdsInXmlParts(xmlParts);
|
|
419
|
+
for (const [path, xml] of normalizedParts) if (xml !== xmlParts.get(path)) zip.file(path, xml, {
|
|
420
|
+
compression: "DEFLATE",
|
|
421
|
+
compressionOptions: { level: compressionLevel }
|
|
422
|
+
});
|
|
423
|
+
return zip.generateAsync({
|
|
424
|
+
type: "arraybuffer",
|
|
425
|
+
compression: "DEFLATE",
|
|
426
|
+
compressionOptions: { level: compressionLevel }
|
|
427
|
+
});
|
|
416
428
|
};
|
|
429
|
+
const parsedZipSources = /* @__PURE__ */ new WeakMap();
|
|
417
430
|
const loadParsedZipSource = async (document, buffer) => {
|
|
418
431
|
const cached = parsedZipSources.get(document);
|
|
419
432
|
if (cached?.buffer === buffer) return cached;
|
|
420
433
|
const zip = await JSZip.loadAsync(buffer);
|
|
421
|
-
|
|
434
|
+
removeUnsafeEntries(zip);
|
|
422
435
|
const [documentXml, corePropertiesXml] = await Promise.all([zip.file("word/document.xml")?.async("text"), zip.file("docProps/core.xml")?.async("text")]);
|
|
423
436
|
const source = {
|
|
424
437
|
buffer,
|
|
@@ -486,7 +499,7 @@ async function repackDocx(doc, options = {}) {
|
|
|
486
499
|
const exportDocument = withoutOrphanCommentRanges(doc);
|
|
487
500
|
const originalZip = await JSZip.loadAsync(doc.originalBuffer);
|
|
488
501
|
const [originalDocumentXml, originalCorePropertiesXml] = await Promise.all([originalZip.file("word/document.xml")?.async("text"), originalZip.file("docProps/core.xml")?.async("text")]);
|
|
489
|
-
|
|
502
|
+
removeUnsafeEntries(originalZip);
|
|
490
503
|
const newZip = cloneDocxZip(originalZip);
|
|
491
504
|
return finishRepack({
|
|
492
505
|
document: exportDocument,
|
|
@@ -517,7 +530,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
|
|
|
517
530
|
newZip.folder(path.replace(/\/$/u, ""));
|
|
518
531
|
continue;
|
|
519
532
|
}
|
|
520
|
-
if (
|
|
533
|
+
if (isUnsafePackagePath(path)) continue;
|
|
521
534
|
const content = await file.async("arraybuffer");
|
|
522
535
|
newZip.file(path, content, {
|
|
523
536
|
compression: "DEFLATE",
|
|
@@ -559,11 +572,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
|
|
|
559
572
|
compressionOptions: { level: compressionLevel }
|
|
560
573
|
});
|
|
561
574
|
}
|
|
562
|
-
return
|
|
563
|
-
type: "arraybuffer",
|
|
564
|
-
compression: "DEFLATE",
|
|
565
|
-
compressionOptions: { level: compressionLevel }
|
|
566
|
-
});
|
|
575
|
+
return generateDocxZip(newZip, compressionLevel);
|
|
567
576
|
}
|
|
568
577
|
const COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
|
|
569
578
|
const COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
|
|
@@ -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
|
|
@@ -368,6 +368,14 @@ function serializeMoveRangeStart(tag, marker) {
|
|
|
368
368
|
function rewriteRunTextAsDeleted(xml) {
|
|
369
369
|
return xml.replace(/<w:t\b/gu, "<w:delText").replace(/<\/w:t>/gu, "</w:delText>").replace(/<w:instrText\b/gu, "<w:delInstrText").replace(/<\/w:instrText>/gu, "</w:delInstrText>");
|
|
370
370
|
}
|
|
371
|
+
function trackedChangeTag(change) {
|
|
372
|
+
switch (change.type) {
|
|
373
|
+
case "insertion": return "ins";
|
|
374
|
+
case "deletion": return "del";
|
|
375
|
+
case "moveFrom": return "moveFrom";
|
|
376
|
+
case "moveTo": return "moveTo";
|
|
377
|
+
}
|
|
378
|
+
}
|
|
371
379
|
function serializeTrackedChange(tag, change) {
|
|
372
380
|
const info = change.info;
|
|
373
381
|
const normalizedId = normalizeRevisionId(info.id);
|
|
@@ -394,7 +402,13 @@ function serializeTrackedChange(tag, change) {
|
|
|
394
402
|
if (tag === "del" || tag === "moveFrom") return serializeDeletedRun(item);
|
|
395
403
|
return serializeRun(item);
|
|
396
404
|
}
|
|
397
|
-
return serializeHyperlink(item);
|
|
405
|
+
if (item.type === "hyperlink") return serializeHyperlink(item);
|
|
406
|
+
if (item.type === "simpleField" || item.type === "complexField") {
|
|
407
|
+
const xml = item.type === "simpleField" ? serializeSimpleField(item) : serializeComplexField(item);
|
|
408
|
+
return tag === "del" || tag === "moveFrom" ? rewriteRunTextAsDeleted(xml) : xml;
|
|
409
|
+
}
|
|
410
|
+
if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") return serializeTrackedChange(trackedChangeTag(item), item);
|
|
411
|
+
return item.type === "bookmarkStart" ? serializeBookmarkStart(item) : serializeBookmarkEnd(item);
|
|
398
412
|
}).join("");
|
|
399
413
|
return `<w:${tag} ${attrs.join(" ")}>${contentXml}</w:${tag}>`;
|
|
400
414
|
}
|
|
@@ -470,12 +484,27 @@ function serializeParagraph(paragraph) {
|
|
|
470
484
|
return `<w:p${attrsStr}>${parts.join("")}</w:p>`;
|
|
471
485
|
}
|
|
472
486
|
function injectRenderedPageBreakIntoFirstRun(xml) {
|
|
473
|
-
const
|
|
474
|
-
const openingTag = runOpeningTag.exec(xml);
|
|
487
|
+
const openingTag = /<w:r(?=[\s>/])[^>]*>/u.exec(xml);
|
|
475
488
|
if (!openingTag) return null;
|
|
476
489
|
const runEnd = xml.indexOf("</w:r>", openingTag.index + openingTag[0].length);
|
|
477
490
|
if (runEnd !== -1 && xml.slice(openingTag.index + openingTag[0].length, runEnd).includes("<w:lastRenderedPageBreak/>")) return xml;
|
|
478
|
-
|
|
491
|
+
const contentStart = openingTag.index + openingTag[0].length;
|
|
492
|
+
let insertionOffset = contentStart;
|
|
493
|
+
if (xml.startsWith("<w:rPr", contentStart)) {
|
|
494
|
+
const propertyTag = /<\/?w:rPr(?=[\s>/])[^>]*>/gu;
|
|
495
|
+
propertyTag.lastIndex = contentStart;
|
|
496
|
+
let depth = 0;
|
|
497
|
+
for (const match of xml.matchAll(propertyTag)) {
|
|
498
|
+
if (match.index !== contentStart && depth === 0) break;
|
|
499
|
+
if (match[0].startsWith("</")) depth--;
|
|
500
|
+
else if (!match[0].endsWith("/>")) depth++;
|
|
501
|
+
if (depth === 0) {
|
|
502
|
+
insertionOffset = match.index + match[0].length;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return `${xml.slice(0, insertionOffset)}<w:lastRenderedPageBreak/>${xml.slice(insertionOffset)}`;
|
|
479
508
|
}
|
|
480
509
|
/**
|
|
481
510
|
* Serialize multiple paragraphs to OOXML XML
|
|
@@ -92,13 +92,6 @@ function serializeTextFormatting(formatting) {
|
|
|
92
92
|
if (formatting.fontFamily.csTheme) fontAttrs.push(`w:cstheme="${escapeXml(formatting.fontFamily.csTheme)}"`);
|
|
93
93
|
if (fontAttrs.length > 0) parts.push(`<w:rFonts ${fontAttrs.join(" ")}/>`);
|
|
94
94
|
}
|
|
95
|
-
if (formatting.language) {
|
|
96
|
-
const languageAttrs = [];
|
|
97
|
-
if (formatting.language.val) languageAttrs.push(`w:val="${escapeXml(formatting.language.val)}"`);
|
|
98
|
-
if (formatting.language.eastAsia) languageAttrs.push(`w:eastAsia="${escapeXml(formatting.language.eastAsia)}"`);
|
|
99
|
-
if (formatting.language.bidi) languageAttrs.push(`w:bidi="${escapeXml(formatting.language.bidi)}"`);
|
|
100
|
-
if (languageAttrs.length > 0) parts.push(`<w:lang ${languageAttrs.join(" ")}/>`);
|
|
101
|
-
}
|
|
102
95
|
if (formatting.bold === true) parts.push("<w:b/>");
|
|
103
96
|
else if (formatting.bold === false) parts.push("<w:b w:val=\"0\"/>");
|
|
104
97
|
if (formatting.boldCs === true) parts.push("<w:bCs/>");
|
|
@@ -134,11 +127,12 @@ function serializeTextFormatting(formatting) {
|
|
|
134
127
|
if (formatting.position !== void 0) parts.push(`<w:position w:val="${intAttr(formatting.position)}"/>`);
|
|
135
128
|
if (formatting.fontSize !== void 0) parts.push(`<w:sz w:val="${intAttr(formatting.fontSize)}"/>`);
|
|
136
129
|
if (formatting.fontSizeCs !== void 0) parts.push(`<w:szCs w:val="${intAttr(formatting.fontSizeCs)}"/>`);
|
|
130
|
+
let customHighlightShadingXml = "";
|
|
137
131
|
if (formatting.highlight) {
|
|
138
132
|
if (VALID_HIGHLIGHT_COLORS.has(formatting.highlight)) parts.push(`<w:highlight w:val="${formatting.highlight}"/>`);
|
|
139
133
|
else if (!formatting.shading) {
|
|
140
134
|
const hex = formatting.highlight.replace(/^#/u, "");
|
|
141
|
-
if (/^[0-9a-fA-F]{6}$/u.test(hex))
|
|
135
|
+
if (/^[0-9a-fA-F]{6}$/u.test(hex)) customHighlightShadingXml = `<w:shd w:val="clear" w:color="auto" w:fill="${hex}"/>`;
|
|
142
136
|
}
|
|
143
137
|
}
|
|
144
138
|
if (formatting.underline) {
|
|
@@ -152,14 +146,21 @@ function serializeTextFormatting(formatting) {
|
|
|
152
146
|
parts.push(`<w:u ${uAttrs.join(" ")}/>`);
|
|
153
147
|
}
|
|
154
148
|
if (formatting.effect && formatting.effect !== "none") parts.push(`<w:effect w:val="${formatting.effect}"/>`);
|
|
155
|
-
|
|
156
|
-
const shadingXml = serializeShading(formatting.shading);
|
|
149
|
+
const shadingXml = serializeShading(formatting.shading) || customHighlightShadingXml;
|
|
157
150
|
if (shadingXml) parts.push(shadingXml);
|
|
158
151
|
if (formatting.vertAlign) parts.push(`<w:vertAlign w:val="${formatting.vertAlign}"/>`);
|
|
159
152
|
if (formatting.rtl === true) parts.push("<w:rtl/>");
|
|
160
153
|
else if (formatting.rtl === false) parts.push("<w:rtl w:val=\"0\"/>");
|
|
161
154
|
if (formatting.cs === true) parts.push("<w:cs/>");
|
|
162
155
|
else if (formatting.cs === false) parts.push("<w:cs w:val=\"0\"/>");
|
|
156
|
+
if (formatting.emphasisMark) parts.push(`<w:em w:val="${formatting.emphasisMark}"/>`);
|
|
157
|
+
if (formatting.language) {
|
|
158
|
+
const languageAttrs = [];
|
|
159
|
+
if (formatting.language.val) languageAttrs.push(`w:val="${escapeXml(formatting.language.val)}"`);
|
|
160
|
+
if (formatting.language.eastAsia) languageAttrs.push(`w:eastAsia="${escapeXml(formatting.language.eastAsia)}"`);
|
|
161
|
+
if (formatting.language.bidi) languageAttrs.push(`w:bidi="${escapeXml(formatting.language.bidi)}"`);
|
|
162
|
+
if (languageAttrs.length > 0) parts.push(`<w:lang ${languageAttrs.join(" ")}/>`);
|
|
163
|
+
}
|
|
163
164
|
if (parts.length === 0) return "";
|
|
164
165
|
return `<w:rPr>${parts.join("")}</w:rPr>`;
|
|
165
166
|
}
|
|
@@ -250,7 +250,8 @@ function serializeTableCellFormatting(formatting, propertyChanges, structuralCha
|
|
|
250
250
|
if (formatting.textDirection) parts.push(`<w:textDirection w:val="${formatting.textDirection}"/>`);
|
|
251
251
|
if (formatting.fitText) parts.push("<w:tcFitText/>");
|
|
252
252
|
if (formatting.verticalAlign) parts.push(`<w:vAlign w:val="${formatting.verticalAlign}"/>`);
|
|
253
|
-
if (formatting.hideMark
|
|
253
|
+
if (formatting.hideMark === true) parts.push("<w:hideMark/>");
|
|
254
|
+
else if (formatting.hideMark === false) parts.push("<w:hideMark w:val=\"off\"/>");
|
|
254
255
|
}
|
|
255
256
|
if (structuralChange) {
|
|
256
257
|
if (structuralChange.type === "tableCellInsertion") parts.push(`<w:cellIns ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
|
|
@@ -2,8 +2,8 @@ import { loadDocxArchive } from "./boundedArchive.js";
|
|
|
2
2
|
import { FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, InvalidFolioDocxXmlPatchProposalError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal } from "./evaluateDocxXmlPatchProposal.js";
|
|
3
3
|
import { FOLIO_DOCX_CONFORMANCE_PROFILE, validateDocxConformance } from "./validateDocxConformance.js";
|
|
4
4
|
import { TaggedError, panic } from "better-result";
|
|
5
|
-
import JSZip from "jszip";
|
|
6
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import JSZip from "jszip";
|
|
7
7
|
//#region src/docx/server/applyDocxXmlPatchProposal.ts
|
|
8
8
|
const FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION = 1;
|
|
9
9
|
const FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE = "folio-xml-patch-application-v1";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FolioDocxReviewer } from "../../ai-edits/headless.js";
|
|
2
|
+
import { isFolioAIContentBlock } from "../../ai-edits/snapshot.js";
|
|
2
3
|
import { toArrayBuffer } from "../../utils/docxInput.js";
|
|
3
4
|
import { ensureParaIds } from "../ensureParaIds.js";
|
|
4
5
|
import { parseDocx } from "../parser.js";
|
|
@@ -14,7 +15,7 @@ import { createBilingualDocument, readBilingualDocument } from "./createBilingua
|
|
|
14
15
|
*/
|
|
15
16
|
async function createBilingualDocx(input, options) {
|
|
16
17
|
const stampedBuffer = await toArrayBuffer((await ensureParaIds(input)).docx);
|
|
17
|
-
const editableParagraphIds = new Set((await FolioDocxReviewer.fromBuffer(stampedBuffer)).snapshot().blocks.map(({ id }) => id));
|
|
18
|
+
const editableParagraphIds = new Set((await FolioDocxReviewer.fromBuffer(stampedBuffer)).snapshot().blocks.filter(isFolioAIContentBlock).map(({ id }) => id));
|
|
18
19
|
const { document, warnings } = createBilingualDocument(await parseDocx(stampedBuffer, { preloadFonts: false }), {
|
|
19
20
|
...options,
|
|
20
21
|
editableParagraphIds
|
|
@@ -29,7 +30,7 @@ async function createBilingualDocx(input, options) {
|
|
|
29
30
|
/** Bytes-in form of {@link readBilingualDocument}. */
|
|
30
31
|
async function readBilingualDocx(input) {
|
|
31
32
|
const buffer = await toArrayBuffer(input);
|
|
32
|
-
return readBilingualDocument(await parseDocx(buffer, { preloadFonts: false }), new Set((await FolioDocxReviewer.fromBuffer(buffer)).snapshot().blocks.map(({ id }) => id)));
|
|
33
|
+
return readBilingualDocument(await parseDocx(buffer, { preloadFonts: false }), new Set((await FolioDocxReviewer.fromBuffer(buffer)).snapshot().blocks.filter(isFolioAIContentBlock).map(({ id }) => id)));
|
|
33
34
|
}
|
|
34
35
|
//#endregion
|
|
35
36
|
export { createBilingualDocx, readBilingualDocx };
|
|
@@ -11,6 +11,18 @@ type ParseXmlResult = {
|
|
|
11
11
|
* pass. Unsupported or malformed constructs return a sentinel so callers can
|
|
12
12
|
* retain the general-purpose parser as a compatibility fallback.
|
|
13
13
|
*/
|
|
14
|
+
type AttributeValueSpan = {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
};
|
|
18
|
+
type OpenTagVisitor = (element: XmlElement, attributeValueSpans: ReadonlyMap<string, AttributeValueSpan>) => ReadonlyMap<string, string> | null;
|
|
14
19
|
declare const parseStreamingXml: (xml: string) => ParseXmlResult;
|
|
20
|
+
/** Rewrite selected decimal attribute values while preserving every other source byte. */
|
|
21
|
+
declare const rewriteStreamingXmlDecimalAttributes: (xml: string, visitOpenTag: OpenTagVisitor) => {
|
|
22
|
+
status: "rewritten";
|
|
23
|
+
value: string;
|
|
24
|
+
} | {
|
|
25
|
+
status: "unsupported";
|
|
26
|
+
};
|
|
15
27
|
//#endregion
|
|
16
|
-
export { parseStreamingXml };
|
|
28
|
+
export { parseStreamingXml, rewriteStreamingXmlDecimalAttributes };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachXmlNamespaceContext } from "./xmlParser.js";
|
|
1
2
|
import { FOLIO_XML_RESOURCE_LIMITS } from "./xmlResourceLimits.js";
|
|
2
3
|
//#region src/docx/streamingXmlParser.ts
|
|
3
4
|
const BUILT_IN_ENTITIES = {
|
|
@@ -7,14 +8,10 @@ const BUILT_IN_ENTITIES = {
|
|
|
7
8
|
lt: "<",
|
|
8
9
|
quot: "\""
|
|
9
10
|
};
|
|
10
|
-
|
|
11
|
-
* Parse ordinary OOXML into Folio's existing element representation in one
|
|
12
|
-
* pass. Unsupported or malformed constructs return a sentinel so callers can
|
|
13
|
-
* retain the general-purpose parser as a compatibility fallback.
|
|
14
|
-
*/
|
|
15
|
-
const parseStreamingXml = (xml) => {
|
|
11
|
+
const parseStreamingXmlInternal = (xml, visitOpenTag) => {
|
|
16
12
|
const root = { elements: [] };
|
|
17
13
|
const stack = [];
|
|
14
|
+
const replacements = [];
|
|
18
15
|
let cursor = 0;
|
|
19
16
|
let mergeAdjacentText = false;
|
|
20
17
|
while (cursor < xml.length) {
|
|
@@ -55,9 +52,19 @@ const parseStreamingXml = (xml) => {
|
|
|
55
52
|
mergeAdjacentText = false;
|
|
56
53
|
continue;
|
|
57
54
|
}
|
|
58
|
-
const parsedTag = parseOpenTag(xml, open + 1, close);
|
|
55
|
+
const parsedTag = parseOpenTag(xml, open + 1, close, visitOpenTag !== void 0);
|
|
59
56
|
if (parsedTag.status === "unsupported") return parsedTag;
|
|
60
57
|
const parent = stack.at(-1)?.element ?? root;
|
|
58
|
+
attachXmlNamespaceContext(parsedTag.element, parent.namespaceScope);
|
|
59
|
+
const rewritten = visitOpenTag?.(parsedTag.element, parsedTag.attributeValueSpans ?? /* @__PURE__ */ new Map());
|
|
60
|
+
if (rewritten) for (const [attributeName, value] of rewritten) {
|
|
61
|
+
const span = parsedTag.attributeValueSpans?.get(attributeName);
|
|
62
|
+
if (!span) return { status: "unsupported" };
|
|
63
|
+
replacements.push({
|
|
64
|
+
...span,
|
|
65
|
+
value
|
|
66
|
+
});
|
|
67
|
+
}
|
|
61
68
|
appendElement(parent, parsedTag.element);
|
|
62
69
|
if (!parsedTag.selfClosing) {
|
|
63
70
|
if (stack.length >= FOLIO_XML_RESOURCE_LIMITS.maxDepth) return { status: "unsupported" };
|
|
@@ -72,10 +79,35 @@ const parseStreamingXml = (xml) => {
|
|
|
72
79
|
if (stack.length > 0) return { status: "unsupported" };
|
|
73
80
|
return {
|
|
74
81
|
status: "parsed",
|
|
75
|
-
value: root
|
|
82
|
+
value: root,
|
|
83
|
+
replacements
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const parseStreamingXml = (xml) => {
|
|
87
|
+
const parsed = parseStreamingXmlInternal(xml);
|
|
88
|
+
return parsed.status === "parsed" ? {
|
|
89
|
+
status: "parsed",
|
|
90
|
+
value: parsed.value
|
|
91
|
+
} : { status: "unsupported" };
|
|
92
|
+
};
|
|
93
|
+
/** Rewrite selected decimal attribute values while preserving every other source byte. */
|
|
94
|
+
const rewriteStreamingXmlDecimalAttributes = (xml, visitOpenTag) => {
|
|
95
|
+
const parsed = parseStreamingXmlInternal(xml, visitOpenTag);
|
|
96
|
+
if (parsed.status === "unsupported") return parsed;
|
|
97
|
+
const chunks = [];
|
|
98
|
+
let cursor = 0;
|
|
99
|
+
for (const replacement of parsed.replacements.toSorted((left, right) => left.start - right.start)) {
|
|
100
|
+
if (!/^\d+$/u.test(replacement.value)) return { status: "unsupported" };
|
|
101
|
+
chunks.push(xml.slice(cursor, replacement.start), replacement.value);
|
|
102
|
+
cursor = replacement.end;
|
|
103
|
+
}
|
|
104
|
+
chunks.push(xml.slice(cursor));
|
|
105
|
+
return {
|
|
106
|
+
status: "rewritten",
|
|
107
|
+
value: chunks.join("")
|
|
76
108
|
};
|
|
77
109
|
};
|
|
78
|
-
const parseOpenTag = (xml, start, close) => {
|
|
110
|
+
const parseOpenTag = (xml, start, close, captureAttributeSpans) => {
|
|
79
111
|
let cursor = skipWhitespace(xml, start, close);
|
|
80
112
|
const nameStart = cursor;
|
|
81
113
|
cursor = scanName(xml, cursor, close);
|
|
@@ -83,6 +115,7 @@ const parseOpenTag = (xml, start, close) => {
|
|
|
83
115
|
const name = xml.slice(nameStart, cursor);
|
|
84
116
|
if (isUnsafePropertyName(name)) return { status: "unsupported" };
|
|
85
117
|
let attributes;
|
|
118
|
+
const attributeValueSpans = captureAttributeSpans ? /* @__PURE__ */ new Map() : void 0;
|
|
86
119
|
let selfClosing = false;
|
|
87
120
|
while (cursor < close) {
|
|
88
121
|
cursor = skipWhitespace(xml, cursor, close);
|
|
@@ -110,6 +143,10 @@ const parseOpenTag = (xml, start, close) => {
|
|
|
110
143
|
if (decoded === null) return { status: "unsupported" };
|
|
111
144
|
attributes ??= {};
|
|
112
145
|
attributes[attributeName] = decoded;
|
|
146
|
+
attributeValueSpans?.set(attributeName, {
|
|
147
|
+
start: valueStart,
|
|
148
|
+
end: cursor
|
|
149
|
+
});
|
|
113
150
|
cursor += 1;
|
|
114
151
|
}
|
|
115
152
|
const element = {
|
|
@@ -121,7 +158,8 @@ const parseOpenTag = (xml, start, close) => {
|
|
|
121
158
|
status: "parsed",
|
|
122
159
|
element,
|
|
123
160
|
name,
|
|
124
|
-
selfClosing
|
|
161
|
+
selfClosing,
|
|
162
|
+
attributeValueSpans
|
|
125
163
|
};
|
|
126
164
|
};
|
|
127
165
|
const appendElement = (parent, child) => {
|
|
@@ -246,4 +284,4 @@ const isUnsafePropertyName = (name) => {
|
|
|
246
284
|
}
|
|
247
285
|
};
|
|
248
286
|
//#endregion
|
|
249
|
-
export { parseStreamingXml };
|
|
287
|
+
export { parseStreamingXml, rewriteStreamingXmlDecimalAttributes };
|
package/dist/docx/unzip.d.ts
CHANGED
|
@@ -59,7 +59,6 @@ type RawDocxContent = {
|
|
|
59
59
|
* @returns Promise resolving to extracted content
|
|
60
60
|
*/
|
|
61
61
|
declare function unzipDocx(buffer: ArrayBuffer, options?: DocxUnzipOptions): Promise<RawDocxContent>;
|
|
62
|
-
declare function isPreservableDocxEntry(path: string): boolean;
|
|
63
62
|
/**
|
|
64
63
|
* Get a list of all files in the DOCX
|
|
65
64
|
*
|
|
@@ -120,4 +119,4 @@ declare function getContentSummary(content: RawDocxContent): {
|
|
|
120
119
|
totalFiles: number;
|
|
121
120
|
};
|
|
122
121
|
//#endregion
|
|
123
|
-
export { DocxSecurityError, DocxUnzipLimits, DocxUnzipOptions, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile,
|
|
122
|
+
export { DocxSecurityError, DocxUnzipLimits, DocxUnzipOptions, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, mediaToDataUrl, unzipDocx };
|
package/dist/docx/unzip.js
CHANGED
|
@@ -137,13 +137,13 @@ async function unzipDocx(buffer, options = {}) {
|
|
|
137
137
|
const extractionTasks = [];
|
|
138
138
|
for (const [path, file] of entries) {
|
|
139
139
|
if (!isSafeDocxPath(path)) throw new DocxSecurityError("DOCX file contains an unsafe entry path");
|
|
140
|
-
if (!isPreservableDocxEntry(path)) continue;
|
|
141
|
-
const lowerPath = path.toLowerCase();
|
|
142
140
|
const declaredSize = getEntryUncompressedSize(file);
|
|
143
141
|
if (declaredSize !== null) {
|
|
144
142
|
totalUncompressedBytes += declaredSize;
|
|
145
143
|
if (totalUncompressedBytes > limits.maxTotalUncompressedBytes) throw new DocxSecurityError("DOCX file expands beyond the maximum allowed size");
|
|
146
144
|
}
|
|
145
|
+
if (!isParsedDocxEntry(path)) continue;
|
|
146
|
+
const lowerPath = path.toLowerCase();
|
|
147
147
|
if (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels")) {
|
|
148
148
|
assertEntrySize(path, declaredSize, limits.maxXmlBytes);
|
|
149
149
|
if (options.extractAllXml === false && !shouldExtractXmlPart(lowerPath)) continue;
|
|
@@ -328,9 +328,28 @@ function isSafeDocxPath(path) {
|
|
|
328
328
|
if (!path || path.startsWith("/") || path.includes("\\")) return false;
|
|
329
329
|
return !path.split("/").some((part) => part === "..");
|
|
330
330
|
}
|
|
331
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Parts folio never looks inside. A macro project and an ActiveX control carry
|
|
333
|
+
* code; folio models neither, so it reads neither — not into the document
|
|
334
|
+
* model, not even into the raw XML map. The save path carries them anyway,
|
|
335
|
+
* straight from the source archive, which is what keeps a `.docm` a `.docm`.
|
|
336
|
+
*/
|
|
337
|
+
const UNINTERPRETED_PART_NAMES = /* @__PURE__ */ new Set(["word/vbaproject.bin", "word/vbadata.xml"]);
|
|
338
|
+
const isUninterpretedPart = (lowerPath) => UNINTERPRETED_PART_NAMES.has(lowerPath) || lowerPath.startsWith("word/activex/");
|
|
339
|
+
/**
|
|
340
|
+
* Whether this entry is one the PARSER reads into the document model. It says
|
|
341
|
+
* nothing about what a save keeps: a repack hands back every part of the
|
|
342
|
+
* package (see `packageParts.ts`), including the ones this returns false for.
|
|
343
|
+
*
|
|
344
|
+
* That asymmetry is the point. A macro project, an ActiveX control and an OLE
|
|
345
|
+
* embedding are opaque bytes to folio — never decoded, never interpreted,
|
|
346
|
+
* never executed — and they travel from the source archive to the saved one
|
|
347
|
+
* without any code here looking inside them.
|
|
348
|
+
*/
|
|
349
|
+
function isParsedDocxEntry(path) {
|
|
332
350
|
if (!isSafeDocxPath(path)) return false;
|
|
333
351
|
const lowerPath = path.toLowerCase();
|
|
352
|
+
if (isUninterpretedPart(lowerPath)) return false;
|
|
334
353
|
if (lowerPath.startsWith("word/media/")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
|
|
335
354
|
if (lowerPath.startsWith("docprops/thumbnail.")) return PRESERVABLE_MEDIA_MIME_TYPES.has(getMediaMimeType(path));
|
|
336
355
|
if (lowerPath.startsWith("word/fonts/")) return true;
|
|
@@ -374,7 +393,7 @@ function isMediaContentAllowed(data, mimeType) {
|
|
|
374
393
|
*/
|
|
375
394
|
function getFileList(content) {
|
|
376
395
|
const files = [];
|
|
377
|
-
for (const path of Object.keys(content.originalZip.files)) if (!content.originalZip.files[path]?.dir &&
|
|
396
|
+
for (const path of Object.keys(content.originalZip.files)) if (!content.originalZip.files[path]?.dir && isParsedDocxEntry(path)) files.push(path);
|
|
378
397
|
return files.toSorted();
|
|
379
398
|
}
|
|
380
399
|
/**
|
|
@@ -425,7 +444,7 @@ function mediaToDataUrl(data, mimeType) {
|
|
|
425
444
|
*/
|
|
426
445
|
function extractFile(content, path) {
|
|
427
446
|
const file = content.originalZip.file(path);
|
|
428
|
-
if (!file || !
|
|
447
|
+
if (!file || !isParsedDocxEntry(path)) return Promise.resolve(null);
|
|
429
448
|
const lowerPath = path.toLowerCase();
|
|
430
449
|
if (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels")) return file.async("text");
|
|
431
450
|
return file.async("arraybuffer");
|
|
@@ -464,4 +483,4 @@ function getContentSummary(content) {
|
|
|
464
483
|
};
|
|
465
484
|
}
|
|
466
485
|
//#endregion
|
|
467
|
-
export { DocxSecurityError, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile,
|
|
486
|
+
export { DocxSecurityError, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, mediaToDataUrl, unzipDocx };
|
package/dist/docx/xmlParser.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ type XmlNamespaceScope = {
|
|
|
47
47
|
bindings: ReadonlyMap<string, string>;
|
|
48
48
|
parent?: XmlNamespaceScope;
|
|
49
49
|
};
|
|
50
|
+
/** Attach the element's resolved namespace metadata from its in-scope declarations. */
|
|
51
|
+
declare const attachXmlNamespaceContext: (element: XmlElement, inheritedNamespaceScope?: XmlNamespaceScope) => XmlNamespaceScope;
|
|
50
52
|
/**
|
|
51
53
|
* Common OOXML namespace URIs — re-exported from @stll/docx-utils.
|
|
52
54
|
*/
|
|
@@ -344,4 +346,4 @@ declare function mergeXmlnsDeclarations(inherited: Record<string, string>, eleme
|
|
|
344
346
|
*/
|
|
345
347
|
declare function cloneWithXmlnsDeclarations(element: XmlElement, xmlnsDecls: Record<string, string>): XmlElement;
|
|
346
348
|
//#endregion
|
|
347
|
-
export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
|
|
349
|
+
export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, attachXmlNamespaceContext, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
|