@ansonlai/docx-redline-js 0.8.3 → 0.9.0
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/ARCHITECTURE.md +221 -12
- package/CHANGELOG.md +803 -586
- package/README.md +1018 -901
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/dist/docx-redline-js.esm.js +4551 -967
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +112 -106
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4690 -1106
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4690 -1106
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +23 -4
- package/docs/SKILL_AUTHORING.md +165 -150
- package/docs/TESTING.md +1142 -1059
- package/docs/schemas/document-operations.schema.json +221 -168
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +567 -560
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-mode.js +183 -183
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +51 -1
- package/index.js +47 -3
- package/node/cli-help.js +1 -0
- package/node/cli.js +992 -974
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/pipeline.js +206 -204
- package/pipeline/serialization.js +111 -94
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +26 -0
- package/services/capture-engine.js +228 -226
- package/services/document-operation-applier.js +215 -0
- package/services/document-operation-contract.js +628 -537
- package/services/document-operation-mutations.js +789 -46
- package/services/error-recovery.js +17 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +612 -436
- package/services/operation-heuristics.js +197 -173
- package/services/operation-preflight.js +1 -0
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +634 -594
- package/services/revision-fidelity-verifier.js +498 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +606 -427
- package/services/table-reconciliation.js +103 -49
|
@@ -1,288 +1,288 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal mutation receipt collector for tracking allocated revision,
|
|
3
|
-
* comment, numbering, and relationship IDs per operation.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export class ReceiptCollector {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.receipts = [];
|
|
9
|
-
this.activeReceipt = null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
beginOperation(operationIndex, operationId = null, authorUsed = null) {
|
|
13
|
-
this.activeReceipt = {
|
|
14
|
-
operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
|
|
15
|
-
...(operationId ? { operationId: String(operationId) } : {}),
|
|
16
|
-
attemptedDisposition: 'applied',
|
|
17
|
-
finalDisposition: 'applied',
|
|
18
|
-
committed: true,
|
|
19
|
-
...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
|
|
20
|
-
revisionItems: [],
|
|
21
|
-
commentIds: [],
|
|
22
|
-
numberingIds: [],
|
|
23
|
-
relationshipIds: [],
|
|
24
|
-
affectedTargets: [],
|
|
25
|
-
warnings: []
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
recordRevision(id, kind = 'structural', partName = 'word/document.xml') {
|
|
30
|
-
if (!this.activeReceipt || id == null) return;
|
|
31
|
-
const strId = String(id);
|
|
32
|
-
if (!this.activeReceipt.revisionItems.some(item => item.id === strId && item.kind === kind && item.partName === partName)) {
|
|
33
|
-
this.activeReceipt.revisionItems.push({
|
|
34
|
-
id: strId,
|
|
35
|
-
kind,
|
|
36
|
-
partName
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
recordComment(id, _partName = 'word/comments.xml') {
|
|
42
|
-
if (!this.activeReceipt || id == null) return;
|
|
43
|
-
const strId = String(id);
|
|
44
|
-
if (!this.activeReceipt.commentIds.includes(strId)) {
|
|
45
|
-
this.activeReceipt.commentIds.push(strId);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
recordNumbering(id, _partName = 'word/numbering.xml') {
|
|
50
|
-
if (!this.activeReceipt || id == null) return;
|
|
51
|
-
const strId = String(id);
|
|
52
|
-
if (!this.activeReceipt.numberingIds.includes(strId)) {
|
|
53
|
-
this.activeReceipt.numberingIds.push(strId);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
recordRelationship(id, _partName = 'word/_rels/document.xml.rels') {
|
|
58
|
-
if (!this.activeReceipt || id == null) return;
|
|
59
|
-
const strId = String(id);
|
|
60
|
-
if (!this.activeReceipt.relationshipIds.includes(strId)) {
|
|
61
|
-
this.activeReceipt.relationshipIds.push(strId);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
recordAffectedTarget(target) {
|
|
66
|
-
if (!this.activeReceipt || !target) return;
|
|
67
|
-
this.activeReceipt.affectedTargets.push(JSON.parse(JSON.stringify(target)));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
recordWarning(warning) {
|
|
71
|
-
if (!this.activeReceipt || !warning) return;
|
|
72
|
-
this.activeReceipt.warnings.push(String(warning));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
commitOperation(disposition = 'applied') {
|
|
76
|
-
if (!this.activeReceipt) return null;
|
|
77
|
-
this.activeReceipt.attemptedDisposition = disposition;
|
|
78
|
-
this.activeReceipt.finalDisposition = disposition;
|
|
79
|
-
this.activeReceipt.committed = disposition === 'applied';
|
|
80
|
-
const committed = JSON.parse(JSON.stringify(this.activeReceipt));
|
|
81
|
-
this.receipts.push(committed);
|
|
82
|
-
this.activeReceipt = null;
|
|
83
|
-
return committed;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
abortOperation(disposition = 'refused') {
|
|
87
|
-
if (!this.activeReceipt) return null;
|
|
88
|
-
this.activeReceipt.attemptedDisposition = disposition;
|
|
89
|
-
this.activeReceipt.finalDisposition = disposition;
|
|
90
|
-
this.activeReceipt.committed = false;
|
|
91
|
-
const aborted = JSON.parse(JSON.stringify(this.activeReceipt));
|
|
92
|
-
this.activeReceipt = null;
|
|
93
|
-
return aborted;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
createSavepoint() {
|
|
97
|
-
return {
|
|
98
|
-
receipts: JSON.parse(JSON.stringify(this.receipts)),
|
|
99
|
-
activeReceipt: this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
restoreSavepoint(savepoint) {
|
|
104
|
-
if (!savepoint) return;
|
|
105
|
-
this.receipts = Array.isArray(savepoint.receipts)
|
|
106
|
-
? JSON.parse(JSON.stringify(savepoint.receipts))
|
|
107
|
-
: [];
|
|
108
|
-
this.activeReceipt = savepoint.activeReceipt
|
|
109
|
-
? JSON.parse(JSON.stringify(savepoint.activeReceipt))
|
|
110
|
-
: null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
clear() {
|
|
114
|
-
this.receipts = [];
|
|
115
|
-
this.activeReceipt = null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
markRolledBack() {
|
|
119
|
-
for (const receipt of this.receipts) {
|
|
120
|
-
if (receipt.attemptedDisposition === 'applied') {
|
|
121
|
-
receipt.finalDisposition = 'rolled_back';
|
|
122
|
-
receipt.committed = false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
this.activeReceipt = null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
getReceipts() {
|
|
129
|
-
return JSON.parse(JSON.stringify(this.receipts));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
getCurrentReceipt() {
|
|
133
|
-
return this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Creates an empty receipt structure for unattempted, refused, or no-op operations.
|
|
139
|
-
*
|
|
140
|
-
* @param {number} operationIndex
|
|
141
|
-
* @param {string|null} [operationId]
|
|
142
|
-
* @param {string|null} [authorUsed]
|
|
143
|
-
* @param {'applied'|'no_change'|'refused'|'not_attempted'} [disposition]
|
|
144
|
-
* @returns {Object}
|
|
145
|
-
*/
|
|
146
|
-
export function createEmptyReceipt(operationIndex, operationId = null, authorUsed = null, disposition = 'not_attempted') {
|
|
147
|
-
return {
|
|
148
|
-
operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
|
|
149
|
-
...(operationId ? { operationId: String(operationId) } : {}),
|
|
150
|
-
attemptedDisposition: disposition,
|
|
151
|
-
finalDisposition: disposition,
|
|
152
|
-
committed: false,
|
|
153
|
-
...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
|
|
154
|
-
revisionItems: [],
|
|
155
|
-
commentIds: [],
|
|
156
|
-
numberingIds: [],
|
|
157
|
-
relationshipIds: [],
|
|
158
|
-
affectedTargets: [],
|
|
159
|
-
warnings: []
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Reconciles committed mutation receipts against the generated/committed output parts.
|
|
165
|
-
*
|
|
166
|
-
* @param {Object} parts
|
|
167
|
-
* @param {string} parts.documentXml - Serialized word/document.xml
|
|
168
|
-
* @param {string|null} [parts.commentsXml] - Serialized word/comments.xml
|
|
169
|
-
* @param {string|null} [parts.numberingXml] - Serialized word/numbering.xml
|
|
170
|
-
* @param {string[]} [parts.numberingXmlParts] - Additional numbering XML parts
|
|
171
|
-
* @param {string|null} [parts.relationshipsXml] - Serialized word/_rels/document.xml.rels
|
|
172
|
-
* @param {Array<Object>} receipts - Array of MutationReceipt objects
|
|
173
|
-
* @returns {{ valid: boolean, error?: { code: string, message: string } }}
|
|
174
|
-
*/
|
|
175
|
-
export function reconcileReceiptsAgainstOutput(parts, receipts) {
|
|
176
|
-
if (!Array.isArray(receipts) || receipts.length === 0) {
|
|
177
|
-
return { valid: true };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const committedReceipts = receipts.filter(r => r && r.committed === true && r.finalDisposition === 'applied');
|
|
181
|
-
if (committedReceipts.length === 0) {
|
|
182
|
-
return { valid: true };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const revisionIdSet = new Set();
|
|
186
|
-
if (parts?.documentXml && typeof parts.documentXml === 'string') {
|
|
187
|
-
const revRegex = /<(?:w:)?(?:ins|del|rPrChange|pPrChange|moveFrom|moveTo)\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
|
|
188
|
-
let m;
|
|
189
|
-
while ((m = revRegex.exec(parts.documentXml)) !== null) {
|
|
190
|
-
revisionIdSet.add(m[1]);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const commentIdSet = new Set();
|
|
195
|
-
if (parts?.commentsXml && typeof parts.commentsXml === 'string') {
|
|
196
|
-
const comRegex = /<(?:w:)?comment\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
|
|
197
|
-
let m;
|
|
198
|
-
while ((m = comRegex.exec(parts.commentsXml)) !== null) {
|
|
199
|
-
commentIdSet.add(m[1]);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const numberingIdSet = new Set();
|
|
204
|
-
const combinedNumbering = [parts?.numberingXml, ...(parts?.numberingXmlParts || [])].filter(Boolean).join('\n');
|
|
205
|
-
if (combinedNumbering) {
|
|
206
|
-
const numRegex = /<(?:w:)?num\b[^>]*?\b(?:w:)?numId="([^"]+)"/g;
|
|
207
|
-
let m;
|
|
208
|
-
while ((m = numRegex.exec(combinedNumbering)) !== null) {
|
|
209
|
-
numberingIdSet.add(m[1]);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (parts?.documentXml && typeof parts.documentXml === 'string') {
|
|
213
|
-
const docNumRegex = /<(?:w:)?numId\b[^>]*?\b(?:w:)?val="([^"]+)"/g;
|
|
214
|
-
let m;
|
|
215
|
-
while ((m = docNumRegex.exec(parts.documentXml)) !== null) {
|
|
216
|
-
numberingIdSet.add(m[1]);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const relIdSet = new Set();
|
|
221
|
-
if (parts?.relationshipsXml && typeof parts.relationshipsXml === 'string') {
|
|
222
|
-
const relRegex = /<Relationship\b[^>]*?\bId="([^"]+)"/g;
|
|
223
|
-
let m;
|
|
224
|
-
while ((m = relRegex.exec(parts.relationshipsXml)) !== null) {
|
|
225
|
-
relIdSet.add(m[1]);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
for (const receipt of committedReceipts) {
|
|
230
|
-
if (Array.isArray(receipt.revisionItems)) {
|
|
231
|
-
for (const item of receipt.revisionItems) {
|
|
232
|
-
if (item.partName === 'word/document.xml' && !revisionIdSet.has(String(item.id))) {
|
|
233
|
-
return {
|
|
234
|
-
valid: false,
|
|
235
|
-
error: {
|
|
236
|
-
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
237
|
-
message: `Committed revision id '${item.id}' (kind: ${item.kind}) was not found in word/document.xml.`
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (Array.isArray(receipt.commentIds)) {
|
|
245
|
-
for (const id of receipt.commentIds) {
|
|
246
|
-
if (!commentIdSet.has(String(id))) {
|
|
247
|
-
return {
|
|
248
|
-
valid: false,
|
|
249
|
-
error: {
|
|
250
|
-
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
251
|
-
message: `Committed comment id '${id}' was not found in word/comments.xml.`
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (Array.isArray(receipt.numberingIds)) {
|
|
259
|
-
for (const id of receipt.numberingIds) {
|
|
260
|
-
if (!numberingIdSet.has(String(id))) {
|
|
261
|
-
return {
|
|
262
|
-
valid: false,
|
|
263
|
-
error: {
|
|
264
|
-
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
265
|
-
message: `Committed numbering id '${id}' was not found in numbering parts or document.`
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (parts?.relationshipsXml && Array.isArray(receipt.relationshipIds)) {
|
|
273
|
-
for (const id of receipt.relationshipIds) {
|
|
274
|
-
if (!relIdSet.has(String(id))) {
|
|
275
|
-
return {
|
|
276
|
-
valid: false,
|
|
277
|
-
error: {
|
|
278
|
-
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
279
|
-
message: `Committed relationship id '${id}' was not found in document.xml.rels.`
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return { valid: true };
|
|
288
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Internal mutation receipt collector for tracking allocated revision,
|
|
3
|
+
* comment, numbering, and relationship IDs per operation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ReceiptCollector {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.receipts = [];
|
|
9
|
+
this.activeReceipt = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
beginOperation(operationIndex, operationId = null, authorUsed = null) {
|
|
13
|
+
this.activeReceipt = {
|
|
14
|
+
operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
|
|
15
|
+
...(operationId ? { operationId: String(operationId) } : {}),
|
|
16
|
+
attemptedDisposition: 'applied',
|
|
17
|
+
finalDisposition: 'applied',
|
|
18
|
+
committed: true,
|
|
19
|
+
...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
|
|
20
|
+
revisionItems: [],
|
|
21
|
+
commentIds: [],
|
|
22
|
+
numberingIds: [],
|
|
23
|
+
relationshipIds: [],
|
|
24
|
+
affectedTargets: [],
|
|
25
|
+
warnings: []
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
recordRevision(id, kind = 'structural', partName = 'word/document.xml') {
|
|
30
|
+
if (!this.activeReceipt || id == null) return;
|
|
31
|
+
const strId = String(id);
|
|
32
|
+
if (!this.activeReceipt.revisionItems.some(item => item.id === strId && item.kind === kind && item.partName === partName)) {
|
|
33
|
+
this.activeReceipt.revisionItems.push({
|
|
34
|
+
id: strId,
|
|
35
|
+
kind,
|
|
36
|
+
partName
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
recordComment(id, _partName = 'word/comments.xml') {
|
|
42
|
+
if (!this.activeReceipt || id == null) return;
|
|
43
|
+
const strId = String(id);
|
|
44
|
+
if (!this.activeReceipt.commentIds.includes(strId)) {
|
|
45
|
+
this.activeReceipt.commentIds.push(strId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
recordNumbering(id, _partName = 'word/numbering.xml') {
|
|
50
|
+
if (!this.activeReceipt || id == null) return;
|
|
51
|
+
const strId = String(id);
|
|
52
|
+
if (!this.activeReceipt.numberingIds.includes(strId)) {
|
|
53
|
+
this.activeReceipt.numberingIds.push(strId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
recordRelationship(id, _partName = 'word/_rels/document.xml.rels') {
|
|
58
|
+
if (!this.activeReceipt || id == null) return;
|
|
59
|
+
const strId = String(id);
|
|
60
|
+
if (!this.activeReceipt.relationshipIds.includes(strId)) {
|
|
61
|
+
this.activeReceipt.relationshipIds.push(strId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
recordAffectedTarget(target) {
|
|
66
|
+
if (!this.activeReceipt || !target) return;
|
|
67
|
+
this.activeReceipt.affectedTargets.push(JSON.parse(JSON.stringify(target)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
recordWarning(warning) {
|
|
71
|
+
if (!this.activeReceipt || !warning) return;
|
|
72
|
+
this.activeReceipt.warnings.push(String(warning));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
commitOperation(disposition = 'applied') {
|
|
76
|
+
if (!this.activeReceipt) return null;
|
|
77
|
+
this.activeReceipt.attemptedDisposition = disposition;
|
|
78
|
+
this.activeReceipt.finalDisposition = disposition;
|
|
79
|
+
this.activeReceipt.committed = disposition === 'applied';
|
|
80
|
+
const committed = JSON.parse(JSON.stringify(this.activeReceipt));
|
|
81
|
+
this.receipts.push(committed);
|
|
82
|
+
this.activeReceipt = null;
|
|
83
|
+
return committed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
abortOperation(disposition = 'refused') {
|
|
87
|
+
if (!this.activeReceipt) return null;
|
|
88
|
+
this.activeReceipt.attemptedDisposition = disposition;
|
|
89
|
+
this.activeReceipt.finalDisposition = disposition;
|
|
90
|
+
this.activeReceipt.committed = false;
|
|
91
|
+
const aborted = JSON.parse(JSON.stringify(this.activeReceipt));
|
|
92
|
+
this.activeReceipt = null;
|
|
93
|
+
return aborted;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
createSavepoint() {
|
|
97
|
+
return {
|
|
98
|
+
receipts: JSON.parse(JSON.stringify(this.receipts)),
|
|
99
|
+
activeReceipt: this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
restoreSavepoint(savepoint) {
|
|
104
|
+
if (!savepoint) return;
|
|
105
|
+
this.receipts = Array.isArray(savepoint.receipts)
|
|
106
|
+
? JSON.parse(JSON.stringify(savepoint.receipts))
|
|
107
|
+
: [];
|
|
108
|
+
this.activeReceipt = savepoint.activeReceipt
|
|
109
|
+
? JSON.parse(JSON.stringify(savepoint.activeReceipt))
|
|
110
|
+
: null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
clear() {
|
|
114
|
+
this.receipts = [];
|
|
115
|
+
this.activeReceipt = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
markRolledBack() {
|
|
119
|
+
for (const receipt of this.receipts) {
|
|
120
|
+
if (receipt.attemptedDisposition === 'applied') {
|
|
121
|
+
receipt.finalDisposition = 'rolled_back';
|
|
122
|
+
receipt.committed = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
this.activeReceipt = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getReceipts() {
|
|
129
|
+
return JSON.parse(JSON.stringify(this.receipts));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getCurrentReceipt() {
|
|
133
|
+
return this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Creates an empty receipt structure for unattempted, refused, or no-op operations.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} operationIndex
|
|
141
|
+
* @param {string|null} [operationId]
|
|
142
|
+
* @param {string|null} [authorUsed]
|
|
143
|
+
* @param {'applied'|'no_change'|'refused'|'not_attempted'} [disposition]
|
|
144
|
+
* @returns {Object}
|
|
145
|
+
*/
|
|
146
|
+
export function createEmptyReceipt(operationIndex, operationId = null, authorUsed = null, disposition = 'not_attempted') {
|
|
147
|
+
return {
|
|
148
|
+
operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
|
|
149
|
+
...(operationId ? { operationId: String(operationId) } : {}),
|
|
150
|
+
attemptedDisposition: disposition,
|
|
151
|
+
finalDisposition: disposition,
|
|
152
|
+
committed: false,
|
|
153
|
+
...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
|
|
154
|
+
revisionItems: [],
|
|
155
|
+
commentIds: [],
|
|
156
|
+
numberingIds: [],
|
|
157
|
+
relationshipIds: [],
|
|
158
|
+
affectedTargets: [],
|
|
159
|
+
warnings: []
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Reconciles committed mutation receipts against the generated/committed output parts.
|
|
165
|
+
*
|
|
166
|
+
* @param {Object} parts
|
|
167
|
+
* @param {string} parts.documentXml - Serialized word/document.xml
|
|
168
|
+
* @param {string|null} [parts.commentsXml] - Serialized word/comments.xml
|
|
169
|
+
* @param {string|null} [parts.numberingXml] - Serialized word/numbering.xml
|
|
170
|
+
* @param {string[]} [parts.numberingXmlParts] - Additional numbering XML parts
|
|
171
|
+
* @param {string|null} [parts.relationshipsXml] - Serialized word/_rels/document.xml.rels
|
|
172
|
+
* @param {Array<Object>} receipts - Array of MutationReceipt objects
|
|
173
|
+
* @returns {{ valid: boolean, error?: { code: string, message: string } }}
|
|
174
|
+
*/
|
|
175
|
+
export function reconcileReceiptsAgainstOutput(parts, receipts) {
|
|
176
|
+
if (!Array.isArray(receipts) || receipts.length === 0) {
|
|
177
|
+
return { valid: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const committedReceipts = receipts.filter(r => r && r.committed === true && r.finalDisposition === 'applied');
|
|
181
|
+
if (committedReceipts.length === 0) {
|
|
182
|
+
return { valid: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const revisionIdSet = new Set();
|
|
186
|
+
if (parts?.documentXml && typeof parts.documentXml === 'string') {
|
|
187
|
+
const revRegex = /<(?:w:)?(?:ins|del|rPrChange|pPrChange|moveFrom|moveTo)\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
|
|
188
|
+
let m;
|
|
189
|
+
while ((m = revRegex.exec(parts.documentXml)) !== null) {
|
|
190
|
+
revisionIdSet.add(m[1]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const commentIdSet = new Set();
|
|
195
|
+
if (parts?.commentsXml && typeof parts.commentsXml === 'string') {
|
|
196
|
+
const comRegex = /<(?:w:)?comment\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
|
|
197
|
+
let m;
|
|
198
|
+
while ((m = comRegex.exec(parts.commentsXml)) !== null) {
|
|
199
|
+
commentIdSet.add(m[1]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const numberingIdSet = new Set();
|
|
204
|
+
const combinedNumbering = [parts?.numberingXml, ...(parts?.numberingXmlParts || [])].filter(Boolean).join('\n');
|
|
205
|
+
if (combinedNumbering) {
|
|
206
|
+
const numRegex = /<(?:w:)?num\b[^>]*?\b(?:w:)?numId="([^"]+)"/g;
|
|
207
|
+
let m;
|
|
208
|
+
while ((m = numRegex.exec(combinedNumbering)) !== null) {
|
|
209
|
+
numberingIdSet.add(m[1]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (parts?.documentXml && typeof parts.documentXml === 'string') {
|
|
213
|
+
const docNumRegex = /<(?:w:)?numId\b[^>]*?\b(?:w:)?val="([^"]+)"/g;
|
|
214
|
+
let m;
|
|
215
|
+
while ((m = docNumRegex.exec(parts.documentXml)) !== null) {
|
|
216
|
+
numberingIdSet.add(m[1]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const relIdSet = new Set();
|
|
221
|
+
if (parts?.relationshipsXml && typeof parts.relationshipsXml === 'string') {
|
|
222
|
+
const relRegex = /<Relationship\b[^>]*?\bId="([^"]+)"/g;
|
|
223
|
+
let m;
|
|
224
|
+
while ((m = relRegex.exec(parts.relationshipsXml)) !== null) {
|
|
225
|
+
relIdSet.add(m[1]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const receipt of committedReceipts) {
|
|
230
|
+
if (Array.isArray(receipt.revisionItems)) {
|
|
231
|
+
for (const item of receipt.revisionItems) {
|
|
232
|
+
if (item.partName === 'word/document.xml' && !revisionIdSet.has(String(item.id))) {
|
|
233
|
+
return {
|
|
234
|
+
valid: false,
|
|
235
|
+
error: {
|
|
236
|
+
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
237
|
+
message: `Committed revision id '${item.id}' (kind: ${item.kind}) was not found in word/document.xml.`
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (Array.isArray(receipt.commentIds)) {
|
|
245
|
+
for (const id of receipt.commentIds) {
|
|
246
|
+
if (!commentIdSet.has(String(id))) {
|
|
247
|
+
return {
|
|
248
|
+
valid: false,
|
|
249
|
+
error: {
|
|
250
|
+
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
251
|
+
message: `Committed comment id '${id}' was not found in word/comments.xml.`
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (Array.isArray(receipt.numberingIds)) {
|
|
259
|
+
for (const id of receipt.numberingIds) {
|
|
260
|
+
if (!numberingIdSet.has(String(id))) {
|
|
261
|
+
return {
|
|
262
|
+
valid: false,
|
|
263
|
+
error: {
|
|
264
|
+
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
265
|
+
message: `Committed numbering id '${id}' was not found in numbering parts or document.`
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (parts?.relationshipsXml && Array.isArray(receipt.relationshipIds)) {
|
|
273
|
+
for (const id of receipt.relationshipIds) {
|
|
274
|
+
if (!relIdSet.has(String(id))) {
|
|
275
|
+
return {
|
|
276
|
+
valid: false,
|
|
277
|
+
error: {
|
|
278
|
+
code: 'RECEIPT_RECONCILIATION_FAILED',
|
|
279
|
+
message: `Committed relationship id '${id}' was not found in document.xml.rels.`
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { valid: true };
|
|
288
|
+
}
|