@ansonlai/docx-redline-js 0.1.3 → 0.1.6
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 +91 -5
- package/ARCHITECTURE.md +62 -9
- package/README.md +94 -3
- package/core/types.js +35 -8
- package/core/word-xml.js +90 -0
- package/dist/docx-redline-js.esm.js +1149 -367
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +78 -74
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/VALIDATION.md +48 -0
- package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
- package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
- package/engine/format-application.js +13 -14
- package/engine/format-span-application.js +7 -6
- package/engine/formatting-removal.js +15 -12
- package/engine/oxml-engine.js +146 -55
- package/engine/reconstruction-mapper.js +35 -8
- package/engine/reconstruction-mode.js +14 -13
- package/engine/reconstruction-writer.js +97 -78
- package/engine/rpr-helpers.js +34 -32
- package/engine/run-builders.js +150 -39
- package/engine/surgical-diff-application.js +216 -0
- package/engine/surgical-mode.js +84 -519
- package/engine/surgical-run-splitting.js +96 -0
- package/engine/surgical-spans.js +169 -0
- package/engine/table-cell-context.js +15 -13
- package/engine/table-mode.js +39 -35
- package/index.d.ts +148 -0
- package/index.js +26 -19
- package/package.json +8 -1
- package/pipeline/ingestion-export.js +1 -0
- package/pipeline/ingestion-paragraph.js +37 -12
- package/pipeline/ingestion-table.js +11 -8
- package/scripts/build.mjs +35 -0
- package/scripts/check-types.mjs +28 -0
- package/scripts/export-validation-fixtures.mjs +68 -0
- package/scripts/run-tests.mjs +43 -0
- package/scripts/word-com-smoke.ps1 +48 -0
- package/services/comment-locator.js +10 -9
- package/services/revision-comment-management.js +501 -0
- package/services/standalone-operation-runner.js +119 -69
- package/services/table-reconciliation.js +7 -8
package/engine/run-builders.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* and `w:pPrChange` elements used by surgical and reconstruction modes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { RPR_SCHEMA_ORDER } from './rpr-helpers.js';
|
|
9
|
-
import { createRevisionMetadata
|
|
10
|
-
import { getFirstElementByTag } from '../core/xml-query.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import { RPR_SCHEMA_ORDER } from './rpr-helpers.js';
|
|
9
|
+
import { createRevisionMetadata } from '../core/types.js';
|
|
10
|
+
import { getFirstElementByTag } from '../core/xml-query.js';
|
|
11
|
+
import { createWordElement } from '../core/word-xml.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates an insertion/deletion wrapper.
|
|
14
15
|
*
|
|
15
16
|
* @param {Document} xmlDoc - XML document
|
|
16
17
|
* @param {'ins'|'del'} type - Wrapper type
|
|
@@ -18,8 +19,8 @@ import { getFirstElementByTag } from '../core/xml-query.js';
|
|
|
18
19
|
* @param {string} author - Change author
|
|
19
20
|
* @returns {Element}
|
|
20
21
|
*/
|
|
21
|
-
export function createTrackChange(xmlDoc, type, run, author) {
|
|
22
|
-
const wrapper = xmlDoc
|
|
22
|
+
export function createTrackChange(xmlDoc, type, run, author) {
|
|
23
|
+
const wrapper = createWordElement(xmlDoc, type === 'ins' ? 'w:ins' : 'w:del');
|
|
23
24
|
const metadata = createRevisionMetadata(author);
|
|
24
25
|
wrapper.setAttribute('w:id', String(metadata.id));
|
|
25
26
|
wrapper.setAttribute('w:author', metadata.author);
|
|
@@ -28,7 +29,78 @@ export function createTrackChange(xmlDoc, type, run, author) {
|
|
|
28
29
|
wrapper.appendChild(run);
|
|
29
30
|
}
|
|
30
31
|
return wrapper;
|
|
31
|
-
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getDirectWordChild(node, localName) {
|
|
35
|
+
return Array.from(node?.childNodes || []).find(child => child.nodeType === 1
|
|
36
|
+
&& (child.localName === localName || child.nodeName === `w:${localName}`)) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureParagraphProperties(xmlDoc, paragraph) {
|
|
40
|
+
let pPr = getDirectWordChild(paragraph, 'pPr');
|
|
41
|
+
if (!pPr) {
|
|
42
|
+
pPr = createWordElement(xmlDoc, 'w:pPr');
|
|
43
|
+
paragraph.insertBefore(pPr, paragraph.firstChild || null);
|
|
44
|
+
} else if (paragraph.firstChild !== pPr) {
|
|
45
|
+
paragraph.insertBefore(pPr, paragraph.firstChild || null);
|
|
46
|
+
}
|
|
47
|
+
return pPr;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureParagraphMarkRunProperties(xmlDoc, pPr) {
|
|
51
|
+
let rPr = getDirectWordChild(pPr, 'rPr');
|
|
52
|
+
if (!rPr) {
|
|
53
|
+
rPr = createWordElement(xmlDoc, 'w:rPr');
|
|
54
|
+
pPr.appendChild(rPr);
|
|
55
|
+
} else if (pPr.lastChild !== rPr) {
|
|
56
|
+
pPr.appendChild(rPr);
|
|
57
|
+
}
|
|
58
|
+
return rPr;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function markParagraphMark(xmlDoc, paragraph, author, type) {
|
|
62
|
+
const pPr = ensureParagraphProperties(xmlDoc, paragraph);
|
|
63
|
+
const rPr = ensureParagraphMarkRunProperties(xmlDoc, pPr);
|
|
64
|
+
|
|
65
|
+
for (const child of Array.from(rPr.childNodes || [])) {
|
|
66
|
+
if (child.nodeType !== 1) continue;
|
|
67
|
+
if (child.localName === 'ins' || child.localName === 'del' || child.nodeName === 'w:ins' || child.nodeName === 'w:del') {
|
|
68
|
+
rPr.removeChild(child);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const marker = createWordElement(xmlDoc, type === 'ins' ? 'w:ins' : 'w:del');
|
|
73
|
+
const metadata = createRevisionMetadata(author);
|
|
74
|
+
marker.setAttribute('w:id', String(metadata.id));
|
|
75
|
+
marker.setAttribute('w:author', metadata.author);
|
|
76
|
+
marker.setAttribute('w:date', metadata.date);
|
|
77
|
+
rPr.appendChild(marker);
|
|
78
|
+
return marker;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Marks a paragraph mark as inserted.
|
|
83
|
+
*
|
|
84
|
+
* @param {Document} xmlDoc - XML document
|
|
85
|
+
* @param {Element} paragraph - Paragraph to mark
|
|
86
|
+
* @param {string} author - Change author
|
|
87
|
+
* @returns {Element}
|
|
88
|
+
*/
|
|
89
|
+
export function markParagraphMarkInserted(xmlDoc, paragraph, author) {
|
|
90
|
+
return markParagraphMark(xmlDoc, paragraph, author, 'ins');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Marks a paragraph mark as deleted.
|
|
95
|
+
*
|
|
96
|
+
* @param {Document} xmlDoc - XML document
|
|
97
|
+
* @param {Element} paragraph - Paragraph to mark
|
|
98
|
+
* @param {string} author - Change author
|
|
99
|
+
* @returns {Element}
|
|
100
|
+
*/
|
|
101
|
+
export function markParagraphMarkDeleted(xmlDoc, paragraph, author) {
|
|
102
|
+
return markParagraphMark(xmlDoc, paragraph, author, 'del');
|
|
103
|
+
}
|
|
32
104
|
|
|
33
105
|
/**
|
|
34
106
|
* Creates a text run with optional formatting.
|
|
@@ -39,13 +111,18 @@ export function createTrackChange(xmlDoc, type, run, author) {
|
|
|
39
111
|
* @param {boolean} isDelete - Use `w:delText` instead of `w:t`
|
|
40
112
|
* @returns {Element}
|
|
41
113
|
*/
|
|
42
|
-
export function createTextRun(xmlDoc, text, rPr, isDelete) {
|
|
43
|
-
const run = xmlDoc
|
|
44
|
-
if (rPr) run.appendChild(rPr.cloneNode(true));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
114
|
+
export function createTextRun(xmlDoc, text, rPr, isDelete) {
|
|
115
|
+
const run = createWordElement(xmlDoc, 'w:r');
|
|
116
|
+
if (rPr) run.appendChild(rPr.cloneNode(true));
|
|
117
|
+
|
|
118
|
+
if (!isDelete) {
|
|
119
|
+
appendVisibleTextPieces(xmlDoc, run, text);
|
|
120
|
+
return run;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const textEl = createWordElement(xmlDoc, isDelete ? 'w:delText' : 'w:t');
|
|
124
|
+
textEl.setAttribute('xml:space', 'preserve');
|
|
125
|
+
textEl.textContent = text;
|
|
49
126
|
run.appendChild(textEl);
|
|
50
127
|
|
|
51
128
|
return run;
|
|
@@ -111,17 +188,50 @@ export function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffs
|
|
|
111
188
|
* @param {boolean} isDelete - Use `w:delText` instead of `w:t`
|
|
112
189
|
* @returns {Element}
|
|
113
190
|
*/
|
|
114
|
-
export function createTextRunWithRPrElement(xmlDoc, text, rPrElement, isDelete) {
|
|
115
|
-
const run = xmlDoc
|
|
116
|
-
if (rPrElement) run.appendChild(rPrElement);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
191
|
+
export function createTextRunWithRPrElement(xmlDoc, text, rPrElement, isDelete) {
|
|
192
|
+
const run = createWordElement(xmlDoc, 'w:r');
|
|
193
|
+
if (rPrElement) run.appendChild(rPrElement);
|
|
194
|
+
|
|
195
|
+
if (!isDelete) {
|
|
196
|
+
appendVisibleTextPieces(xmlDoc, run, text);
|
|
197
|
+
return run;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const textEl = createWordElement(xmlDoc, isDelete ? 'w:delText' : 'w:t');
|
|
201
|
+
textEl.setAttribute('xml:space', 'preserve');
|
|
202
|
+
textEl.textContent = text;
|
|
121
203
|
run.appendChild(textEl);
|
|
122
204
|
|
|
123
|
-
return run;
|
|
124
|
-
}
|
|
205
|
+
return run;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function appendVisibleTextPieces(xmlDoc, run, text) {
|
|
209
|
+
const source = String(text || '');
|
|
210
|
+
const parts = source.split(/(\t|\n|\u2011)/);
|
|
211
|
+
|
|
212
|
+
for (const part of parts) {
|
|
213
|
+
if (!part) continue;
|
|
214
|
+
if (part === '\t') {
|
|
215
|
+
run.appendChild(createWordElement(xmlDoc, 'w:tab'));
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (part === '\n') {
|
|
219
|
+
run.appendChild(createWordElement(xmlDoc, 'w:br'));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (part === '\u2011') {
|
|
223
|
+
run.appendChild(createWordElement(xmlDoc, 'w:noBreakHyphen'));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const textEl = createWordElement(xmlDoc, 'w:t');
|
|
228
|
+
if (/^\s|\s$/.test(part)) {
|
|
229
|
+
textEl.setAttribute('xml:space', 'preserve');
|
|
230
|
+
}
|
|
231
|
+
textEl.textContent = part;
|
|
232
|
+
run.appendChild(textEl);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
125
235
|
|
|
126
236
|
/**
|
|
127
237
|
* Creates a new rPr synchronized to the requested core formatting flags.
|
|
@@ -133,8 +243,8 @@ export function createTextRunWithRPrElement(xmlDoc, text, rPrElement, isDelete)
|
|
|
133
243
|
* @param {boolean} [generateRedlines] - Whether to create rPrChange
|
|
134
244
|
* @returns {Element}
|
|
135
245
|
*/
|
|
136
|
-
export function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines) {
|
|
137
|
-
const rPr = xmlDoc
|
|
246
|
+
export function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines) {
|
|
247
|
+
const rPr = createWordElement(xmlDoc, 'w:rPr');
|
|
138
248
|
|
|
139
249
|
if (baseRPr) {
|
|
140
250
|
Array.from(baseRPr.childNodes).forEach(child => {
|
|
@@ -150,8 +260,8 @@ export function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateR
|
|
|
150
260
|
createRPrChange(xmlDoc, rPr, author, baseRPr);
|
|
151
261
|
}
|
|
152
262
|
|
|
153
|
-
const syncElement = (tagName, isOn, valOn = null, valOff = '0') => {
|
|
154
|
-
const el = xmlDoc
|
|
263
|
+
const syncElement = (tagName, isOn, valOn = null, valOff = '0') => {
|
|
264
|
+
const el = createWordElement(xmlDoc, tagName);
|
|
155
265
|
if (isOn) {
|
|
156
266
|
if (valOn) el.setAttribute('w:val', valOn);
|
|
157
267
|
} else if (valOff) {
|
|
@@ -195,13 +305,14 @@ export function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateR
|
|
|
195
305
|
* @param {Element} [sourceNode] - Optional source for previous state snapshot
|
|
196
306
|
* @returns {Element}
|
|
197
307
|
*/
|
|
198
|
-
export function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
|
|
199
|
-
const rPrChange = xmlDoc
|
|
200
|
-
|
|
201
|
-
rPrChange.setAttribute('w:
|
|
202
|
-
rPrChange.setAttribute('w:
|
|
203
|
-
|
|
204
|
-
|
|
308
|
+
export function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
|
|
309
|
+
const rPrChange = createWordElement(xmlDoc, 'w:rPrChange');
|
|
310
|
+
const metadata = createRevisionMetadata(author);
|
|
311
|
+
rPrChange.setAttribute('w:id', String(metadata.id));
|
|
312
|
+
rPrChange.setAttribute('w:author', metadata.author);
|
|
313
|
+
rPrChange.setAttribute('w:date', dateStr || metadata.date);
|
|
314
|
+
|
|
315
|
+
const previousRPr = createWordElement(xmlDoc, 'w:rPr');
|
|
205
316
|
const source = sourceNode || rPr;
|
|
206
317
|
|
|
207
318
|
Array.from(source.childNodes).forEach(child => {
|
|
@@ -230,6 +341,6 @@ export function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceN
|
|
|
230
341
|
* @param {Element} [previousRPrArg] - Optional explicit previous-state source
|
|
231
342
|
* @returns {void}
|
|
232
343
|
*/
|
|
233
|
-
function createRPrChange(xmlDoc, rPr, author, previousRPrArg) {
|
|
234
|
-
snapshotAndAttachRPrChange(xmlDoc, rPr, author,
|
|
235
|
-
}
|
|
344
|
+
function createRPrChange(xmlDoc, rPr, author, previousRPrArg) {
|
|
345
|
+
snapshotAndAttachRPrChange(xmlDoc, rPr, author, null, previousRPrArg || rPr);
|
|
346
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { getApplicableFormatHints } from '../pipeline/markdown-processor.js';
|
|
2
|
+
import { isWordElement } from '../core/word-xml.js';
|
|
3
|
+
import {
|
|
4
|
+
createTrackChange,
|
|
5
|
+
createTextRun,
|
|
6
|
+
createFormattedRuns,
|
|
7
|
+
createTextRunWithRPrElement,
|
|
8
|
+
injectFormattingToRPr
|
|
9
|
+
} from './run-builders.js';
|
|
10
|
+
import {
|
|
11
|
+
createRunFromPieces,
|
|
12
|
+
getRunContentPieces,
|
|
13
|
+
getRunTextLength,
|
|
14
|
+
insertRunPiecesBefore,
|
|
15
|
+
sliceRunPieces
|
|
16
|
+
} from './surgical-run-splitting.js';
|
|
17
|
+
import {
|
|
18
|
+
findContainingSpan,
|
|
19
|
+
findFirstSpanEndingAt,
|
|
20
|
+
findLastSpanEndingBeforeOrAt,
|
|
21
|
+
forEachOverlappingSpan
|
|
22
|
+
} from './surgical-spans.js';
|
|
23
|
+
|
|
24
|
+
export function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHints, author, generateRedlines) {
|
|
25
|
+
const desiredFormat = {};
|
|
26
|
+
if (applicableHints.length > 0) {
|
|
27
|
+
applicableHints.forEach(h => Object.assign(desiredFormat, h.format));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rPr = span.rPr;
|
|
31
|
+
const hasElement = (localName) => {
|
|
32
|
+
if (!rPr) return false;
|
|
33
|
+
for (let node = rPr.firstChild; node; node = node.nextSibling) {
|
|
34
|
+
if (isWordElement(node, localName)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const existingFormat = {
|
|
42
|
+
bold: hasElement('b'),
|
|
43
|
+
italic: hasElement('i'),
|
|
44
|
+
underline: hasElement('u'),
|
|
45
|
+
strikethrough: hasElement('strike')
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const formatsToCheck = ['bold', 'italic', 'underline', 'strikethrough'];
|
|
49
|
+
const changesNeeded = formatsToCheck.some(f => !!desiredFormat[f] !== existingFormat[f]);
|
|
50
|
+
|
|
51
|
+
if (!changesNeeded) return false;
|
|
52
|
+
|
|
53
|
+
const parent = span.runElement.parentNode;
|
|
54
|
+
if (!parent) return false;
|
|
55
|
+
|
|
56
|
+
const fullText = span.textElement.textContent || '';
|
|
57
|
+
const runStart = span.charStart;
|
|
58
|
+
|
|
59
|
+
const localStart = start - runStart;
|
|
60
|
+
const localEnd = end - runStart;
|
|
61
|
+
|
|
62
|
+
const beforeText = fullText.substring(0, localStart);
|
|
63
|
+
const affectedText = fullText.substring(localStart, localEnd);
|
|
64
|
+
const afterText = fullText.substring(localEnd);
|
|
65
|
+
|
|
66
|
+
if (beforeText.length > 0) {
|
|
67
|
+
const beforeRun = createTextRun(xmlDoc, beforeText, rPr, false);
|
|
68
|
+
parent.insertBefore(beforeRun, span.runElement);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const newRPr = injectFormattingToRPr(xmlDoc, rPr, desiredFormat, author, generateRedlines);
|
|
72
|
+
const newRun = createTextRunWithRPrElement(xmlDoc, affectedText, newRPr, false);
|
|
73
|
+
parent.insertBefore(newRun, span.runElement);
|
|
74
|
+
|
|
75
|
+
if (afterText.length > 0) {
|
|
76
|
+
const afterRun = createTextRun(xmlDoc, afterText, rPr, false);
|
|
77
|
+
parent.insertBefore(afterRun, span.runElement);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
parent.removeChild(span.runElement);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedlines) {
|
|
85
|
+
const spans = [];
|
|
86
|
+
forEachOverlappingSpan(spanIndex, startPos, endPos, span => {
|
|
87
|
+
spans.push(span);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (spans.length === 0) return false;
|
|
91
|
+
|
|
92
|
+
const spansByRun = new Map();
|
|
93
|
+
spans.forEach(span => {
|
|
94
|
+
if (!span.runElement?.parentNode) return;
|
|
95
|
+
if (!spansByRun.has(span.runElement)) spansByRun.set(span.runElement, []);
|
|
96
|
+
spansByRun.get(span.runElement).push(span);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let changed = false;
|
|
100
|
+
spansByRun.forEach((runSpans, runElement) => {
|
|
101
|
+
const parent = runElement.parentNode;
|
|
102
|
+
if (!parent) return;
|
|
103
|
+
|
|
104
|
+
const pieces = getRunContentPieces(runElement);
|
|
105
|
+
if (pieces.length === 0) return;
|
|
106
|
+
|
|
107
|
+
let deleteStart = Infinity;
|
|
108
|
+
let deleteEnd = -Infinity;
|
|
109
|
+
runSpans.forEach(span => {
|
|
110
|
+
const piece = pieces.find(candidate => candidate.node === span.textElement);
|
|
111
|
+
if (!piece) return;
|
|
112
|
+
|
|
113
|
+
const spanDeleteStart = Math.max(0, startPos - span.charStart);
|
|
114
|
+
const spanDeleteEnd = Math.min(span.charEnd - span.charStart, endPos - span.charStart);
|
|
115
|
+
if (spanDeleteEnd <= spanDeleteStart) return;
|
|
116
|
+
|
|
117
|
+
deleteStart = Math.min(deleteStart, piece.start + spanDeleteStart);
|
|
118
|
+
deleteEnd = Math.max(deleteEnd, piece.start + spanDeleteEnd);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!Number.isFinite(deleteStart) || deleteEnd <= deleteStart) return;
|
|
122
|
+
|
|
123
|
+
const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, deleteStart, false);
|
|
124
|
+
const deletedPieces = sliceRunPieces(xmlDoc, pieces, deleteStart, deleteEnd, true);
|
|
125
|
+
const afterPieces = sliceRunPieces(xmlDoc, pieces, deleteEnd, getRunTextLength(pieces), false);
|
|
126
|
+
|
|
127
|
+
insertRunPiecesBefore(xmlDoc, parent, runElement, beforePieces, runSpans[0].rPr);
|
|
128
|
+
|
|
129
|
+
if (generateRedlines && deletedPieces.length > 0) {
|
|
130
|
+
const delRun = createRunFromPieces(xmlDoc, deletedPieces, runSpans[0].rPr);
|
|
131
|
+
const delWrapper = createTrackChange(xmlDoc, 'del', delRun, author);
|
|
132
|
+
parent.insertBefore(delWrapper, runElement);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
insertRunPiecesBefore(xmlDoc, parent, runElement, afterPieces, runSpans[0].rPr);
|
|
136
|
+
parent.removeChild(runElement);
|
|
137
|
+
changed = true;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return changed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null) {
|
|
144
|
+
let targetSpan = findContainingSpan(spanIndex, pos);
|
|
145
|
+
|
|
146
|
+
if (!targetSpan && pos > 0) {
|
|
147
|
+
targetSpan = findFirstSpanEndingAt(spanIndex, pos);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!targetSpan && pos > 0) {
|
|
151
|
+
targetSpan = findLastSpanEndingBeforeOrAt(spanIndex, pos);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!targetSpan && spanIndex.spans.length > 0) {
|
|
155
|
+
targetSpan = spanIndex.spans[spanIndex.spans.length - 1];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!targetSpan) {
|
|
159
|
+
if (!fallbackParagraph) return false;
|
|
160
|
+
insertTextRuns(xmlDoc, fallbackParagraph, null, text, null, author, formatHints, insertOffset, generateRedlines);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const parent = targetSpan.runElement.parentNode;
|
|
165
|
+
if (!parent) {
|
|
166
|
+
if (!fallbackParagraph) return false;
|
|
167
|
+
insertTextRuns(xmlDoc, fallbackParagraph, null, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const pieces = getRunContentPieces(targetSpan.runElement);
|
|
172
|
+
const targetPiece = pieces.find(piece => piece.node === targetSpan.textElement);
|
|
173
|
+
const localInsertPos = targetPiece
|
|
174
|
+
? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart))
|
|
175
|
+
: (pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces));
|
|
176
|
+
|
|
177
|
+
if (localInsertPos > 0 && localInsertPos < getRunTextLength(pieces)) {
|
|
178
|
+
const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
|
|
179
|
+
const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
|
|
180
|
+
|
|
181
|
+
insertRunPiecesBefore(xmlDoc, parent, targetSpan.runElement, beforePieces, targetSpan.rPr);
|
|
182
|
+
insertTextRuns(xmlDoc, parent, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
|
|
183
|
+
insertRunPiecesBefore(xmlDoc, parent, targetSpan.runElement, afterPieces, targetSpan.rPr);
|
|
184
|
+
parent.removeChild(targetSpan.runElement);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const referenceNode = pos <= targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
|
|
189
|
+
insertTextRuns(xmlDoc, parent, referenceNode, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines) {
|
|
194
|
+
const applicableHints = getApplicableFormatHints(formatHints, insertOffset, insertOffset + text.length);
|
|
195
|
+
|
|
196
|
+
if (applicableHints.length === 0) {
|
|
197
|
+
const insRun = createTextRun(xmlDoc, text, baseRPr, false);
|
|
198
|
+
if (generateRedlines) {
|
|
199
|
+
const insWrapper = createTrackChange(xmlDoc, 'ins', insRun, author);
|
|
200
|
+
parent.insertBefore(insWrapper, referenceNode);
|
|
201
|
+
} else {
|
|
202
|
+
parent.insertBefore(insRun, referenceNode);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const runs = createFormattedRuns(xmlDoc, text, baseRPr, applicableHints, insertOffset, author, generateRedlines);
|
|
208
|
+
|
|
209
|
+
if (generateRedlines) {
|
|
210
|
+
const insWrapper = createTrackChange(xmlDoc, 'ins', null, author);
|
|
211
|
+
runs.forEach(run => insWrapper.appendChild(run));
|
|
212
|
+
parent.insertBefore(insWrapper, referenceNode);
|
|
213
|
+
} else {
|
|
214
|
+
runs.forEach(run => parent.insertBefore(run, referenceNode));
|
|
215
|
+
}
|
|
216
|
+
}
|