@ansonlai/docx-redline-js 0.1.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 +176 -0
- package/ARCHITECTURE.md +121 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/adapters/config.js +43 -0
- package/adapters/logger.js +89 -0
- package/adapters/xml-adapter.js +74 -0
- package/core/list-targeting.js +398 -0
- package/core/ooxml-identifiers.js +15 -0
- package/core/paragraph-offset-policy.js +50 -0
- package/core/paragraph-targeting.js +501 -0
- package/core/table-targeting.js +233 -0
- package/core/types.js +204 -0
- package/core/xml-query.js +99 -0
- package/dist/docx-redline-js.esm.js +8801 -0
- package/dist/docx-redline-js.esm.js.map +7 -0
- package/dist/docx-redline-js.esm.min.js +195 -0
- package/dist/docx-redline-js.esm.min.js.map +7 -0
- package/engine/format-application.js +358 -0
- package/engine/format-extraction.js +232 -0
- package/engine/format-paragraph-targeting.js +208 -0
- package/engine/format-span-application.js +178 -0
- package/engine/formatting-removal.js +330 -0
- package/engine/oxml-engine.js +279 -0
- package/engine/reconstruction-mapper.js +270 -0
- package/engine/reconstruction-mode.js +38 -0
- package/engine/reconstruction-writer.js +276 -0
- package/engine/rpr-helpers.js +194 -0
- package/engine/run-builders.js +235 -0
- package/engine/surgical-mode.js +520 -0
- package/engine/table-cell-context.js +151 -0
- package/engine/table-mode.js +172 -0
- package/index.js +308 -0
- package/orchestration/list-markdown.js +141 -0
- package/orchestration/list-parsing.js +73 -0
- package/orchestration/list-structural-fallback.js +530 -0
- package/orchestration/redline-operation-converter.js +141 -0
- package/orchestration/route-plan.js +160 -0
- package/package.json +76 -0
- package/pipeline/content-analysis.js +107 -0
- package/pipeline/diff-engine.js +204 -0
- package/pipeline/ingestion-export.js +255 -0
- package/pipeline/ingestion-paragraph.js +351 -0
- package/pipeline/ingestion-table.js +169 -0
- package/pipeline/ingestion-xml.js +39 -0
- package/pipeline/ingestion.js +8 -0
- package/pipeline/list-generation.js +280 -0
- package/pipeline/list-markers.js +77 -0
- package/pipeline/markdown-processor.js +160 -0
- package/pipeline/patching.js +408 -0
- package/pipeline/pipeline.js +326 -0
- package/pipeline/serialization.js +395 -0
- package/services/browser-demo-prompt-context.js +345 -0
- package/services/comment-builders.js +60 -0
- package/services/comment-engine.js +248 -0
- package/services/comment-locator.js +197 -0
- package/services/comment-package.js +113 -0
- package/services/numbering-helpers.js +416 -0
- package/services/numbering-service.js +290 -0
- package/services/package-builder.js +147 -0
- package/services/standalone-docx-plumbing.js +443 -0
- package/services/standalone-operation-runner.js +1169 -0
- package/services/table-reconciliation.js +344 -0
- package/standalone.js +5 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconstruction mapper.
|
|
3
|
+
*
|
|
4
|
+
* Builds paragraph/property/sentinel mappings and indexed lookups used by reconstruction writing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { appendParagraphBoundary } from '../core/paragraph-offset-policy.js';
|
|
8
|
+
import { getDocumentParagraphs } from './format-extraction.js';
|
|
9
|
+
import { getElementsByTag, getFirstElementByTag } from '../core/xml-query.js';
|
|
10
|
+
|
|
11
|
+
function createRangeCursorLookup(ranges) {
|
|
12
|
+
let cursor = 0;
|
|
13
|
+
return {
|
|
14
|
+
at(index) {
|
|
15
|
+
while (cursor < ranges.length && ranges[cursor].end <= index) {
|
|
16
|
+
cursor++;
|
|
17
|
+
}
|
|
18
|
+
const match = ranges[cursor];
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
if (match.start <= index && index < match.end) return match;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function indexSentinelsByStart(sentinelMap) {
|
|
27
|
+
const sentinelMapByStart = new Map();
|
|
28
|
+
sentinelMap.forEach(sentinel => {
|
|
29
|
+
if (!sentinelMapByStart.has(sentinel.start)) {
|
|
30
|
+
sentinelMapByStart.set(sentinel.start, []);
|
|
31
|
+
}
|
|
32
|
+
sentinelMapByStart.get(sentinel.start).push(sentinel);
|
|
33
|
+
});
|
|
34
|
+
return sentinelMapByStart;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds reconstruction mapping and cursor-based lookup helpers.
|
|
39
|
+
*
|
|
40
|
+
* @param {Document} xmlDoc - XML document
|
|
41
|
+
* @param {string} modifiedText - Modified text
|
|
42
|
+
* @returns {{
|
|
43
|
+
* paragraphs: Element[],
|
|
44
|
+
* body: Element|Document,
|
|
45
|
+
* paragraphMap: Array<{start:number,end:number,pPr:Element|null,container:Node}>,
|
|
46
|
+
* paragraphStarts: Set<number>,
|
|
47
|
+
* propertyMap: Array<{start:number,end:number,rPr:Element|null,wrapper?:Element}>,
|
|
48
|
+
* sentinelMap: Array<Object>,
|
|
49
|
+
* sentinelMapByStart: Map<number, Object[]>,
|
|
50
|
+
* referenceMap: Map<string, Node>,
|
|
51
|
+
* tokenToCharMap: Map<string, string>,
|
|
52
|
+
* containerFragments: Map<Node, DocumentFragment>,
|
|
53
|
+
* replacementContainers: Map<Node, Node>,
|
|
54
|
+
* originalFullText: string,
|
|
55
|
+
* processedModifiedText: string,
|
|
56
|
+
* getParagraphInfo: (index:number) => {start:number,end:number,pPr:Element|null,container:Node},
|
|
57
|
+
* getRunProperties: (index:number) => {rPr:Element|null,wrapper?:Element},
|
|
58
|
+
* getPropertySpanLength: (index:number,maxLength:number) => number,
|
|
59
|
+
* isParagraphStart: (index:number) => boolean
|
|
60
|
+
* }}
|
|
61
|
+
*/
|
|
62
|
+
export function buildReconstructionMapping(xmlDoc, modifiedText) {
|
|
63
|
+
const rootElement = xmlDoc.documentElement;
|
|
64
|
+
const isBodyRoot = rootElement.nodeName === 'w:body' || rootElement.nodeName.endsWith(':package');
|
|
65
|
+
const paragraphs = getDocumentParagraphs(xmlDoc);
|
|
66
|
+
|
|
67
|
+
let body = getFirstElementByTag(xmlDoc, 'w:body');
|
|
68
|
+
if (!body && isBodyRoot) body = rootElement;
|
|
69
|
+
|
|
70
|
+
let originalFullText = '';
|
|
71
|
+
const propertyMap = [];
|
|
72
|
+
const paragraphMap = [];
|
|
73
|
+
const sentinelMap = [];
|
|
74
|
+
const referenceMap = new Map();
|
|
75
|
+
const tokenToCharMap = new Map();
|
|
76
|
+
let nextCharCode = 0xe000;
|
|
77
|
+
const uniqueContainers = new Set();
|
|
78
|
+
|
|
79
|
+
paragraphs.forEach((paragraph, paragraphIndex) => {
|
|
80
|
+
const paragraphStart = originalFullText.length;
|
|
81
|
+
|
|
82
|
+
Array.from(paragraph.childNodes).forEach(child => {
|
|
83
|
+
originalFullText = processChildNode(
|
|
84
|
+
child,
|
|
85
|
+
originalFullText,
|
|
86
|
+
propertyMap,
|
|
87
|
+
sentinelMap,
|
|
88
|
+
referenceMap,
|
|
89
|
+
tokenToCharMap,
|
|
90
|
+
nextCharCode
|
|
91
|
+
);
|
|
92
|
+
if (referenceMap.size > tokenToCharMap.size) {
|
|
93
|
+
nextCharCode++;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
originalFullText = appendParagraphBoundary(originalFullText, paragraphIndex, paragraphs.length);
|
|
98
|
+
|
|
99
|
+
const paragraphEnd = originalFullText.length;
|
|
100
|
+
const pPr = getFirstElementByTag(paragraph, 'w:pPr');
|
|
101
|
+
const container = paragraph.parentNode;
|
|
102
|
+
if (container) uniqueContainers.add(container);
|
|
103
|
+
|
|
104
|
+
paragraphMap.push({
|
|
105
|
+
start: paragraphStart,
|
|
106
|
+
end: paragraphEnd,
|
|
107
|
+
pPr,
|
|
108
|
+
container: container || body
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let processedModifiedText = modifiedText;
|
|
113
|
+
tokenToCharMap.forEach((char, tokenString) => {
|
|
114
|
+
const escapedToken = tokenString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
115
|
+
processedModifiedText = processedModifiedText.replace(new RegExp(escapedToken, 'g'), char);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const containerFragments = new Map();
|
|
119
|
+
uniqueContainers.forEach(container => {
|
|
120
|
+
containerFragments.set(container, xmlDoc.createDocumentFragment());
|
|
121
|
+
});
|
|
122
|
+
if (body && !containerFragments.has(body)) {
|
|
123
|
+
containerFragments.set(body, xmlDoc.createDocumentFragment());
|
|
124
|
+
}
|
|
125
|
+
if (!containerFragments.has(xmlDoc)) {
|
|
126
|
+
containerFragments.set(xmlDoc, xmlDoc.createDocumentFragment());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const replacementContainers = new Map();
|
|
130
|
+
const paragraphStarts = new Set(paragraphMap.map(paragraph => paragraph.start));
|
|
131
|
+
const paragraphLookup = createRangeCursorLookup(paragraphMap);
|
|
132
|
+
const propertyLookup = createRangeCursorLookup(propertyMap);
|
|
133
|
+
const sentinelMapByStart = indexSentinelsByStart(sentinelMap);
|
|
134
|
+
|
|
135
|
+
const getParagraphInfo = (index) => {
|
|
136
|
+
const match = paragraphLookup.at(index);
|
|
137
|
+
if (match) return match;
|
|
138
|
+
if (paragraphMap.length > 0) return paragraphMap[paragraphMap.length - 1];
|
|
139
|
+
return { start: 0, end: 0, pPr: null, container: body || xmlDoc };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getRunProperties = (index) => {
|
|
143
|
+
const match = propertyLookup.at(index);
|
|
144
|
+
return match ? { rPr: match.rPr, wrapper: match.wrapper } : { rPr: null };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const getPropertySpanLength = (index, maxLength) => {
|
|
148
|
+
const match = propertyLookup.at(index);
|
|
149
|
+
if (!match) return 1;
|
|
150
|
+
return Math.min(match.end - index, maxLength);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
paragraphs,
|
|
155
|
+
body: body || xmlDoc,
|
|
156
|
+
paragraphMap,
|
|
157
|
+
paragraphStarts,
|
|
158
|
+
propertyMap,
|
|
159
|
+
sentinelMap,
|
|
160
|
+
sentinelMapByStart,
|
|
161
|
+
referenceMap,
|
|
162
|
+
tokenToCharMap,
|
|
163
|
+
containerFragments,
|
|
164
|
+
replacementContainers,
|
|
165
|
+
originalFullText,
|
|
166
|
+
processedModifiedText,
|
|
167
|
+
getParagraphInfo,
|
|
168
|
+
getRunProperties,
|
|
169
|
+
getPropertySpanLength,
|
|
170
|
+
isParagraphStart: index => paragraphStarts.has(index)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function processChildNode(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
|
|
175
|
+
if (child.nodeName === 'w:r') {
|
|
176
|
+
return processRunForReconstruction(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode);
|
|
177
|
+
}
|
|
178
|
+
if (child.nodeName === 'w:hyperlink') {
|
|
179
|
+
return processHyperlinkForReconstruction(child, originalFullText, propertyMap);
|
|
180
|
+
}
|
|
181
|
+
if (['w:sdt', 'w:oMath', 'm:oMath', 'w:bookmarkStart', 'w:bookmarkEnd'].includes(child.nodeName)) {
|
|
182
|
+
sentinelMap.push({ start: originalFullText.length, node: child });
|
|
183
|
+
return originalFullText + '\uFFFC';
|
|
184
|
+
}
|
|
185
|
+
if (['w:commentRangeStart', 'w:commentRangeEnd'].includes(child.nodeName)) {
|
|
186
|
+
sentinelMap.push({ start: originalFullText.length, node: child, isCommentMarker: true });
|
|
187
|
+
return originalFullText;
|
|
188
|
+
}
|
|
189
|
+
return originalFullText;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function processRunForReconstruction(runElement, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
|
|
193
|
+
let fullText = originalFullText;
|
|
194
|
+
const rPr = getFirstElementByTag(runElement, 'w:rPr');
|
|
195
|
+
|
|
196
|
+
Array.from(runElement.childNodes).forEach(runChild => {
|
|
197
|
+
if (runChild.nodeName === 'w:t') {
|
|
198
|
+
const textContent = runChild.textContent || '';
|
|
199
|
+
if (textContent.length > 0) {
|
|
200
|
+
propertyMap.push({
|
|
201
|
+
start: fullText.length,
|
|
202
|
+
end: fullText.length + textContent.length,
|
|
203
|
+
rPr
|
|
204
|
+
});
|
|
205
|
+
fullText += textContent;
|
|
206
|
+
}
|
|
207
|
+
} else if (runChild.nodeName === 'w:br' || runChild.nodeName === 'w:cr') {
|
|
208
|
+
fullText += '\n';
|
|
209
|
+
propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
|
|
210
|
+
} else if (runChild.nodeName === 'w:tab') {
|
|
211
|
+
fullText += '\t';
|
|
212
|
+
propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
|
|
213
|
+
} else if (runChild.nodeName === 'w:noBreakHyphen') {
|
|
214
|
+
fullText += '\u2011';
|
|
215
|
+
propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
|
|
216
|
+
} else if (['w:drawing', 'w:pict', 'w:object', 'w:fldChar', 'w:instrText', 'w:sym'].includes(runChild.nodeName)) {
|
|
217
|
+
const textBoxContent = getFirstElementByTag(runChild, 'w:txbxContent');
|
|
218
|
+
const hasTextBox = runChild.nodeName === 'w:pict' && !!textBoxContent;
|
|
219
|
+
|
|
220
|
+
sentinelMap.push({
|
|
221
|
+
start: fullText.length,
|
|
222
|
+
node: runChild,
|
|
223
|
+
isTextBox: hasTextBox,
|
|
224
|
+
originalContainer: hasTextBox ? textBoxContent : undefined
|
|
225
|
+
});
|
|
226
|
+
fullText += '\uFFFC';
|
|
227
|
+
propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
|
|
228
|
+
} else if (runChild.nodeName === 'w:footnoteReference' || runChild.nodeName === 'w:endnoteReference') {
|
|
229
|
+
const id = runChild.getAttribute('w:id');
|
|
230
|
+
if (id) {
|
|
231
|
+
const type = runChild.nodeName === 'w:footnoteReference' ? 'FN' : 'EN';
|
|
232
|
+
const tokenString = `{{__${type}_${id}__}}`;
|
|
233
|
+
const char = String.fromCharCode(nextCharCode);
|
|
234
|
+
referenceMap.set(char, runChild);
|
|
235
|
+
tokenToCharMap.set(tokenString, char);
|
|
236
|
+
fullText += char;
|
|
237
|
+
propertyMap.push({ start: fullText.length - 1, end: fullText.length, rPr });
|
|
238
|
+
}
|
|
239
|
+
} else if (runChild.nodeName === 'w:commentReference') {
|
|
240
|
+
sentinelMap.push({ start: fullText.length, node: runChild, isCommentMarker: true });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return fullText;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function processHyperlinkForReconstruction(hyperlinkElement, originalFullText, propertyMap) {
|
|
248
|
+
let fullText = originalFullText;
|
|
249
|
+
|
|
250
|
+
Array.from(hyperlinkElement.childNodes).forEach(hyperlinkChild => {
|
|
251
|
+
if (hyperlinkChild.nodeName !== 'w:r') return;
|
|
252
|
+
|
|
253
|
+
const rPr = getFirstElementByTag(hyperlinkChild, 'w:rPr');
|
|
254
|
+
const texts = getElementsByTag(hyperlinkChild, 'w:t');
|
|
255
|
+
texts.forEach(textNode => {
|
|
256
|
+
const textContent = textNode.textContent || '';
|
|
257
|
+
if (textContent.length === 0) return;
|
|
258
|
+
|
|
259
|
+
propertyMap.push({
|
|
260
|
+
start: fullText.length,
|
|
261
|
+
end: fullText.length + textContent.length,
|
|
262
|
+
rPr,
|
|
263
|
+
wrapper: hyperlinkElement
|
|
264
|
+
});
|
|
265
|
+
fullText += textContent;
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return fullText;
|
|
270
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconstruction reconciliation mode orchestration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { computeWordDiffs } from '../pipeline/diff-engine.js';
|
|
6
|
+
import { buildReconstructionMapping } from './reconstruction-mapper.js';
|
|
7
|
+
import { applyReconstructionDiffs } from './reconstruction-writer.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Applies reconstruction mode reconciliation.
|
|
11
|
+
*
|
|
12
|
+
* @param {Document} xmlDoc - XML document
|
|
13
|
+
* @param {string} originalText - Original text (kept for signature compatibility)
|
|
14
|
+
* @param {string} modifiedText - Modified text
|
|
15
|
+
* @param {XMLSerializer} serializer - Serializer instance
|
|
16
|
+
* @param {string} author - Author name
|
|
17
|
+
* @param {Array} formatHints - Format hints
|
|
18
|
+
* @param {boolean} [generateRedlines=true] - Track change toggle
|
|
19
|
+
* @returns {{ oxml: string, hasChanges: boolean }}
|
|
20
|
+
*/
|
|
21
|
+
export function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true) {
|
|
22
|
+
const mapping = buildReconstructionMapping(xmlDoc, modifiedText);
|
|
23
|
+
if (mapping.paragraphs.length === 0) {
|
|
24
|
+
return { oxml: serializer.serializeToString(xmlDoc), hasChanges: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText);
|
|
28
|
+
|
|
29
|
+
return applyReconstructionDiffs(
|
|
30
|
+
xmlDoc,
|
|
31
|
+
diffs,
|
|
32
|
+
mapping,
|
|
33
|
+
serializer,
|
|
34
|
+
author,
|
|
35
|
+
formatHints,
|
|
36
|
+
generateRedlines
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconstruction writer.
|
|
3
|
+
*
|
|
4
|
+
* Applies diff segments to mapped reconstruction context and writes updated DOM content.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getApplicableFormatHints } from '../pipeline/markdown-processor.js';
|
|
8
|
+
import { createTrackChange, createFormattedRuns } from './run-builders.js';
|
|
9
|
+
import { getFirstElementByTag } from '../core/xml-query.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Applies diffs to reconstruction context and writes updated XML.
|
|
13
|
+
*
|
|
14
|
+
* @param {Document} xmlDoc - XML document
|
|
15
|
+
* @param {Array<[number, string]>} diffs - Diff tuples from diff-match-patch
|
|
16
|
+
* @param {ReturnType<import('./reconstruction-mapper.js').buildReconstructionMapping>} context - Reconstruction mapping
|
|
17
|
+
* @param {XMLSerializer} serializer - Serializer instance
|
|
18
|
+
* @param {string} author - Author name
|
|
19
|
+
* @param {Array} formatHints - Format hints
|
|
20
|
+
* @param {boolean} [generateRedlines=true] - Track change toggle
|
|
21
|
+
* @returns {{ oxml: string, hasChanges: boolean }}
|
|
22
|
+
*/
|
|
23
|
+
export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true) {
|
|
24
|
+
const {
|
|
25
|
+
paragraphs,
|
|
26
|
+
paragraphMap,
|
|
27
|
+
containerFragments,
|
|
28
|
+
sentinelMapByStart,
|
|
29
|
+
referenceMap,
|
|
30
|
+
replacementContainers,
|
|
31
|
+
getParagraphInfo,
|
|
32
|
+
getRunProperties,
|
|
33
|
+
getPropertySpanLength,
|
|
34
|
+
isParagraphStart
|
|
35
|
+
} = context;
|
|
36
|
+
|
|
37
|
+
const createNewParagraph = (pPr) => {
|
|
38
|
+
const newParagraph = xmlDoc.createElement('w:p');
|
|
39
|
+
if (pPr) newParagraph.appendChild(pPr.cloneNode(true));
|
|
40
|
+
return newParagraph;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const startInfo = getParagraphInfo(0);
|
|
44
|
+
let currentParagraph = createNewParagraph(startInfo.pPr);
|
|
45
|
+
const initialFragment = containerFragments.get(startInfo.container);
|
|
46
|
+
if (initialFragment) {
|
|
47
|
+
initialFragment.appendChild(currentParagraph);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let currentOriginalIndex = 0;
|
|
51
|
+
let currentInsertOffset = 0;
|
|
52
|
+
|
|
53
|
+
for (const [op, text] of diffs) {
|
|
54
|
+
if (op === 0 || op === -1) {
|
|
55
|
+
const type = op === 0 ? 'equal' : 'delete';
|
|
56
|
+
let offset = 0;
|
|
57
|
+
|
|
58
|
+
while (offset < text.length) {
|
|
59
|
+
const chunkStart = currentOriginalIndex + offset;
|
|
60
|
+
const properties = getRunProperties(chunkStart);
|
|
61
|
+
const chunkLength = getPropertySpanLength(chunkStart, text.length - offset);
|
|
62
|
+
const chunk = text.substring(offset, offset + chunkLength);
|
|
63
|
+
|
|
64
|
+
appendTextToCurrent(
|
|
65
|
+
xmlDoc,
|
|
66
|
+
chunk,
|
|
67
|
+
type,
|
|
68
|
+
properties.rPr,
|
|
69
|
+
properties.wrapper,
|
|
70
|
+
chunkStart,
|
|
71
|
+
currentParagraph,
|
|
72
|
+
containerFragments,
|
|
73
|
+
sentinelMapByStart,
|
|
74
|
+
referenceMap,
|
|
75
|
+
replacementContainers,
|
|
76
|
+
getParagraphInfo,
|
|
77
|
+
createNewParagraph,
|
|
78
|
+
author,
|
|
79
|
+
formatHints,
|
|
80
|
+
currentInsertOffset,
|
|
81
|
+
generateRedlines
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (op === 0) {
|
|
85
|
+
currentInsertOffset += chunkLength;
|
|
86
|
+
}
|
|
87
|
+
offset += chunkLength;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
currentOriginalIndex += text.length;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (op === 1) {
|
|
95
|
+
const properties = currentOriginalIndex > 0 && !isParagraphStart(currentOriginalIndex)
|
|
96
|
+
? getRunProperties(currentOriginalIndex - 1)
|
|
97
|
+
: getRunProperties(currentOriginalIndex);
|
|
98
|
+
|
|
99
|
+
appendTextToCurrent(
|
|
100
|
+
xmlDoc,
|
|
101
|
+
text,
|
|
102
|
+
'insert',
|
|
103
|
+
properties.rPr,
|
|
104
|
+
properties.wrapper,
|
|
105
|
+
currentOriginalIndex,
|
|
106
|
+
currentParagraph,
|
|
107
|
+
containerFragments,
|
|
108
|
+
sentinelMapByStart,
|
|
109
|
+
referenceMap,
|
|
110
|
+
replacementContainers,
|
|
111
|
+
getParagraphInfo,
|
|
112
|
+
createNewParagraph,
|
|
113
|
+
author,
|
|
114
|
+
formatHints,
|
|
115
|
+
currentInsertOffset,
|
|
116
|
+
generateRedlines
|
|
117
|
+
);
|
|
118
|
+
currentInsertOffset += text.length;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
paragraphs.forEach(paragraph => {
|
|
123
|
+
if (paragraph.parentNode) {
|
|
124
|
+
paragraph.parentNode.removeChild(paragraph);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
containerFragments.forEach((fragment, container) => {
|
|
129
|
+
const replacement = replacementContainers.get(container);
|
|
130
|
+
const target = replacement || container;
|
|
131
|
+
|
|
132
|
+
if (target.nodeType === 9) {
|
|
133
|
+
const firstChild = fragment.firstChild;
|
|
134
|
+
if (firstChild) {
|
|
135
|
+
target.appendChild(firstChild);
|
|
136
|
+
while (fragment.firstChild) {
|
|
137
|
+
target.documentElement.appendChild(fragment.firstChild);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
target.appendChild(fragment);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function appendTextToCurrent(
|
|
150
|
+
xmlDoc,
|
|
151
|
+
text,
|
|
152
|
+
type,
|
|
153
|
+
rPr,
|
|
154
|
+
wrapper,
|
|
155
|
+
baseIndex,
|
|
156
|
+
currentParagraphRef,
|
|
157
|
+
containerFragments,
|
|
158
|
+
sentinelMapByStart,
|
|
159
|
+
referenceMap,
|
|
160
|
+
replacementContainers,
|
|
161
|
+
getParagraphInfo,
|
|
162
|
+
createNewParagraph,
|
|
163
|
+
author,
|
|
164
|
+
formatHints = [],
|
|
165
|
+
insertOffset = 0,
|
|
166
|
+
generateRedlines = true
|
|
167
|
+
) {
|
|
168
|
+
let localBaseIndex = baseIndex;
|
|
169
|
+
let localInsertOffset = insertOffset;
|
|
170
|
+
let localParagraph = currentParagraphRef;
|
|
171
|
+
|
|
172
|
+
const parts = text.split(/([\n\uFFFC]|[\uE000-\uF8FF])/);
|
|
173
|
+
|
|
174
|
+
parts.forEach(part => {
|
|
175
|
+
const sentinelsAtOffset = sentinelMapByStart.get(localBaseIndex) || [];
|
|
176
|
+
const commentMarkers = sentinelsAtOffset.filter(sentinel => sentinel.isCommentMarker);
|
|
177
|
+
|
|
178
|
+
commentMarkers.forEach(marker => {
|
|
179
|
+
if (marker.node.nodeName === 'w:commentReference') {
|
|
180
|
+
const run = xmlDoc.createElement('w:r');
|
|
181
|
+
run.appendChild(marker.node.cloneNode(true));
|
|
182
|
+
localParagraph.appendChild(run);
|
|
183
|
+
} else {
|
|
184
|
+
localParagraph.appendChild(marker.node.cloneNode(true));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (part === '\n') {
|
|
189
|
+
if (type !== 'delete') {
|
|
190
|
+
const info = getParagraphInfo(localBaseIndex);
|
|
191
|
+
const nextParagraph = createNewParagraph(info.pPr);
|
|
192
|
+
const fragment = containerFragments.get(info.container);
|
|
193
|
+
if (fragment) {
|
|
194
|
+
fragment.appendChild(nextParagraph);
|
|
195
|
+
localParagraph = nextParagraph;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
localBaseIndex++;
|
|
199
|
+
if (type !== 'delete') localInsertOffset++;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (part === '\uFFFC') {
|
|
204
|
+
const sentinel = sentinelsAtOffset.find(entry => !entry.isCommentMarker) || sentinelsAtOffset[0];
|
|
205
|
+
if (sentinel) {
|
|
206
|
+
const clone = sentinel.node.cloneNode(true);
|
|
207
|
+
if (sentinel.isTextBox && sentinel.originalContainer) {
|
|
208
|
+
const newContainer = getFirstElementByTag(clone, 'w:txbxContent');
|
|
209
|
+
if (newContainer) {
|
|
210
|
+
while (newContainer.firstChild) newContainer.removeChild(newContainer.firstChild);
|
|
211
|
+
replacementContainers.set(sentinel.originalContainer, newContainer);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
localParagraph.appendChild(clone);
|
|
215
|
+
}
|
|
216
|
+
localBaseIndex++;
|
|
217
|
+
if (type !== 'delete') localInsertOffset++;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (referenceMap.has(part)) {
|
|
222
|
+
if (type !== 'delete') {
|
|
223
|
+
const refNode = referenceMap.get(part);
|
|
224
|
+
if (refNode) {
|
|
225
|
+
const clone = refNode.cloneNode(true);
|
|
226
|
+
const run = xmlDoc.createElement('w:r');
|
|
227
|
+
if (rPr) run.appendChild(rPr.cloneNode(true));
|
|
228
|
+
run.appendChild(clone);
|
|
229
|
+
localParagraph.appendChild(run);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
localBaseIndex++;
|
|
233
|
+
if (type !== 'delete') localInsertOffset++;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (part.length === 0) return;
|
|
238
|
+
|
|
239
|
+
let parent = localParagraph;
|
|
240
|
+
if (wrapper) {
|
|
241
|
+
const wrapperClone = wrapper.cloneNode(false);
|
|
242
|
+
parent = wrapperClone;
|
|
243
|
+
localParagraph.appendChild(wrapperClone);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (type === 'delete') {
|
|
247
|
+
const run = xmlDoc.createElement('w:r');
|
|
248
|
+
if (rPr) run.appendChild(rPr.cloneNode(true));
|
|
249
|
+
const delText = xmlDoc.createElement('w:delText');
|
|
250
|
+
delText.setAttribute('xml:space', 'preserve');
|
|
251
|
+
delText.textContent = part;
|
|
252
|
+
run.appendChild(delText);
|
|
253
|
+
|
|
254
|
+
if (generateRedlines) {
|
|
255
|
+
const del = createTrackChange(xmlDoc, 'del', run, author);
|
|
256
|
+
parent.appendChild(del);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const applicableHints = getApplicableFormatHints(formatHints, localInsertOffset, localInsertOffset + part.length);
|
|
260
|
+
const runs = createFormattedRuns(xmlDoc, part, rPr, applicableHints, localInsertOffset, author, generateRedlines);
|
|
261
|
+
|
|
262
|
+
if (type === 'insert' && generateRedlines) {
|
|
263
|
+
const ins = createTrackChange(xmlDoc, 'ins', null, author);
|
|
264
|
+
runs.forEach(run => ins.appendChild(run));
|
|
265
|
+
parent.appendChild(ins);
|
|
266
|
+
} else {
|
|
267
|
+
runs.forEach(run => parent.appendChild(run));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (type !== 'delete') {
|
|
272
|
+
localInsertOffset += part.length;
|
|
273
|
+
}
|
|
274
|
+
localBaseIndex += part.length;
|
|
275
|
+
});
|
|
276
|
+
}
|