@ansonlai/docx-redline-js 0.8.0 → 0.8.1
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/CHANGELOG.md +539 -486
- package/README.md +331 -309
- package/dist/docx-redline-js.esm.js +1083 -163
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +107 -107
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +1083 -163
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +1083 -163
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_KNOWLEDGE_BASE.md +69 -8
- package/docs/schemas/document-operations.schema.json +29 -0
- package/document/docx-document.js +416 -22
- package/engine/reconstruction-writer.js +18 -0
- package/index.d.ts +598 -575
- package/index.js +349 -348
- package/node/cli-help.js +5 -5
- package/node/cli.js +760 -754
- package/node/index.d.ts +1 -1
- package/package.json +2 -1
- package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
- package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
- package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
- package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
- package/scripts/generate-word-package-integrity-cases.mjs +141 -0
- package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
- package/scripts/word-com-package-integrity.ps1 +98 -0
- package/services/batch-operation-orchestrator.js +262 -259
- package/services/comment-replies.js +96 -95
- package/services/comment-thread-parts.js +280 -0
- package/services/document-inspection.js +366 -365
- package/services/document-operation-applier.js +230 -202
- package/services/document-operation-contract.js +17 -2
- package/services/error-recovery.js +6 -0
- package/services/headers-footers.js +206 -0
- package/services/operation-batch-compiler.js +1 -0
- package/services/operation-preflight.js +231 -218
- package/services/package-parts.js +80 -0
- package/services/standalone-docx-plumbing.js +631 -569
- package/services/standalone-operation-runner.d.ts +19 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { getDefaultAuthor } from '../adapters/config.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
3
|
normalizeDocumentOperation,
|
|
4
4
|
resolveDocumentOperationAuthor,
|
|
5
5
|
validateDocumentOperation
|
|
6
6
|
} from './document-operation-contract.js';
|
|
7
|
-
import { DocumentOperationSession } from './document-operation-session.js';
|
|
8
|
-
import { compileOperationBatch } from './operation-batch-compiler.js';
|
|
7
|
+
import { DocumentOperationSession } from './document-operation-session.js';
|
|
8
|
+
import { compileOperationBatch } from './operation-batch-compiler.js';
|
|
9
9
|
import {
|
|
10
10
|
validateRevisionToken,
|
|
11
11
|
computeDocumentPartsRevisionToken,
|
|
@@ -15,12 +15,13 @@ import {
|
|
|
15
15
|
applyCommentToParagraphByExactText,
|
|
16
16
|
applyFormattingToParagraphByExactText,
|
|
17
17
|
applyHighlightToParagraphByExactText,
|
|
18
|
-
applyParagraphFormatToParagraphByExactText,
|
|
19
|
-
insertIntoRejectedDeletedText,
|
|
20
|
-
restoreDeletedParagraphByExactText,
|
|
21
|
-
applyToParagraphByExactText
|
|
18
|
+
applyParagraphFormatToParagraphByExactText,
|
|
19
|
+
insertIntoRejectedDeletedText,
|
|
20
|
+
restoreDeletedParagraphByExactText,
|
|
21
|
+
applyToParagraphByExactText
|
|
22
22
|
} from './document-operation-mutations.js';
|
|
23
23
|
import { applyCommentReplyToParts } from './comment-replies.js';
|
|
24
|
+
import { anchorReplyInDocument, applyThreadResolutionToParts } from './comment-thread-parts.js';
|
|
24
25
|
import {
|
|
25
26
|
deriveCapturedEntity,
|
|
26
27
|
invalidateAffectedCaptures
|
|
@@ -28,27 +29,27 @@ import {
|
|
|
28
29
|
import {
|
|
29
30
|
createEmptyReceipt,
|
|
30
31
|
reconcileReceiptsAgainstOutput
|
|
31
|
-
} from './receipt-collector.js';
|
|
32
|
-
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
33
|
-
import { subtractValidationIssueMultiset, validationErrors } from '../core/validation-delta.js';
|
|
34
|
-
import { normalizeErrorWithRecovery } from './error-recovery.js';
|
|
35
|
-
import { extractCanonicalParagraphText } from '../core/paragraph-text.js';
|
|
36
|
-
import { buildLocalizedReplacementChange } from './localized-replacement-compiler.js';
|
|
37
|
-
|
|
38
|
-
export function normalizeOperationError(error, context = {}) {
|
|
39
|
-
return normalizeErrorWithRecovery(error, context);
|
|
40
|
-
}
|
|
32
|
+
} from './receipt-collector.js';
|
|
33
|
+
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
34
|
+
import { subtractValidationIssueMultiset, validationErrors } from '../core/validation-delta.js';
|
|
35
|
+
import { normalizeErrorWithRecovery } from './error-recovery.js';
|
|
36
|
+
import { extractCanonicalParagraphText } from '../core/paragraph-text.js';
|
|
37
|
+
import { buildLocalizedReplacementChange } from './localized-replacement-compiler.js';
|
|
38
|
+
|
|
39
|
+
export function normalizeOperationError(error, context = {}) {
|
|
40
|
+
return normalizeErrorWithRecovery(error, context);
|
|
41
|
+
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Validates and dispatches one structured operation against full document XML.
|
|
44
45
|
* Result metadata is assembled here so every mutation path exposes the same
|
|
45
46
|
* */
|
|
46
|
-
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
47
|
-
const operationIndex = typeof options._operationIndex === 'number' ? options._operationIndex : 1;
|
|
48
|
-
const errorContext = {
|
|
49
|
-
operationIndex,
|
|
50
|
-
...(typeof op?.operationId === 'string' ? { operationId: op.operationId } : {})
|
|
51
|
-
};
|
|
47
|
+
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
48
|
+
const operationIndex = typeof options._operationIndex === 'number' ? options._operationIndex : 1;
|
|
49
|
+
const errorContext = {
|
|
50
|
+
operationIndex,
|
|
51
|
+
...(typeof op?.operationId === 'string' ? { operationId: op.operationId } : {})
|
|
52
|
+
};
|
|
52
53
|
const validation = validateDocumentOperation(op);
|
|
53
54
|
if (!validation.valid) {
|
|
54
55
|
const authorUsed = resolveDocumentOperationAuthor(op, author, getDefaultAuthor());
|
|
@@ -56,33 +57,34 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
56
57
|
documentXml,
|
|
57
58
|
hasChanges: false,
|
|
58
59
|
status: 'error',
|
|
59
|
-
error: normalizeOperationError(validation.error, errorContext),
|
|
60
|
+
error: normalizeOperationError(validation.error, errorContext),
|
|
60
61
|
operationType: normalizeDocumentOperation(op).operationKind,
|
|
61
62
|
authorUsed,
|
|
62
63
|
receipt: createEmptyReceipt(operationIndex, op?.operationId, authorUsed, 'refused')
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
let operation = validation.operation || normalizeDocumentOperation(op);
|
|
67
|
+
let operation = validation.operation || normalizeDocumentOperation(op);
|
|
67
68
|
const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
|
|
68
69
|
|
|
69
|
-
if (
|
|
70
|
-
operation.operationKind !== 'comment_reply'
|
|
71
|
-
&& operation.operationKind !== '
|
|
72
|
-
&& operation.operationKind !== '
|
|
73
|
-
&&
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
if (
|
|
71
|
+
operation.operationKind !== 'comment_reply'
|
|
72
|
+
&& operation.operationKind !== 'comment_resolve'
|
|
73
|
+
&& operation.operationKind !== 'rejected-insert'
|
|
74
|
+
&& operation.operationKind !== 'restore'
|
|
75
|
+
&& (
|
|
76
|
+
operation.targetDescriptor?.revisionView === 'rejected'
|
|
77
|
+
|| operation.targetEndDescriptor?.revisionView === 'rejected'
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
78
80
|
return {
|
|
79
81
|
documentXml,
|
|
80
82
|
hasChanges: false,
|
|
81
83
|
status: 'error',
|
|
82
|
-
error: normalizeOperationError({
|
|
83
|
-
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
84
|
-
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
85
|
-
}, errorContext),
|
|
84
|
+
error: normalizeOperationError({
|
|
85
|
+
code: 'UNSUPPORTED_REVISION_VIEW_MUTATION',
|
|
86
|
+
message: 'Targeting rejected revision view for mutation is not supported yet.'
|
|
87
|
+
}, errorContext),
|
|
86
88
|
operationType: operation.operationKind,
|
|
87
89
|
authorUsed,
|
|
88
90
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -96,10 +98,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
96
98
|
documentXml,
|
|
97
99
|
hasChanges: false,
|
|
98
100
|
status: 'error',
|
|
99
|
-
error: normalizeOperationError({
|
|
100
|
-
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
101
|
-
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
102
|
-
}, errorContext),
|
|
101
|
+
error: normalizeOperationError({
|
|
102
|
+
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
103
|
+
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
104
|
+
}, errorContext),
|
|
103
105
|
operationType: operation.operationKind,
|
|
104
106
|
authorUsed,
|
|
105
107
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -110,10 +112,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
110
112
|
documentXml,
|
|
111
113
|
hasChanges: false,
|
|
112
114
|
status: 'error',
|
|
113
|
-
error: normalizeOperationError({
|
|
114
|
-
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
115
|
-
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
116
|
-
}, errorContext),
|
|
115
|
+
error: normalizeOperationError({
|
|
116
|
+
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
117
|
+
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
118
|
+
}, errorContext),
|
|
117
119
|
operationType: operation.operationKind,
|
|
118
120
|
authorUsed,
|
|
119
121
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -131,12 +133,12 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
131
133
|
documentXml,
|
|
132
134
|
hasChanges: false,
|
|
133
135
|
status: 'error',
|
|
134
|
-
error: normalizeOperationError({
|
|
135
|
-
code: 'REVISION_MISMATCH',
|
|
136
|
-
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`,
|
|
137
|
-
expectedRevision: options.expectedRevision,
|
|
138
|
-
currentRevision: currentToken
|
|
139
|
-
}, errorContext),
|
|
136
|
+
error: normalizeOperationError({
|
|
137
|
+
code: 'REVISION_MISMATCH',
|
|
138
|
+
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`,
|
|
139
|
+
expectedRevision: options.expectedRevision,
|
|
140
|
+
currentRevision: currentToken
|
|
141
|
+
}, errorContext),
|
|
140
142
|
operationType: operation.operationKind,
|
|
141
143
|
authorUsed,
|
|
142
144
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
@@ -147,67 +149,67 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
147
149
|
const session = options?._documentOperationSession instanceof DocumentOperationSession
|
|
148
150
|
? options._documentOperationSession
|
|
149
151
|
: new DocumentOperationSession(documentXml, options);
|
|
150
|
-
if (!session.valid) {
|
|
152
|
+
if (!session.valid) {
|
|
151
153
|
return {
|
|
152
154
|
documentXml,
|
|
153
155
|
hasChanges: false,
|
|
154
156
|
status: 'error',
|
|
155
|
-
error: normalizeOperationError(session.parseResult.error, errorContext),
|
|
157
|
+
error: normalizeOperationError(session.parseResult.error, errorContext),
|
|
156
158
|
warnings: session.parseResult.warnings,
|
|
157
159
|
operationType: operation.operationKind,
|
|
158
160
|
authorUsed,
|
|
159
161
|
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
Array.isArray(operation.replacements)
|
|
165
|
-
|| (operation.operationKind === 'restore' && op?.modified === undefined)
|
|
166
|
-
) {
|
|
167
|
-
const compilation = compileOperationBatch(session.document, [op], {
|
|
168
|
-
strictTargets: options.strictTargets !== false,
|
|
169
|
-
onInfo: options.onInfo,
|
|
170
|
-
onWarn: options.onWarn
|
|
171
|
-
});
|
|
172
|
-
const compilationError = compilation.bindings[0]?.error || compilation.conflicts[0] || null;
|
|
173
|
-
if (compilationError) {
|
|
174
|
-
return {
|
|
175
|
-
documentXml,
|
|
176
|
-
hasChanges: false,
|
|
177
|
-
status: 'error',
|
|
178
|
-
error: normalizeOperationError(compilationError, errorContext),
|
|
179
|
-
operationType: operation.operationKind,
|
|
180
|
-
authorUsed,
|
|
181
|
-
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
const compiledValidation = validateDocumentOperation(compilation.compiledOperations[0]);
|
|
185
|
-
if (!compiledValidation.valid) {
|
|
186
|
-
return {
|
|
187
|
-
documentXml,
|
|
188
|
-
hasChanges: false,
|
|
189
|
-
status: 'error',
|
|
190
|
-
error: normalizeOperationError(compiledValidation.error, errorContext),
|
|
191
|
-
operationType: operation.operationKind,
|
|
192
|
-
authorUsed,
|
|
193
|
-
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
session.sourceTargetRegistry = compilation.registry;
|
|
197
|
-
operation = compiledValidation.operation;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const resolutionCapture = {};
|
|
201
|
-
let localizedChange = null;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
Array.isArray(operation.replacements)
|
|
167
|
+
|| (operation.operationKind === 'restore' && op?.modified === undefined)
|
|
168
|
+
) {
|
|
169
|
+
const compilation = compileOperationBatch(session.document, [op], {
|
|
170
|
+
strictTargets: options.strictTargets !== false,
|
|
171
|
+
onInfo: options.onInfo,
|
|
172
|
+
onWarn: options.onWarn
|
|
173
|
+
});
|
|
174
|
+
const compilationError = compilation.bindings[0]?.error || compilation.conflicts[0] || null;
|
|
175
|
+
if (compilationError) {
|
|
176
|
+
return {
|
|
177
|
+
documentXml,
|
|
178
|
+
hasChanges: false,
|
|
179
|
+
status: 'error',
|
|
180
|
+
error: normalizeOperationError(compilationError, errorContext),
|
|
181
|
+
operationType: operation.operationKind,
|
|
182
|
+
authorUsed,
|
|
183
|
+
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const compiledValidation = validateDocumentOperation(compilation.compiledOperations[0]);
|
|
187
|
+
if (!compiledValidation.valid) {
|
|
188
|
+
return {
|
|
189
|
+
documentXml,
|
|
190
|
+
hasChanges: false,
|
|
191
|
+
status: 'error',
|
|
192
|
+
error: normalizeOperationError(compiledValidation.error, errorContext),
|
|
193
|
+
operationType: operation.operationKind,
|
|
194
|
+
authorUsed,
|
|
195
|
+
receipt: createEmptyReceipt(operationIndex, operation.operationId, authorUsed, 'refused')
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
session.sourceTargetRegistry = compilation.registry;
|
|
199
|
+
operation = compiledValidation.operation;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const resolutionCapture = {};
|
|
203
|
+
let localizedChange = null;
|
|
202
204
|
const savepoint = session.createSavepoint();
|
|
203
205
|
session.receiptCollector?.beginOperation(
|
|
204
206
|
operationIndex,
|
|
205
207
|
operation.operationId,
|
|
206
208
|
authorUsed
|
|
207
209
|
);
|
|
208
|
-
const operationWarnings = Array.isArray(operation._compiledWarnings)
|
|
209
|
-
? [...operation._compiledWarnings]
|
|
210
|
-
: [];
|
|
210
|
+
const operationWarnings = Array.isArray(operation._compiledWarnings)
|
|
211
|
+
? [...operation._compiledWarnings]
|
|
212
|
+
: [];
|
|
211
213
|
const operationOptions = {
|
|
212
214
|
...options,
|
|
213
215
|
...(typeof operation.generateRedlines === 'boolean' ? { generateRedlines: operation.generateRedlines } : {}),
|
|
@@ -217,12 +219,12 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
217
219
|
pairReplacements: typeof operation.pairReplacements === 'boolean' ? operation.pairReplacements : (options.pairReplacements !== false),
|
|
218
220
|
...(operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {}),
|
|
219
221
|
...(operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {}),
|
|
220
|
-
targetDescriptor: operation.targetDescriptor,
|
|
221
|
-
targetEndDescriptor: operation.targetEndDescriptor,
|
|
222
|
-
_compiledSourceId: operation._compiledSourceId || null,
|
|
223
|
-
_compiledSourceEndId: operation._compiledSourceEndId || null,
|
|
224
|
-
_compiledResolvedBy: operation._compiledResolvedBy || null,
|
|
225
|
-
_sourceTargetRegistry: session.sourceTargetRegistry || null,
|
|
222
|
+
targetDescriptor: operation.targetDescriptor,
|
|
223
|
+
targetEndDescriptor: operation.targetEndDescriptor,
|
|
224
|
+
_compiledSourceId: operation._compiledSourceId || null,
|
|
225
|
+
_compiledSourceEndId: operation._compiledSourceEndId || null,
|
|
226
|
+
_compiledResolvedBy: operation._compiledResolvedBy || null,
|
|
227
|
+
_sourceTargetRegistry: session.sourceTargetRegistry || null,
|
|
226
228
|
_resolutionCapture: resolutionCapture,
|
|
227
229
|
_revisionIdAllocator: session.revisionIdAllocator,
|
|
228
230
|
_documentOperationSession: session,
|
|
@@ -253,6 +255,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
253
255
|
result = applyCommentReplyToParts({
|
|
254
256
|
commentsXml: existingCommentsXml,
|
|
255
257
|
commentsExtendedXml: existingExtendedXml,
|
|
258
|
+
documentXml,
|
|
256
259
|
parentCommentId: operation.parentCommentId,
|
|
257
260
|
commentId,
|
|
258
261
|
commentContent: operation.commentContent,
|
|
@@ -260,8 +263,33 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
260
263
|
date: operation.date || new Date().toISOString()
|
|
261
264
|
});
|
|
262
265
|
result.documentXml = documentXml;
|
|
263
|
-
if (result.
|
|
266
|
+
if (result.status !== 'error' && result.hasChanges) {
|
|
267
|
+
// Word only shows a reply that has its own range/reference markers in the body.
|
|
268
|
+
if (!session.document || !anchorReplyInDocument(session.document, result.threadCommentIds, commentId)) {
|
|
269
|
+
result = { documentXml, hasChanges: false, status: 'error', error: { code: 'PARENT_ANCHOR_NOT_FOUND', message: `Comment '${operation.parentCommentId}' has no anchor in the document body, so a reply cannot be placed.` } };
|
|
270
|
+
} else if (session.receiptCollector) {
|
|
271
|
+
session.receiptCollector.recordComment(commentId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
264
274
|
}
|
|
275
|
+
} else if (operation.operationKind === 'comment_resolve') {
|
|
276
|
+
const resolution = applyThreadResolutionToParts({
|
|
277
|
+
commentsXml: session.commentsXml || runtimeContext?.commentsXml || options.commentsXml,
|
|
278
|
+
commentsExtendedXml: session.commentsExtendedXml || runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
|
|
279
|
+
commentId: operation.commentId,
|
|
280
|
+
resolved: operation.resolved !== false
|
|
281
|
+
});
|
|
282
|
+
result = resolution.status === 'error'
|
|
283
|
+
? { documentXml, hasChanges: false, status: 'error', error: resolution.error }
|
|
284
|
+
: {
|
|
285
|
+
documentXml,
|
|
286
|
+
hasChanges: resolution.hasChanges,
|
|
287
|
+
...(resolution.commentsXml ? { commentsXml: resolution.commentsXml, commentsXmlMode: 'replace' } : {}),
|
|
288
|
+
...(resolution.commentsExtendedXml ? { commentsExtendedXml: resolution.commentsExtendedXml, commentsExtendedXmlMode: 'replace' } : {}),
|
|
289
|
+
resolved: resolution.resolved,
|
|
290
|
+
threadRootId: resolution.threadRootId,
|
|
291
|
+
commentIds: resolution.commentIds
|
|
292
|
+
};
|
|
265
293
|
} else if (operation.operationKind === 'highlight') {
|
|
266
294
|
result = await applyHighlightToParagraphByExactText(
|
|
267
295
|
documentXml,
|
|
@@ -305,29 +333,29 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
305
333
|
runtimeContext,
|
|
306
334
|
operationOptions
|
|
307
335
|
);
|
|
308
|
-
} else if (operation.operationKind === 'rejected-insert') {
|
|
309
|
-
result = await insertIntoRejectedDeletedText(
|
|
310
|
-
documentXml,
|
|
311
|
-
operation.target,
|
|
312
|
-
operation.anchor,
|
|
313
|
-
operation.modified,
|
|
314
|
-
authorUsed,
|
|
315
|
-
operation.targetRef,
|
|
316
|
-
runtimeContext,
|
|
317
|
-
operationOptions
|
|
318
|
-
);
|
|
319
|
-
} else if (operation.operationKind === 'restore') {
|
|
320
|
-
result = await restoreDeletedParagraphByExactText(
|
|
321
|
-
documentXml,
|
|
322
|
-
operation.target,
|
|
323
|
-
operation.modified,
|
|
324
|
-
authorUsed,
|
|
325
|
-
operation.targetRef,
|
|
326
|
-
operation.targetEndRef,
|
|
327
|
-
runtimeContext,
|
|
328
|
-
operationOptions
|
|
329
|
-
);
|
|
330
|
-
} else {
|
|
336
|
+
} else if (operation.operationKind === 'rejected-insert') {
|
|
337
|
+
result = await insertIntoRejectedDeletedText(
|
|
338
|
+
documentXml,
|
|
339
|
+
operation.target,
|
|
340
|
+
operation.anchor,
|
|
341
|
+
operation.modified,
|
|
342
|
+
authorUsed,
|
|
343
|
+
operation.targetRef,
|
|
344
|
+
runtimeContext,
|
|
345
|
+
operationOptions
|
|
346
|
+
);
|
|
347
|
+
} else if (operation.operationKind === 'restore') {
|
|
348
|
+
result = await restoreDeletedParagraphByExactText(
|
|
349
|
+
documentXml,
|
|
350
|
+
operation.target,
|
|
351
|
+
operation.modified,
|
|
352
|
+
authorUsed,
|
|
353
|
+
operation.targetRef,
|
|
354
|
+
operation.targetEndRef,
|
|
355
|
+
runtimeContext,
|
|
356
|
+
operationOptions
|
|
357
|
+
);
|
|
358
|
+
} else {
|
|
331
359
|
result = await applyToParagraphByExactText(
|
|
332
360
|
documentXml,
|
|
333
361
|
operation.target,
|
|
@@ -377,72 +405,72 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
377
405
|
operationReceipt.warnings.push(String(w));
|
|
378
406
|
}
|
|
379
407
|
}
|
|
380
|
-
} else {
|
|
381
|
-
const beforeValidation = validateRedlineOoxml(savepoint.document);
|
|
382
|
-
const afterValidation = validateRedlineOoxml(session.document);
|
|
383
|
-
const generatedIssues = subtractValidationIssueMultiset(afterValidation.issues, beforeValidation.issues);
|
|
384
|
-
const generatedErrors = validationErrors(generatedIssues);
|
|
385
|
-
if (generatedErrors.length > 0) {
|
|
386
|
-
session.restoreSavepoint(savepoint);
|
|
387
|
-
operationReceipt = createEmptyReceipt(
|
|
388
|
-
operationIndex,
|
|
389
|
-
operation.operationId,
|
|
390
|
-
authorUsed,
|
|
391
|
-
'refused'
|
|
392
|
-
);
|
|
393
|
-
const codes = [...new Set(generatedErrors.map(issue => issue.code))].join(', ');
|
|
394
|
-
return {
|
|
395
|
-
documentXml,
|
|
396
|
-
hasChanges: false,
|
|
397
|
-
status: 'error',
|
|
398
|
-
error: normalizeOperationError({
|
|
399
|
-
code: 'GENERATED_OOXML_INVALID',
|
|
400
|
-
stage: 'validation',
|
|
401
|
-
message: `Operation introduced invalid OOXML (${codes}).`,
|
|
402
|
-
generatedIssues: generatedErrors
|
|
403
|
-
}, errorContext),
|
|
404
|
-
operationType: operation.operationKind,
|
|
405
|
-
authorUsed,
|
|
406
|
-
receipt: operationReceipt,
|
|
407
|
-
...resolutionCapture
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
session.markMutationCommitted(
|
|
411
|
-
if (operation._compiledSourceId && session.sourceTargetRegistry) {
|
|
412
|
-
session.sourceTargetRegistry.commitMutation(
|
|
413
|
-
operation._compiledSourceId,
|
|
414
|
-
operationOptions._mutationRemovedNodes,
|
|
415
|
-
operationOptions._mutationLiveNodes,
|
|
416
|
-
operationIndex
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
if (operation._localizedReplacementCompilation) {
|
|
420
|
-
let outputText = null;
|
|
421
|
-
if (operation.operationKind === 'restore') {
|
|
422
|
-
const restoredParagraph = operationOptions._mutationLiveNodes.find(node => node?.localName === 'p');
|
|
423
|
-
if (restoredParagraph) {
|
|
424
|
-
outputText = extractCanonicalParagraphText(restoredParagraph, {
|
|
425
|
-
revisionView: 'accepted'
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
} else if (operation._compiledSourceId && session.sourceTargetRegistry) {
|
|
429
|
-
const bound = session.sourceTargetRegistry.resolve(
|
|
430
|
-
operation._compiledSourceId,
|
|
431
|
-
session.document
|
|
432
|
-
);
|
|
433
|
-
if (bound.paragraph) {
|
|
434
|
-
outputText = extractCanonicalParagraphText(bound.paragraph, {
|
|
435
|
-
revisionView: 'accepted'
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
localizedChange = buildLocalizedReplacementChange(
|
|
440
|
-
operation._localizedReplacementCompilation,
|
|
441
|
-
outputText,
|
|
442
|
-
resolutionCapture.resolvedTarget || {},
|
|
443
|
-
operation._speculativeContext || null
|
|
444
|
-
);
|
|
445
|
-
}
|
|
408
|
+
} else {
|
|
409
|
+
const beforeValidation = validateRedlineOoxml(savepoint.document);
|
|
410
|
+
const afterValidation = validateRedlineOoxml(session.document);
|
|
411
|
+
const generatedIssues = subtractValidationIssueMultiset(afterValidation.issues, beforeValidation.issues);
|
|
412
|
+
const generatedErrors = validationErrors(generatedIssues);
|
|
413
|
+
if (generatedErrors.length > 0) {
|
|
414
|
+
session.restoreSavepoint(savepoint);
|
|
415
|
+
operationReceipt = createEmptyReceipt(
|
|
416
|
+
operationIndex,
|
|
417
|
+
operation.operationId,
|
|
418
|
+
authorUsed,
|
|
419
|
+
'refused'
|
|
420
|
+
);
|
|
421
|
+
const codes = [...new Set(generatedErrors.map(issue => issue.code))].join(', ');
|
|
422
|
+
return {
|
|
423
|
+
documentXml,
|
|
424
|
+
hasChanges: false,
|
|
425
|
+
status: 'error',
|
|
426
|
+
error: normalizeOperationError({
|
|
427
|
+
code: 'GENERATED_OOXML_INVALID',
|
|
428
|
+
stage: 'validation',
|
|
429
|
+
message: `Operation introduced invalid OOXML (${codes}).`,
|
|
430
|
+
generatedIssues: generatedErrors
|
|
431
|
+
}, errorContext),
|
|
432
|
+
operationType: operation.operationKind,
|
|
433
|
+
authorUsed,
|
|
434
|
+
receipt: operationReceipt,
|
|
435
|
+
...resolutionCapture
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
session.markMutationCommitted(true);
|
|
439
|
+
if (operation._compiledSourceId && session.sourceTargetRegistry) {
|
|
440
|
+
session.sourceTargetRegistry.commitMutation(
|
|
441
|
+
operation._compiledSourceId,
|
|
442
|
+
operationOptions._mutationRemovedNodes,
|
|
443
|
+
operationOptions._mutationLiveNodes,
|
|
444
|
+
operationIndex
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (operation._localizedReplacementCompilation) {
|
|
448
|
+
let outputText = null;
|
|
449
|
+
if (operation.operationKind === 'restore') {
|
|
450
|
+
const restoredParagraph = operationOptions._mutationLiveNodes.find(node => node?.localName === 'p');
|
|
451
|
+
if (restoredParagraph) {
|
|
452
|
+
outputText = extractCanonicalParagraphText(restoredParagraph, {
|
|
453
|
+
revisionView: 'accepted'
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
} else if (operation._compiledSourceId && session.sourceTargetRegistry) {
|
|
457
|
+
const bound = session.sourceTargetRegistry.resolve(
|
|
458
|
+
operation._compiledSourceId,
|
|
459
|
+
session.document
|
|
460
|
+
);
|
|
461
|
+
if (bound.paragraph) {
|
|
462
|
+
outputText = extractCanonicalParagraphText(bound.paragraph, {
|
|
463
|
+
revisionView: 'accepted'
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
localizedChange = buildLocalizedReplacementChange(
|
|
468
|
+
operation._localizedReplacementCompilation,
|
|
469
|
+
outputText,
|
|
470
|
+
resolutionCapture.resolvedTarget || {},
|
|
471
|
+
operation._speculativeContext || null
|
|
472
|
+
);
|
|
473
|
+
}
|
|
446
474
|
if (operation.captureKey && session.captureTable) {
|
|
447
475
|
session.captureTable.set(
|
|
448
476
|
operation.captureKey,
|
|
@@ -479,7 +507,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
479
507
|
documentXml,
|
|
480
508
|
hasChanges: false,
|
|
481
509
|
status: 'error',
|
|
482
|
-
error: normalizeOperationError(reconciliation.error, errorContext),
|
|
510
|
+
error: normalizeOperationError(reconciliation.error, errorContext),
|
|
483
511
|
warnings: [reconciliation.error.message],
|
|
484
512
|
operationType: operation.operationKind,
|
|
485
513
|
authorUsed,
|
|
@@ -489,18 +517,18 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
489
517
|
}
|
|
490
518
|
}
|
|
491
519
|
}
|
|
492
|
-
if (result?.error) result.error = normalizeOperationError(result.error, errorContext);
|
|
493
|
-
return {
|
|
520
|
+
if (result?.error) result.error = normalizeOperationError(result.error, errorContext);
|
|
521
|
+
return {
|
|
494
522
|
...result,
|
|
495
523
|
operationType: operation.operationKind,
|
|
496
524
|
authorUsed,
|
|
497
|
-
receipt: operationReceipt,
|
|
498
|
-
...(localizedChange ? { change: localizedChange } : {}),
|
|
499
|
-
...resolutionCapture
|
|
525
|
+
receipt: operationReceipt,
|
|
526
|
+
...(localizedChange ? { change: localizedChange } : {}),
|
|
527
|
+
...resolutionCapture
|
|
500
528
|
};
|
|
501
529
|
} catch (error) {
|
|
502
530
|
session.restoreSavepoint(savepoint);
|
|
503
|
-
const normalizedError = normalizeOperationError(error, errorContext);
|
|
531
|
+
const normalizedError = normalizeOperationError(error, errorContext);
|
|
504
532
|
const operationReceipt = createEmptyReceipt(
|
|
505
533
|
operationIndex,
|
|
506
534
|
operation.operationId,
|
|
@@ -20,6 +20,7 @@ const SUPPORTED_OPERATION_TYPES = new Set([
|
|
|
20
20
|
'delete',
|
|
21
21
|
'comment',
|
|
22
22
|
'comment_reply',
|
|
23
|
+
'comment_resolve',
|
|
23
24
|
'highlight'
|
|
24
25
|
]);
|
|
25
26
|
|
|
@@ -43,11 +44,16 @@ function nonEmptyString(value) {
|
|
|
43
44
|
return typeof value === 'string' && value.trim().length > 0;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/** Operations that address an existing comment thread by id instead of targeting a paragraph. */
|
|
48
|
+
export function isCommentThreadOperationKind(kind) {
|
|
49
|
+
return kind === 'comment_reply' || kind === 'comment_resolve';
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
export function getCanonicalOperationType(operation) {
|
|
47
53
|
const type = operation?.type;
|
|
48
54
|
if (type === 'insert' && operation?.target?.revisionView === 'rejected') return 'rejected-insert';
|
|
49
55
|
if (type === 'restore') return 'restore';
|
|
50
|
-
if (type === 'comment' || type === 'comment_reply' || type === 'highlight') return type;
|
|
56
|
+
if (type === 'comment' || type === 'comment_reply' || type === 'comment_resolve' || type === 'highlight') return type;
|
|
51
57
|
if (type === 'paragraph-format') return 'paragraph-format';
|
|
52
58
|
if (type === 'character-format' || (type === 'format' && (operation?.textToFormat != null || operation?.properties != null))) return 'format';
|
|
53
59
|
return 'redline';
|
|
@@ -191,7 +197,7 @@ export function validateDocumentOperation(operation) {
|
|
|
191
197
|
|
|
192
198
|
const target = normalized.targetDescriptor;
|
|
193
199
|
if (
|
|
194
|
-
normalized.operationKind
|
|
200
|
+
!isCommentThreadOperationKind(normalized.operationKind)
|
|
195
201
|
&& !nonEmptyString(target.text)
|
|
196
202
|
&& target.index == null
|
|
197
203
|
&& !target.paragraphId
|
|
@@ -460,6 +466,15 @@ export function validateDocumentOperation(operation) {
|
|
|
460
466
|
}
|
|
461
467
|
}
|
|
462
468
|
|
|
469
|
+
if (normalized.operationKind === 'comment_resolve') {
|
|
470
|
+
if (!nonEmptyString(String(normalized.commentId ?? ''))) {
|
|
471
|
+
return { valid: false, error: { code: 'INVALID_OPERATION', message: 'Comment resolve operations require a "commentId".' } };
|
|
472
|
+
}
|
|
473
|
+
if (normalized.resolved != null && typeof normalized.resolved !== 'boolean') {
|
|
474
|
+
return { valid: false, error: { code: 'INVALID_OPERATION', message: 'Comment resolve "resolved" must be a boolean when provided.' } };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
463
478
|
if (normalized.operationKind === 'highlight') {
|
|
464
479
|
if (!nonEmptyString(normalized.textToHighlight)) {
|
|
465
480
|
return {
|
|
@@ -35,6 +35,12 @@ const RULES = Object.freeze({
|
|
|
35
35
|
STRUCTURED_CONTENT_INVALID: ['request', 'request_fixable', 'change_request'],
|
|
36
36
|
UNSUPPORTED_INSERTION_AFFINITY: ['request', 'request_fixable', 'change_request'],
|
|
37
37
|
EXISTING_REVISIONS: ['target-safety', 'policy_choice_required', 'set_option'],
|
|
38
|
+
PART_NOT_FOUND: ['target-resolution', 'target_refresh_required', 'reinspect'],
|
|
39
|
+
PART_AMBIGUOUS: ['target-resolution', 'candidate_selection_required', 'choose_candidate'],
|
|
40
|
+
COMMENT_IN_HEADER_FOOTER: ['request', 'request_fixable', 'change_request'],
|
|
41
|
+
FIELD_EDIT_REFUSED: ['target-safety', 'request_fixable', 'change_request'],
|
|
42
|
+
COMMENT_NOT_FOUND: ['target-resolution', 'target_refresh_required', 'reinspect'],
|
|
43
|
+
PARENT_ANCHOR_NOT_FOUND: ['target-resolution', 'target_refresh_required', 'reinspect'],
|
|
38
44
|
COMMENTED_CONTENT_MERGE: ['target-safety', 'user_authorization_required', 'resolve_comments'],
|
|
39
45
|
COMMENTED_CONTENT_DELETE: ['target-safety', 'user_authorization_required', 'resolve_comments'],
|
|
40
46
|
OVERLAPPING_SOURCE_TARGETS: ['batch-compilation', 'source_conflict', 'consolidate_operations'],
|