@ansonlai/docx-redline-js 0.2.1 → 0.4.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/AGENTS.md +36 -10
- package/README.md +83 -6
- package/adapters/xml-adapter.js +73 -10
- package/core/list-targeting.js +3 -0
- package/core/paragraph-targeting.js +33 -7
- package/core/redline-validation.js +22 -0
- package/core/types.js +122 -27
- package/core/xml-query.js +3 -1
- package/dist/docx-redline-js.esm.js +1124 -545
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +79 -78
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +687 -0
- package/docs/VALIDATION.md +81 -2
- package/docs/WORD-MANUAL-REVIEW.md +138 -0
- package/docs/plans/2026-08-30-reliability-testing-improvements.md +488 -0
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +210 -0
- package/docs/plans/{2026-03-01-release-0.1.4-design.md → completed/2026-03-01-release-0.1.4-design.md} +2 -0
- package/docs/plans/{2026-03-01-release-0.1.4.md → completed/2026-03-01-release-0.1.4.md} +5 -3
- package/docs/plans/{2026-05-31-architectural changes.md → completed/2026-05-31-architectural changes.md } +2 -0
- package/docs/plans/completed/2026-08-02-reliability-improvements.md +1155 -0
- package/docs/test-comparison-dashboard.html +95 -0
- package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +22 -0
- package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +24 -0
- package/docs/validation-reports/2026-08-30-phase-3-coverage.md +73 -0
- package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +82 -0
- package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +114 -0
- package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +79 -0
- package/engine/format-extraction.js +1 -1
- package/engine/formatting-removal.js +84 -99
- package/engine/oxml-engine.js +176 -83
- package/engine/reconstruction-mapper.js +276 -79
- package/engine/reconstruction-mode.js +20 -6
- package/engine/reconstruction-writer.js +117 -72
- package/engine/run-builders.js +3 -3
- package/engine/surgical-mode.js +3 -2
- package/engine/table-mode.js +27 -16
- package/index.d.ts +95 -3
- package/index.js +14 -13
- package/orchestration/list-structural-fallback.js +16 -39
- package/package.json +22 -4
- package/pipeline/diff-engine.js +174 -55
- package/pipeline/ingestion-export.js +39 -24
- package/pipeline/ingestion-paragraph.js +7 -5
- package/pipeline/list-generation.js +27 -18
- package/pipeline/patching.js +2 -3
- package/pipeline/pipeline.js +65 -36
- package/pipeline/serialization.js +13 -5
- package/scripts/build-test-dashboard.mjs +43 -0
- package/scripts/check-types.mjs +16 -24
- package/scripts/export-validation-fixtures.mjs +191 -45
- package/scripts/fetch-superdoc-corpus.mjs +61 -0
- package/scripts/generate-test-dashboard.mjs +199 -0
- package/scripts/inspect-visual-evidence.mjs +271 -0
- package/scripts/lib/minimal-zip.mjs +199 -18
- package/scripts/lib/word-coverage-catalogue.mjs +207 -0
- package/scripts/lib/word-coverage-metadata.mjs +93 -0
- package/scripts/lib/zip-reader.mjs +64 -0
- package/scripts/package-superdoc-word-fixtures.ps1 +64 -0
- package/scripts/prepare-corpus-word-visual-review.mjs +84 -0
- package/scripts/prepare-superdoc-word-corpus.mjs +284 -0
- package/scripts/prepare-word-review.mjs +77 -0
- package/scripts/prepare-word-visual-review.mjs +90 -0
- package/scripts/render-agenda-multilevel.mjs +70 -0
- package/scripts/render-case22.mjs +73 -0
- package/scripts/render-case40.ps1 +35 -0
- package/scripts/render-multilevel-bullet-images.py +58 -0
- package/scripts/render-multilevel-bullet-visual.ps1 +32 -0
- package/scripts/render-multilevel-cases.mjs +80 -0
- package/scripts/report-coverage-gaps.mjs +103 -0
- package/scripts/report-word-coverage.mjs +71 -0
- package/scripts/sample-multimodal-visual-check.mjs +221 -0
- package/scripts/test-multilevel-bullet-visual.mjs +187 -0
- package/scripts/word-com-corpus-suite.ps1 +43 -0
- package/scripts/word-com-corpus-visual-suite.ps1 +116 -0
- package/scripts/word-com-differential.ps1 +158 -16
- package/scripts/word-com-suite.ps1 +19 -0
- package/scripts/word-com-visual-suite.ps1 +132 -0
- package/services/comment-engine.js +51 -46
- package/services/comment-locator.js +0 -1
- package/services/comment-package.js +11 -10
- package/services/numbering-service.js +1 -1
- package/services/revision-comment-management.js +31 -10
- package/services/standalone-docx-plumbing.js +45 -34
- package/services/standalone-operation-runner.js +315 -75
- package/services/table-reconciliation.js +23 -11
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
* stay focused on UI + prompt orchestration.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
9
|
+
import { findReconstructionParagraphRange } from '../engine/reconstruction-mapper.js';
|
|
10
|
+
import {
|
|
11
|
+
RevisionIdAllocator,
|
|
12
|
+
createRevisionMetadata,
|
|
13
|
+
seedRevisionIdsFromDocument,
|
|
14
|
+
setRevisionIdAllocatorForDocument
|
|
15
|
+
} from '../core/types.js';
|
|
10
16
|
import { createWordElement } from '../core/word-xml.js';
|
|
17
|
+
import { markParagraphMarkInserted } from '../engine/run-builders.js';
|
|
11
18
|
import {
|
|
12
19
|
applyRedlineToOxml,
|
|
13
20
|
reconcileMarkdownTableOoxml,
|
|
@@ -16,6 +23,7 @@ import {
|
|
|
16
23
|
getParagraphText as getParagraphTextFromOxml,
|
|
17
24
|
isMarkdownTableText,
|
|
18
25
|
findContainingWordElement,
|
|
26
|
+
buildTargetReferenceSnapshot,
|
|
19
27
|
resolveTargetParagraphWithSnapshot as resolveTargetParagraphWithSnapshotShared,
|
|
20
28
|
buildSingleLineListStructuralFallbackPlan,
|
|
21
29
|
executeSingleLineListStructuralFallback,
|
|
@@ -43,6 +51,15 @@ import {
|
|
|
43
51
|
|
|
44
52
|
const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
45
53
|
|
|
54
|
+
function prepareRevisionAllocator(xmlDoc, options = {}) {
|
|
55
|
+
const allocator = options?._revisionIdAllocator instanceof RevisionIdAllocator
|
|
56
|
+
? options._revisionIdAllocator
|
|
57
|
+
: new RevisionIdAllocator();
|
|
58
|
+
seedRevisionIdsFromDocument(xmlDoc, allocator);
|
|
59
|
+
setRevisionIdAllocatorForDocument(xmlDoc, allocator);
|
|
60
|
+
return allocator;
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
function getParagraphText(paragraph) {
|
|
47
64
|
return getParagraphTextFromOxml(paragraph);
|
|
48
65
|
}
|
|
@@ -68,68 +85,15 @@ function normalizeBodySectionOrder(xmlDoc) {
|
|
|
68
85
|
normalizeBodySectionOrderStandalone(xmlDoc);
|
|
69
86
|
}
|
|
70
87
|
|
|
71
|
-
function directFieldCharType(node) {
|
|
72
|
-
if (!node || node.nodeType !== 1 || node.namespaceURI !== NS_W || node.localName !== 'r') return '';
|
|
73
|
-
const fldChar = Array.from(node.childNodes || []).find(
|
|
74
|
-
child => child && child.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'fldChar'
|
|
75
|
-
) || null;
|
|
76
|
-
return fldChar?.getAttribute('w:fldCharType') || fldChar?.getAttribute('fldCharType') || '';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function hasInstrText(node) {
|
|
80
|
-
if (!node || node.nodeType !== 1) return false;
|
|
81
|
-
return Array.from(node.childNodes || []).some(
|
|
82
|
-
child => child && child.nodeType === 1 && child.namespaceURI === NS_W && child.localName === 'instrText'
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
88
|
function removeProofErrNodes(paragraph) {
|
|
87
89
|
for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, 'proofErr') || [])) {
|
|
88
90
|
node.parentNode?.removeChild(node);
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
function unlinkFieldsInParagraph(paragraph) {
|
|
93
|
-
const children = Array.from(paragraph?.childNodes || []);
|
|
94
|
-
let inField = false;
|
|
95
|
-
let seenSeparate = false;
|
|
96
|
-
|
|
97
|
-
for (const child of children) {
|
|
98
|
-
if (child.nodeType !== 1) continue;
|
|
99
|
-
|
|
100
|
-
const fieldType = directFieldCharType(child);
|
|
101
|
-
if (fieldType === 'begin') {
|
|
102
|
-
inField = true;
|
|
103
|
-
seenSeparate = false;
|
|
104
|
-
child.parentNode?.removeChild(child);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!inField) continue;
|
|
109
|
-
|
|
110
|
-
if (fieldType === 'separate') {
|
|
111
|
-
seenSeparate = true;
|
|
112
|
-
child.parentNode?.removeChild(child);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (fieldType === 'end') {
|
|
117
|
-
inField = false;
|
|
118
|
-
seenSeparate = false;
|
|
119
|
-
child.parentNode?.removeChild(child);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!seenSeparate || hasInstrText(child)) {
|
|
124
|
-
child.parentNode?.removeChild(child);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
94
|
function preprocessRedlineTargetParagraph(targetParagraph) {
|
|
130
95
|
if (!targetParagraph) return;
|
|
131
96
|
removeProofErrNodes(targetParagraph);
|
|
132
|
-
unlinkFieldsInParagraph(targetParagraph);
|
|
133
97
|
}
|
|
134
98
|
|
|
135
99
|
function getDirectWordChild(element, localName) {
|
|
@@ -201,6 +165,14 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
|
|
|
201
165
|
}
|
|
202
166
|
ensureListProperties(xmlDoc, paragraph, entry.ilvl, entry.numId);
|
|
203
167
|
|
|
168
|
+
// A new list item is a whole inserted paragraph, not merely inserted text.
|
|
169
|
+
// Tracking its paragraph mark lets Word (and our accept/reject helpers)
|
|
170
|
+
// remove the list paragraph itself on Reject All instead of leaving an
|
|
171
|
+
// empty bullet or number behind.
|
|
172
|
+
if (generateRedlines) {
|
|
173
|
+
markParagraphMarkInserted(xmlDoc, paragraph, author);
|
|
174
|
+
}
|
|
175
|
+
|
|
204
176
|
const run = createWordElement(xmlDoc, 'w:r');
|
|
205
177
|
const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W, 'r'))[0] || null;
|
|
206
178
|
const anchorRunPr = anchorFirstRun ? getDirectWordChild(anchorFirstRun, 'rPr') : null;
|
|
@@ -214,7 +186,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
|
|
|
214
186
|
textNode.textContent = safeText;
|
|
215
187
|
run.appendChild(textNode);
|
|
216
188
|
if (generateRedlines) {
|
|
217
|
-
const metadata = revisionMetadata || createRevisionMetadata(author);
|
|
189
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc);
|
|
218
190
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
219
191
|
ins.setAttribute('w:id', String(metadata.id));
|
|
220
192
|
ins.setAttribute('w:author', metadata.author);
|
|
@@ -355,7 +327,7 @@ function applyExplicitRangeListInsertions({
|
|
|
355
327
|
numId: entry.numId,
|
|
356
328
|
text: entry.text
|
|
357
329
|
},
|
|
358
|
-
generateRedlines ? createRevisionMetadata(author) : null,
|
|
330
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
359
331
|
author,
|
|
360
332
|
{ generateRedlines }
|
|
361
333
|
);
|
|
@@ -485,7 +457,7 @@ function buildFallbackInsertedPlainParagraph(xmlDoc, text, revisionMetadata, aut
|
|
|
485
457
|
run.appendChild(textNode);
|
|
486
458
|
|
|
487
459
|
if (generateRedlines) {
|
|
488
|
-
const metadata = revisionMetadata || createRevisionMetadata(author);
|
|
460
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc);
|
|
489
461
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
490
462
|
ins.setAttribute('w:id', String(metadata.id));
|
|
491
463
|
ins.setAttribute('w:author', metadata.author);
|
|
@@ -522,7 +494,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
|
|
|
522
494
|
if (pPr) wrappedParagraph.appendChild(pPr.cloneNode(true));
|
|
523
495
|
|
|
524
496
|
const ins = createWordElement(xmlDoc, 'w:ins');
|
|
525
|
-
const metadata = revisionMetadata || createRevisionMetadata(author);
|
|
497
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc);
|
|
526
498
|
ins.setAttribute('w:id', String(metadata.id));
|
|
527
499
|
ins.setAttribute('w:author', metadata.author);
|
|
528
500
|
ins.setAttribute('w:date', metadata.date);
|
|
@@ -818,14 +790,14 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
818
790
|
const generateRedlines = options.generateRedlines !== false;
|
|
819
791
|
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
820
792
|
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
821
|
-
const parser = createParser();
|
|
822
793
|
const serializer = createSerializer();
|
|
823
|
-
const xmlDoc =
|
|
824
|
-
|
|
794
|
+
const xmlDoc = parseOoxmlSafe(documentXml, 'application/xml').doc;
|
|
795
|
+
if (!xmlDoc) return { documentXml, hasChanges: false, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
796
|
+
const revisionIdAllocator = prepareRevisionAllocator(xmlDoc, options);
|
|
825
797
|
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'redline', runtimeContext, { onInfo, onWarn });
|
|
826
798
|
const targetParagraph = resolved.paragraph;
|
|
827
799
|
preprocessRedlineTargetParagraph(targetParagraph);
|
|
828
|
-
const currentParagraphText = getParagraphText(targetParagraph)
|
|
800
|
+
const currentParagraphText = getParagraphText(targetParagraph);
|
|
829
801
|
const containingTable = findContainingWordElement(targetParagraph, 'tbl');
|
|
830
802
|
const rawTableStructuralCandidate = !!containingTable
|
|
831
803
|
&& !targetEndRef
|
|
@@ -865,7 +837,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
865
837
|
onInfo,
|
|
866
838
|
onWarn
|
|
867
839
|
})
|
|
868
|
-
:
|
|
840
|
+
: (typeof targetText === 'string' && /\r?\n/.test(targetText)
|
|
841
|
+
? findReconstructionParagraphRange(xmlDoc, targetText)
|
|
842
|
+
: null);
|
|
869
843
|
const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
|
|
870
844
|
if (!useTableScope && hasExplicitRangeScope) {
|
|
871
845
|
const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
|
|
@@ -928,7 +902,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
928
902
|
markerType: 'numbered',
|
|
929
903
|
text: adjacencyInsertionCandidate.text
|
|
930
904
|
},
|
|
931
|
-
generateRedlines ? createRevisionMetadata(author) : null,
|
|
905
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
932
906
|
author,
|
|
933
907
|
{ generateRedlines }
|
|
934
908
|
);
|
|
@@ -961,7 +935,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
961
935
|
xmlDoc,
|
|
962
936
|
targetParagraph,
|
|
963
937
|
paragraphText,
|
|
964
|
-
generateRedlines ? createRevisionMetadata(author) : null,
|
|
938
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
965
939
|
author,
|
|
966
940
|
{ generateRedlines }
|
|
967
941
|
);
|
|
@@ -992,7 +966,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
992
966
|
xmlDoc,
|
|
993
967
|
targetParagraph,
|
|
994
968
|
{ ...entry, numId: insertionOnlyPlan.numId },
|
|
995
|
-
generateRedlines ? createRevisionMetadata(author) : null,
|
|
969
|
+
generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
|
|
996
970
|
author,
|
|
997
971
|
{ generateRedlines }
|
|
998
972
|
);
|
|
@@ -1070,12 +1044,14 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
1070
1044
|
author,
|
|
1071
1045
|
generateRedlines,
|
|
1072
1046
|
existingRevisions: options.existingRevisions,
|
|
1047
|
+
_revisionIdAllocator: revisionIdAllocator,
|
|
1073
1048
|
_isolatedTableCell: useTableScope
|
|
1074
1049
|
})
|
|
1075
1050
|
: await applyRedlineToOxml(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
1076
1051
|
author,
|
|
1077
1052
|
generateRedlines,
|
|
1078
1053
|
existingRevisions: options.existingRevisions,
|
|
1054
|
+
_revisionIdAllocator: revisionIdAllocator,
|
|
1079
1055
|
_isolatedTableCell: useTableScope
|
|
1080
1056
|
});
|
|
1081
1057
|
if (!result?.hasChanges) {
|
|
@@ -1131,13 +1107,18 @@ async function applyHighlightToParagraphByExactText(documentXml, targetText, tex
|
|
|
1131
1107
|
const generateRedlines = options.generateRedlines !== false;
|
|
1132
1108
|
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
1133
1109
|
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
1134
|
-
const parser = createParser();
|
|
1135
1110
|
const serializer = createSerializer();
|
|
1136
|
-
const xmlDoc =
|
|
1111
|
+
const xmlDoc = parseOoxmlSafe(documentXml, 'application/xml').doc;
|
|
1112
|
+
if (!xmlDoc) return { documentXml, hasChanges: false, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
1113
|
+
const revisionIdAllocator = prepareRevisionAllocator(xmlDoc, options);
|
|
1137
1114
|
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'highlight', runtimeContext, { onInfo, onWarn });
|
|
1138
1115
|
const targetParagraph = resolved.paragraph;
|
|
1139
1116
|
const paragraphXml = serializer.serializeToString(targetParagraph);
|
|
1140
|
-
const highlightedXml = applyHighlightToOoxml(paragraphXml, textToHighlight, color, {
|
|
1117
|
+
const highlightedXml = applyHighlightToOoxml(paragraphXml, textToHighlight, color, {
|
|
1118
|
+
generateRedlines,
|
|
1119
|
+
author,
|
|
1120
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
1121
|
+
});
|
|
1141
1122
|
if (!highlightedXml || highlightedXml === paragraphXml) return { documentXml, hasChanges: false };
|
|
1142
1123
|
const { replacementNodes } = extractReplacementNodes(highlightedXml);
|
|
1143
1124
|
const parent = targetParagraph.parentNode;
|
|
@@ -1150,9 +1131,10 @@ async function applyHighlightToParagraphByExactText(documentXml, targetText, tex
|
|
|
1150
1131
|
async function applyCommentToParagraphByExactText(documentXml, targetText, textToComment, commentContent, author, targetRef = null, runtimeContext = null, options = {}) {
|
|
1151
1132
|
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
1152
1133
|
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
1153
|
-
const parser = createParser();
|
|
1154
1134
|
const serializer = createSerializer();
|
|
1155
|
-
const xmlDoc =
|
|
1135
|
+
const xmlDoc = parseOoxmlSafe(documentXml, 'application/xml').doc;
|
|
1136
|
+
if (!xmlDoc) return { documentXml, hasChanges: false, commentsXml: null, status: 'error', error: { code: 'PARSE_ERROR', message: 'Could not parse document OOXML.' } };
|
|
1137
|
+
prepareRevisionAllocator(xmlDoc, options);
|
|
1156
1138
|
const resolved = resolveTargetParagraph(xmlDoc, targetText, targetRef, 'comment', runtimeContext, { onInfo, onWarn });
|
|
1157
1139
|
const targetParagraph = resolved.paragraph;
|
|
1158
1140
|
const paragraphXml = serializer.serializeToString(targetParagraph);
|
|
@@ -1166,6 +1148,127 @@ async function applyCommentToParagraphByExactText(documentXml, targetText, textT
|
|
|
1166
1148
|
return { documentXml: serializer.serializeToString(xmlDoc), hasChanges: true, commentsXml: commentResult.commentsXml || null, warnings: commentResult.warnings || [] };
|
|
1167
1149
|
}
|
|
1168
1150
|
|
|
1151
|
+
function operationTargetPriority(op) {
|
|
1152
|
+
if (op?.type === 'comment') return 0;
|
|
1153
|
+
return 1;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Returns a stable operation order that resolves anchor-based operations before
|
|
1158
|
+
* text edits can mutate their target text. Comments run first; all other
|
|
1159
|
+
* operation types retain their original relative order.
|
|
1160
|
+
*
|
|
1161
|
+
* @param {Object[]} operations - Structured document operations
|
|
1162
|
+
* @returns {Object[]} A reordered copy; input objects and input array are not mutated
|
|
1163
|
+
*/
|
|
1164
|
+
export function orderOperationsForStableTargets(operations = []) {
|
|
1165
|
+
return (Array.isArray(operations) ? operations : [])
|
|
1166
|
+
.map((operation, index) => ({ operation, index }))
|
|
1167
|
+
.sort((a, b) => operationTargetPriority(a.operation) - operationTargetPriority(b.operation) || a.index - b.index)
|
|
1168
|
+
.map(entry => entry.operation);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function mergeCommentsXml(existingXml, incomingXml) {
|
|
1172
|
+
if (!incomingXml) return existingXml || null;
|
|
1173
|
+
if (!existingXml) return incomingXml;
|
|
1174
|
+
|
|
1175
|
+
const serializer = createSerializer();
|
|
1176
|
+
const existingDoc = parseOoxmlSafe(existingXml, 'application/xml').doc;
|
|
1177
|
+
const incomingDoc = parseOoxmlSafe(incomingXml, 'application/xml').doc;
|
|
1178
|
+
if (!existingDoc || !incomingDoc) return existingXml;
|
|
1179
|
+
const existingRoot = existingDoc.documentElement;
|
|
1180
|
+
const existingIds = new Set(
|
|
1181
|
+
Array.from(existingRoot.getElementsByTagNameNS(NS_W, 'comment'))
|
|
1182
|
+
.map(comment => comment.getAttribute('w:id') || comment.getAttribute('id'))
|
|
1183
|
+
.filter(Boolean)
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W, 'comment'))) {
|
|
1187
|
+
const id = comment.getAttribute('w:id') || comment.getAttribute('id');
|
|
1188
|
+
if (id && existingIds.has(id)) continue;
|
|
1189
|
+
existingRoot.appendChild(existingDoc.importNode(comment, true));
|
|
1190
|
+
if (id) existingIds.add(id);
|
|
1191
|
+
}
|
|
1192
|
+
return serializer.serializeToString(existingDoc);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function cloneBatchRuntimeContext(runtimeContext) {
|
|
1196
|
+
if (!runtimeContext || typeof runtimeContext !== 'object') return {};
|
|
1197
|
+
|
|
1198
|
+
const context = { ...runtimeContext };
|
|
1199
|
+
if (runtimeContext.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1200
|
+
context.listFallbackSharedNumIdByKey = new Map(runtimeContext.listFallbackSharedNumIdByKey);
|
|
1201
|
+
}
|
|
1202
|
+
if (runtimeContext.tableStructuralRedlineKeys instanceof Set) {
|
|
1203
|
+
context.tableStructuralRedlineKeys = new Set(runtimeContext.tableStructuralRedlineKeys);
|
|
1204
|
+
}
|
|
1205
|
+
if (runtimeContext.numberingIdState && typeof runtimeContext.numberingIdState === 'object') {
|
|
1206
|
+
context.numberingIdState = {
|
|
1207
|
+
...runtimeContext.numberingIdState,
|
|
1208
|
+
usedNumIds: runtimeContext.numberingIdState.usedNumIds instanceof Set
|
|
1209
|
+
? new Set(runtimeContext.numberingIdState.usedNumIds)
|
|
1210
|
+
: runtimeContext.numberingIdState.usedNumIds,
|
|
1211
|
+
usedAbstractNumIds: runtimeContext.numberingIdState.usedAbstractNumIds instanceof Set
|
|
1212
|
+
? new Set(runtimeContext.numberingIdState.usedAbstractNumIds)
|
|
1213
|
+
: runtimeContext.numberingIdState.usedAbstractNumIds
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
if (runtimeContext.listFallbackSequenceState && typeof runtimeContext.listFallbackSequenceState === 'object') {
|
|
1217
|
+
context.listFallbackSequenceState = {
|
|
1218
|
+
...runtimeContext.listFallbackSequenceState,
|
|
1219
|
+
explicitByNumberingKey: runtimeContext.listFallbackSequenceState.explicitByNumberingKey instanceof Map
|
|
1220
|
+
? new Map(runtimeContext.listFallbackSequenceState.explicitByNumberingKey)
|
|
1221
|
+
: runtimeContext.listFallbackSequenceState.explicitByNumberingKey
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
return context;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function commitBatchRuntimeContext(runtimeContext, context) {
|
|
1228
|
+
if (!runtimeContext || typeof runtimeContext !== 'object') return;
|
|
1229
|
+
|
|
1230
|
+
if (runtimeContext.listFallbackSharedNumIdByKey instanceof Map && context.listFallbackSharedNumIdByKey instanceof Map) {
|
|
1231
|
+
runtimeContext.listFallbackSharedNumIdByKey.clear();
|
|
1232
|
+
for (const entry of context.listFallbackSharedNumIdByKey) runtimeContext.listFallbackSharedNumIdByKey.set(...entry);
|
|
1233
|
+
context.listFallbackSharedNumIdByKey = runtimeContext.listFallbackSharedNumIdByKey;
|
|
1234
|
+
}
|
|
1235
|
+
if (runtimeContext.tableStructuralRedlineKeys instanceof Set && context.tableStructuralRedlineKeys instanceof Set) {
|
|
1236
|
+
runtimeContext.tableStructuralRedlineKeys.clear();
|
|
1237
|
+
for (const value of context.tableStructuralRedlineKeys) runtimeContext.tableStructuralRedlineKeys.add(value);
|
|
1238
|
+
context.tableStructuralRedlineKeys = runtimeContext.tableStructuralRedlineKeys;
|
|
1239
|
+
}
|
|
1240
|
+
if (runtimeContext.numberingIdState && context.numberingIdState) {
|
|
1241
|
+
for (const key of ['usedNumIds', 'usedAbstractNumIds']) {
|
|
1242
|
+
if (runtimeContext.numberingIdState[key] instanceof Set && context.numberingIdState[key] instanceof Set) {
|
|
1243
|
+
runtimeContext.numberingIdState[key].clear();
|
|
1244
|
+
for (const value of context.numberingIdState[key]) runtimeContext.numberingIdState[key].add(value);
|
|
1245
|
+
context.numberingIdState[key] = runtimeContext.numberingIdState[key];
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
Object.assign(runtimeContext.numberingIdState, context.numberingIdState);
|
|
1249
|
+
context.numberingIdState = runtimeContext.numberingIdState;
|
|
1250
|
+
}
|
|
1251
|
+
if (runtimeContext.listFallbackSequenceState && context.listFallbackSequenceState) {
|
|
1252
|
+
const originalMap = runtimeContext.listFallbackSequenceState.explicitByNumberingKey;
|
|
1253
|
+
const updatedMap = context.listFallbackSequenceState.explicitByNumberingKey;
|
|
1254
|
+
if (originalMap instanceof Map && updatedMap instanceof Map) {
|
|
1255
|
+
originalMap.clear();
|
|
1256
|
+
for (const entry of updatedMap) originalMap.set(...entry);
|
|
1257
|
+
context.listFallbackSequenceState.explicitByNumberingKey = originalMap;
|
|
1258
|
+
}
|
|
1259
|
+
Object.assign(runtimeContext.listFallbackSequenceState, context.listFallbackSequenceState);
|
|
1260
|
+
context.listFallbackSequenceState = runtimeContext.listFallbackSequenceState;
|
|
1261
|
+
}
|
|
1262
|
+
Object.assign(runtimeContext, context);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function normalizeOperationError(error) {
|
|
1266
|
+
return {
|
|
1267
|
+
code: typeof error?.code === 'string' && error.code ? error.code : 'OPERATION_ERROR',
|
|
1268
|
+
message: error?.message || String(error)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1169
1272
|
/**
|
|
1170
1273
|
* Applies one structured operation (`redline`, `highlight`, or `comment`) to
|
|
1171
1274
|
* a full `word/document.xml` payload.
|
|
@@ -1182,6 +1285,14 @@ async function applyCommentToParagraphByExactText(documentXml, targetText, textT
|
|
|
1182
1285
|
* @returns {Promise<{ documentXml: string, hasChanges: boolean, numberingXml?: string|null, commentsXml?: string|null, warnings?: string[] }>}
|
|
1183
1286
|
*/
|
|
1184
1287
|
export async function applyOperationToDocumentXml(documentXml, op, author, runtimeContext = null, options = {}) {
|
|
1288
|
+
const parsed = parseOoxmlSafe(documentXml, 'application/xml');
|
|
1289
|
+
if (parsed.error || !parsed.doc) {
|
|
1290
|
+
return { documentXml, hasChanges: false, status: 'error', error: parsed.error, warnings: parsed.warnings };
|
|
1291
|
+
}
|
|
1292
|
+
const operationOptions = {
|
|
1293
|
+
...options,
|
|
1294
|
+
_revisionIdAllocator: prepareRevisionAllocator(parsed.doc, options)
|
|
1295
|
+
};
|
|
1185
1296
|
if (op?.type === 'highlight') {
|
|
1186
1297
|
return applyHighlightToParagraphByExactText(
|
|
1187
1298
|
documentXml,
|
|
@@ -1191,7 +1302,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
1191
1302
|
author,
|
|
1192
1303
|
op.targetRef,
|
|
1193
1304
|
runtimeContext,
|
|
1194
|
-
|
|
1305
|
+
operationOptions
|
|
1195
1306
|
);
|
|
1196
1307
|
}
|
|
1197
1308
|
if (op?.type === 'comment') {
|
|
@@ -1203,7 +1314,7 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
1203
1314
|
author,
|
|
1204
1315
|
op.targetRef,
|
|
1205
1316
|
runtimeContext,
|
|
1206
|
-
|
|
1317
|
+
operationOptions
|
|
1207
1318
|
);
|
|
1208
1319
|
}
|
|
1209
1320
|
return applyToParagraphByExactText(
|
|
@@ -1214,6 +1325,135 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
1214
1325
|
op?.targetRef,
|
|
1215
1326
|
op?.targetEndRef,
|
|
1216
1327
|
runtimeContext,
|
|
1217
|
-
|
|
1328
|
+
operationOptions
|
|
1218
1329
|
);
|
|
1219
1330
|
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Applies a batch of operations using stable target ordering. This prevents a
|
|
1334
|
+
* later comment from missing original text changed by an earlier replacement
|
|
1335
|
+
* in the same batch.
|
|
1336
|
+
*
|
|
1337
|
+
* Results retain each operation's original 1-based index even though execution
|
|
1338
|
+
* is reordered. Numbering payloads are returned as an array so package callers
|
|
1339
|
+
* can merge each one with `ensureNumberingArtifactsInZip`.
|
|
1340
|
+
*
|
|
1341
|
+
* @param {string} documentXml - Full `word/document.xml` payload
|
|
1342
|
+
* @param {Object[]} operations - Structured operations
|
|
1343
|
+
* @param {string} author - Revision/comment author
|
|
1344
|
+
* @param {Object|null} [runtimeContext=null] - Shared turn context
|
|
1345
|
+
* @param {{
|
|
1346
|
+
* atomic?: boolean,
|
|
1347
|
+
* continueOnError?: boolean,
|
|
1348
|
+
* generateRedlines?: boolean,
|
|
1349
|
+
* onInfo?: (message: string) => void,
|
|
1350
|
+
* onWarn?: (message: string) => void
|
|
1351
|
+
* }} [options={}] - Runner options. `atomic` defaults to `true`, returning the
|
|
1352
|
+
* original document and no package artifacts if any operation fails.
|
|
1353
|
+
* `continueOnError` defaults to `true`, so all operations are attempted and
|
|
1354
|
+
* represented in `results`; `false` stops after the first operation error.
|
|
1355
|
+
* @returns {Promise<{
|
|
1356
|
+
* documentXml: string,
|
|
1357
|
+
* hasChanges: boolean,
|
|
1358
|
+
* commentsXml: string|null,
|
|
1359
|
+
* numberingXmlParts: string[],
|
|
1360
|
+
* results: Array<{index:number,type:string,status:string,warnings?:string[],error?:Object}>,
|
|
1361
|
+
* executionOrder: number[],
|
|
1362
|
+
* rolledBack?: boolean
|
|
1363
|
+
* }>}
|
|
1364
|
+
*/
|
|
1365
|
+
export async function applyOperationsToDocumentXml(documentXml, operations, author, runtimeContext = null, options = {}) {
|
|
1366
|
+
const parsed = parseOoxmlSafe(documentXml, 'application/xml');
|
|
1367
|
+
if (parsed.error || !parsed.doc) {
|
|
1368
|
+
return {
|
|
1369
|
+
documentXml,
|
|
1370
|
+
hasChanges: false,
|
|
1371
|
+
commentsXml: null,
|
|
1372
|
+
numberingXmlParts: [],
|
|
1373
|
+
results: [],
|
|
1374
|
+
executionOrder: [],
|
|
1375
|
+
status: 'error',
|
|
1376
|
+
error: parsed.error,
|
|
1377
|
+
warnings: parsed.warnings
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
const sourceOperations = Array.isArray(operations) ? operations : [];
|
|
1381
|
+
const scheduled = sourceOperations
|
|
1382
|
+
.map((operation, index) => ({ operation, index }))
|
|
1383
|
+
.sort((a, b) => operationTargetPriority(a.operation) - operationTargetPriority(b.operation) || a.index - b.index);
|
|
1384
|
+
|
|
1385
|
+
const atomic = options.atomic !== false;
|
|
1386
|
+
const continueOnError = options.continueOnError !== false;
|
|
1387
|
+
const context = cloneBatchRuntimeContext(runtimeContext);
|
|
1388
|
+
if (!(context.targetRefSnapshot instanceof Map)) {
|
|
1389
|
+
context.targetRefSnapshot = buildTargetReferenceSnapshot(parsed.doc);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
let currentDocumentXml = documentXml;
|
|
1393
|
+
let commentsXml = null;
|
|
1394
|
+
let hasChanges = false;
|
|
1395
|
+
const numberingXmlParts = [];
|
|
1396
|
+
const results = [];
|
|
1397
|
+
const executionOrder = [];
|
|
1398
|
+
let operationFailed = false;
|
|
1399
|
+
|
|
1400
|
+
for (const entry of scheduled) {
|
|
1401
|
+
const { operation, index } = entry;
|
|
1402
|
+
executionOrder.push(index + 1);
|
|
1403
|
+
try {
|
|
1404
|
+
const result = await applyOperationToDocumentXml(
|
|
1405
|
+
currentDocumentXml,
|
|
1406
|
+
operation,
|
|
1407
|
+
author,
|
|
1408
|
+
context,
|
|
1409
|
+
options
|
|
1410
|
+
);
|
|
1411
|
+
currentDocumentXml = result.documentXml;
|
|
1412
|
+
hasChanges = hasChanges || result.hasChanges === true;
|
|
1413
|
+
commentsXml = mergeCommentsXml(commentsXml, result.commentsXml || null);
|
|
1414
|
+
if (result.numberingXml) numberingXmlParts.push(result.numberingXml);
|
|
1415
|
+
const isError = result.status === 'error' || !!result.error;
|
|
1416
|
+
operationFailed = operationFailed || isError;
|
|
1417
|
+
results.push({
|
|
1418
|
+
index: index + 1,
|
|
1419
|
+
type: operation?.type || 'redline',
|
|
1420
|
+
status: isError ? 'error' : (result.hasChanges ? 'applied' : 'no_change'),
|
|
1421
|
+
...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
1422
|
+
...(result.error ? { error: result.error } : {})
|
|
1423
|
+
});
|
|
1424
|
+
if (isError && !continueOnError) break;
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
const normalizedError = normalizeOperationError(error);
|
|
1427
|
+
operationFailed = true;
|
|
1428
|
+
results.push({
|
|
1429
|
+
index: index + 1,
|
|
1430
|
+
type: operation?.type || 'redline',
|
|
1431
|
+
status: 'error',
|
|
1432
|
+
warnings: [normalizedError.message],
|
|
1433
|
+
error: normalizedError
|
|
1434
|
+
});
|
|
1435
|
+
if (!continueOnError) break;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
results.sort((a, b) => a.index - b.index);
|
|
1440
|
+
const rolledBack = atomic && operationFailed;
|
|
1441
|
+
if (!rolledBack) commitBatchRuntimeContext(runtimeContext, context);
|
|
1442
|
+
|
|
1443
|
+
return {
|
|
1444
|
+
documentXml: rolledBack ? documentXml : currentDocumentXml,
|
|
1445
|
+
hasChanges: rolledBack ? false : hasChanges,
|
|
1446
|
+
commentsXml: rolledBack ? null : commentsXml,
|
|
1447
|
+
numberingXmlParts: rolledBack ? [] : numberingXmlParts,
|
|
1448
|
+
results,
|
|
1449
|
+
executionOrder,
|
|
1450
|
+
...(rolledBack ? {
|
|
1451
|
+
rolledBack: true,
|
|
1452
|
+
status: 'error',
|
|
1453
|
+
error: {
|
|
1454
|
+
code: 'BATCH_OPERATION_FAILED',
|
|
1455
|
+
message: 'Atomic batch rolled back because one or more operations failed.'
|
|
1456
|
+
}
|
|
1457
|
+
} : {})
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { computeWordLevelDiffOps } from '../pipeline/diff-engine.js';
|
|
8
8
|
import { splitRunsAtDiffBoundaries, applyPatches } from '../pipeline/patching.js';
|
|
9
9
|
import { serializeToOoxml } from '../pipeline/serialization.js';
|
|
10
|
-
import {
|
|
10
|
+
import { createRevisionMetadata, escapeXml, RunKind } from '../core/types.js';
|
|
11
11
|
import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -19,8 +19,8 @@ import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
|
|
|
19
19
|
* @returns {string} Complete w:tbl OOXML
|
|
20
20
|
*/
|
|
21
21
|
export function generateTableOoxml(tableData, options = {}) {
|
|
22
|
-
const { generateRedlines = false, author = 'AI' } = options;
|
|
23
|
-
const tableInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
|
|
22
|
+
const { generateRedlines = false, author = 'AI', revisionIdAllocator = null } = options;
|
|
23
|
+
const tableInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
24
24
|
|
|
25
25
|
// Determine number of columns
|
|
26
26
|
const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
|
|
@@ -68,7 +68,11 @@ export function generateTableOoxml(tableData, options = {}) {
|
|
|
68
68
|
endOffset: cleanText.length
|
|
69
69
|
}];
|
|
70
70
|
|
|
71
|
-
const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
|
|
71
|
+
const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
|
|
72
|
+
author,
|
|
73
|
+
generateRedlines,
|
|
74
|
+
revisionIdAllocator
|
|
75
|
+
});
|
|
72
76
|
|
|
73
77
|
// Cell properties
|
|
74
78
|
const tcPr = '<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>';
|
|
@@ -164,7 +168,7 @@ export function diffTablesWithVirtualGrid(oldGrid, newTableData) {
|
|
|
164
168
|
* @returns {string} Reconciled w:tbl OOXML
|
|
165
169
|
*/
|
|
166
170
|
export function serializeVirtualGridToOoxml(grid, operations, options) {
|
|
167
|
-
const { generateRedlines, author } = options;
|
|
171
|
+
const { generateRedlines, author, revisionIdAllocator = null } = options;
|
|
168
172
|
const opIndex = buildTableOperationIndex(operations);
|
|
169
173
|
|
|
170
174
|
let rowsXml = '';
|
|
@@ -206,7 +210,7 @@ export function serializeVirtualGridToOoxml(grid, operations, options) {
|
|
|
206
210
|
let trPr = grid.trPrList[row] || '<w:trPr/>';
|
|
207
211
|
|
|
208
212
|
if (rowDeleteOp && generateRedlines) {
|
|
209
|
-
const metadata = createRevisionMetadata(author);
|
|
213
|
+
const metadata = createRevisionMetadata(author, revisionIdAllocator);
|
|
210
214
|
const delMark = `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"/>`;
|
|
211
215
|
if (trPr.includes('</w:trPr>')) {
|
|
212
216
|
trPr = trPr.replace('</w:trPr>', `${delMark}</w:trPr>`);
|
|
@@ -222,7 +226,7 @@ export function serializeVirtualGridToOoxml(grid, operations, options) {
|
|
|
222
226
|
const insertOps = opIndex.rowInsertOperations;
|
|
223
227
|
for (const op of insertOps) {
|
|
224
228
|
let cellsXml = '';
|
|
225
|
-
const rowInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
|
|
229
|
+
const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
226
230
|
|
|
227
231
|
for (const cellText of op.cells) {
|
|
228
232
|
const { cleanText, formatHints } = preprocessMarkdown(cellText);
|
|
@@ -236,7 +240,11 @@ export function serializeVirtualGridToOoxml(grid, operations, options) {
|
|
|
236
240
|
endOffset: cleanText.length
|
|
237
241
|
}];
|
|
238
242
|
|
|
239
|
-
const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
|
|
243
|
+
const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
|
|
244
|
+
author,
|
|
245
|
+
generateRedlines,
|
|
246
|
+
revisionIdAllocator
|
|
247
|
+
});
|
|
240
248
|
// serializeToOoxml already returns one or more w:p blocks.
|
|
241
249
|
cellsXml += `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>${runsOoxml}</w:tc>`;
|
|
242
250
|
}
|
|
@@ -293,7 +301,7 @@ function buildTableOperationIndex(operations) {
|
|
|
293
301
|
}
|
|
294
302
|
|
|
295
303
|
function reconcileCellContent(cell, newText, options) {
|
|
296
|
-
const { generateRedlines, author } = options;
|
|
304
|
+
const { generateRedlines, author, revisionIdAllocator = null } = options;
|
|
297
305
|
const { cleanText, formatHints } = preprocessMarkdown(newText);
|
|
298
306
|
|
|
299
307
|
// For now, satisfy with single block or join blocks
|
|
@@ -310,7 +318,11 @@ function reconcileCellContent(cell, newText, options) {
|
|
|
310
318
|
formatHints
|
|
311
319
|
});
|
|
312
320
|
|
|
313
|
-
const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, {
|
|
321
|
+
const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, {
|
|
322
|
+
author,
|
|
323
|
+
generateRedlines,
|
|
324
|
+
revisionIdAllocator
|
|
325
|
+
});
|
|
314
326
|
return runsOoxml;
|
|
315
327
|
}
|
|
316
328
|
|
|
@@ -322,7 +334,7 @@ function serializeCellBlocks(blocks) {
|
|
|
322
334
|
}).join('');
|
|
323
335
|
}
|
|
324
336
|
|
|
325
|
-
function buildTcXml(cell, content,
|
|
337
|
+
function buildTcXml(cell, content, _options) {
|
|
326
338
|
let tcPr = cell.tcPrXml;
|
|
327
339
|
|
|
328
340
|
// Ensure gridSpan is preserved
|