@ansonlai/docx-redline-js 0.5.2 → 0.5.3

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.
@@ -6,15 +6,20 @@ import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
6
6
  import { getDefaultAuthor } from '../adapters/config.js';
7
7
  import { containsTrackedChanges, getTrackedChangeAuthors } from '../core/word-xml.js';
8
8
  import { NS_W } from '../core/types.js';
9
- import { extractCanonicalParagraphText } from '../core/paragraph-text.js';
9
+ import { extractCanonicalParagraphText } from '../core/paragraph-text.js';
10
+ import {
11
+ getParagraphRestorationRefusal,
12
+ inspectForeignDeletedParagraphTarget
13
+ } from '../core/paragraph-revision-safety.js';
10
14
  import {
11
15
  buildParagraphMetadataIndex,
12
16
  createParagraphFingerprint,
13
17
  findContainingWordElement,
14
18
  getDocumentParagraphNodes,
15
19
  getParagraphId,
16
- normalizeWhitespaceForTargeting,
17
- resolveTargetParagraph,
20
+ normalizeWhitespaceForTargeting,
21
+ resolveParagraphRangeByRefs,
22
+ resolveTargetParagraph,
18
23
  validateParagraphBoundaryMutation
19
24
  } from '../core/paragraph-targeting.js';
20
25
  import { createParagraphTextIndex, resolveTextInParagraphIndex } from './comment-locator.js';
@@ -219,10 +224,69 @@ export function preflightOperations(documentXml, operations, author, options = {
219
224
  || options.existingRevisions
220
225
  || 'merge-same-author';
221
226
  const deletingWholeParagraph = operation.operationKind === 'redline' && operation.modified === '';
222
- const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
223
-
224
- let error = null;
225
- if (!anchorFound) {
227
+ const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
228
+
229
+ let error = null;
230
+ if (operation.operationKind === 'restore') {
231
+ let restorationParagraphs = operation.targetEndRef
232
+ ? resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
233
+ opType: 'restore',
234
+ targetRefSnapshot: options.targetRefSnapshot || null,
235
+ onInfo: options.onInfo,
236
+ onWarn: options.onWarn
237
+ })
238
+ : [paragraph];
239
+ if (!operation.targetEndRef && operation.targetEndDescriptor) {
240
+ const endResolved = resolveTargetParagraph(xmlDoc, {
241
+ targetText: operation.targetEndDescriptor.text,
242
+ targetRef: operation.targetEndDescriptor.index,
243
+ targetDescriptor: operation.targetEndDescriptor,
244
+ opType: 'restore',
245
+ strictAmbiguity: strictTargets,
246
+ paragraphMetadataIndex: currentMetadataIndex,
247
+ metadataIndices,
248
+ onInfo: options.onInfo,
249
+ onWarn: options.onWarn
250
+ });
251
+ const allParagraphs = getDocumentParagraphNodes(xmlDoc);
252
+ const startIndex = allParagraphs.indexOf(paragraph);
253
+ const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
254
+ restorationParagraphs = startIndex >= 0 && endIndex >= startIndex
255
+ ? allParagraphs.slice(startIndex, endIndex + 1)
256
+ : null;
257
+ }
258
+ const replacements = Array.isArray(operation.modified) ? operation.modified : [operation.modified];
259
+ if (!restorationParagraphs || restorationParagraphs[0] !== paragraph || replacements.length !== restorationParagraphs.length) {
260
+ error = {
261
+ code: 'RESTORATION_COUNT_MISMATCH',
262
+ message: 'Restoration requires one replacement for every paragraph in a contiguous resolved range.'
263
+ };
264
+ } else {
265
+ for (const restorationParagraph of restorationParagraphs) {
266
+ const structuralRefusal = getParagraphRestorationRefusal(restorationParagraph);
267
+ if (structuralRefusal?.code === 'UNSUPPORTED_MOVE_REVISION') {
268
+ error = structuralRefusal;
269
+ break;
270
+ }
271
+ const state = inspectForeignDeletedParagraphTarget(restorationParagraph, authorUsed);
272
+ const refusal = state.matches
273
+ ? (structuralRefusal || getParagraphRestorationRefusal(restorationParagraph))
274
+ : null;
275
+ if (!state.matches) {
276
+ error = {
277
+ code: 'RESTORATION_STATE_REQUIRED',
278
+ message: 'Explicit restoration requires a foreign paragraph-mark deletion with every pre-existing content node deleted.',
279
+ ...(state.ownerAuthor ? { ownerAuthor: state.ownerAuthor } : {})
280
+ };
281
+ break;
282
+ }
283
+ if (refusal) {
284
+ error = refusal;
285
+ break;
286
+ }
287
+ }
288
+ }
289
+ } else if (!anchorFound) {
226
290
  error = anchorResolution?.error || {
227
291
  code: 'ANCHOR_NOT_FOUND',
228
292
  message: `Anchor text was not found in target paragraph: "${anchor}".`
@@ -354,7 +418,7 @@ export function preflightOperations(documentXml, operations, author, options = {
354
418
  }
355
419
 
356
420
  for (const [targetIndex, targetResults] of byTarget) {
357
- const redlines = targetResults.filter(result => result.operationType === 'redline');
421
+ const redlines = targetResults.filter(result => ['redline', 'restore'].includes(result.operationType));
358
422
  const highlights = targetResults.filter(result => result.operationType === 'highlight');
359
423
  const target = targetResults[0].resolvedTarget;
360
424
  if (redlines.length > 1) {
@@ -43,6 +43,13 @@ export interface RedlineDocumentOperation extends DocumentOperationBase {
43
43
  targetEndRef?: number | string | null;
44
44
  }
45
45
 
46
+ export interface RestoreDocumentOperation extends DocumentOperationBase {
47
+ type: 'restore';
48
+ modified: string | string[];
49
+ targetEnd?: ParagraphTargetDescriptor;
50
+ targetEndRef?: number | string | null;
51
+ }
52
+
46
53
  export interface DeleteDocumentOperation extends DocumentOperationBase {
47
54
  type: 'delete';
48
55
  modified?: '';
@@ -103,6 +110,7 @@ export interface ParagraphFormatDocumentOperation extends DocumentOperationBase
103
110
 
104
111
  export type DocumentOperation =
105
112
  | RedlineDocumentOperation
113
+ | RestoreDocumentOperation
106
114
  | DeleteDocumentOperation
107
115
  | CommentDocumentOperation
108
116
  | CommentReplyDocumentOperation