@stll/folio-core 0.6.1 → 0.8.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/blockRange.d.ts +13 -2
- package/dist/ai-edits/blockRange.js +23 -2
- package/dist/ai-edits/index.d.ts +4 -2
- package/dist/ai-edits/index.js +3 -1
- package/dist/ai-edits/scoped-reading.d.ts +9 -0
- package/dist/ai-edits/scoped-reading.js +60 -0
- package/dist/ai-edits/snapshot.js +13 -4
- package/dist/ai-edits/types.d.ts +44 -2
- package/dist/compat/eigenpal.d.ts +5 -2
- package/dist/compat/eigenpal.js +4 -1
- package/dist/controller/fontReadiness.d.ts +29 -0
- package/dist/controller/fontReadiness.js +139 -0
- package/dist/controller/layoutPipeline.js +86 -11
- package/dist/docx/blockContentParser.js +51 -8
- package/dist/docx/capabilities.d.ts +41 -0
- package/dist/docx/capabilities.js +322 -0
- package/dist/docx/compatibility.d.ts +34 -2
- package/dist/docx/compatibility.js +128 -38
- package/dist/docx/conformance.d.ts +6 -0
- package/dist/docx/conformance.js +18 -0
- package/dist/docx/ensureParaIds.d.ts +29 -0
- package/dist/docx/ensureParaIds.js +423 -0
- package/dist/docx/groupDrawingParser.d.ts +1 -1
- package/dist/docx/groupDrawingParser.js +49 -8
- package/dist/docx/numberingParser.js +1 -0
- package/dist/docx/paragraphParser.js +9 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +141 -2
- package/dist/docx/parser.js +2 -0
- package/dist/docx/runParser.js +11 -2
- package/dist/docx/selectiveXmlPatch.d.ts +10 -1
- package/dist/docx/selectiveXmlPatch.js +1 -1
- package/dist/docx/serializer/paragraphSerializer.js +4 -0
- package/dist/docx/serializer/runSerializer.js +7 -2
- package/dist/docx/serializer/tableSerializer.js +4 -0
- package/dist/docx/server/boundedArchive.d.ts +24 -0
- package/dist/docx/server/boundedArchive.js +106 -0
- package/dist/docx/server/extractDocxText.d.ts +23 -0
- package/dist/docx/server/extractDocxText.js +154 -0
- package/dist/docx/tableParser.js +8 -0
- package/dist/docx/textBoxParser.js +6 -1
- package/dist/docx/vmlImageParser.js +145 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/layout-bridge/convert/headerFooterLayout.js +16 -2
- package/dist/layout-bridge/convert/paragraphFrames.d.ts +7 -0
- package/dist/layout-bridge/convert/paragraphFrames.js +136 -0
- package/dist/layout-bridge/convert/toFlowBlocks.js +106 -13
- package/dist/layout-bridge/engine/hitTest.js +2 -1
- package/dist/layout-bridge/engine/selectionRects.js +2 -1
- package/dist/layout-engine/headerFooterRefs.d.ts +7 -0
- package/dist/layout-engine/headerFooterRefs.js +40 -0
- package/dist/layout-engine/index.d.ts +3 -2
- package/dist/layout-engine/index.js +125 -72
- package/dist/layout-engine/keep-together.d.ts +7 -5
- package/dist/layout-engine/keep-together.js +20 -4
- package/dist/layout-engine/measure/cache.js +2 -0
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
- package/dist/layout-engine/measure/listMarkerWidth.js +23 -4
- package/dist/layout-engine/measure/measureBlocks.d.ts +3 -0
- package/dist/layout-engine/measure/measureBlocks.js +133 -51
- package/dist/layout-engine/measure/measureParagraph.js +42 -15
- package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -0
- package/dist/layout-engine/measure/tableCellGrid.js +62 -0
- package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +18 -0
- package/dist/layout-engine/measure/textBoxParagraphLayout.js +32 -0
- package/dist/layout-engine/paginator.d.ts +2 -1
- package/dist/layout-engine/paginator.js +17 -8
- package/dist/layout-engine/paragraphFrame.d.ts +22 -0
- package/dist/layout-engine/paragraphFrame.js +17 -0
- package/dist/layout-engine/paragraphSequence.d.ts +7 -0
- package/dist/layout-engine/paragraphSequence.js +25 -0
- package/dist/layout-engine/paragraphSpacing.d.ts +17 -0
- package/dist/layout-engine/paragraphSpacing.js +30 -0
- package/dist/layout-engine/renderedBreakReconciliation.d.ts +58 -0
- package/dist/layout-engine/renderedBreakReconciliation.js +63 -0
- package/dist/layout-engine/types.d.ts +31 -10
- package/dist/layout-engine/types.js +15 -2
- package/dist/layout-painter/renderPage.js +2 -2
- package/dist/layout-painter/renderParagraph.js +94 -10
- package/dist/layout-painter/renderTable.js +42 -12
- package/dist/layout-painter/renderTextBox.js +17 -7
- package/dist/managers/DocumentLoaderManager.js +1 -1
- package/dist/paged-layout/sectionBlockWidths.d.ts +3 -0
- package/dist/paged-layout/sectionBlockWidths.js +9 -0
- package/dist/paged-layout/sectionGeometry.js +1 -1
- package/dist/prosemirror/attrs/index.js +11 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +2 -0
- package/dist/prosemirror/conversion/toProseDoc.js +29 -23
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
- package/dist/prosemirror/extensions/features/ListExtension.js +1 -0
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +68 -16
- package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -1
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
- package/dist/prosemirror/schema/nodes.d.ts +6 -3
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
- package/dist/prosemirror/utils/tabCalculator.js +1 -1
- package/dist/server.d.ts +6 -2
- package/dist/server.js +5 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/utils/createDocument.js +26 -18
- package/dist/utils/formatToStyle.js +3 -3
- package/dist/utils/hexId.d.ts +8 -1
- package/dist/utils/hexId.js +11 -3
- package/dist/utils/units.d.ts +6 -6
- package/dist/utils/units.js +8 -8
- package/package.json +1 -1
|
@@ -1,77 +1,167 @@
|
|
|
1
|
+
import { getFolioDocxCapability } from "./capabilities.js";
|
|
2
|
+
import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
|
|
1
3
|
//#region src/docx/compatibility.ts
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const resolveCompatibilityContext = (doc, options) => ({
|
|
5
|
+
host: options.host ?? "unknown",
|
|
6
|
+
profile: options.profile ?? doc.package.conformanceClass ?? DOCX_CONFORMANCE_CLASSES.UNKNOWN
|
|
7
|
+
});
|
|
8
|
+
const getCoverageState = (value, coveredValues) => {
|
|
9
|
+
if (value === "unknown") return "unknown";
|
|
10
|
+
return coveredValues.includes(value) ? "covered" : "unverified";
|
|
6
11
|
};
|
|
7
|
-
|
|
12
|
+
const inspectDocxCompatibility = (doc, options = {}) => {
|
|
13
|
+
const context = resolveCompatibilityContext(doc, options);
|
|
8
14
|
const reasons = /* @__PURE__ */ new Set();
|
|
9
|
-
|
|
10
|
-
const record = (
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
const issues = [];
|
|
16
|
+
const record = (location) => {
|
|
17
|
+
const code = "opaqueDrawing";
|
|
18
|
+
const capability = getFolioDocxCapability(code);
|
|
19
|
+
reasons.add(code);
|
|
20
|
+
issues.push({
|
|
21
|
+
code,
|
|
22
|
+
capability,
|
|
23
|
+
coverage: {
|
|
24
|
+
host: getCoverageState(context.host, capability.hosts),
|
|
25
|
+
profile: getCoverageState(context.profile, capability.profiles)
|
|
26
|
+
},
|
|
27
|
+
location
|
|
28
|
+
});
|
|
13
29
|
};
|
|
14
|
-
inspectBlocks(doc.package.document.content,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
inspectBlocks(doc.package.document.content, {
|
|
31
|
+
part: { type: "document" },
|
|
32
|
+
path: "package.document.content",
|
|
33
|
+
record
|
|
34
|
+
});
|
|
35
|
+
for (const [relationshipId, header] of doc.package.headers?.entries() ?? []) inspectHeaderFooter(header, {
|
|
36
|
+
part: {
|
|
37
|
+
type: "header",
|
|
38
|
+
relationshipId
|
|
39
|
+
},
|
|
40
|
+
path: `package.headers.get(${JSON.stringify(relationshipId)}).content`,
|
|
41
|
+
record
|
|
42
|
+
});
|
|
43
|
+
for (const [relationshipId, footer] of doc.package.footers?.entries() ?? []) inspectHeaderFooter(footer, {
|
|
44
|
+
part: {
|
|
45
|
+
type: "footer",
|
|
46
|
+
relationshipId
|
|
47
|
+
},
|
|
48
|
+
path: `package.footers.get(${JSON.stringify(relationshipId)}).content`,
|
|
49
|
+
record
|
|
50
|
+
});
|
|
51
|
+
for (const footnote of doc.package.footnotes ?? []) inspectBlocks(footnote.content, {
|
|
52
|
+
part: {
|
|
53
|
+
type: "footnote",
|
|
54
|
+
id: footnote.id
|
|
55
|
+
},
|
|
56
|
+
path: `package.footnotes[id=${footnote.id}].content`,
|
|
57
|
+
record
|
|
58
|
+
});
|
|
59
|
+
for (const endnote of doc.package.endnotes ?? []) inspectBlocks(endnote.content, {
|
|
60
|
+
part: {
|
|
61
|
+
type: "endnote",
|
|
62
|
+
id: endnote.id
|
|
63
|
+
},
|
|
64
|
+
path: `package.endnotes[id=${endnote.id}].content`,
|
|
65
|
+
record
|
|
66
|
+
});
|
|
20
67
|
return {
|
|
21
|
-
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
context,
|
|
70
|
+
canSafelyEdit: issues.length === 0,
|
|
71
|
+
issues,
|
|
22
72
|
reasons: Array.from(reasons),
|
|
23
|
-
unsupportedContentCount
|
|
73
|
+
unsupportedContentCount: issues.length
|
|
24
74
|
};
|
|
25
|
-
}
|
|
26
|
-
function inspectBlocks(blocks,
|
|
27
|
-
for (const block of blocks) {
|
|
75
|
+
};
|
|
76
|
+
function inspectBlocks(blocks, context) {
|
|
77
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
78
|
+
const blockPath = `${context.path}[${blockIndex}]`;
|
|
28
79
|
if (block.type === "paragraph") {
|
|
29
|
-
inspectParagraphContent(block.content,
|
|
80
|
+
inspectParagraphContent(block.content, {
|
|
81
|
+
...block.paraId === void 0 ? {} : { blockId: block.paraId },
|
|
82
|
+
part: context.part,
|
|
83
|
+
path: `${blockPath}.content`,
|
|
84
|
+
record: context.record
|
|
85
|
+
});
|
|
30
86
|
continue;
|
|
31
87
|
}
|
|
32
88
|
if (block.type === "table") {
|
|
33
|
-
for (const row of block.rows) for (const cell of row.cells) inspectBlocks(cell.content,
|
|
89
|
+
for (const [rowIndex, row] of block.rows.entries()) for (const [cellIndex, cell] of row.cells.entries()) inspectBlocks(cell.content, {
|
|
90
|
+
part: context.part,
|
|
91
|
+
path: `${blockPath}.rows[${rowIndex}].cells[${cellIndex}].content`,
|
|
92
|
+
record: context.record
|
|
93
|
+
});
|
|
34
94
|
continue;
|
|
35
95
|
}
|
|
36
|
-
inspectBlocks(block.content,
|
|
96
|
+
inspectBlocks(block.content, {
|
|
97
|
+
part: context.part,
|
|
98
|
+
path: `${blockPath}.content`,
|
|
99
|
+
record: context.record
|
|
100
|
+
});
|
|
37
101
|
}
|
|
38
102
|
}
|
|
39
|
-
function inspectHeaderFooter(headerFooter,
|
|
40
|
-
inspectBlocks(headerFooter.content,
|
|
103
|
+
function inspectHeaderFooter(headerFooter, context) {
|
|
104
|
+
inspectBlocks(headerFooter.content, context);
|
|
41
105
|
}
|
|
42
|
-
function inspectParagraphContent(content,
|
|
43
|
-
for (const item of content) {
|
|
106
|
+
function inspectParagraphContent(content, context) {
|
|
107
|
+
for (const [itemIndex, item] of content.entries()) {
|
|
108
|
+
const itemContext = {
|
|
109
|
+
...context,
|
|
110
|
+
path: `${context.path}[${itemIndex}]`
|
|
111
|
+
};
|
|
44
112
|
if (item.type === "run") {
|
|
45
|
-
inspectRun(item,
|
|
113
|
+
inspectRun(item, itemContext);
|
|
46
114
|
continue;
|
|
47
115
|
}
|
|
48
116
|
if (item.type === "hyperlink") {
|
|
49
|
-
inspectHyperlink(item,
|
|
117
|
+
inspectHyperlink(item, itemContext);
|
|
50
118
|
continue;
|
|
51
119
|
}
|
|
52
120
|
if (item.type === "inlineSdt") {
|
|
53
|
-
inspectParagraphContent(item.content,
|
|
121
|
+
inspectParagraphContent(item.content, {
|
|
122
|
+
...itemContext,
|
|
123
|
+
path: `${itemContext.path}.content`
|
|
124
|
+
});
|
|
54
125
|
continue;
|
|
55
126
|
}
|
|
56
127
|
if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") {
|
|
57
|
-
inspectParagraphContent(item.content,
|
|
128
|
+
inspectParagraphContent(item.content, {
|
|
129
|
+
...itemContext,
|
|
130
|
+
path: `${itemContext.path}.content`
|
|
131
|
+
});
|
|
58
132
|
continue;
|
|
59
133
|
}
|
|
60
134
|
if (item.type === "simpleField") {
|
|
61
|
-
inspectParagraphContent(item.content,
|
|
135
|
+
inspectParagraphContent(item.content, {
|
|
136
|
+
...itemContext,
|
|
137
|
+
path: `${itemContext.path}.content`
|
|
138
|
+
});
|
|
62
139
|
continue;
|
|
63
140
|
}
|
|
64
141
|
if (item.type === "complexField") {
|
|
65
|
-
for (const run of item.fieldCode) inspectRun(run,
|
|
66
|
-
|
|
142
|
+
for (const [runIndex, run] of item.fieldCode.entries()) inspectRun(run, {
|
|
143
|
+
...itemContext,
|
|
144
|
+
path: `${itemContext.path}.fieldCode[${runIndex}]`
|
|
145
|
+
});
|
|
146
|
+
for (const [runIndex, run] of item.fieldResult.entries()) inspectRun(run, {
|
|
147
|
+
...itemContext,
|
|
148
|
+
path: `${itemContext.path}.fieldResult[${runIndex}]`
|
|
149
|
+
});
|
|
67
150
|
}
|
|
68
151
|
}
|
|
69
152
|
}
|
|
70
|
-
function inspectHyperlink(hyperlink,
|
|
71
|
-
for (const child of hyperlink.children) if (child.type === "run") inspectRun(child,
|
|
153
|
+
function inspectHyperlink(hyperlink, context) {
|
|
154
|
+
for (const [childIndex, child] of hyperlink.children.entries()) if (child.type === "run") inspectRun(child, {
|
|
155
|
+
...context,
|
|
156
|
+
path: `${context.path}.children[${childIndex}]`
|
|
157
|
+
});
|
|
72
158
|
}
|
|
73
|
-
function inspectRun(run,
|
|
74
|
-
for (const content of run.content) if (content.type === "drawing" && content.rawXml) record(
|
|
159
|
+
function inspectRun(run, context) {
|
|
160
|
+
for (const [contentIndex, content] of run.content.entries()) if (content.type === "drawing" && content.rawXml) context.record({
|
|
161
|
+
...context.blockId === void 0 ? {} : { blockId: context.blockId },
|
|
162
|
+
part: context.part,
|
|
163
|
+
path: `${context.path}.content[${contentIndex}]`
|
|
164
|
+
});
|
|
75
165
|
}
|
|
76
166
|
//#endregion
|
|
77
167
|
export { inspectDocxCompatibility };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
|
|
3
|
+
//#region src/docx/conformance.d.ts
|
|
4
|
+
declare const detectDocxConformanceClass: (documentXml: string | null) => document_d_exports.DocxConformanceClass;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { detectDocxConformanceClass };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getLocalName, getNamespacePrefix, parseXmlDocument } from "./xmlParser.js";
|
|
2
|
+
import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
|
|
3
|
+
//#region src/docx/conformance.ts
|
|
4
|
+
const STRICT_MAIN_NAMESPACE = "http://purl.oclc.org/ooxml/wordprocessingml/main";
|
|
5
|
+
const TRANSITIONAL_MAIN_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
6
|
+
const detectDocxConformanceClass = (documentXml) => {
|
|
7
|
+
if (documentXml === null) return DOCX_CONFORMANCE_CLASSES.UNKNOWN;
|
|
8
|
+
const root = parseXmlDocument(documentXml);
|
|
9
|
+
if (root === null || getLocalName(root.name ?? "") !== "document") return DOCX_CONFORMANCE_CLASSES.UNKNOWN;
|
|
10
|
+
const prefix = getNamespacePrefix(root.name ?? "");
|
|
11
|
+
const namespaceAttribute = prefix === null ? "xmlns" : `xmlns:${prefix}`;
|
|
12
|
+
const namespace = root.attributes?.[namespaceAttribute];
|
|
13
|
+
if (namespace === STRICT_MAIN_NAMESPACE) return DOCX_CONFORMANCE_CLASSES.STRICT;
|
|
14
|
+
if (namespace === TRANSITIONAL_MAIN_NAMESPACE) return DOCX_CONFORMANCE_CLASSES.TRANSITIONAL;
|
|
15
|
+
return DOCX_CONFORMANCE_CLASSES.UNKNOWN;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { detectDocxConformanceClass };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/docx/ensureParaIds.d.ts
|
|
2
|
+
declare const EnsureParaIdsError_base: import("better-result").TaggedErrorClass<"EnsureParaIdsError", {
|
|
3
|
+
message: string;
|
|
4
|
+
cause?: unknown;
|
|
5
|
+
}>;
|
|
6
|
+
/** A malformed or unsupported package prevented paragraph-ID normalization. */
|
|
7
|
+
declare class EnsureParaIdsError extends EnsureParaIdsError_base {}
|
|
8
|
+
/** Counts and normalized bytes returned by {@link ensureParaIds}. */
|
|
9
|
+
type EnsureParaIdsResult = {
|
|
10
|
+
/** The normalized `.docx`; the input bytes verbatim when `alreadyComplete`. */docx: Uint8Array; /** Paragraphs that received a paraId (missing or all-zero before). */
|
|
11
|
+
assigned: number; /** Duplicate paraIds reassigned (the first occurrence keeps the id). */
|
|
12
|
+
deduplicated: number; /** True when the input already had full, unique coverage. */
|
|
13
|
+
alreadyComplete: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** Controls mutation of package metadata that has security implications. */
|
|
16
|
+
type EnsureParaIdsOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Allow normalization to invalidate existing OPC digital signatures.
|
|
19
|
+
* Callers must warn the user before opting in.
|
|
20
|
+
*/
|
|
21
|
+
allowSignedPackageMutation?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Backfill `w14:paraId` on every paragraph of a `.docx` buffer. All package,
|
|
25
|
+
* XML, and compression failures are surfaced as {@link EnsureParaIdsError}.
|
|
26
|
+
*/
|
|
27
|
+
declare const ensureParaIds: (docx: Uint8Array | ArrayBuffer, options?: EnsureParaIdsOptions) => Promise<EnsureParaIdsResult>;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { EnsureParaIdsError, EnsureParaIdsOptions, EnsureParaIdsResult, ensureParaIds };
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { isXmlNameBoundary } from "./selectiveXmlPatch.js";
|
|
2
|
+
import { deterministicHexId } from "../utils/hexId.js";
|
|
3
|
+
import { TaggedError } from "better-result";
|
|
4
|
+
import JSZip from "jszip";
|
|
5
|
+
//#region src/docx/ensureParaIds.ts
|
|
6
|
+
/**
|
|
7
|
+
* Headless `w14:paraId` normalization for a `.docx` buffer.
|
|
8
|
+
*
|
|
9
|
+
* Word 2010+ stamps every paragraph with a `w14:paraId`, but Google Docs
|
|
10
|
+
* exports, LibreOffice, python-docx, and docx4j generally do not. Paragraphs
|
|
11
|
+
* without one are invisible to everything that anchors on paraId (block ids,
|
|
12
|
+
* comment threads, AI-edit targeting) and fall back to positional `seq-NNNN`
|
|
13
|
+
* ids that renumber after structural edits. Hosts call {@link ensureParaIds}
|
|
14
|
+
* once at ingest so every stored version has full id coverage before any
|
|
15
|
+
* snapshot or external anchor is created.
|
|
16
|
+
*
|
|
17
|
+
* IDs remain stable in folio and identity-preserving DOCX round-trips.
|
|
18
|
+
* Microsoft Word can establish a new `w14:docId` and replace all paragraph
|
|
19
|
+
* IDs on its first save of a non-Word-produced package; callers that ingest
|
|
20
|
+
* such an externally edited version must normalize it again and cannot assume
|
|
21
|
+
* the pre-save IDs still match.
|
|
22
|
+
*
|
|
23
|
+
* The pass patches part XML in place (string splices, no model round-trip),
|
|
24
|
+
* so documents carrying features folio's parser does not model come back
|
|
25
|
+
* with those features byte-identical. Contract:
|
|
26
|
+
*
|
|
27
|
+
* - Parts covered: `word/document.xml`, `word/header*.xml`, `word/footer*.xml`,
|
|
28
|
+
* `word/footnotes.xml`, `word/endnotes.xml`. Paragraphs nested in table
|
|
29
|
+
* cells and in `mc:Choice` text boxes are plain `<w:p>` elements inside
|
|
30
|
+
* those parts and are covered by the same scan. The comments part mints its
|
|
31
|
+
* own deterministic paraIds at save time (see `commentSerializer`) and is
|
|
32
|
+
* left untouched; its ids still count toward uniqueness.
|
|
33
|
+
* - Paragraphs inside `mc:Fallback` are never modified: Word regenerates the
|
|
34
|
+
* fallback branch (duplicating the `mc:Choice` ids) on save, so stamping or
|
|
35
|
+
* deduplicating there would churn on every Word round-trip.
|
|
36
|
+
* - Existing ids are preserved. Only three cases get a fresh id: a missing
|
|
37
|
+
* `paraId`, the reserved all-zero value (Word reads `00000000` as "no id"),
|
|
38
|
+
* and a duplicate of an id already seen earlier in the scan (first
|
|
39
|
+
* occurrence keeps it — the same rule `ParaIdAllocatorExtension` applies in
|
|
40
|
+
* the editor). `w14:textId` is written alongside a newly minted `paraId`
|
|
41
|
+
* (same value) and never touched otherwise; it is a text-revision marker,
|
|
42
|
+
* not identity.
|
|
43
|
+
* - Fresh ids are deterministic (`deterministicHexId` over document content,
|
|
44
|
+
* part path, and paragraph ordinal), so the pass is a pure function of the
|
|
45
|
+
* input bytes: retrying an ingest produces identical output.
|
|
46
|
+
* - Each patched part's root element gets `xmlns:w14` / `xmlns:mc`
|
|
47
|
+
* declarations and a `mc:Ignorable` listing `w14` when missing — non-Word
|
|
48
|
+
* producers declare neither, and absent `mc:Ignorable` handling is what
|
|
49
|
+
* makes pre-2010 consumers choke on the new attributes.
|
|
50
|
+
* - Idempotent: a document that already has full coverage is returned as the
|
|
51
|
+
* original bytes, untouched (`alreadyComplete: true`).
|
|
52
|
+
* - Digitally signed packages are returned untouched when already complete.
|
|
53
|
+
* When normalization would rewrite the package, it fails unless the caller
|
|
54
|
+
* explicitly allows signature invalidation after warning the user.
|
|
55
|
+
*/
|
|
56
|
+
/** A malformed or unsupported package prevented paragraph-ID normalization. */
|
|
57
|
+
var EnsureParaIdsError = class extends TaggedError("EnsureParaIdsError")() {};
|
|
58
|
+
const W14_NAMESPACE_URI = "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
59
|
+
const MC_NAMESPACE_URI = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
60
|
+
const MC_IGNORABLE_W14 = "w14";
|
|
61
|
+
const DIGITAL_SIGNATURE_PART_PREFIX = "_xmlsignatures/";
|
|
62
|
+
/** Word reads an all-zero `w14:paraId` as "no id assigned". */
|
|
63
|
+
const RESERVED_ZERO_ID_PATTERN = /^0*$/u;
|
|
64
|
+
const DOCUMENT_PART = "word/document.xml";
|
|
65
|
+
const TARGET_PART_PATTERNS = [
|
|
66
|
+
/^word\/document\.xml$/u,
|
|
67
|
+
/^word\/header\d*\.xml$/u,
|
|
68
|
+
/^word\/footer\d*\.xml$/u,
|
|
69
|
+
/^word\/footnotes\.xml$/u,
|
|
70
|
+
/^word\/endnotes\.xml$/u
|
|
71
|
+
];
|
|
72
|
+
const PARAGRAPH_OPEN = "<w:p";
|
|
73
|
+
const FALLBACK_OPEN = "<mc:Fallback";
|
|
74
|
+
const FALLBACK_CLOSE = "</mc:Fallback>";
|
|
75
|
+
const OPAQUE_XML_REGIONS = [
|
|
76
|
+
{
|
|
77
|
+
open: "<!--",
|
|
78
|
+
close: "-->"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
open: "<![CDATA[",
|
|
82
|
+
close: "]]>"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
open: "<?",
|
|
86
|
+
close: "?>"
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
/**
|
|
90
|
+
* The parser accepts the `w:` fallback prefix for paraId, and comment parts
|
|
91
|
+
* link replies via `w15:paraId` — all three count toward uniqueness.
|
|
92
|
+
*/
|
|
93
|
+
const ANY_PARA_ID_PATTERN = /\bw(?:14|15)?:paraId=(?<quote>["'])(?<id>[\s\S]*?)\k<quote>/gu;
|
|
94
|
+
const OPEN_TAG_PARA_ID_PATTERN = /\sw(?:14)?:paraId=(?<quote>["'])(?<id>[\s\S]*?)\k<quote>/u;
|
|
95
|
+
const OPEN_TAG_TEXT_ID_PATTERN = /\sw(?:14)?:textId=(?<quote>["'])(?<id>[\s\S]*?)\k<quote>/u;
|
|
96
|
+
const XMLNS_W14_PATTERN = /\sxmlns:w14=(?<quote>["'])(?<value>[\s\S]*?)\k<quote>/u;
|
|
97
|
+
const XMLNS_MC_PATTERN = /\sxmlns:mc=(?<quote>["'])(?<value>[\s\S]*?)\k<quote>/u;
|
|
98
|
+
const MC_IGNORABLE_PATTERN = /\smc:Ignorable=(?<quote>["'])(?<value>[\s\S]*?)\k<quote>/u;
|
|
99
|
+
const applySplices = (xml, edits) => {
|
|
100
|
+
const ordered = [...edits].sort((a, b) => b.start - a.start);
|
|
101
|
+
let result = xml;
|
|
102
|
+
for (const { start, end, text } of ordered) result = result.slice(0, start) + text + result.slice(end);
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
const createEnsureParaIdsError = (message, cause) => new EnsureParaIdsError({
|
|
106
|
+
message,
|
|
107
|
+
...cause === void 0 ? {} : { cause }
|
|
108
|
+
});
|
|
109
|
+
const skipOpaqueXmlRegion = (xml, start, partPath) => {
|
|
110
|
+
for (const { open, close } of OPAQUE_XML_REGIONS) {
|
|
111
|
+
if (!xml.startsWith(open, start)) continue;
|
|
112
|
+
const closeStart = xml.indexOf(close, start + open.length);
|
|
113
|
+
if (closeStart === -1) throw createEnsureParaIdsError(`Unterminated ${open} region in ${partPath}`);
|
|
114
|
+
return closeStart + close.length;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
118
|
+
const attributeValueRange = (match, absoluteOffset, group) => {
|
|
119
|
+
const quote = match.groups["quote"];
|
|
120
|
+
const value = match.groups[group];
|
|
121
|
+
const start = absoluteOffset + match.index + match[0].indexOf(quote) + 1;
|
|
122
|
+
return {
|
|
123
|
+
start,
|
|
124
|
+
end: start + value.length
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const collectExistingParaIds = (xml, into) => {
|
|
128
|
+
for (const match of xml.matchAll(ANY_PARA_ID_PATTERN)) {
|
|
129
|
+
const id = match.groups["id"];
|
|
130
|
+
if (id.length > 0) into.add(id.toUpperCase());
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const collectFallbackParaIds = (xml, partPath, into) => {
|
|
134
|
+
let fallbackDepth = 0;
|
|
135
|
+
let pos = 0;
|
|
136
|
+
while (pos < xml.length) {
|
|
137
|
+
const tagStart = xml.indexOf("<", pos);
|
|
138
|
+
if (tagStart === -1) return;
|
|
139
|
+
const opaqueEnd = skipOpaqueXmlRegion(xml, tagStart, partPath);
|
|
140
|
+
if (opaqueEnd !== null) {
|
|
141
|
+
pos = opaqueEnd;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (xml.startsWith(FALLBACK_OPEN, tagStart) && isXmlNameBoundary(xml[tagStart + 12])) {
|
|
145
|
+
const tagEnd = xml.indexOf(">", tagStart);
|
|
146
|
+
if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <mc:Fallback> tag in ${partPath}`);
|
|
147
|
+
if (xml[tagEnd - 1] !== "/") fallbackDepth += 1;
|
|
148
|
+
pos = tagEnd + 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (xml.startsWith(FALLBACK_CLOSE, tagStart)) {
|
|
152
|
+
fallbackDepth = Math.max(0, fallbackDepth - 1);
|
|
153
|
+
pos = tagStart + 14;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (fallbackDepth === 0 || !xml.startsWith(PARAGRAPH_OPEN, tagStart) || !isXmlNameBoundary(xml[tagStart + 4])) {
|
|
157
|
+
pos = tagStart + 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const tagEnd = xml.indexOf(">", tagStart);
|
|
161
|
+
if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <w:p> tag in ${partPath}`);
|
|
162
|
+
const id = OPEN_TAG_PARA_ID_PATTERN.exec(xml.slice(tagStart, tagEnd + 1))?.groups?.["id"];
|
|
163
|
+
if (id) into.add(id.toUpperCase());
|
|
164
|
+
pos = tagEnd + 1;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Deterministic fresh id for the paragraph at `ordinal` in `partPath`,
|
|
169
|
+
* salted past collisions with any id anywhere in the document.
|
|
170
|
+
*/
|
|
171
|
+
const mintParaId = (context, partPath, ordinal) => {
|
|
172
|
+
const seed = `${context.docKey}:${partPath}:${ordinal}`;
|
|
173
|
+
let id = deterministicHexId(seed);
|
|
174
|
+
for (let salt = 1; context.taken.has(id); salt += 1) id = deterministicHexId(`${seed}:${salt}`);
|
|
175
|
+
context.taken.add(id);
|
|
176
|
+
return id;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* One forward scan over a part: every `<w:p>` open tag outside `mc:Fallback`
|
|
180
|
+
* either keeps its paraId (valid, first occurrence) or gets a splice edit
|
|
181
|
+
* minting / replacing one. `seen` spans parts so the first-occurrence rule is
|
|
182
|
+
* document-wide across the scan order.
|
|
183
|
+
*/
|
|
184
|
+
const scanPart = (xml, partPath, context, seen) => {
|
|
185
|
+
const edits = [];
|
|
186
|
+
let assigned = 0;
|
|
187
|
+
let deduplicated = 0;
|
|
188
|
+
let ordinal = 0;
|
|
189
|
+
let fallbackDepth = 0;
|
|
190
|
+
let pos = 0;
|
|
191
|
+
while (pos < xml.length) {
|
|
192
|
+
const tagStart = xml.indexOf("<", pos);
|
|
193
|
+
if (tagStart === -1) break;
|
|
194
|
+
const opaqueEnd = skipOpaqueXmlRegion(xml, tagStart, partPath);
|
|
195
|
+
if (opaqueEnd !== null) {
|
|
196
|
+
pos = opaqueEnd;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (xml.startsWith(FALLBACK_OPEN, tagStart) && isXmlNameBoundary(xml[tagStart + 12])) {
|
|
200
|
+
const tagEnd = xml.indexOf(">", tagStart);
|
|
201
|
+
if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <mc:Fallback> tag in ${partPath}`);
|
|
202
|
+
if (xml[tagEnd - 1] !== "/") fallbackDepth += 1;
|
|
203
|
+
pos = tagEnd + 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (xml.startsWith(FALLBACK_CLOSE, tagStart)) {
|
|
207
|
+
fallbackDepth = Math.max(0, fallbackDepth - 1);
|
|
208
|
+
pos = tagStart + 14;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!xml.startsWith(PARAGRAPH_OPEN, tagStart) || !isXmlNameBoundary(xml[tagStart + 4])) {
|
|
212
|
+
pos = tagStart + 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const tagEnd = xml.indexOf(">", tagStart);
|
|
216
|
+
if (tagEnd === -1) throw createEnsureParaIdsError(`Unterminated <w:p> tag in ${partPath}`);
|
|
217
|
+
pos = tagEnd + 1;
|
|
218
|
+
if (fallbackDepth > 0) continue;
|
|
219
|
+
ordinal += 1;
|
|
220
|
+
const openTag = xml.slice(tagStart, tagEnd + 1);
|
|
221
|
+
const paraIdMatch = OPEN_TAG_PARA_ID_PATTERN.exec(openTag);
|
|
222
|
+
if (!paraIdMatch) {
|
|
223
|
+
const id = mintParaId(context, partPath, ordinal);
|
|
224
|
+
const insertAt = tagStart + 4;
|
|
225
|
+
const textIdMatch = OPEN_TAG_TEXT_ID_PATTERN.exec(openTag);
|
|
226
|
+
if (textIdMatch) {
|
|
227
|
+
edits.push({
|
|
228
|
+
start: insertAt,
|
|
229
|
+
end: insertAt,
|
|
230
|
+
text: ` w14:paraId="${id}"`
|
|
231
|
+
});
|
|
232
|
+
const range = attributeValueRange(textIdMatch, tagStart, "id");
|
|
233
|
+
edits.push({
|
|
234
|
+
...range,
|
|
235
|
+
text: id
|
|
236
|
+
});
|
|
237
|
+
} else edits.push({
|
|
238
|
+
start: insertAt,
|
|
239
|
+
end: insertAt,
|
|
240
|
+
text: ` w14:paraId="${id}" w14:textId="${id}"`
|
|
241
|
+
});
|
|
242
|
+
assigned += 1;
|
|
243
|
+
seen.add(id);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const value = paraIdMatch.groups["id"].toUpperCase();
|
|
247
|
+
const unassigned = RESERVED_ZERO_ID_PATTERN.test(value);
|
|
248
|
+
if (!unassigned && !seen.has(value)) {
|
|
249
|
+
seen.add(value);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const id = mintParaId(context, partPath, ordinal);
|
|
253
|
+
edits.push({
|
|
254
|
+
...attributeValueRange(paraIdMatch, tagStart, "id"),
|
|
255
|
+
text: id
|
|
256
|
+
});
|
|
257
|
+
const textIdMatch = OPEN_TAG_TEXT_ID_PATTERN.exec(openTag);
|
|
258
|
+
if (textIdMatch) edits.push({
|
|
259
|
+
...attributeValueRange(textIdMatch, tagStart, "id"),
|
|
260
|
+
text: id
|
|
261
|
+
});
|
|
262
|
+
else {
|
|
263
|
+
const insertAt = tagStart + 4;
|
|
264
|
+
edits.push({
|
|
265
|
+
start: insertAt,
|
|
266
|
+
end: insertAt,
|
|
267
|
+
text: ` w14:textId="${id}"`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (unassigned) assigned += 1;
|
|
271
|
+
else deduplicated += 1;
|
|
272
|
+
seen.add(id);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
edits,
|
|
276
|
+
assigned,
|
|
277
|
+
deduplicated
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Splice edits ensuring the part's root element declares `xmlns:w14` /
|
|
282
|
+
* `xmlns:mc` and lists `w14` in `mc:Ignorable`, so consumers that predate the
|
|
283
|
+
* 2010 extensions skip the new attributes instead of rejecting the part.
|
|
284
|
+
*/
|
|
285
|
+
const ensureRootNamespaces = (xml, partPath) => {
|
|
286
|
+
let pos = 0;
|
|
287
|
+
let rootStart = -1;
|
|
288
|
+
while (pos < xml.length) {
|
|
289
|
+
const lt = xml.indexOf("<", pos);
|
|
290
|
+
if (lt === -1) break;
|
|
291
|
+
const opaqueEnd = skipOpaqueXmlRegion(xml, lt, partPath);
|
|
292
|
+
if (opaqueEnd !== null) {
|
|
293
|
+
pos = opaqueEnd;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (xml[lt + 1] === "!") {
|
|
297
|
+
const skipTo = xml.indexOf(">", lt);
|
|
298
|
+
if (skipTo === -1) throw createEnsureParaIdsError(`Unterminated prolog in ${partPath}`);
|
|
299
|
+
pos = skipTo + 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
rootStart = lt;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (rootStart === -1) throw createEnsureParaIdsError(`No root element in ${partPath}`);
|
|
306
|
+
const rootEnd = xml.indexOf(">", rootStart);
|
|
307
|
+
if (rootEnd === -1) throw createEnsureParaIdsError(`Unterminated root element in ${partPath}`);
|
|
308
|
+
const rootTag = xml.slice(rootStart, rootEnd + 1);
|
|
309
|
+
const edits = [];
|
|
310
|
+
const declarations = [];
|
|
311
|
+
const mcNamespace = XMLNS_MC_PATTERN.exec(rootTag);
|
|
312
|
+
if (mcNamespace) {
|
|
313
|
+
if (mcNamespace.groups["value"] !== MC_NAMESPACE_URI) throw createEnsureParaIdsError(`xmlns:mc has an unexpected namespace URI in ${partPath}`);
|
|
314
|
+
} else declarations.push(` xmlns:mc="${MC_NAMESPACE_URI}"`);
|
|
315
|
+
const w14Namespace = XMLNS_W14_PATTERN.exec(rootTag);
|
|
316
|
+
if (w14Namespace) {
|
|
317
|
+
if (w14Namespace.groups["value"] !== W14_NAMESPACE_URI) throw createEnsureParaIdsError(`xmlns:w14 has an unexpected namespace URI in ${partPath}`);
|
|
318
|
+
} else declarations.push(` xmlns:w14="${W14_NAMESPACE_URI}"`);
|
|
319
|
+
const ignorable = MC_IGNORABLE_PATTERN.exec(rootTag);
|
|
320
|
+
if (ignorable) {
|
|
321
|
+
const tokens = ignorable.groups["value"].split(/\s+/u).filter((token) => token.length > 0);
|
|
322
|
+
if (!tokens.includes(MC_IGNORABLE_W14)) {
|
|
323
|
+
const { end } = attributeValueRange(ignorable, rootStart, "value");
|
|
324
|
+
const text = tokens.length === 0 ? MC_IGNORABLE_W14 : ` ${MC_IGNORABLE_W14}`;
|
|
325
|
+
edits.push({
|
|
326
|
+
start: end,
|
|
327
|
+
end,
|
|
328
|
+
text
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
} else declarations.push(` mc:Ignorable="${MC_IGNORABLE_W14}"`);
|
|
332
|
+
if (declarations.length > 0) {
|
|
333
|
+
let nameEnd = rootStart + 1;
|
|
334
|
+
while (nameEnd < xml.length && !isXmlNameBoundary(xml[nameEnd])) nameEnd += 1;
|
|
335
|
+
edits.push({
|
|
336
|
+
start: nameEnd,
|
|
337
|
+
end: nameEnd,
|
|
338
|
+
text: declarations.join("")
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return edits;
|
|
342
|
+
};
|
|
343
|
+
/** OPC part names are case-insensitive; compare lowercased. */
|
|
344
|
+
const isTargetPart = (path) => TARGET_PART_PATTERNS.some((pattern) => pattern.test(path.toLowerCase()));
|
|
345
|
+
const toUint8Array = (docx) => docx instanceof Uint8Array ? docx : new Uint8Array(docx);
|
|
346
|
+
const hasDigitalSignatureParts = (zip) => Object.values(zip.files).some(({ dir, name }) => !dir && name.toLowerCase().startsWith(DIGITAL_SIGNATURE_PART_PREFIX));
|
|
347
|
+
/**
|
|
348
|
+
* Backfill `w14:paraId` on every paragraph of a `.docx` buffer. See the
|
|
349
|
+
* module doc for the exact contract. Throws {@link EnsureParaIdsError} when
|
|
350
|
+
* the buffer is not a WordprocessingML package or a part is malformed.
|
|
351
|
+
*/
|
|
352
|
+
const ensureParaIdsInternal = async (docx, options) => {
|
|
353
|
+
const zip = await JSZip.loadAsync(docx);
|
|
354
|
+
const xmlPartNames = [];
|
|
355
|
+
zip.forEach((relativePath, entry) => {
|
|
356
|
+
const lower = relativePath.toLowerCase();
|
|
357
|
+
if (!entry.dir && lower.startsWith("word/") && lower.endsWith(".xml")) xmlPartNames.push(relativePath);
|
|
358
|
+
});
|
|
359
|
+
const documentPartName = xmlPartNames.find((name) => name.toLowerCase() === DOCUMENT_PART);
|
|
360
|
+
if (documentPartName === void 0) throw createEnsureParaIdsError("word/document.xml not found: not a WordprocessingML package");
|
|
361
|
+
const partTexts = /* @__PURE__ */ new Map();
|
|
362
|
+
for (const name of xmlPartNames) {
|
|
363
|
+
const entry = zip.file(name);
|
|
364
|
+
if (entry) partTexts.set(name, await entry.async("text"));
|
|
365
|
+
}
|
|
366
|
+
const taken = /* @__PURE__ */ new Set();
|
|
367
|
+
for (const text of partTexts.values()) collectExistingParaIds(text, taken);
|
|
368
|
+
const context = {
|
|
369
|
+
taken,
|
|
370
|
+
docKey: deterministicHexId(partTexts.get(documentPartName))
|
|
371
|
+
};
|
|
372
|
+
const targetParts = [documentPartName, ...xmlPartNames.filter((name) => name !== documentPartName && isTargetPart(name)).sort((a, b) => a.localeCompare(b))];
|
|
373
|
+
const seen = /* @__PURE__ */ new Set();
|
|
374
|
+
for (const [partPath, xml] of partTexts) if (isTargetPart(partPath)) collectFallbackParaIds(xml, partPath, seen);
|
|
375
|
+
else collectExistingParaIds(xml, seen);
|
|
376
|
+
const updates = /* @__PURE__ */ new Map();
|
|
377
|
+
let assigned = 0;
|
|
378
|
+
let deduplicated = 0;
|
|
379
|
+
for (const partPath of targetParts) {
|
|
380
|
+
const xml = partTexts.get(partPath);
|
|
381
|
+
const scan = scanPart(xml, partPath, context, seen);
|
|
382
|
+
if (scan.edits.length === 0) continue;
|
|
383
|
+
assigned += scan.assigned;
|
|
384
|
+
deduplicated += scan.deduplicated;
|
|
385
|
+
updates.set(partPath, applySplices(xml, [...scan.edits, ...ensureRootNamespaces(xml, partPath)]));
|
|
386
|
+
}
|
|
387
|
+
if (updates.size === 0) return {
|
|
388
|
+
docx: toUint8Array(docx),
|
|
389
|
+
assigned: 0,
|
|
390
|
+
deduplicated: 0,
|
|
391
|
+
alreadyComplete: true
|
|
392
|
+
};
|
|
393
|
+
if (hasDigitalSignatureParts(zip) && options.allowSignedPackageMutation !== true) throw createEnsureParaIdsError("Refusing to normalize a digitally signed package because rewriting OOXML invalidates its signatures. Warn the user and pass allowSignedPackageMutation only if invalidation is acceptable.");
|
|
394
|
+
for (const [partPath, content] of updates) zip.file(partPath, content, {
|
|
395
|
+
compression: "DEFLATE",
|
|
396
|
+
compressionOptions: { level: 6 }
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
docx: await zip.generateAsync({
|
|
400
|
+
type: "uint8array",
|
|
401
|
+
compression: "DEFLATE",
|
|
402
|
+
compressionOptions: { level: 6 }
|
|
403
|
+
}),
|
|
404
|
+
assigned,
|
|
405
|
+
deduplicated,
|
|
406
|
+
alreadyComplete: false
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* Backfill `w14:paraId` on every paragraph of a `.docx` buffer. All package,
|
|
411
|
+
* XML, and compression failures are surfaced as {@link EnsureParaIdsError}.
|
|
412
|
+
*/
|
|
413
|
+
const ensureParaIds = async (docx, options = {}) => {
|
|
414
|
+
try {
|
|
415
|
+
return await ensureParaIdsInternal(docx, options);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (error instanceof EnsureParaIdsError) throw error;
|
|
418
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
419
|
+
throw createEnsureParaIdsError(`Failed to normalize paragraph IDs: ${message}`, error);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
//#endregion
|
|
423
|
+
export { EnsureParaIdsError, ensureParaIds };
|