@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.
- package/AGENTS.md +53 -4
- package/ARCHITECTURE.md +75 -11
- package/README.md +62 -3
- package/core/redline-validation.js +156 -0
- package/core/types.js +35 -8
- package/core/word-xml.js +90 -0
- package/dist/docx-redline-js.esm.js +3195 -2592
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +71 -67
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/VALIDATION.md +104 -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/docs/plans/2026-05-31-architectural changes.md +591 -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 +172 -0
- package/index.js +50 -47
- package/package.json +10 -2
- 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 +40 -0
- package/scripts/check-types.mjs +29 -0
- package/scripts/export-validation-fixtures.mjs +125 -0
- package/scripts/lib/minimal-zip.mjs +155 -0
- package/scripts/run-tests.mjs +43 -0
- package/scripts/validate-fixtures-xsd.sh +37 -0
- package/scripts/word-com-differential.ps1 +133 -0
- package/scripts/word-com-smoke.ps1 +48 -0
- package/services/comment-locator.js +10 -9
- package/services/revision-comment-management.js +115 -1
- package/services/standalone-operation-runner.js +119 -69
- 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 {
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
run
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
|
248
|
-
if (rPr) run.appendChild(rPr.cloneNode(true));
|
|
249
|
-
const delText = xmlDoc
|
|
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
|
+
}
|
package/engine/rpr-helpers.js
CHANGED
|
@@ -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
|
|
87
|
-
b.setAttribute('w:val', mode === 'add' ? '1' : '0');
|
|
88
|
-
insertRPrChildInOrder(rPr, b);
|
|
89
|
-
|
|
90
|
-
const bCs = xmlDoc
|
|
91
|
-
bCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
|
|
92
|
-
insertRPrChildInOrder(rPr, bCs);
|
|
93
|
-
}
|
|
94
|
-
if (applyItalic) {
|
|
95
|
-
const i = xmlDoc
|
|
96
|
-
i.setAttribute('w:val', mode === 'add' ? '1' : '0');
|
|
97
|
-
insertRPrChildInOrder(rPr, i);
|
|
98
|
-
|
|
99
|
-
const iCs = xmlDoc
|
|
100
|
-
iCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
|
|
101
|
-
insertRPrChildInOrder(rPr, iCs);
|
|
102
|
-
}
|
|
103
|
-
if (applyUnderline) {
|
|
104
|
-
const u = xmlDoc
|
|
105
|
-
u.setAttribute('w:val', mode === 'add' ? 'single' : 'none');
|
|
106
|
-
insertRPrChildInOrder(rPr, u);
|
|
107
|
-
}
|
|
108
|
-
if (applyStrike) {
|
|
109
|
-
const strike = xmlDoc
|
|
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
|
|
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);
|
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
|
+
}
|