@usejunior/docx-core 0.19.1 → 0.21.2
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/README.md +12 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/conformance-adapter.d.ts.map +1 -1
- package/dist/cli/conformance-adapter.js +0 -27
- package/dist/cli/conformance-adapter.js.map +1 -1
- package/dist/core-types.d.ts +1 -139
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +1 -3
- package/dist/core-types.js.map +1 -1
- package/dist/generation/compile.d.ts.map +1 -1
- package/dist/generation/compile.js +1 -3
- package/dist/generation/compile.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/integration/generation-probes.d.ts +36 -2
- package/dist/integration/generation-probes.d.ts.map +1 -1
- package/dist/integration/generation-probes.js +137 -15
- package/dist/integration/generation-probes.js.map +1 -1
- package/dist/integration/libreoffice-oracle.d.ts +3 -1
- package/dist/integration/libreoffice-oracle.d.ts.map +1 -1
- package/dist/integration/libreoffice-oracle.js +10 -4
- package/dist/integration/libreoffice-oracle.js.map +1 -1
- package/dist/integration/synthetic-docx-fixture.d.ts.map +1 -1
- package/dist/integration/synthetic-docx-fixture.js +8 -0
- package/dist/integration/synthetic-docx-fixture.js.map +1 -1
- package/dist/primitives/accept_ai_edits.js +14 -2
- package/dist/primitives/accept_ai_edits.js.map +1 -1
- package/dist/primitives/accept_changes.d.ts +6 -0
- package/dist/primitives/accept_changes.d.ts.map +1 -1
- package/dist/primitives/accept_changes.js +236 -62
- package/dist/primitives/accept_changes.js.map +1 -1
- package/dist/primitives/bookmarks.d.ts +8 -2
- package/dist/primitives/bookmarks.d.ts.map +1 -1
- package/dist/primitives/bookmarks.js +31 -7
- package/dist/primitives/bookmarks.js.map +1 -1
- package/dist/primitives/comments.d.ts +101 -5
- package/dist/primitives/comments.d.ts.map +1 -1
- package/dist/primitives/comments.js +356 -156
- package/dist/primitives/comments.js.map +1 -1
- package/dist/primitives/conformance.d.ts +42 -0
- package/dist/primitives/conformance.d.ts.map +1 -0
- package/dist/primitives/conformance.js +61 -0
- package/dist/primitives/conformance.js.map +1 -0
- package/dist/primitives/document.d.ts +145 -2
- package/dist/primitives/document.d.ts.map +1 -1
- package/dist/primitives/document.js +505 -27
- package/dist/primitives/document.js.map +1 -1
- package/dist/primitives/document_view-headings.d.ts +3 -1
- package/dist/primitives/document_view-headings.d.ts.map +1 -1
- package/dist/primitives/document_view-headings.js +5 -5
- package/dist/primitives/document_view-headings.js.map +1 -1
- package/dist/primitives/document_view.d.ts +2 -0
- package/dist/primitives/document_view.d.ts.map +1 -1
- package/dist/primitives/document_view.js +18 -4
- package/dist/primitives/document_view.js.map +1 -1
- package/dist/primitives/errors.d.ts +3 -2
- package/dist/primitives/errors.d.ts.map +1 -1
- package/dist/primitives/errors.js +3 -1
- package/dist/primitives/errors.js.map +1 -1
- package/dist/primitives/extract_revisions.d.ts +23 -6
- package/dist/primitives/extract_revisions.d.ts.map +1 -1
- package/dist/primitives/extract_revisions.js +208 -38
- package/dist/primitives/extract_revisions.js.map +1 -1
- package/dist/primitives/field_evaluation.d.ts +70 -0
- package/dist/primitives/field_evaluation.d.ts.map +1 -0
- package/dist/primitives/field_evaluation.js +462 -0
- package/dist/primitives/field_evaluation.js.map +1 -0
- package/dist/primitives/footnotes.d.ts +53 -3
- package/dist/primitives/footnotes.d.ts.map +1 -1
- package/dist/primitives/footnotes.js +228 -83
- package/dist/primitives/footnotes.js.map +1 -1
- package/dist/primitives/index.d.ts +12 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +12 -1
- package/dist/primitives/index.js.map +1 -1
- package/dist/primitives/merge_runs.d.ts +3 -1
- package/dist/primitives/merge_runs.d.ts.map +1 -1
- package/dist/primitives/merge_runs.js +12 -2
- package/dist/primitives/merge_runs.js.map +1 -1
- package/dist/primitives/namespaces.d.ts +10 -2
- package/dist/primitives/namespaces.d.ts.map +1 -1
- package/dist/primitives/namespaces.js +11 -2
- package/dist/primitives/namespaces.js.map +1 -1
- package/dist/primitives/note_conversion.d.ts +11 -0
- package/dist/primitives/note_conversion.d.ts.map +1 -0
- package/dist/primitives/note_conversion.js +12 -0
- package/dist/primitives/note_conversion.js.map +1 -0
- package/dist/primitives/paragraph-index.d.ts +31 -0
- package/dist/primitives/paragraph-index.d.ts.map +1 -0
- package/dist/primitives/paragraph-index.js +140 -0
- package/dist/primitives/paragraph-index.js.map +1 -0
- package/dist/primitives/paragraph_merge_formatting.d.ts +24 -0
- package/dist/primitives/paragraph_merge_formatting.d.ts.map +1 -0
- package/dist/primitives/paragraph_merge_formatting.js +86 -0
- package/dist/primitives/paragraph_merge_formatting.js.map +1 -0
- package/dist/primitives/paragraph_numbering.d.ts +40 -0
- package/dist/primitives/paragraph_numbering.d.ts.map +1 -0
- package/dist/primitives/paragraph_numbering.js +198 -0
- package/dist/primitives/paragraph_numbering.js.map +1 -0
- package/dist/primitives/paragraph_structure.d.ts +19 -0
- package/dist/primitives/paragraph_structure.d.ts.map +1 -0
- package/dist/primitives/paragraph_structure.js +66 -0
- package/dist/primitives/paragraph_structure.js.map +1 -0
- package/dist/primitives/reject_changes.d.ts +6 -0
- package/dist/primitives/reject_changes.d.ts.map +1 -1
- package/dist/primitives/reject_changes.js +278 -68
- package/dist/primitives/reject_changes.js.map +1 -1
- package/dist/primitives/relationships.d.ts +22 -0
- package/dist/primitives/relationships.d.ts.map +1 -1
- package/dist/primitives/relationships.js +87 -0
- package/dist/primitives/relationships.js.map +1 -1
- package/dist/primitives/revision-parts.d.ts +15 -0
- package/dist/primitives/revision-parts.d.ts.map +1 -1
- package/dist/primitives/revision-parts.js +33 -1
- package/dist/primitives/revision-parts.js.map +1 -1
- package/dist/primitives/sectPrAudit.d.ts.map +1 -1
- package/dist/primitives/sectPrAudit.js +10 -1
- package/dist/primitives/sectPrAudit.js.map +1 -1
- package/dist/primitives/sections.d.ts +135 -0
- package/dist/primitives/sections.d.ts.map +1 -0
- package/dist/primitives/sections.js +644 -0
- package/dist/primitives/sections.js.map +1 -0
- package/dist/primitives/styles.d.ts +61 -0
- package/dist/primitives/styles.d.ts.map +1 -1
- package/dist/primitives/styles.js +207 -25
- package/dist/primitives/styles.js.map +1 -1
- package/dist/primitives/symbol_run_content.d.ts +42 -0
- package/dist/primitives/symbol_run_content.d.ts.map +1 -0
- package/dist/primitives/symbol_run_content.js +79 -0
- package/dist/primitives/symbol_run_content.js.map +1 -0
- package/dist/primitives/table_cells.d.ts +49 -0
- package/dist/primitives/table_cells.d.ts.map +1 -0
- package/dist/primitives/table_cells.js +149 -0
- package/dist/primitives/table_cells.js.map +1 -0
- package/dist/primitives/table_columns.d.ts +50 -0
- package/dist/primitives/table_columns.d.ts.map +1 -0
- package/dist/primitives/table_columns.js +138 -0
- package/dist/primitives/table_columns.js.map +1 -0
- package/dist/primitives/table_edit_common.d.ts +39 -0
- package/dist/primitives/table_edit_common.d.ts.map +1 -0
- package/dist/primitives/table_edit_common.js +163 -0
- package/dist/primitives/table_edit_common.js.map +1 -0
- package/dist/primitives/table_occupancy.d.ts +39 -0
- package/dist/primitives/table_occupancy.d.ts.map +1 -0
- package/dist/primitives/table_occupancy.js +145 -0
- package/dist/primitives/table_occupancy.js.map +1 -0
- package/dist/primitives/table_rows.d.ts +56 -0
- package/dist/primitives/table_rows.d.ts.map +1 -0
- package/dist/primitives/table_rows.js +390 -0
- package/dist/primitives/table_rows.js.map +1 -0
- package/dist/primitives/text.d.ts +69 -1
- package/dist/primitives/text.d.ts.map +1 -1
- package/dist/primitives/text.js +592 -102
- package/dist/primitives/text.js.map +1 -1
- package/dist/primitives/track-changes-emitter.d.ts +23 -0
- package/dist/primitives/track-changes-emitter.d.ts.map +1 -1
- package/dist/primitives/track-changes-emitter.js +64 -1
- package/dist/primitives/track-changes-emitter.js.map +1 -1
- package/dist/primitives/validate_document.d.ts.map +1 -1
- package/dist/primitives/validate_document.js +22 -1
- package/dist/primitives/validate_document.js.map +1 -1
- package/dist/primitives/zip.d.ts +22 -1
- package/dist/primitives/zip.d.ts.map +1 -1
- package/dist/primitives/zip.js +47 -8
- package/dist/primitives/zip.js.map +1 -1
- package/dist/shared/docx/DocxArchive.d.ts +6 -2
- package/dist/shared/docx/DocxArchive.d.ts.map +1 -1
- package/dist/shared/docx/DocxArchive.js +17 -4
- package/dist/shared/docx/DocxArchive.js.map +1 -1
- package/dist/shared/field-semantics.d.ts +26 -0
- package/dist/shared/field-semantics.d.ts.map +1 -0
- package/dist/shared/field-semantics.js +232 -0
- package/dist/shared/field-semantics.js.map +1 -0
- package/dist/shared/field-structure.d.ts +10 -1
- package/dist/shared/field-structure.d.ts.map +1 -1
- package/dist/shared/field-structure.js +31 -12
- package/dist/shared/field-structure.js.map +1 -1
- package/package.json +4 -5
|
@@ -2,18 +2,21 @@ import { DocxZip } from './zip.js';
|
|
|
2
2
|
import { parseXml, serializeXml } from './xml.js';
|
|
3
3
|
import { maybeCaptureEmittedDocumentXml } from './schema-corpus-capture.js';
|
|
4
4
|
import { OOXML, W } from './namespaces.js';
|
|
5
|
+
import { assertTransitionalWordprocessingML } from './conformance.js';
|
|
5
6
|
import { createWmlElement, isW, getDirectChildrenByName } from './dom-helpers.js';
|
|
6
|
-
import { findParagraphByBookmarkId, insertParagraphBookmarks, cleanupInternalBookmarks, getParagraphBookmarkId, insertSingleParagraphBookmark, } from './bookmarks.js';
|
|
7
|
-
import { getParagraphRuns, getParagraphText, replaceParagraphTextRange } from './text.js';
|
|
7
|
+
import { findParagraphByBookmarkId, insertParagraphBookmarks, cleanupInternalBookmarks, collectBookmarkReservation, getParagraphBookmarkId, insertSingleParagraphBookmark, } from './bookmarks.js';
|
|
8
|
+
import { formatParagraphTextRange, getParagraphRuns, getParagraphText, replaceParagraphTextRange } from './text.js';
|
|
8
9
|
import { allocateRevisionId, createRevisionContainer, } from './track-changes-emitter.js';
|
|
9
10
|
import { buildNodesForDocumentView, collectViewParagraphs } from './document_view.js';
|
|
10
11
|
import { serializeToMarkdown } from './serialize_markdown.js';
|
|
11
12
|
import { serializeToHtml } from './serialize_html.js';
|
|
12
13
|
import { serializeToPlainText } from './serialize_plaintext.js';
|
|
13
|
-
import { parseStylesXml } from './styles.js';
|
|
14
|
+
import { parseStylesXml, parseThemeXml } from './styles.js';
|
|
14
15
|
import { parseNumberingXml } from './numbering.js';
|
|
16
|
+
import { getDirectParagraphNumbering, setDirectParagraphNumbering, } from './paragraph_numbering.js';
|
|
17
|
+
import { getDocumentSections, insertSectionBreak, setSectionPageNumberStart, updateSectionProperties, } from './sections.js';
|
|
15
18
|
import { findUniqueSubstringMatch } from './matching.js';
|
|
16
|
-
import { parseDocumentRels } from './relationships.js';
|
|
19
|
+
import { parseDocumentRels, relationshipPartPath } from './relationships.js';
|
|
17
20
|
import { setParagraphSpacing, setTableCellPadding, setTableRowHeight, } from './layout.js';
|
|
18
21
|
import { extractTables, } from './tables.js';
|
|
19
22
|
import { mergeRuns } from './merge_runs.js';
|
|
@@ -22,16 +25,25 @@ import { simplifyRedlines } from './simplify_redlines.js';
|
|
|
22
25
|
import { preventDoubleElevation } from './prevent_double_elevation.js';
|
|
23
26
|
import { validateDocument } from './validate_document.js';
|
|
24
27
|
import { validateAiRevisions as validateAiRevisionsImpl, } from './validate_ai_revisions.js';
|
|
25
|
-
import {
|
|
28
|
+
import { enumerateSelectedHeaderFooterPartPaths, enumerateSelectedRevisionStoryPartPaths, } from './revision-parts.js';
|
|
26
29
|
import { acceptChanges as acceptChangesImpl } from './accept_changes.js';
|
|
27
30
|
import { rejectChanges as rejectChangesImpl } from './reject_changes.js';
|
|
28
31
|
import { collectRevisionElements, resolveSelectedIds, detectAmbiguousOverlaps, selectedIdFilter, AmbiguousRevisionOverlapError, } from './accept_ai_edits.js';
|
|
29
32
|
import { revisionElementId } from './accept_changes.js';
|
|
30
33
|
import { TRACKED_CHANGE_ELEMENT_NAME_SET } from './revision-vocabulary.js';
|
|
31
|
-
import {
|
|
34
|
+
import { insertTableRow as insertTableRowImpl, deleteTableRow as deleteTableRowImpl, removeOrphanedRangeEndpointsForSubtree, } from './table_rows.js';
|
|
35
|
+
import { insertTableColumn as insertTableColumnImpl, deleteTableColumn as deleteTableColumnImpl, } from './table_columns.js';
|
|
36
|
+
import { splitTableCell as splitTableCellImpl, absorbTableCell as absorbTableCellImpl, } from './table_cells.js';
|
|
37
|
+
import { bootstrapCommentParts, addComment as addCommentImpl, addCommentReply as addCommentReplyImpl, getComments as getCommentsImpl, getComment as getCommentImpl, deleteComment as deleteCommentImpl, updateCommentBody as updateCommentBodyImpl, } from './comments.js';
|
|
32
38
|
import { bootstrapFootnoteParts, getFootnotes as getFootnotesImpl, getFootnote as getFootnoteImpl, addFootnote as addFootnoteImpl, updateFootnoteText as updateFootnoteTextImpl, deleteFootnote as deleteFootnoteImpl, } from './footnotes.js';
|
|
33
39
|
function emptyAcceptChangesResult() {
|
|
34
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
insertionsAccepted: 0,
|
|
42
|
+
deletionsAccepted: 0,
|
|
43
|
+
movesResolved: 0,
|
|
44
|
+
propertyChangesResolved: 0,
|
|
45
|
+
unresolvedRowRevisions: 0,
|
|
46
|
+
};
|
|
35
47
|
}
|
|
36
48
|
function hasAcceptedChanges(result) {
|
|
37
49
|
return (result.insertionsAccepted > 0 ||
|
|
@@ -44,9 +56,16 @@ function addAcceptChangesResult(total, result) {
|
|
|
44
56
|
total.deletionsAccepted += result.deletionsAccepted;
|
|
45
57
|
total.movesResolved += result.movesResolved;
|
|
46
58
|
total.propertyChangesResolved += result.propertyChangesResolved;
|
|
59
|
+
total.unresolvedRowRevisions += result.unresolvedRowRevisions;
|
|
47
60
|
}
|
|
48
61
|
function emptyRejectChangesResult() {
|
|
49
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
insertionsRemoved: 0,
|
|
64
|
+
deletionsRestored: 0,
|
|
65
|
+
movesReverted: 0,
|
|
66
|
+
propertyChangesReverted: 0,
|
|
67
|
+
unresolvedRowRevisions: 0,
|
|
68
|
+
};
|
|
50
69
|
}
|
|
51
70
|
function hasRejectedChanges(result) {
|
|
52
71
|
return (result.insertionsRemoved > 0 ||
|
|
@@ -59,6 +78,7 @@ function addRejectChangesResult(total, result) {
|
|
|
59
78
|
total.deletionsRestored += result.deletionsRestored;
|
|
60
79
|
total.movesReverted += result.movesReverted;
|
|
61
80
|
total.propertyChangesReverted += result.propertyChangesReverted;
|
|
81
|
+
total.unresolvedRowRevisions += result.unresolvedRowRevisions;
|
|
62
82
|
}
|
|
63
83
|
function parseWId(el) {
|
|
64
84
|
const idStr = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
@@ -138,6 +158,74 @@ function nextElementSibling(node) {
|
|
|
138
158
|
}
|
|
139
159
|
return null;
|
|
140
160
|
}
|
|
161
|
+
function nearestWordAncestor(element, localName) {
|
|
162
|
+
for (let current = element.parentElement; current; current = current.parentElement) {
|
|
163
|
+
if (isW(current, localName))
|
|
164
|
+
return current;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Classify the physical table-cell constraints around a story paragraph.
|
|
170
|
+
*
|
|
171
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.4.66
|
|
172
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.4.84
|
|
173
|
+
* @see https://github.com/UseJunior/safe-docx/issues/998
|
|
174
|
+
*/
|
|
175
|
+
function storyParagraphTableContext(paragraph) {
|
|
176
|
+
const cell = nearestWordAncestor(paragraph, W.tc);
|
|
177
|
+
if (!cell) {
|
|
178
|
+
return {
|
|
179
|
+
inTableCell: false,
|
|
180
|
+
verticalMergeContinuation: false,
|
|
181
|
+
directCellParagraphs: 0,
|
|
182
|
+
isTrailingDirectCellParagraph: false,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const directParagraphs = getDirectChildrenByName(cell, W.p);
|
|
186
|
+
const cellProperties = getDirectChildrenByName(cell, W.tcPr)[0];
|
|
187
|
+
const verticalMerge = cellProperties
|
|
188
|
+
? getDirectChildrenByName(cellProperties, 'vMerge')[0]
|
|
189
|
+
: undefined;
|
|
190
|
+
const mergeValue = verticalMerge?.getAttributeNS(OOXML.W_NS, 'val')
|
|
191
|
+
?? verticalMerge?.getAttribute('w:val')
|
|
192
|
+
?? verticalMerge?.getAttribute('val');
|
|
193
|
+
return {
|
|
194
|
+
inTableCell: true,
|
|
195
|
+
verticalMergeContinuation: Boolean(verticalMerge && (!mergeValue || mergeValue === 'continue')),
|
|
196
|
+
directCellParagraphs: directParagraphs.length,
|
|
197
|
+
isTrailingDirectCellParagraph: directParagraphs.at(-1) === paragraph,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function assertStoryParagraphMutationSafe(paragraph, operation, styleSource) {
|
|
201
|
+
const parent = paragraph.parentElement;
|
|
202
|
+
const root = paragraph.ownerDocument.documentElement;
|
|
203
|
+
const cell = nearestWordAncestor(paragraph, W.tc);
|
|
204
|
+
if (parent !== root && parent !== cell) {
|
|
205
|
+
throw new Error('Story paragraph must be a direct child of the story root or table cell');
|
|
206
|
+
}
|
|
207
|
+
const context = storyParagraphTableContext(paragraph);
|
|
208
|
+
if (context.verticalMergeContinuation) {
|
|
209
|
+
throw new Error('Story paragraph mutation in a vertical-merge continuation cell is unsupported');
|
|
210
|
+
}
|
|
211
|
+
const table = cell ? nearestWordAncestor(cell, W.tbl) : null;
|
|
212
|
+
if (table && nearestWordAncestor(table, W.tc)) {
|
|
213
|
+
throw new Error('Story paragraph mutation inside a nested table is unsupported');
|
|
214
|
+
}
|
|
215
|
+
if (operation === 'insert' && styleSource) {
|
|
216
|
+
const styleCell = nearestWordAncestor(styleSource, W.tc);
|
|
217
|
+
if (styleCell !== cell)
|
|
218
|
+
throw new Error('Story paragraph style source must be in the anchor table cell');
|
|
219
|
+
}
|
|
220
|
+
if (operation !== 'delete' || !cell)
|
|
221
|
+
return;
|
|
222
|
+
const remainingBlocks = Array.from(cell.childNodes).filter((child) => child.nodeType === 1
|
|
223
|
+
&& child !== paragraph
|
|
224
|
+
&& ['p', 'tbl', 'sdt', 'customXml', 'altChunk'].includes(child.localName));
|
|
225
|
+
if (remainingBlocks.at(-1)?.localName !== W.p) {
|
|
226
|
+
throw new Error('Story paragraph deletion would leave a table cell without a trailing paragraph');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
141
229
|
function collectNamedBookmarkIds(doc) {
|
|
142
230
|
const ids = new Set();
|
|
143
231
|
const starts = doc.getElementsByTagNameNS(OOXML.W_NS, W.bookmarkStart);
|
|
@@ -154,36 +242,45 @@ export class DocxDocument {
|
|
|
154
242
|
zip;
|
|
155
243
|
documentXml;
|
|
156
244
|
stylesXml;
|
|
245
|
+
themeXml;
|
|
157
246
|
numberingXml;
|
|
158
247
|
footnotesXml;
|
|
159
248
|
relsMap;
|
|
160
249
|
dirty;
|
|
161
250
|
documentViewCache;
|
|
251
|
+
paragraphBookmarkReservation;
|
|
162
252
|
/**
|
|
163
253
|
* Raw document.xml text as loaded, before normalize()/edits mutate the DOM.
|
|
164
254
|
* Reference for minimal re-serialization in toBuffer(); null for instances
|
|
165
255
|
* not created via load().
|
|
166
256
|
*/
|
|
167
257
|
originalDocumentXmlText;
|
|
168
|
-
constructor(zip, documentXml, stylesXml, numberingXml, footnotesXml, relsMap, originalDocumentXmlText = null) {
|
|
258
|
+
constructor(zip, documentXml, stylesXml, themeXml, numberingXml, footnotesXml, relsMap, originalDocumentXmlText = null) {
|
|
169
259
|
this.zip = zip;
|
|
170
260
|
this.documentXml = documentXml;
|
|
171
261
|
this.stylesXml = stylesXml;
|
|
262
|
+
this.themeXml = themeXml;
|
|
172
263
|
this.numberingXml = numberingXml;
|
|
173
264
|
this.footnotesXml = footnotesXml;
|
|
174
265
|
this.relsMap = relsMap;
|
|
175
266
|
this.dirty = false;
|
|
176
267
|
this.documentViewCache = null;
|
|
268
|
+
this.paragraphBookmarkReservation = undefined;
|
|
177
269
|
this.originalDocumentXmlText = originalDocumentXmlText;
|
|
178
270
|
}
|
|
179
271
|
static async load(buffer) {
|
|
180
272
|
const zip = await DocxZip.load(buffer);
|
|
181
273
|
const xml = await zip.readText('word/document.xml');
|
|
182
274
|
const doc = parseXml(xml);
|
|
275
|
+
// Refuse WML Strict packages here, before any Transitional-only lookup
|
|
276
|
+
// could read them as empty (#1025).
|
|
277
|
+
assertTransitionalWordprocessingML(doc);
|
|
183
278
|
// Optional parts used for fidelity: list labels + style fingerprints.
|
|
184
279
|
const stylesText = await zip.readTextOrNull('word/styles.xml');
|
|
280
|
+
const themeText = await zip.readTextOrNull('word/theme/theme1.xml');
|
|
185
281
|
const numberingText = await zip.readTextOrNull('word/numbering.xml');
|
|
186
282
|
const stylesXml = stylesText ? parseXml(stylesText) : null;
|
|
283
|
+
const themeXml = themeText ? parseXml(themeText) : null;
|
|
187
284
|
const numberingXml = numberingText ? parseXml(numberingText) : null;
|
|
188
285
|
// Load footnotes for [^N] marker rendering in document view.
|
|
189
286
|
const footnotesText = await zip.readTextOrNull('word/footnotes.xml');
|
|
@@ -191,7 +288,15 @@ export class DocxDocument {
|
|
|
191
288
|
// Load document relationships for hyperlink resolution.
|
|
192
289
|
const relsText = await zip.readTextOrNull('word/_rels/document.xml.rels');
|
|
193
290
|
const relsMap = relsText ? parseDocumentRels(parseXml(relsText)) : new Map();
|
|
194
|
-
|
|
291
|
+
const document = new DocxDocument(zip, doc, stylesXml, themeXml, numberingXml, footnotesXml, relsMap, xml);
|
|
292
|
+
const storyDocuments = [];
|
|
293
|
+
for (const partPath of await enumerateSelectedHeaderFooterPartPaths(zip)) {
|
|
294
|
+
const storyXml = await zip.readTextOrNull(partPath);
|
|
295
|
+
if (storyXml)
|
|
296
|
+
storyDocuments.push(parseXml(storyXml));
|
|
297
|
+
}
|
|
298
|
+
document.paragraphBookmarkReservation = collectBookmarkReservation([doc, ...storyDocuments]);
|
|
299
|
+
return document;
|
|
195
300
|
}
|
|
196
301
|
getParagraphs() {
|
|
197
302
|
const body = this.documentXml.getElementsByTagNameNS(OOXML.W_NS, W.body).item(0);
|
|
@@ -208,8 +313,184 @@ export class DocxDocument {
|
|
|
208
313
|
return null;
|
|
209
314
|
return getParagraphText(p);
|
|
210
315
|
}
|
|
316
|
+
async selectedStoryDocument(partPath) {
|
|
317
|
+
const selected = await enumerateSelectedHeaderFooterPartPaths(this.zip);
|
|
318
|
+
if (!selected.includes(partPath)) {
|
|
319
|
+
throw new Error(`Header/footer story is not selected by a section relationship: ${partPath}`);
|
|
320
|
+
}
|
|
321
|
+
const xml = await this.zip.readTextOrNull(partPath);
|
|
322
|
+
if (!xml)
|
|
323
|
+
throw new Error(`Selected header/footer story part is missing: ${partPath}`);
|
|
324
|
+
return parseXml(xml);
|
|
325
|
+
}
|
|
326
|
+
withTemporaryDocumentXml(story, mutation, bookmarkReservation) {
|
|
327
|
+
const bodyDocument = this.documentXml;
|
|
328
|
+
const dirty = this.dirty;
|
|
329
|
+
const viewCache = this.documentViewCache;
|
|
330
|
+
const previousReservation = this.paragraphBookmarkReservation;
|
|
331
|
+
this.documentXml = story;
|
|
332
|
+
this.paragraphBookmarkReservation = bookmarkReservation;
|
|
333
|
+
try {
|
|
334
|
+
return mutation();
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
this.documentXml = bodyDocument;
|
|
338
|
+
this.dirty = dirty;
|
|
339
|
+
this.documentViewCache = viewCache;
|
|
340
|
+
this.paragraphBookmarkReservation = previousReservation;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async getStoryParagraphTextById(partPath, bookmarkId) {
|
|
344
|
+
const story = await this.selectedStoryDocument(partPath);
|
|
345
|
+
const paragraph = findParagraphByBookmarkId(story, bookmarkId);
|
|
346
|
+
return paragraph ? getParagraphText(paragraph) : null;
|
|
347
|
+
}
|
|
348
|
+
async getStoryParagraphById(partPath, bookmarkId) {
|
|
349
|
+
const text = await this.getStoryParagraphTextById(partPath, bookmarkId);
|
|
350
|
+
return text === null ? null : { id: bookmarkId, text };
|
|
351
|
+
}
|
|
352
|
+
async insertStoryParagraphBookmarks(partPath, attachmentId) {
|
|
353
|
+
const story = await this.selectedStoryDocument(partPath);
|
|
354
|
+
const reservation = this.paragraphBookmarkReservation;
|
|
355
|
+
const result = insertParagraphBookmarks(story, attachmentId, reservation);
|
|
356
|
+
if (result.indexedParagraphs > 0)
|
|
357
|
+
this.zip.writeText(partPath, serializeXml(story));
|
|
358
|
+
return { paragraphCount: result.indexedParagraphs };
|
|
359
|
+
}
|
|
360
|
+
async validateStoryParagraphTableCell(partPath, bookmarkId) {
|
|
361
|
+
const story = await this.selectedStoryDocument(partPath);
|
|
362
|
+
const paragraph = findParagraphByBookmarkId(story, bookmarkId);
|
|
363
|
+
if (!paragraph)
|
|
364
|
+
throw new Error(`Paragraph not found in ${partPath}: ${bookmarkId}`);
|
|
365
|
+
return storyParagraphTableContext(paragraph);
|
|
366
|
+
}
|
|
367
|
+
/** Mutate intended-clean story text; comparison is responsible for tracked output. */
|
|
368
|
+
async replaceStoryTextAtRange(params) {
|
|
369
|
+
const story = await this.selectedStoryDocument(params.partPath);
|
|
370
|
+
const paragraph = findParagraphByBookmarkId(story, params.targetParagraphId);
|
|
371
|
+
if (!paragraph)
|
|
372
|
+
throw new Error(`Paragraph not found in ${params.partPath}: ${params.targetParagraphId}`);
|
|
373
|
+
assertStoryParagraphMutationSafe(paragraph, 'replace');
|
|
374
|
+
replaceParagraphTextRange(paragraph, params.start, params.end, params.replaceText);
|
|
375
|
+
this.zip.writeText(params.partPath, serializeXml(story));
|
|
376
|
+
}
|
|
377
|
+
/** Mutate intended-clean story text; comparison is responsible for tracked output. */
|
|
378
|
+
async replaceStoryText(params) {
|
|
379
|
+
const story = await this.selectedStoryDocument(params.partPath);
|
|
380
|
+
const paragraph = findParagraphByBookmarkId(story, params.targetParagraphId);
|
|
381
|
+
if (!paragraph)
|
|
382
|
+
throw new Error(`Paragraph not found in ${params.partPath}: ${params.targetParagraphId}`);
|
|
383
|
+
assertStoryParagraphMutationSafe(paragraph, 'replace');
|
|
384
|
+
const match = findUniqueSubstringMatch(getParagraphText(paragraph), params.findText);
|
|
385
|
+
if (match.status === 'not_found') {
|
|
386
|
+
throw new Error(`Text not found in paragraph ${params.targetParagraphId}`);
|
|
387
|
+
}
|
|
388
|
+
if (match.status === 'multiple') {
|
|
389
|
+
throw new Error(`Multiple matches (${match.matchCount}) found in paragraph ${params.targetParagraphId} using ${match.mode} matching`);
|
|
390
|
+
}
|
|
391
|
+
replaceParagraphTextRange(paragraph, match.start, match.end, params.replaceText);
|
|
392
|
+
this.zip.writeText(params.partPath, serializeXml(story));
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Apply a bounded run-format edit in one selected physical story paragraph.
|
|
396
|
+
*
|
|
397
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.3.2.28
|
|
398
|
+
* @see #1034
|
|
399
|
+
*/
|
|
400
|
+
async formatStoryTextAtRange(params) {
|
|
401
|
+
const story = await this.selectedStoryDocument(params.partPath);
|
|
402
|
+
const paragraph = findParagraphByBookmarkId(story, params.targetParagraphId);
|
|
403
|
+
if (!paragraph)
|
|
404
|
+
throw new Error(`Paragraph not found in ${params.partPath}: ${params.targetParagraphId}`);
|
|
405
|
+
assertStoryParagraphMutationSafe(paragraph, 'replace');
|
|
406
|
+
formatParagraphTextRange(paragraph, params.start, params.end, params.format);
|
|
407
|
+
this.zip.writeText(params.partPath, serializeXml(story));
|
|
408
|
+
}
|
|
409
|
+
async insertStoryParagraph(params, ctx) {
|
|
410
|
+
const story = await this.selectedStoryDocument(params.partPath);
|
|
411
|
+
const anchor = findParagraphByBookmarkId(story, params.positionalAnchorNodeId);
|
|
412
|
+
if (!anchor)
|
|
413
|
+
throw new Error(`Anchor paragraph not found in ${params.partPath}: ${params.positionalAnchorNodeId}`);
|
|
414
|
+
const styleSource = params.styleSourceId
|
|
415
|
+
? findParagraphByBookmarkId(story, params.styleSourceId)
|
|
416
|
+
: anchor;
|
|
417
|
+
if (params.styleSourceId && !styleSource) {
|
|
418
|
+
throw new Error(`Style source paragraph not found in ${params.partPath}: ${params.styleSourceId}`);
|
|
419
|
+
}
|
|
420
|
+
assertStoryParagraphMutationSafe(anchor, 'insert', styleSource ?? undefined);
|
|
421
|
+
const bookmarkReservation = this.paragraphBookmarkReservation;
|
|
422
|
+
const result = this.withTemporaryDocumentXml(story, () => this.insertParagraph({
|
|
423
|
+
positionalAnchorNodeId: params.positionalAnchorNodeId,
|
|
424
|
+
relativePosition: params.relativePosition,
|
|
425
|
+
newText: params.newText,
|
|
426
|
+
styleSourceId: params.styleSourceId,
|
|
427
|
+
runStyleSourceText: params.runStyleSourceText,
|
|
428
|
+
}, ctx), bookmarkReservation);
|
|
429
|
+
this.zip.writeText(params.partPath, serializeXml(story));
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
/** Remove an intended-clean story paragraph; comparison is responsible for tracked output. */
|
|
433
|
+
async deleteStoryParagraph(partPath, paragraphId) {
|
|
434
|
+
const story = await this.selectedStoryDocument(partPath);
|
|
435
|
+
const paragraph = findParagraphByBookmarkId(story, paragraphId);
|
|
436
|
+
if (!paragraph)
|
|
437
|
+
throw new Error(`Paragraph not found in ${partPath}: ${paragraphId}`);
|
|
438
|
+
assertStoryParagraphMutationSafe(paragraph, 'delete');
|
|
439
|
+
const bookmarkStart = Array.from(story.getElementsByTagNameNS(OOXML.W_NS, W.bookmarkStart))
|
|
440
|
+
.find((start) => (start.getAttributeNS(OOXML.W_NS, 'name') ?? start.getAttribute('w:name')) === paragraphId);
|
|
441
|
+
const bookmarkNumericId = bookmarkStart?.getAttributeNS(OOXML.W_NS, 'id')
|
|
442
|
+
?? bookmarkStart?.getAttribute('w:id');
|
|
443
|
+
const bookmarkEnd = bookmarkNumericId
|
|
444
|
+
? Array.from(story.getElementsByTagNameNS(OOXML.W_NS, W.bookmarkEnd)).find((end) => (end.getAttributeNS(OOXML.W_NS, 'id') ?? end.getAttribute('w:id')) === bookmarkNumericId)
|
|
445
|
+
: undefined;
|
|
446
|
+
removeOrphanedRangeEndpointsForSubtree(story.documentElement, paragraph);
|
|
447
|
+
paragraph.parentNode?.removeChild(paragraph);
|
|
448
|
+
bookmarkStart?.parentNode?.removeChild(bookmarkStart);
|
|
449
|
+
bookmarkEnd?.parentNode?.removeChild(bookmarkEnd);
|
|
450
|
+
this.zip.writeText(partPath, serializeXml(story));
|
|
451
|
+
}
|
|
452
|
+
insertTableRow(params, ctx) {
|
|
453
|
+
const result = insertTableRowImpl(this.documentXml, params, ctx, this.paragraphBookmarkReservation);
|
|
454
|
+
this.dirty = true;
|
|
455
|
+
this.documentViewCache = null;
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
deleteTableRow(params, ctx) {
|
|
459
|
+
const result = deleteTableRowImpl(this.documentXml, params, ctx);
|
|
460
|
+
this.dirty = true;
|
|
461
|
+
this.documentViewCache = null;
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
/** Apply a planned clean column edit to the existing DOM, preserving held element references. */
|
|
465
|
+
insertTableColumn(params, ctx) {
|
|
466
|
+
const result = insertTableColumnImpl(this.documentXml, params, ctx, this.paragraphBookmarkReservation);
|
|
467
|
+
this.dirty = true;
|
|
468
|
+
this.documentViewCache = null;
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
/** Apply a planned clean column deletion to the existing DOM. */
|
|
472
|
+
deleteTableColumn(params, ctx) {
|
|
473
|
+
const result = deleteTableColumnImpl(this.documentXml, params, ctx);
|
|
474
|
+
this.dirty = true;
|
|
475
|
+
this.documentViewCache = null;
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
/** Split one horizontal-spanning physical cell, preserving its authored content on the chosen side. */
|
|
479
|
+
splitTableCell(params, ctx) {
|
|
480
|
+
const result = splitTableCellImpl(this.documentXml, params, ctx, this.paragraphBookmarkReservation);
|
|
481
|
+
this.dirty = true;
|
|
482
|
+
this.documentViewCache = null;
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
/** Remove a physical cell and discard its content while an adjacent sibling absorbs its span. */
|
|
486
|
+
absorbTableCell(params, ctx) {
|
|
487
|
+
const result = absorbTableCellImpl(this.documentXml, params, ctx);
|
|
488
|
+
this.dirty = true;
|
|
489
|
+
this.documentViewCache = null;
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
211
492
|
insertParagraphBookmarks(attachmentId) {
|
|
212
|
-
const res = insertParagraphBookmarks(this.documentXml, attachmentId);
|
|
493
|
+
const res = insertParagraphBookmarks(this.documentXml, attachmentId, this.paragraphBookmarkReservation);
|
|
213
494
|
if (res.indexedParagraphs > 0)
|
|
214
495
|
this.dirty = true;
|
|
215
496
|
return { paragraphCount: res.indexedParagraphs };
|
|
@@ -250,7 +531,7 @@ export class DocxDocument {
|
|
|
250
531
|
}
|
|
251
532
|
async validateAiRevisions(aiAuthor, touched) {
|
|
252
533
|
const stories = [{ part: 'word/document.xml', doc: this.documentXml }];
|
|
253
|
-
for (const partPath of
|
|
534
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
254
535
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
255
536
|
if (!xml)
|
|
256
537
|
continue;
|
|
@@ -278,7 +559,7 @@ export class DocxDocument {
|
|
|
278
559
|
const liveFootnoteRefIds = bodyResult.deletionsAccepted > 0
|
|
279
560
|
? collectLiveFootnoteRefIds(this.documentXml)
|
|
280
561
|
: null;
|
|
281
|
-
for (const partPath of
|
|
562
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
282
563
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
283
564
|
if (!xml)
|
|
284
565
|
continue;
|
|
@@ -316,7 +597,7 @@ export class DocxDocument {
|
|
|
316
597
|
const liveFootnoteRefIds = bodyResult.insertionsRemoved > 0
|
|
317
598
|
? collectLiveFootnoteRefIds(this.documentXml)
|
|
318
599
|
: null;
|
|
319
|
-
for (const partPath of
|
|
600
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
320
601
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
321
602
|
if (!xml)
|
|
322
603
|
continue;
|
|
@@ -350,7 +631,7 @@ export class DocxDocument {
|
|
|
350
631
|
const stories = [
|
|
351
632
|
{ path: null, doc: this.documentXml },
|
|
352
633
|
];
|
|
353
|
-
for (const partPath of
|
|
634
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
354
635
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
355
636
|
if (xml)
|
|
356
637
|
stories.push({ path: partPath, doc: parseXml(xml) });
|
|
@@ -501,6 +782,7 @@ export class DocxDocument {
|
|
|
501
782
|
const { nodes, styles } = buildNodesForDocumentView({
|
|
502
783
|
paragraphs,
|
|
503
784
|
stylesXml: this.stylesXml,
|
|
785
|
+
themeXml: this.themeXml,
|
|
504
786
|
numberingXml: this.numberingXml,
|
|
505
787
|
include_semantic_tags: includeSemanticTags,
|
|
506
788
|
show_formatting: showFormatting,
|
|
@@ -543,8 +825,16 @@ export class DocxDocument {
|
|
|
543
825
|
this.dirty = true;
|
|
544
826
|
this.documentViewCache = null;
|
|
545
827
|
}
|
|
828
|
+
formatTextAtRange(params) {
|
|
829
|
+
const paragraph = findParagraphByBookmarkId(this.documentXml, params.targetParagraphId);
|
|
830
|
+
if (!paragraph)
|
|
831
|
+
throw new Error(`Paragraph not found: ${params.targetParagraphId}`);
|
|
832
|
+
formatParagraphTextRange(paragraph, params.start, params.end, params.format);
|
|
833
|
+
this.dirty = true;
|
|
834
|
+
this.documentViewCache = null;
|
|
835
|
+
}
|
|
546
836
|
insertParagraph(params, ctx) {
|
|
547
|
-
const { positionalAnchorNodeId, relativePosition, newText, newParagraphId: _newParagraphId, styleSourceId } = params;
|
|
837
|
+
const { positionalAnchorNodeId, relativePosition, newText, newParagraphId: _newParagraphId, styleSourceId, runStyleSourceText } = params;
|
|
548
838
|
const anchor = findParagraphByBookmarkId(this.documentXml, positionalAnchorNodeId);
|
|
549
839
|
if (!anchor)
|
|
550
840
|
throw new Error(`Anchor paragraph not found: ${positionalAnchorNodeId}`);
|
|
@@ -713,14 +1003,41 @@ export class DocxDocument {
|
|
|
713
1003
|
return next.nextSibling;
|
|
714
1004
|
return anchorP.nextSibling;
|
|
715
1005
|
}
|
|
716
|
-
// Choose a run in the formatting source to use as formatting template
|
|
1006
|
+
// Choose a run in the formatting source to use as formatting template. A
|
|
1007
|
+
// caller may pin one unique visible source substring; legacy callers retain
|
|
1008
|
+
// the predominant-run behavior.
|
|
717
1009
|
const sourceVisibleRuns = getParagraphRuns(formattingSource);
|
|
718
1010
|
let templateRun = null;
|
|
719
1011
|
let bestLen = -1;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1012
|
+
if (runStyleSourceText !== undefined) {
|
|
1013
|
+
const sourceText = sourceVisibleRuns.map((run) => run.text).join('');
|
|
1014
|
+
const start = sourceText.indexOf(runStyleSourceText);
|
|
1015
|
+
if (!runStyleSourceText || start < 0 || sourceText.indexOf(runStyleSourceText, start + runStyleSourceText.length) >= 0) {
|
|
1016
|
+
throw new Error('runStyleSourceText must identify one non-empty substring in the formatting source paragraph');
|
|
1017
|
+
}
|
|
1018
|
+
let offset = 0;
|
|
1019
|
+
const selectedRuns = [];
|
|
1020
|
+
for (const tr of sourceVisibleRuns) {
|
|
1021
|
+
if (start < offset + tr.text.length && start + runStyleSourceText.length > offset) {
|
|
1022
|
+
selectedRuns.push(tr.r);
|
|
1023
|
+
}
|
|
1024
|
+
offset += tr.text.length;
|
|
1025
|
+
}
|
|
1026
|
+
const runPropertySignatures = new Set(selectedRuns.map((run) => {
|
|
1027
|
+
const rPr = getDirectChildrenByName(run, W.rPr)[0];
|
|
1028
|
+
return rPr?.toString() ?? '';
|
|
1029
|
+
}));
|
|
1030
|
+
if (selectedRuns.length === 0 || runPropertySignatures.size !== 1) {
|
|
1031
|
+
throw new Error('runStyleSourceText must occupy one direct run-formatting class');
|
|
1032
|
+
}
|
|
1033
|
+
templateRun = selectedRuns[0];
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
for (const tr of sourceVisibleRuns) {
|
|
1037
|
+
if (tr.text.length > bestLen) {
|
|
1038
|
+
bestLen = tr.text.length;
|
|
1039
|
+
templateRun = tr.r;
|
|
1040
|
+
}
|
|
724
1041
|
}
|
|
725
1042
|
}
|
|
726
1043
|
if (!templateRun) {
|
|
@@ -746,7 +1063,7 @@ export class DocxDocument {
|
|
|
746
1063
|
newP.appendChild(newRun);
|
|
747
1064
|
}
|
|
748
1065
|
parent.insertBefore(newP, cursor);
|
|
749
|
-
const id = insertSingleParagraphBookmark(doc, newP);
|
|
1066
|
+
const id = insertSingleParagraphBookmark(doc, newP, this.paragraphBookmarkReservation);
|
|
750
1067
|
insertedIds.push(id);
|
|
751
1068
|
if (relativePosition === 'AFTER') {
|
|
752
1069
|
const endEl = nextElementSibling(newP);
|
|
@@ -771,6 +1088,42 @@ export class DocxDocument {
|
|
|
771
1088
|
}
|
|
772
1089
|
return result;
|
|
773
1090
|
}
|
|
1091
|
+
getDirectParagraphNumbering(paragraphId) {
|
|
1092
|
+
return getDirectParagraphNumbering(this.documentXml, paragraphId);
|
|
1093
|
+
}
|
|
1094
|
+
setDirectParagraphNumbering(mutation, ctx) {
|
|
1095
|
+
const result = setDirectParagraphNumbering(this.documentXml, this.numberingXml, mutation, ctx);
|
|
1096
|
+
if (result.changed) {
|
|
1097
|
+
this.dirty = true;
|
|
1098
|
+
this.documentViewCache = null;
|
|
1099
|
+
}
|
|
1100
|
+
return result;
|
|
1101
|
+
}
|
|
1102
|
+
getSections() {
|
|
1103
|
+
return getDocumentSections(this.documentXml);
|
|
1104
|
+
}
|
|
1105
|
+
insertSectionBreak(mutation, ctx) {
|
|
1106
|
+
const result = insertSectionBreak(this.documentXml, mutation, ctx, this.paragraphBookmarkReservation);
|
|
1107
|
+
this.dirty = true;
|
|
1108
|
+
this.documentViewCache = null;
|
|
1109
|
+
return result;
|
|
1110
|
+
}
|
|
1111
|
+
setSectionPageNumberStart(mutation, ctx) {
|
|
1112
|
+
const result = setSectionPageNumberStart(this.documentXml, mutation, ctx);
|
|
1113
|
+
if (result.changed) {
|
|
1114
|
+
this.dirty = true;
|
|
1115
|
+
this.documentViewCache = null;
|
|
1116
|
+
}
|
|
1117
|
+
return result;
|
|
1118
|
+
}
|
|
1119
|
+
updateSectionProperties(mutation, ctx) {
|
|
1120
|
+
const result = updateSectionProperties(this.documentXml, mutation, ctx);
|
|
1121
|
+
if (result.changed) {
|
|
1122
|
+
this.dirty = true;
|
|
1123
|
+
this.documentViewCache = null;
|
|
1124
|
+
}
|
|
1125
|
+
return result;
|
|
1126
|
+
}
|
|
774
1127
|
setTableRowHeight(mutation, ctx) {
|
|
775
1128
|
const result = setTableRowHeight(this.documentXml, mutation, ctx);
|
|
776
1129
|
if (result.affectedRows > 0) {
|
|
@@ -826,6 +1179,8 @@ export class DocxDocument {
|
|
|
826
1179
|
author: params.author,
|
|
827
1180
|
text: params.text,
|
|
828
1181
|
initials: params.initials,
|
|
1182
|
+
body: params.body,
|
|
1183
|
+
date: params.date,
|
|
829
1184
|
}, ctx);
|
|
830
1185
|
this.dirty = true;
|
|
831
1186
|
this.documentViewCache = null;
|
|
@@ -844,13 +1199,15 @@ export class DocxDocument {
|
|
|
844
1199
|
author: params.author,
|
|
845
1200
|
text: params.text,
|
|
846
1201
|
initials: params.initials,
|
|
1202
|
+
body: params.body,
|
|
1203
|
+
date: params.date,
|
|
847
1204
|
}, ctx);
|
|
848
1205
|
this.dirty = true;
|
|
849
1206
|
this.documentViewCache = null;
|
|
850
1207
|
return result;
|
|
851
1208
|
}
|
|
852
1209
|
async getComments() {
|
|
853
|
-
return getCommentsImpl(this.zip, this.documentXml);
|
|
1210
|
+
return getCommentsImpl(this.zip, this.documentXml, this.getStylesModel(), parseThemeXml(this.themeXml));
|
|
854
1211
|
}
|
|
855
1212
|
async getComment(commentId) {
|
|
856
1213
|
return getCommentImpl(this.zip, this.documentXml, commentId);
|
|
@@ -860,13 +1217,113 @@ export class DocxDocument {
|
|
|
860
1217
|
this.dirty = true;
|
|
861
1218
|
this.documentViewCache = null;
|
|
862
1219
|
}
|
|
1220
|
+
async updateCommentBody(params) {
|
|
1221
|
+
await updateCommentBodyImpl(this.zip, params);
|
|
1222
|
+
this.dirty = true;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Convert selected root Word comments to footnotes in document order.
|
|
1226
|
+
* The method preflights the complete selection before mutating the package;
|
|
1227
|
+
* callers that need buffer-level atomicity should invoke it on a freshly
|
|
1228
|
+
* loaded document and publish only the returned serialization.
|
|
1229
|
+
*
|
|
1230
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.13.4.2
|
|
1231
|
+
* @conformance ECMA-376 edition 5, Part 1 § 17.11.14
|
|
1232
|
+
*/
|
|
1233
|
+
async convertCommentsToFootnotes(options = {}) {
|
|
1234
|
+
const colors = [options.presentation?.prefixStyle?.color, options.presentation?.bodyStyle?.color];
|
|
1235
|
+
for (const color of colors) {
|
|
1236
|
+
if (color && !/^[0-9A-Fa-f]{6}$/.test(color)) {
|
|
1237
|
+
throw new Error(`Invalid Word color '${color}'; expected six hexadecimal digits`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const allowedHighlights = new Set([
|
|
1241
|
+
'black', 'blue', 'cyan', 'green', 'magenta', 'red', 'yellow', 'white',
|
|
1242
|
+
'darkBlue', 'darkCyan', 'darkGreen', 'darkMagenta', 'darkRed',
|
|
1243
|
+
'darkYellow', 'darkGray', 'lightGray', 'none',
|
|
1244
|
+
]);
|
|
1245
|
+
const highlights = [options.presentation?.prefixStyle?.highlight, options.presentation?.bodyStyle?.highlight];
|
|
1246
|
+
for (const highlight of highlights) {
|
|
1247
|
+
if (highlight && !allowedHighlights.has(highlight)) {
|
|
1248
|
+
throw new Error(`Invalid Word highlight '${highlight}'`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
// Materialize stable paragraph bookmarks before comment-range extraction;
|
|
1252
|
+
// source DOCX files are not required to carry Safe DOCX anchors already.
|
|
1253
|
+
this.insertParagraphBookmarks('note-conversion');
|
|
1254
|
+
const roots = await this.getComments();
|
|
1255
|
+
const beforeFootnotes = (await this.getFootnotes()).length;
|
|
1256
|
+
const requested = options.commentIds ? new Set(options.commentIds) : null;
|
|
1257
|
+
const selected = requested ? roots.filter((comment) => requested.has(comment.id)) : roots;
|
|
1258
|
+
if (requested) {
|
|
1259
|
+
const found = new Set(selected.map((comment) => comment.id));
|
|
1260
|
+
const missing = [...requested].filter((id) => !found.has(id));
|
|
1261
|
+
if (missing.length)
|
|
1262
|
+
throw new Error(`Root comment IDs not found: ${missing.join(', ')}`);
|
|
1263
|
+
}
|
|
1264
|
+
const prepared = selected.map((comment) => {
|
|
1265
|
+
if (!comment.anchoredParagraphId || !comment.endParagraphId || comment.anchoredParagraphId !== comment.endParagraphId) {
|
|
1266
|
+
throw new Error(`Comment ID ${comment.id} does not have one supported paragraph anchor`);
|
|
1267
|
+
}
|
|
1268
|
+
if (comment.replies.length && !options.flattenThreads) {
|
|
1269
|
+
throw new Error(`Comment ID ${comment.id} has replies; pass flattenThreads to perform a lossy conversion`);
|
|
1270
|
+
}
|
|
1271
|
+
const paragraph = findParagraphByBookmarkId(this.documentXml, comment.anchoredParagraphId);
|
|
1272
|
+
if (!paragraph)
|
|
1273
|
+
throw new Error(`Comment ID ${comment.id} anchor was not found`);
|
|
1274
|
+
if (comment.endTextOffset !== undefined) {
|
|
1275
|
+
const paragraphText = getParagraphText(paragraph);
|
|
1276
|
+
if (comment.endTextOffset > paragraphText.length) {
|
|
1277
|
+
throw new Error(`Comment ID ${comment.id} has an invalid visible-text range endpoint`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const replyText = comment.replies.map((reply) => `\n${reply.author ? `${reply.author}: ` : ''}${reply.text}`).join('');
|
|
1281
|
+
return {
|
|
1282
|
+
comment,
|
|
1283
|
+
paragraph,
|
|
1284
|
+
visibleOffset: comment.endTextOffset,
|
|
1285
|
+
text: `${comment.text}${replyText}`,
|
|
1286
|
+
};
|
|
1287
|
+
});
|
|
1288
|
+
const converted = [];
|
|
1289
|
+
if (prepared.length > 0)
|
|
1290
|
+
await bootstrapFootnoteParts(this.zip);
|
|
1291
|
+
for (const item of prepared) {
|
|
1292
|
+
// Insert first while the preflighted comment markers still identify the
|
|
1293
|
+
// source range. deleteComment removes only comment markers/references,
|
|
1294
|
+
// not the adjacent footnote run introduced here.
|
|
1295
|
+
const result = await addFootnoteImpl(this.documentXml, this.zip, {
|
|
1296
|
+
paragraphEl: item.paragraph,
|
|
1297
|
+
visibleOffset: item.visibleOffset,
|
|
1298
|
+
text: item.text,
|
|
1299
|
+
presentation: options.presentation,
|
|
1300
|
+
});
|
|
1301
|
+
await deleteCommentImpl(this.documentXml, this.zip, { commentId: item.comment.id });
|
|
1302
|
+
converted.push({
|
|
1303
|
+
commentId: item.comment.id,
|
|
1304
|
+
footnoteId: result.noteId,
|
|
1305
|
+
paragraphId: item.comment.anchoredParagraphId,
|
|
1306
|
+
flattenedReplies: item.comment.replies.length,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
await this.refreshFootnotesXml();
|
|
1310
|
+
this.dirty = converted.length > 0;
|
|
1311
|
+
this.documentViewCache = null;
|
|
1312
|
+
return {
|
|
1313
|
+
selected: selected.length,
|
|
1314
|
+
converted,
|
|
1315
|
+
before: { comments: roots.length, footnotes: beforeFootnotes },
|
|
1316
|
+
after: { comments: (await this.getComments()).length, footnotes: (await this.getFootnotes()).length },
|
|
1317
|
+
lossy: converted.some((entry) => entry.flattenedReplies > 0),
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
863
1320
|
// ── Footnote methods ──────────────────────────────────────────────────
|
|
864
1321
|
async refreshFootnotesXml() {
|
|
865
1322
|
const text = await this.zip.readTextOrNull('word/footnotes.xml');
|
|
866
1323
|
this.footnotesXml = text ? parseXml(text) : null;
|
|
867
1324
|
}
|
|
868
1325
|
async getFootnotes() {
|
|
869
|
-
return getFootnotesImpl(this.zip, this.documentXml, this.getStylesModel());
|
|
1326
|
+
return getFootnotesImpl(this.zip, this.documentXml, this.getStylesModel(), parseThemeXml(this.themeXml));
|
|
870
1327
|
}
|
|
871
1328
|
/**
|
|
872
1329
|
* Serialize the document to GitHub-Flavored Markdown. Convenience wrapper that wires the
|
|
@@ -910,7 +1367,7 @@ export class DocxDocument {
|
|
|
910
1367
|
return serializeToPlainText(nodes, footnotes, opts);
|
|
911
1368
|
}
|
|
912
1369
|
async getFootnote(noteId) {
|
|
913
|
-
return getFootnoteImpl(this.zip, this.documentXml, noteId, this.getStylesModel());
|
|
1370
|
+
return getFootnoteImpl(this.zip, this.documentXml, noteId, this.getStylesModel(), parseThemeXml(this.themeXml));
|
|
914
1371
|
}
|
|
915
1372
|
/**
|
|
916
1373
|
* Add a footnote anchored to a paragraph, optionally after specific text.
|
|
@@ -926,7 +1383,9 @@ export class DocxDocument {
|
|
|
926
1383
|
const result = await addFootnoteImpl(this.documentXml, this.zip, {
|
|
927
1384
|
paragraphEl: p,
|
|
928
1385
|
afterText: params.afterText,
|
|
1386
|
+
visibleOffset: params.visibleOffset,
|
|
929
1387
|
text: params.text,
|
|
1388
|
+
presentation: params.presentation,
|
|
930
1389
|
}, ctx);
|
|
931
1390
|
await this.refreshFootnotesXml();
|
|
932
1391
|
this.dirty = true;
|
|
@@ -969,6 +1428,20 @@ export class DocxDocument {
|
|
|
969
1428
|
return null;
|
|
970
1429
|
return parseXml(commentsText);
|
|
971
1430
|
}
|
|
1431
|
+
/** Return a deep clone of footnotes.xml, or null when the part is absent. */
|
|
1432
|
+
async getFootnotesXmlClone() {
|
|
1433
|
+
const footnotesText = await this.zip.readTextOrNull('word/footnotes.xml');
|
|
1434
|
+
if (!footnotesText)
|
|
1435
|
+
return null;
|
|
1436
|
+
return parseXml(footnotesText);
|
|
1437
|
+
}
|
|
1438
|
+
/** Return a source part's Relationships DOM, or null when it has none. */
|
|
1439
|
+
async getPartRelationshipsXmlClone(sourcePartPath) {
|
|
1440
|
+
const relationshipsText = await this.zip.readTextOrNull(relationshipPartPath(sourcePartPath));
|
|
1441
|
+
if (!relationshipsText)
|
|
1442
|
+
return null;
|
|
1443
|
+
return parseXml(relationshipsText);
|
|
1444
|
+
}
|
|
972
1445
|
/**
|
|
973
1446
|
* Serialize the document to a .docx buffer.
|
|
974
1447
|
*
|
|
@@ -980,9 +1453,14 @@ export class DocxDocument {
|
|
|
980
1453
|
* Falls back to full re-serialization (blocksRestored: 0) when no original
|
|
981
1454
|
* text was captured or reconciliation fails.
|
|
982
1455
|
*
|
|
1456
|
+
* With `fileDate`, every ZIP entry is written with that date instead of the
|
|
1457
|
+
* wall clock, so the bytes depend only on the document's content.
|
|
1458
|
+
*
|
|
983
1459
|
* @see https://github.com/UseJunior/safe-docx/issues/408
|
|
1460
|
+
* @see https://github.com/UseJunior/safe-docx/issues/1110
|
|
984
1461
|
*/
|
|
985
1462
|
async toBuffer(opts) {
|
|
1463
|
+
const zipOptions = opts?.fileDate ? { fileDate: opts.fileDate } : undefined;
|
|
986
1464
|
// Always write the latest document.xml when saving.
|
|
987
1465
|
// Important: when cleanBookmarks=true (download), we must NOT mutate session state.
|
|
988
1466
|
const xmlWithBookmarks = serializeXml(this.documentXml);
|
|
@@ -1009,11 +1487,11 @@ export class DocxDocument {
|
|
|
1009
1487
|
// Temporarily swap document.xml in the zip for output, then restore.
|
|
1010
1488
|
maybeCaptureEmittedDocumentXml(cleanedXml);
|
|
1011
1489
|
this.zip.writeText('word/document.xml', cleanedXml);
|
|
1012
|
-
const buffer = await this.zip.toBuffer();
|
|
1490
|
+
const buffer = await this.zip.toBuffer(zipOptions);
|
|
1013
1491
|
this.zip.writeText('word/document.xml', xmlWithBookmarks);
|
|
1014
1492
|
return { buffer, bookmarksRemoved, blocksRestored };
|
|
1015
1493
|
}
|
|
1016
|
-
const buffer = await this.zip.toBuffer();
|
|
1494
|
+
const buffer = await this.zip.toBuffer(zipOptions);
|
|
1017
1495
|
return { buffer, bookmarksRemoved: 0, blocksRestored: 0 };
|
|
1018
1496
|
}
|
|
1019
1497
|
/**
|