@ansonlai/docx-redline-js 0.1.4 → 0.2.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.
Files changed (47) hide show
  1. package/AGENTS.md +53 -4
  2. package/ARCHITECTURE.md +75 -11
  3. package/README.md +62 -3
  4. package/core/redline-validation.js +156 -0
  5. package/core/types.js +35 -8
  6. package/core/word-xml.js +90 -0
  7. package/dist/docx-redline-js.esm.js +3195 -2592
  8. package/dist/docx-redline-js.esm.js.map +4 -4
  9. package/dist/docx-redline-js.esm.min.js +71 -67
  10. package/dist/docx-redline-js.esm.min.js.map +4 -4
  11. package/docs/VALIDATION.md +104 -0
  12. package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
  13. package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
  14. package/docs/plans/2026-05-31-architectural changes.md +591 -0
  15. package/engine/format-application.js +13 -14
  16. package/engine/format-span-application.js +7 -6
  17. package/engine/formatting-removal.js +15 -12
  18. package/engine/oxml-engine.js +146 -55
  19. package/engine/reconstruction-mapper.js +35 -8
  20. package/engine/reconstruction-mode.js +14 -13
  21. package/engine/reconstruction-writer.js +97 -78
  22. package/engine/rpr-helpers.js +34 -32
  23. package/engine/run-builders.js +150 -39
  24. package/engine/surgical-diff-application.js +216 -0
  25. package/engine/surgical-mode.js +84 -519
  26. package/engine/surgical-run-splitting.js +96 -0
  27. package/engine/surgical-spans.js +169 -0
  28. package/engine/table-cell-context.js +15 -13
  29. package/engine/table-mode.js +39 -35
  30. package/index.d.ts +172 -0
  31. package/index.js +50 -47
  32. package/package.json +10 -2
  33. package/pipeline/ingestion-export.js +1 -0
  34. package/pipeline/ingestion-paragraph.js +37 -12
  35. package/pipeline/ingestion-table.js +11 -8
  36. package/scripts/build.mjs +40 -0
  37. package/scripts/check-types.mjs +29 -0
  38. package/scripts/export-validation-fixtures.mjs +125 -0
  39. package/scripts/lib/minimal-zip.mjs +155 -0
  40. package/scripts/run-tests.mjs +43 -0
  41. package/scripts/validate-fixtures-xsd.sh +37 -0
  42. package/scripts/word-com-differential.ps1 +133 -0
  43. package/scripts/word-com-smoke.ps1 +48 -0
  44. package/services/comment-locator.js +10 -9
  45. package/services/revision-comment-management.js +115 -1
  46. package/services/standalone-operation-runner.js +119 -69
  47. package/services/table-reconciliation.js +7 -8
@@ -4,9 +4,15 @@
4
4
  * Applies diff segments to mapped reconstruction context and writes updated DOM content.
5
5
  */
6
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';
7
+ import { getApplicableFormatHints } from '../pipeline/markdown-processor.js';
8
+ import {
9
+ createTrackChange,
10
+ createFormattedRuns,
11
+ markParagraphMarkDeleted,
12
+ markParagraphMarkInserted
13
+ } from './run-builders.js';
14
+ import { getFirstElementByTag } from '../core/xml-query.js';
15
+ import { createWordElement } from '../core/word-xml.js';
10
16
 
