@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.
- package/AGENTS.md +24 -3
- package/CHANGELOG.md +13 -1
- package/README.md +16 -3
- package/core/paragraph-revision-safety.js +213 -0
- package/core/paragraph-targeting.js +19 -0
- package/core/redline-validation.js +13 -0
- package/dist/docx-redline-js.esm.js +333 -89
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +79 -79
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +282 -1
- package/docs/schemas/document-operations.schema.json +3 -0
- package/engine/oxml-engine.js +24 -3
- package/engine/surgical-diff-application.js +35 -1
- package/engine/surgical-mode.js +55 -1
- package/index.d.ts +12 -0
- package/package.json +1 -1
- package/services/document-operation-applier.js +24 -5
- package/services/document-operation-contract.js +37 -2
- package/services/document-operation-mutations.js +509 -29
- package/services/operation-preflight.js +72 -8
- package/services/standalone-operation-runner.d.ts +8 -0
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
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
|