@usejunior/docx-core 0.20.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/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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.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 +9 -3
- package/dist/integration/libreoffice-oracle.js.map +1 -1
- package/dist/primitives/accept_changes.d.ts +3 -4
- package/dist/primitives/accept_changes.d.ts.map +1 -1
- package/dist/primitives/accept_changes.js +165 -79
- package/dist/primitives/accept_changes.js.map +1 -1
- package/dist/primitives/bookmarks.d.ts +7 -2
- package/dist/primitives/bookmarks.d.ts.map +1 -1
- package/dist/primitives/bookmarks.js +30 -6
- package/dist/primitives/bookmarks.js.map +1 -1
- package/dist/primitives/comments.d.ts +73 -4
- package/dist/primitives/comments.d.ts.map +1 -1
- package/dist/primitives/comments.js +266 -165
- 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 +98 -1
- package/dist/primitives/document.d.ts.map +1 -1
- package/dist/primitives/document.js +315 -22
- package/dist/primitives/document.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/footnotes.d.ts +36 -0
- package/dist/primitives/footnotes.d.ts.map +1 -1
- package/dist/primitives/footnotes.js +173 -75
- package/dist/primitives/footnotes.js.map +1 -1
- package/dist/primitives/index.d.ts +7 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +7 -1
- package/dist/primitives/index.js.map +1 -1
- 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_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 +3 -4
- package/dist/primitives/reject_changes.d.ts.map +1 -1
- package/dist/primitives/reject_changes.js +166 -79
- 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/sections.d.ts +2 -1
- package/dist/primitives/sections.d.ts.map +1 -1
- package/dist/primitives/sections.js +2 -2
- package/dist/primitives/sections.js.map +1 -1
- package/dist/primitives/styles.d.ts +1 -0
- package/dist/primitives/styles.d.ts.map +1 -1
- package/dist/primitives/styles.js +1 -0
- package/dist/primitives/styles.js.map +1 -1
- 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 +40 -1
- package/dist/primitives/text.d.ts.map +1 -1
- package/dist/primitives/text.js +309 -129
- package/dist/primitives/text.js.map +1 -1
- package/dist/primitives/track-changes-emitter.d.ts +16 -2
- package/dist/primitives/track-changes-emitter.d.ts.map +1 -1
- package/dist/primitives/track-changes-emitter.js +38 -13
- package/dist/primitives/track-changes-emitter.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 +10 -4
- package/dist/shared/docx/DocxArchive.js.map +1 -1
- package/dist/shared/field-structure.d.ts +3 -0
- package/dist/shared/field-structure.d.ts.map +1 -1
- package/dist/shared/field-structure.js +7 -5
- package/dist/shared/field-structure.js.map +1 -1
- package/package.json +3 -4
|
@@ -2,9 +2,10 @@ 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';
|
|
@@ -15,7 +16,7 @@ import { parseNumberingXml } from './numbering.js';
|
|
|
15
16
|
import { getDirectParagraphNumbering, setDirectParagraphNumbering, } from './paragraph_numbering.js';
|
|
16
17
|
import { getDocumentSections, insertSectionBreak, setSectionPageNumberStart, updateSectionProperties, } from './sections.js';
|
|
17
18
|
import { findUniqueSubstringMatch } from './matching.js';
|
|
18
|
-
import { parseDocumentRels } from './relationships.js';
|
|
19
|
+
import { parseDocumentRels, relationshipPartPath } from './relationships.js';
|
|
19
20
|
import { setParagraphSpacing, setTableCellPadding, setTableRowHeight, } from './layout.js';
|
|
20
21
|
import { extractTables, } from './tables.js';
|
|
21
22
|
import { mergeRuns } from './merge_runs.js';
|
|
@@ -24,13 +25,16 @@ import { simplifyRedlines } from './simplify_redlines.js';
|
|
|
24
25
|
import { preventDoubleElevation } from './prevent_double_elevation.js';
|
|
25
26
|
import { validateDocument } from './validate_document.js';
|
|
26
27
|
import { validateAiRevisions as validateAiRevisionsImpl, } from './validate_ai_revisions.js';
|
|
27
|
-
import {
|
|
28
|
+
import { enumerateSelectedHeaderFooterPartPaths, enumerateSelectedRevisionStoryPartPaths, } from './revision-parts.js';
|
|
28
29
|
import { acceptChanges as acceptChangesImpl } from './accept_changes.js';
|
|
29
30
|
import { rejectChanges as rejectChangesImpl } from './reject_changes.js';
|
|
30
31
|
import { collectRevisionElements, resolveSelectedIds, detectAmbiguousOverlaps, selectedIdFilter, AmbiguousRevisionOverlapError, } from './accept_ai_edits.js';
|
|
31
32
|
import { revisionElementId } from './accept_changes.js';
|
|
32
33
|
import { TRACKED_CHANGE_ELEMENT_NAME_SET } from './revision-vocabulary.js';
|
|
33
|
-
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';
|
|
34
38
|
import { bootstrapFootnoteParts, getFootnotes as getFootnotesImpl, getFootnote as getFootnoteImpl, addFootnote as addFootnoteImpl, updateFootnoteText as updateFootnoteTextImpl, deleteFootnote as deleteFootnoteImpl, } from './footnotes.js';
|
|
35
39
|
function emptyAcceptChangesResult() {
|
|
36
40
|
return {
|
|
@@ -154,6 +158,74 @@ function nextElementSibling(node) {
|
|
|
154
158
|
}
|
|
155
159
|
return null;
|
|
156
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
|
+
}
|
|
157
229
|
function collectNamedBookmarkIds(doc) {
|
|
158
230
|
const ids = new Set();
|
|
159
231
|
const starts = doc.getElementsByTagNameNS(OOXML.W_NS, W.bookmarkStart);
|
|
@@ -176,6 +248,7 @@ export class DocxDocument {
|
|
|
176
248
|
relsMap;
|
|
177
249
|
dirty;
|
|
178
250
|
documentViewCache;
|
|
251
|
+
paragraphBookmarkReservation;
|
|
179
252
|
/**
|
|
180
253
|
* Raw document.xml text as loaded, before normalize()/edits mutate the DOM.
|
|
181
254
|
* Reference for minimal re-serialization in toBuffer(); null for instances
|
|
@@ -192,12 +265,16 @@ export class DocxDocument {
|
|
|
192
265
|
this.relsMap = relsMap;
|
|
193
266
|
this.dirty = false;
|
|
194
267
|
this.documentViewCache = null;
|
|
268
|
+
this.paragraphBookmarkReservation = undefined;
|
|
195
269
|
this.originalDocumentXmlText = originalDocumentXmlText;
|
|
196
270
|
}
|
|
197
271
|
static async load(buffer) {
|
|
198
272
|
const zip = await DocxZip.load(buffer);
|
|
199
273
|
const xml = await zip.readText('word/document.xml');
|
|
200
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);
|
|
201
278
|
// Optional parts used for fidelity: list labels + style fingerprints.
|
|
202
279
|
const stylesText = await zip.readTextOrNull('word/styles.xml');
|
|
203
280
|
const themeText = await zip.readTextOrNull('word/theme/theme1.xml');
|
|
@@ -211,7 +288,15 @@ export class DocxDocument {
|
|
|
211
288
|
// Load document relationships for hyperlink resolution.
|
|
212
289
|
const relsText = await zip.readTextOrNull('word/_rels/document.xml.rels');
|
|
213
290
|
const relsMap = relsText ? parseDocumentRels(parseXml(relsText)) : new Map();
|
|
214
|
-
|
|
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;
|
|
215
300
|
}
|
|
216
301
|
getParagraphs() {
|
|
217
302
|
const body = this.documentXml.getElementsByTagNameNS(OOXML.W_NS, W.body).item(0);
|
|
@@ -228,8 +313,184 @@ export class DocxDocument {
|
|
|
228
313
|
return null;
|
|
229
314
|
return getParagraphText(p);
|
|
230
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
|
+
}
|
|
231
492
|
insertParagraphBookmarks(attachmentId) {
|
|
232
|
-
const res = insertParagraphBookmarks(this.documentXml, attachmentId);
|
|
493
|
+
const res = insertParagraphBookmarks(this.documentXml, attachmentId, this.paragraphBookmarkReservation);
|
|
233
494
|
if (res.indexedParagraphs > 0)
|
|
234
495
|
this.dirty = true;
|
|
235
496
|
return { paragraphCount: res.indexedParagraphs };
|
|
@@ -270,7 +531,7 @@ export class DocxDocument {
|
|
|
270
531
|
}
|
|
271
532
|
async validateAiRevisions(aiAuthor, touched) {
|
|
272
533
|
const stories = [{ part: 'word/document.xml', doc: this.documentXml }];
|
|
273
|
-
for (const partPath of
|
|
534
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
274
535
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
275
536
|
if (!xml)
|
|
276
537
|
continue;
|
|
@@ -298,7 +559,7 @@ export class DocxDocument {
|
|
|
298
559
|
const liveFootnoteRefIds = bodyResult.deletionsAccepted > 0
|
|
299
560
|
? collectLiveFootnoteRefIds(this.documentXml)
|
|
300
561
|
: null;
|
|
301
|
-
for (const partPath of
|
|
562
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
302
563
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
303
564
|
if (!xml)
|
|
304
565
|
continue;
|
|
@@ -336,7 +597,7 @@ export class DocxDocument {
|
|
|
336
597
|
const liveFootnoteRefIds = bodyResult.insertionsRemoved > 0
|
|
337
598
|
? collectLiveFootnoteRefIds(this.documentXml)
|
|
338
599
|
: null;
|
|
339
|
-
for (const partPath of
|
|
600
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
340
601
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
341
602
|
if (!xml)
|
|
342
603
|
continue;
|
|
@@ -370,7 +631,7 @@ export class DocxDocument {
|
|
|
370
631
|
const stories = [
|
|
371
632
|
{ path: null, doc: this.documentXml },
|
|
372
633
|
];
|
|
373
|
-
for (const partPath of
|
|
634
|
+
for (const partPath of await enumerateSelectedRevisionStoryPartPaths(this.zip)) {
|
|
374
635
|
const xml = await this.zip.readTextOrNull(partPath);
|
|
375
636
|
if (xml)
|
|
376
637
|
stories.push({ path: partPath, doc: parseXml(xml) });
|
|
@@ -564,6 +825,14 @@ export class DocxDocument {
|
|
|
564
825
|
this.dirty = true;
|
|
565
826
|
this.documentViewCache = null;
|
|
566
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
|
+
}
|
|
567
836
|
insertParagraph(params, ctx) {
|
|
568
837
|
const { positionalAnchorNodeId, relativePosition, newText, newParagraphId: _newParagraphId, styleSourceId, runStyleSourceText } = params;
|
|
569
838
|
const anchor = findParagraphByBookmarkId(this.documentXml, positionalAnchorNodeId);
|
|
@@ -794,7 +1063,7 @@ export class DocxDocument {
|
|
|
794
1063
|
newP.appendChild(newRun);
|
|
795
1064
|
}
|
|
796
1065
|
parent.insertBefore(newP, cursor);
|
|
797
|
-
const id = insertSingleParagraphBookmark(doc, newP);
|
|
1066
|
+
const id = insertSingleParagraphBookmark(doc, newP, this.paragraphBookmarkReservation);
|
|
798
1067
|
insertedIds.push(id);
|
|
799
1068
|
if (relativePosition === 'AFTER') {
|
|
800
1069
|
const endEl = nextElementSibling(newP);
|
|
@@ -834,7 +1103,7 @@ export class DocxDocument {
|
|
|
834
1103
|
return getDocumentSections(this.documentXml);
|
|
835
1104
|
}
|
|
836
1105
|
insertSectionBreak(mutation, ctx) {
|
|
837
|
-
const result = insertSectionBreak(this.documentXml, mutation, ctx);
|
|
1106
|
+
const result = insertSectionBreak(this.documentXml, mutation, ctx, this.paragraphBookmarkReservation);
|
|
838
1107
|
this.dirty = true;
|
|
839
1108
|
this.documentViewCache = null;
|
|
840
1109
|
return result;
|
|
@@ -910,6 +1179,8 @@ export class DocxDocument {
|
|
|
910
1179
|
author: params.author,
|
|
911
1180
|
text: params.text,
|
|
912
1181
|
initials: params.initials,
|
|
1182
|
+
body: params.body,
|
|
1183
|
+
date: params.date,
|
|
913
1184
|
}, ctx);
|
|
914
1185
|
this.dirty = true;
|
|
915
1186
|
this.documentViewCache = null;
|
|
@@ -928,13 +1199,15 @@ export class DocxDocument {
|
|
|
928
1199
|
author: params.author,
|
|
929
1200
|
text: params.text,
|
|
930
1201
|
initials: params.initials,
|
|
1202
|
+
body: params.body,
|
|
1203
|
+
date: params.date,
|
|
931
1204
|
}, ctx);
|
|
932
1205
|
this.dirty = true;
|
|
933
1206
|
this.documentViewCache = null;
|
|
934
1207
|
return result;
|
|
935
1208
|
}
|
|
936
1209
|
async getComments() {
|
|
937
|
-
return getCommentsImpl(this.zip, this.documentXml);
|
|
1210
|
+
return getCommentsImpl(this.zip, this.documentXml, this.getStylesModel(), parseThemeXml(this.themeXml));
|
|
938
1211
|
}
|
|
939
1212
|
async getComment(commentId) {
|
|
940
1213
|
return getCommentImpl(this.zip, this.documentXml, commentId);
|
|
@@ -944,6 +1217,10 @@ export class DocxDocument {
|
|
|
944
1217
|
this.dirty = true;
|
|
945
1218
|
this.documentViewCache = null;
|
|
946
1219
|
}
|
|
1220
|
+
async updateCommentBody(params) {
|
|
1221
|
+
await updateCommentBodyImpl(this.zip, params);
|
|
1222
|
+
this.dirty = true;
|
|
1223
|
+
}
|
|
947
1224
|
/**
|
|
948
1225
|
* Convert selected root Word comments to footnotes in document order.
|
|
949
1226
|
* The method preflights the complete selection before mutating the package;
|
|
@@ -994,21 +1271,17 @@ export class DocxDocument {
|
|
|
994
1271
|
const paragraph = findParagraphByBookmarkId(this.documentXml, comment.anchoredParagraphId);
|
|
995
1272
|
if (!paragraph)
|
|
996
1273
|
throw new Error(`Comment ID ${comment.id} anchor was not found`);
|
|
997
|
-
let afterText;
|
|
998
1274
|
if (comment.endTextOffset !== undefined) {
|
|
999
1275
|
const paragraphText = getParagraphText(paragraph);
|
|
1000
1276
|
if (comment.endTextOffset > paragraphText.length) {
|
|
1001
1277
|
throw new Error(`Comment ID ${comment.id} has an invalid visible-text range endpoint`);
|
|
1002
1278
|
}
|
|
1003
|
-
afterText = paragraphText.slice(0, comment.endTextOffset);
|
|
1004
|
-
if (!afterText)
|
|
1005
|
-
afterText = undefined;
|
|
1006
1279
|
}
|
|
1007
1280
|
const replyText = comment.replies.map((reply) => `\n${reply.author ? `${reply.author}: ` : ''}${reply.text}`).join('');
|
|
1008
1281
|
return {
|
|
1009
1282
|
comment,
|
|
1010
1283
|
paragraph,
|
|
1011
|
-
|
|
1284
|
+
visibleOffset: comment.endTextOffset,
|
|
1012
1285
|
text: `${comment.text}${replyText}`,
|
|
1013
1286
|
};
|
|
1014
1287
|
});
|
|
@@ -1021,7 +1294,7 @@ export class DocxDocument {
|
|
|
1021
1294
|
// not the adjacent footnote run introduced here.
|
|
1022
1295
|
const result = await addFootnoteImpl(this.documentXml, this.zip, {
|
|
1023
1296
|
paragraphEl: item.paragraph,
|
|
1024
|
-
|
|
1297
|
+
visibleOffset: item.visibleOffset,
|
|
1025
1298
|
text: item.text,
|
|
1026
1299
|
presentation: options.presentation,
|
|
1027
1300
|
});
|
|
@@ -1110,6 +1383,7 @@ export class DocxDocument {
|
|
|
1110
1383
|
const result = await addFootnoteImpl(this.documentXml, this.zip, {
|
|
1111
1384
|
paragraphEl: p,
|
|
1112
1385
|
afterText: params.afterText,
|
|
1386
|
+
visibleOffset: params.visibleOffset,
|
|
1113
1387
|
text: params.text,
|
|
1114
1388
|
presentation: params.presentation,
|
|
1115
1389
|
}, ctx);
|
|
@@ -1154,6 +1428,20 @@ export class DocxDocument {
|
|
|
1154
1428
|
return null;
|
|
1155
1429
|
return parseXml(commentsText);
|
|
1156
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
|
+
}
|
|
1157
1445
|
/**
|
|
1158
1446
|
* Serialize the document to a .docx buffer.
|
|
1159
1447
|
*
|
|
@@ -1165,9 +1453,14 @@ export class DocxDocument {
|
|
|
1165
1453
|
* Falls back to full re-serialization (blocksRestored: 0) when no original
|
|
1166
1454
|
* text was captured or reconciliation fails.
|
|
1167
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
|
+
*
|
|
1168
1459
|
* @see https://github.com/UseJunior/safe-docx/issues/408
|
|
1460
|
+
* @see https://github.com/UseJunior/safe-docx/issues/1110
|
|
1169
1461
|
*/
|
|
1170
1462
|
async toBuffer(opts) {
|
|
1463
|
+
const zipOptions = opts?.fileDate ? { fileDate: opts.fileDate } : undefined;
|
|
1171
1464
|
// Always write the latest document.xml when saving.
|
|
1172
1465
|
// Important: when cleanBookmarks=true (download), we must NOT mutate session state.
|
|
1173
1466
|
const xmlWithBookmarks = serializeXml(this.documentXml);
|
|
@@ -1194,11 +1487,11 @@ export class DocxDocument {
|
|
|
1194
1487
|
// Temporarily swap document.xml in the zip for output, then restore.
|
|
1195
1488
|
maybeCaptureEmittedDocumentXml(cleanedXml);
|
|
1196
1489
|
this.zip.writeText('word/document.xml', cleanedXml);
|
|
1197
|
-
const buffer = await this.zip.toBuffer();
|
|
1490
|
+
const buffer = await this.zip.toBuffer(zipOptions);
|
|
1198
1491
|
this.zip.writeText('word/document.xml', xmlWithBookmarks);
|
|
1199
1492
|
return { buffer, bookmarksRemoved, blocksRestored };
|
|
1200
1493
|
}
|
|
1201
|
-
const buffer = await this.zip.toBuffer();
|
|
1494
|
+
const buffer = await this.zip.toBuffer(zipOptions);
|
|
1202
1495
|
return { buffer, bookmarksRemoved: 0, blocksRestored: 0 };
|
|
1203
1496
|
}
|
|
1204
1497
|
/**
|