@ansonlai/docx-redline-js 0.5.2 → 0.5.4
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 +67 -12
- package/ARCHITECTURE.md +38 -3
- package/CHANGELOG.md +19 -1
- package/README.md +52 -12
- package/core/paragraph-revision-safety.js +215 -0
- package/core/paragraph-targeting.js +19 -0
- package/core/redline-validation.js +20 -4
- package/core/revision-cloning.js +21 -0
- package/core/validation-delta.js +23 -0
- package/dist/docx-redline-js.esm.js +493 -99
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +84 -84
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +861 -37
- package/docs/schemas/document-operations.schema.json +14 -1
- package/engine/oxml-engine.js +24 -3
- package/engine/surgical-diff-application.js +35 -1
- package/engine/surgical-mode.js +203 -4
- package/engine/surgical-run-splitting.js +19 -7
- package/engine/surgical-spans.js +2 -1
- package/index.d.ts +12 -0
- package/node/cli.js +128 -13
- package/node/docx-document.js +17 -14
- package/package.json +1 -1
- package/pipeline/diff-engine.js +15 -0
- package/services/document-operation-applier.js +73 -9
- package/services/document-operation-contract.js +77 -2
- package/services/document-operation-mutations.js +846 -49
- package/services/operation-preflight.js +72 -8
- package/services/standalone-operation-runner.d.ts +26 -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 =>
|
|
421
|
+
const redlines = targetResults.filter(result => ['redline', 'restore', 'rejected-insert'].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) {
|
|
@@ -23,6 +23,12 @@ export interface InsertionAffinity {
|
|
|
23
23
|
comment?: 'inside' | 'outside';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface RejectedTextInsertionAnchor {
|
|
27
|
+
exactText: string;
|
|
28
|
+
occurrence?: number;
|
|
29
|
+
offset: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
export interface DocumentOperationBase {
|
|
27
33
|
operationId?: string;
|
|
28
34
|
captureKey?: string;
|
|
@@ -41,6 +47,15 @@ export interface RedlineDocumentOperation extends DocumentOperationBase {
|
|
|
41
47
|
structuredContent?: boolean;
|
|
42
48
|
targetEnd?: ParagraphTargetDescriptor;
|
|
43
49
|
targetEndRef?: number | string | null;
|
|
50
|
+
/** Required when type is insert and target.revisionView is rejected. */
|
|
51
|
+
anchor?: RejectedTextInsertionAnchor;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RestoreDocumentOperation extends DocumentOperationBase {
|
|
55
|
+
type: 'restore';
|
|
56
|
+
modified: string | string[];
|
|
57
|
+
targetEnd?: ParagraphTargetDescriptor;
|
|
58
|
+
targetEndRef?: number | string | null;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
export interface DeleteDocumentOperation extends DocumentOperationBase {
|
|
@@ -103,6 +118,7 @@ export interface ParagraphFormatDocumentOperation extends DocumentOperationBase
|
|
|
103
118
|
|
|
104
119
|
export type DocumentOperation =
|
|
105
120
|
| RedlineDocumentOperation
|
|
121
|
+
| RestoreDocumentOperation
|
|
106
122
|
| DeleteDocumentOperation
|
|
107
123
|
| CommentDocumentOperation
|
|
108
124
|
| CommentReplyDocumentOperation
|
|
@@ -116,6 +132,16 @@ export interface ResolvedDocumentTarget {
|
|
|
116
132
|
text: string;
|
|
117
133
|
fingerprint?: string;
|
|
118
134
|
inTable?: boolean;
|
|
135
|
+
targetTextMatch?: {
|
|
136
|
+
mode: 'exact' | 'space_equivalent' | 'normalized';
|
|
137
|
+
differences?: Array<{
|
|
138
|
+
offset: number;
|
|
139
|
+
sourceCodePoint: string;
|
|
140
|
+
requestedCodePoint: string;
|
|
141
|
+
}>;
|
|
142
|
+
sourceExcerpt?: string;
|
|
143
|
+
requestedExcerpt?: string;
|
|
144
|
+
};
|
|
119
145
|
}
|
|
120
146
|
|
|
121
147
|
export interface ResolvedCommentAnchor {
|