11
17
  /**
12
18
  * Applies diffs to reconstruction context and writes updated XML.
@@ -32,23 +38,24 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
32
38
  getRunProperties,
33
39
  getPropertySpanLength,
34
40
  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
- };
41
+ } = context;
42
+
43
+ const createNewParagraph = (pPr) => {
44
+ const newParagraph = createWordElement(xmlDoc, 'w:p');
45
+ if (pPr) newParagraph.appendChild(pPr.cloneNode(true));
46
+ return newParagraph;
47
+ };
42
48
 
43
49
  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;
50
+ let currentParagraph = createNewParagraph(startInfo.pPr);
51
+ const initialFragment = containerFragments.get(startInfo.container);
52
+ if (initialFragment) {
53
+ initialFragment.appendChild(currentParagraph);
54
+ }
55
+
56
+ let currentOriginalIndex = 0;
57
+ let currentInsertOffset = 0;
58
+ const emittedCommentMarkers = new WeakSet();
52
59
 
53
60
  for (const [op, text] of diffs) {
54
61
  if (op === 0 || op === -1) {
@@ -61,9 +68,9 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
61
68
  const chunkLength = getPropertySpanLength(chunkStart, text.length - offset);
62
69
  const chunk = text.substring(offset, offset + chunkLength);
63
70
 
64
- appendTextToCurrent(
65
- xmlDoc,
66
- chunk,
71
+ const appendResult = appendTextToCurrent(
72
+ xmlDoc,
73
+ chunk,
67
74
  type,
68
75
  properties.rPr,
69
76
  properties.wrapper,
@@ -76,13 +83,15 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
76
83
  getParagraphInfo,
77
84
  createNewParagraph,
78
85
  author,
79
- formatHints,
80
- currentInsertOffset,
81
- generateRedlines
82
- );
83
-
84
- if (op === 0) {
85
- currentInsertOffset += chunkLength;
86
+ formatHints,
87
+ currentInsertOffset,
88
+ generateRedlines,
89
+ emittedCommentMarkers
90
+ );
91
+ currentParagraph = appendResult.currentParagraph;
92
+
93
+ if (op === 0) {
94
+ currentInsertOffset += chunkLength;
86
95
  }
87
96
  offset += chunkLength;
88
97
  }
@@ -96,9 +105,9 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
96
105
  ? getRunProperties(currentOriginalIndex - 1)
97
106
  : getRunProperties(currentOriginalIndex);
98
107
 
99
- appendTextToCurrent(
100
- xmlDoc,
101
- text,
108
+ const appendResult = appendTextToCurrent(
109
+ xmlDoc,
110
+ text,
102
111
  'insert',
103
112
  properties.rPr,
104
113
  properties.wrapper,
@@ -111,13 +120,15 @@ export function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, aut
111
120
  getParagraphInfo,
112
121
  createNewParagraph,
113
122
  author,
114
- formatHints,
115
- currentInsertOffset,
116
- generateRedlines
117
- );
118
- currentInsertOffset += text.length;
119
- }
120
- }
123
+ formatHints,
124
+ currentInsertOffset,
125
+ generateRedlines,
126
+ emittedCommentMarkers
127
+ );
128
+ currentParagraph = appendResult.currentParagraph;
129
+ currentInsertOffset += text.length;
130
+ }
131
+ }
121
132
 
122
133
  paragraphs.forEach(paragraph => {
123
134
  if (paragraph.parentNode) {
@@ -163,8 +174,9 @@ function appendTextToCurrent(
163
174
  author,
164
175
  formatHints = [],
165
176
  insertOffset = 0,
166
- generateRedlines = true
167
- ) {
177
+ generateRedlines = true,
178
+ emittedCommentMarkers = new WeakSet()
179
+ ) {
168
180
  let localBaseIndex = baseIndex;
169
181
  let localInsertOffset = insertOffset;
170
182
  let localParagraph = currentParagraphRef;
@@ -173,30 +185,35 @@ function appendTextToCurrent(
173
185
 
174
186
  parts.forEach(part => {
175
187
  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 {
188
+ const commentMarkers = sentinelsAtOffset.filter(sentinel => sentinel.isCommentMarker && !emittedCommentMarkers.has(sentinel.node));
189
+
190
+ commentMarkers.forEach(marker => {
191
+ emittedCommentMarkers.add(marker.node);
192
+ if (marker.node.nodeName === 'w:commentReference') {
193
+ const run = createWordElement(xmlDoc, 'w:r');
194
+ run.appendChild(marker.node.cloneNode(true));
195
+ localParagraph.appendChild(run);
196
+ } else {
184
197
  localParagraph.appendChild(marker.node.cloneNode(true));
185
198
  }
186
199
  });
187
200
 
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++;
201
+ if (part === '\n') {
202
+ const info = getParagraphInfo(localBaseIndex + 1);
203
+ const nextParagraph = createNewParagraph(info.pPr);
204
+ if (generateRedlines && type === 'insert') {
205
+ markParagraphMarkInserted(xmlDoc, nextParagraph, author);
206
+ } else if (generateRedlines && type === 'delete') {
207
+ markParagraphMarkDeleted(xmlDoc, nextParagraph, author);
208
+ }
209
+
210
+ const fragment = containerFragments.get(info.container);
211
+ if (fragment) {
212
+ fragment.appendChild(nextParagraph);
213
+ localParagraph = nextParagraph;
214
+ }
215
+ localBaseIndex++;
216
+ if (type !== 'delete') localInsertOffset++;
200
217
  return;
201
218
  }
202
219
 
@@ -220,13 +237,13 @@ function appendTextToCurrent(
220
237
 
221
238
  if (referenceMap.has(part)) {
222
239
  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);
240
+ const refNode = referenceMap.get(part);
241
+ if (refNode) {
242
+ const clone = refNode.cloneNode(true);
243
+ const run = createWordElement(xmlDoc, 'w:r');
244
+ if (rPr) run.appendChild(rPr.cloneNode(true));
245
+ run.appendChild(clone);
246
+ localParagraph.appendChild(run);
230
247
  }
231
248
  }
232
249
  localBaseIndex++;
@@ -242,14 +259,14 @@ function appendTextToCurrent(
242
259
  parent = wrapperClone;
243
260
  localParagraph.appendChild(wrapperClone);
244
261
  }
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);
262
+
263
+ if (type === 'delete') {
264
+ const run = createWordElement(xmlDoc, 'w:r');
265
+ if (rPr) run.appendChild(rPr.cloneNode(true));
266
+ const delText = createWordElement(xmlDoc, 'w:delText');
267
+ delText.setAttribute('xml:space', 'preserve');
268
+ delText.textContent = part;
269
+ run.appendChild(delText);
253
270
 
254
271
  if (generateRedlines) {
255
272
  const del = createTrackChange(xmlDoc, 'del', run, author);
@@ -271,6 +288,8 @@ function appendTextToCurrent(
271
288
  if (type !== 'delete') {
272
289
  localInsertOffset += part.length;
273
290
  }
274
- localBaseIndex += part.length;
275
- });
276
- }
291
+ localBaseIndex += part.length;
292
+ });
293
+
294
+ return { currentParagraph: localParagraph };
295
+ }
@@ -2,10 +2,12 @@
2
2
  * Run property (w:rPr) helper utilities.
3
3
  *
4
4
  * This module owns low-level formatting element operations, including
5
- * schema-order insertion, format extraction, and format add/remove transforms.
6
- */
7
-
8
- /**
5
+ * schema-order insertion, format extraction, and format add/remove transforms.
6
+ */
7
+
8
+ import { createWordElement } from '../core/word-xml.js';
9
+
10
+ /**
9
11
  * Canonical OOXML run-property schema ordering.
10
12
  * Shared by all rPr synchronizers.
11
13
  */
