@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.
Files changed (42) hide show
  1. package/AGENTS.md +91 -5
  2. package/ARCHITECTURE.md +62 -9
  3. package/README.md +94 -3
  4. package/core/types.js +35 -8
  5. package/core/word-xml.js +90 -0
  6. package/dist/docx-redline-js.esm.js +1149 -367
  7. package/dist/docx-redline-js.esm.js.map +4 -4
  8. package/dist/docx-redline-js.esm.min.js +78 -74
  9. package/dist/docx-redline-js.esm.min.js.map +4 -4
  10. package/docs/VALIDATION.md +48 -0
  11. package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
  12. package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
  13. package/engine/format-application.js +13 -14
  14. package/engine/format-span-application.js +7 -6
  15. package/engine/formatting-removal.js +15 -12
  16. package/engine/oxml-engine.js +146 -55
  17. package/engine/reconstruction-mapper.js +35 -8
  18. package/engine/reconstruction-mode.js +14 -13
  19. package/engine/reconstruction-writer.js +97 -78
  20. package/engine/rpr-helpers.js +34 -32
  21. package/engine/run-builders.js +150 -39
  22. package/engine/surgical-diff-application.js +216 -0
  23. package/engine/surgical-mode.js +84 -519
  24. package/engine/surgical-run-splitting.js +96 -0
  25. package/engine/surgical-spans.js +169 -0
  26. package/engine/table-cell-context.js +15 -13
  27. package/engine/table-mode.js +39 -35
  28. package/index.d.ts +148 -0
  29. package/index.js +26 -19
  30. package/package.json +8 -1
  31. package/pipeline/ingestion-export.js +1 -0
  32. package/pipeline/ingestion-paragraph.js +37 -12
  33. package/pipeline/ingestion-table.js +11 -8
  34. package/scripts/build.mjs +35 -0
  35. package/scripts/check-types.mjs +28 -0
  36. package/scripts/export-validation-fixtures.mjs +68 -0
  37. package/scripts/run-tests.mjs +43 -0
  38. package/scripts/word-com-smoke.ps1 +48 -0
  39. package/services/comment-locator.js +10 -9
  40. package/services/revision-comment-management.js +501 -0
  41. package/services/standalone-operation-runner.js +119 -69
  42. package/services/table-reconciliation.js +7 -8
@@ -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, getRevisionTimestamp } from '../core/types.js';
10
- import { getFirstElementByTag } from '../core/xml-query.js';
11
-
12
- /**
13
- * Creates an insertion/deletion wrapper.
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.createElement(type === 'ins' ? 'w:ins' : 'w:del');
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.createElement('w:r');
44
- if (rPr) run.appendChild(rPr.cloneNode(true));
45
-
46
- const textEl = xmlDoc.createElement(isDelete ? 'w:delText' : 'w:t');
47
- textEl.setAttribute('xml:space', 'preserve');
48
- textEl.textContent = text;
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.createElement('w:r');
116
- if (rPrElement) run.appendChild(rPrElement);
117
-
118
- const textEl = xmlDoc.createElement(isDelete ? 'w:delText' : 'w:t');
119
- textEl.setAttribute('xml:space', 'preserve');
120
- textEl.textContent = text;
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.createElement('w:rPr');
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.createElement(tagName);
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.createElement('w:rPrChange');
200
- rPrChange.setAttribute('w:id', String(createRevisionMetadata(author).id));
201
- rPrChange.setAttribute('w:author', author);
202
- rPrChange.setAttribute('w:date', dateStr);
203
-
204
- const previousRPr = xmlDoc.createElement('w:rPr');
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, getRevisionTimestamp(), previousRPrArg || rPr);
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
+ }