@@ -82,33 +84,33 @@ function _applyOverrides(xmlDoc, rPr, formatFlags, mode) {
82
84
  }
83
85
  }
84
86
 
85
- if (applyBold) {
86
- const b = xmlDoc.createElement('w:b');
87
- b.setAttribute('w:val', mode === 'add' ? '1' : '0');
88
- insertRPrChildInOrder(rPr, b);
89
-
90
- const bCs = xmlDoc.createElement('w:bCs');
91
- bCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
92
- insertRPrChildInOrder(rPr, bCs);
93
- }
94
- if (applyItalic) {
95
- const i = xmlDoc.createElement('w:i');
96
- i.setAttribute('w:val', mode === 'add' ? '1' : '0');
97
- insertRPrChildInOrder(rPr, i);
98
-
99
- const iCs = xmlDoc.createElement('w:iCs');
100
- iCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
101
- insertRPrChildInOrder(rPr, iCs);
102
- }
103
- if (applyUnderline) {
104
- const u = xmlDoc.createElement('w:u');
105
- u.setAttribute('w:val', mode === 'add' ? 'single' : 'none');
106
- insertRPrChildInOrder(rPr, u);
107
- }
108
- if (applyStrike) {
109
- const strike = xmlDoc.createElement('w:strike');
110
- strike.setAttribute('w:val', mode === 'add' ? '1' : '0');
111
- insertRPrChildInOrder(rPr, strike);
87
+ if (applyBold) {
88
+ const b = createWordElement(xmlDoc, 'w:b');
89
+ b.setAttribute('w:val', mode === 'add' ? '1' : '0');
90
+ insertRPrChildInOrder(rPr, b);
91
+
92
+ const bCs = createWordElement(xmlDoc, 'w:bCs');
93
+ bCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
94
+ insertRPrChildInOrder(rPr, bCs);
95
+ }
96
+ if (applyItalic) {
97
+ const i = createWordElement(xmlDoc, 'w:i');
98
+ i.setAttribute('w:val', mode === 'add' ? '1' : '0');
99
+ insertRPrChildInOrder(rPr, i);
100
+
101
+ const iCs = createWordElement(xmlDoc, 'w:iCs');
102
+ iCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
103
+ insertRPrChildInOrder(rPr, iCs);
104
+ }
105
+ if (applyUnderline) {
106
+ const u = createWordElement(xmlDoc, 'w:u');
107
+ u.setAttribute('w:val', mode === 'add' ? 'single' : 'none');
108
+ insertRPrChildInOrder(rPr, u);
109
+ }
110
+ if (applyStrike) {
111
+ const strike = createWordElement(xmlDoc, 'w:strike');
112
+ strike.setAttribute('w:val', mode === 'add' ? '1' : '0');
113
+ insertRPrChildInOrder(rPr, strike);
112
114
  }
113
115
  }
114
116
 
@@ -123,7 +125,7 @@ function _applyOverrides(xmlDoc, rPr, formatFlags, mode) {
123
125
  */
124
126
  export function buildOverrideRPrXml(xmlDoc, originalRun, formatToRemove, serializer) {
125
127
  const baseRPr = originalRun.getElementsByTagName('w:rPr')[0] || null;
126
- const rPr = baseRPr ? baseRPr.cloneNode(true) : xmlDoc.createElement('w:rPr');
128
+ const rPr = baseRPr ? baseRPr.cloneNode(true) : createWordElement(xmlDoc, 'w:rPr');
127
129
  _applyOverrides(xmlDoc, rPr, formatToRemove, 'remove');
128
130
 
129
131
  let rPrXml = serializer.serializeToString(rPr);
@@ -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
+ }