@ansonlai/docx-redline-js 0.4.0 → 0.5.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 +589 -287
- package/ARCHITECTURE.md +215 -9
- package/CHANGELOG.md +319 -0
- package/README.md +604 -360
- package/adapters/config.js +45 -43
- package/bin/docx-redline.js +3 -0
- package/core/list-targeting.js +101 -110
- package/core/paragraph-targeting.js +501 -61
- package/core/paragraph-text.js +209 -0
- package/core/revision-cloning.js +38 -0
- package/core/types.js +64 -10
- package/core/word-xml.js +43 -15
- package/dist/docx-redline-js.esm.js +2849 -466
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +87 -76
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +342 -23
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
- package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
- package/docs/schemas/document-operations.schema.json +109 -0
- package/docs/test-comparison-dashboard.html +4250 -7
- package/engine/formatting-removal.js +11 -2
- package/engine/oxml-engine.js +491 -336
- package/engine/reconstruction-mode.js +15 -14
- package/engine/reconstruction-writer.js +247 -142
- package/engine/route-selection.js +35 -0
- package/engine/rpr-helpers.js +334 -35
- package/engine/run-builders.js +239 -196
- package/engine/surgical-diff-application.js +222 -37
- package/engine/surgical-mode.js +134 -6
- package/engine/surgical-spans.js +52 -1
- package/engine/table-cell-context.js +3 -6
- package/engine/table-mode.js +1 -1
- package/index.d.ts +234 -6
- package/index.js +24 -1
- package/node/cli.js +317 -0
- package/node/docx-document.js +302 -0
- package/node/index.d.ts +31 -0
- package/node/index.js +2 -0
- package/node/zip-archive.js +52 -0
- package/orchestration/list-markdown.js +10 -16
- package/orchestration/list-parsing.js +7 -12
- package/orchestration/list-structural-fallback.js +21 -10
- package/package.json +24 -3
- package/pipeline/content-analysis.js +12 -17
- package/pipeline/ingestion-export.js +3 -31
- package/pipeline/ingestion-paragraph.js +10 -5
- package/pipeline/list-generation.js +150 -55
- package/pipeline/list-markers.js +70 -3
- package/pipeline/serialization.js +4 -2
- package/pipeline/structured-content.js +160 -0
- package/scripts/apply_changes.mjs +27 -0
- package/scripts/benchmark-operation-session.mjs +137 -0
- package/scripts/benchmark-targeting-browser.html +74 -0
- package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
- package/scripts/benchmark-test-runner.mjs +59 -0
- package/scripts/build-test-dashboard.mjs +23 -0
- package/scripts/export-lane1-fixtures.mjs +380 -0
- package/scripts/export-reredline-stress-fixtures.mjs +317 -0
- package/scripts/export-validation-fixtures.mjs +1 -1
- package/scripts/extract_text.mjs +7 -0
- package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
- package/scripts/generate-test-dashboard.mjs +362 -11
- package/scripts/lib/word-coverage-catalogue.mjs +6 -2
- package/scripts/profile-route-selection.mjs +19 -0
- package/scripts/render-agenda-multilevel.mjs +0 -5
- package/scripts/render-multilevel-cases.mjs +0 -1
- package/scripts/run-tests.mjs +107 -35
- package/scripts/word-com-corpus-suite.ps1 +3 -0
- package/scripts/word-com-differential.ps1 +64 -4
- package/scripts/word-com-suite.ps1 +3 -0
- package/services/batch-operation-orchestrator.js +494 -0
- package/services/capture-engine.js +226 -0
- package/services/comment-builders.js +23 -6
- package/services/comment-engine.js +108 -47
- package/services/comment-locator.js +187 -82
- package/services/comment-replies.js +95 -0
- package/services/document-inspection.js +258 -0
- package/services/document-operation-applier.js +372 -0
- package/services/document-operation-contract.js +323 -0
- package/services/document-operation-mutations.js +1733 -0
- package/services/document-operation-session.js +258 -0
- package/services/numbering-service.js +14 -5
- package/services/operation-heuristics.js +173 -0
- package/services/operation-preflight.js +366 -0
- package/services/receipt-collector.js +288 -0
- package/services/revision-comment-management.js +37 -5
- package/services/revision-token.js +290 -0
- package/services/standalone-docx-plumbing.js +123 -8
- package/services/standalone-operation-runner.d.ts +296 -0
- package/services/standalone-operation-runner.js +10 -1455
- package/services/table-reconciliation.js +15 -6
- package/docs/VALIDATION.md +0 -183
- package/docs/WORD-MANUAL-REVIEW.md +0 -138
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
- /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
package/engine/run-builders.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OOXML run/track-change builders.
|
|
3
|
-
*
|
|
4
|
-
* This module centralizes creation of `w:r`, `w:ins`, `w:del`, `w:rPrChange`,
|
|
5
|
-
* and `w:pPrChange` elements used by surgical and reconstruction modes.
|
|
6
|
-
*/
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* OOXML run/track-change builders.
|
|
3
|
+
*
|
|
4
|
+
* This module centralizes creation of `w:r`, `w:ins`, `w:del`, `w:rPrChange`,
|
|
5
|
+
* and `w:pPrChange` elements used by surgical and reconstruction modes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
8
|
import { extractFormatFromRPr, RPR_SCHEMA_ORDER } from './rpr-helpers.js';
|
|
9
9
|
import { createRevisionMetadata } from '../core/types.js';
|
|
10
10
|
import { getFirstElementByTag } from '../core/xml-query.js';
|
|
@@ -12,23 +12,24 @@ import { createWordElement } from '../core/word-xml.js';
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Creates an insertion/deletion wrapper.
|
|
15
|
-
*
|
|
16
|
-
* @param {Document} xmlDoc - XML document
|
|
17
|
-
* @param {'ins'|'del'} type - Wrapper type
|
|
18
|
-
* @param {Element|null} run - Optional run to append
|
|
19
|
-
* @param {string} author - Change author
|
|
20
|
-
* @
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
*
|
|
16
|
+
* @param {Document} xmlDoc - XML document
|
|
17
|
+
* @param {'ins'|'del'} type - Wrapper type
|
|
18
|
+
* @param {Element|null} run - Optional run to append
|
|
19
|
+
* @param {string} author - Change author
|
|
20
|
+
* @param {Object|null} [revisionMetadata=null] - Optional explicit revision metadata
|
|
21
|
+
* @returns {Element}
|
|
22
|
+
*/
|
|
23
|
+
export function createTrackChange(xmlDoc, type, run, author, revisionMetadata = null) {
|
|
23
24
|
const wrapper = createWordElement(xmlDoc, type === 'ins' ? 'w:ins' : 'w:del');
|
|
24
|
-
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
25
|
-
wrapper.setAttribute('w:id', String(metadata.id));
|
|
26
|
-
wrapper.setAttribute('w:author', metadata.author);
|
|
27
|
-
wrapper.setAttribute('w:date', metadata.date);
|
|
28
|
-
if (run) {
|
|
29
|
-
wrapper.appendChild(run);
|
|
30
|
-
}
|
|
31
|
-
return wrapper;
|
|
25
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, type === 'ins' ? 'ins' : 'del');
|
|
26
|
+
wrapper.setAttribute('w:id', String(metadata.id));
|
|
27
|
+
wrapper.setAttribute('w:author', metadata.author);
|
|
28
|
+
wrapper.setAttribute('w:date', metadata.date);
|
|
29
|
+
if (run) {
|
|
30
|
+
wrapper.appendChild(run);
|
|
31
|
+
}
|
|
32
|
+
return wrapper;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function getDirectWordChild(node, localName) {
|
|
@@ -58,7 +59,7 @@ function ensureParagraphMarkRunProperties(xmlDoc, pPr) {
|
|
|
58
59
|
return rPr;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function markParagraphMark(xmlDoc, paragraph, author, type) {
|
|
62
|
+
function markParagraphMark(xmlDoc, paragraph, author, type, revisionMetadata = null) {
|
|
62
63
|
const pPr = ensureParagraphProperties(xmlDoc, paragraph);
|
|
63
64
|
const rPr = ensureParagraphMarkRunProperties(xmlDoc, pPr);
|
|
64
65
|
|
|
@@ -70,7 +71,7 @@ function markParagraphMark(xmlDoc, paragraph, author, type) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const marker = createWordElement(xmlDoc, type === 'ins' ? 'w:ins' : 'w:del');
|
|
73
|
-
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
74
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, type === 'ins' ? 'ins' : 'del');
|
|
74
75
|
marker.setAttribute('w:id', String(metadata.id));
|
|
75
76
|
marker.setAttribute('w:author', metadata.author);
|
|
76
77
|
marker.setAttribute('w:date', metadata.date);
|
|
@@ -84,10 +85,11 @@ function markParagraphMark(xmlDoc, paragraph, author, type) {
|
|
|
84
85
|
* @param {Document} xmlDoc - XML document
|
|
85
86
|
* @param {Element} paragraph - Paragraph to mark
|
|
86
87
|
* @param {string} author - Change author
|
|
88
|
+
* @param {Object|null} [revisionMetadata=null] - Optional explicit revision metadata
|
|
87
89
|
* @returns {Element}
|
|
88
90
|
*/
|
|
89
|
-
export function markParagraphMarkInserted(xmlDoc, paragraph, author) {
|
|
90
|
-
return markParagraphMark(xmlDoc, paragraph, author, 'ins');
|
|
91
|
+
export function markParagraphMarkInserted(xmlDoc, paragraph, author, revisionMetadata = null) {
|
|
92
|
+
return markParagraphMark(xmlDoc, paragraph, author, 'ins', revisionMetadata);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|
|
@@ -96,21 +98,22 @@ export function markParagraphMarkInserted(xmlDoc, paragraph, author) {
|
|
|
96
98
|
* @param {Document} xmlDoc - XML document
|
|
97
99
|
* @param {Element} paragraph - Paragraph to mark
|
|
98
100
|
* @param {string} author - Change author
|
|
101
|
+
* @param {Object|null} [revisionMetadata=null] - Optional explicit revision metadata
|
|
99
102
|
* @returns {Element}
|
|
100
103
|
*/
|
|
101
|
-
export function markParagraphMarkDeleted(xmlDoc, paragraph, author) {
|
|
102
|
-
return markParagraphMark(xmlDoc, paragraph, author, 'del');
|
|
104
|
+
export function markParagraphMarkDeleted(xmlDoc, paragraph, author, revisionMetadata = null) {
|
|
105
|
+
return markParagraphMark(xmlDoc, paragraph, author, 'del', revisionMetadata);
|
|
103
106
|
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Creates a text run with optional formatting.
|
|
107
|
-
*
|
|
108
|
-
* @param {Document} xmlDoc - XML document
|
|
109
|
-
* @param {string} text - Text content
|
|
110
|
-
* @param {Element|null} rPr - Run properties
|
|
111
|
-
* @param {boolean} isDelete - Use `w:delText` instead of `w:t`
|
|
112
|
-
* @returns {Element}
|
|
113
|
-
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a text run with optional formatting.
|
|
110
|
+
*
|
|
111
|
+
* @param {Document} xmlDoc - XML document
|
|
112
|
+
* @param {string} text - Text content
|
|
113
|
+
* @param {Element|null} rPr - Run properties
|
|
114
|
+
* @param {boolean} isDelete - Use `w:delText` instead of `w:t`
|
|
115
|
+
* @returns {Element}
|
|
116
|
+
*/
|
|
114
117
|
export function createTextRun(xmlDoc, text, rPr, isDelete) {
|
|
115
118
|
const run = createWordElement(xmlDoc, 'w:r');
|
|
116
119
|
if (rPr) run.appendChild(rPr.cloneNode(true));
|
|
@@ -123,50 +126,50 @@ export function createTextRun(xmlDoc, text, rPr, isDelete) {
|
|
|
123
126
|
const textEl = createWordElement(xmlDoc, isDelete ? 'w:delText' : 'w:t');
|
|
124
127
|
textEl.setAttribute('xml:space', 'preserve');
|
|
125
128
|
textEl.textContent = text;
|
|
126
|
-
run.appendChild(textEl);
|
|
127
|
-
|
|
128
|
-
return run;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Creates an array of runs with formatting applied from hints.
|
|
133
|
-
*
|
|
134
|
-
* @param {Document} xmlDoc - XML document
|
|
135
|
-
* @param {string} text - Text to split and format
|
|
136
|
-
* @param {Element|null} baseRPr - Base run properties
|
|
137
|
-
* @param {Array} formatHints - Formatting hints
|
|
138
|
-
* @param {number} baseOffset - Absolute base offset
|
|
139
|
-
* @param {string} [author] - Change author
|
|
140
|
-
* @param {boolean} [generateRedlines] - Whether to create rPrChange
|
|
141
|
-
* @returns {Element[]}
|
|
142
|
-
*/
|
|
129
|
+
run.appendChild(textEl);
|
|
130
|
+
|
|
131
|
+
return run;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates an array of runs with formatting applied from hints.
|
|
136
|
+
*
|
|
137
|
+
* @param {Document} xmlDoc - XML document
|
|
138
|
+
* @param {string} text - Text to split and format
|
|
139
|
+
* @param {Element|null} baseRPr - Base run properties
|
|
140
|
+
* @param {Array} formatHints - Formatting hints
|
|
141
|
+
* @param {number} baseOffset - Absolute base offset
|
|
142
|
+
* @param {string} [author] - Change author
|
|
143
|
+
* @param {boolean} [generateRedlines] - Whether to create rPrChange
|
|
144
|
+
* @returns {Element[]}
|
|
145
|
+
*/
|
|
143
146
|
export function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffset, author, generateRedlines) {
|
|
144
147
|
if (!text) return [];
|
|
145
|
-
|
|
146
|
-
const breaks = new Set([0, text.length]);
|
|
147
|
-
for (const hint of formatHints) {
|
|
148
|
-
const localStart = Math.max(0, hint.start - baseOffset);
|
|
149
|
-
const localEnd = Math.min(text.length, hint.end - baseOffset);
|
|
150
|
-
if (localStart >= 0 && localStart < text.length) breaks.add(localStart);
|
|
151
|
-
if (localEnd > 0 && localEnd <= text.length) breaks.add(localEnd);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const sortedBreaks = Array.from(breaks).sort((a, b) => a - b);
|
|
155
|
-
const runs = [];
|
|
156
|
-
|
|
157
|
-
for (let i = 0; i < sortedBreaks.length - 1; i++) {
|
|
158
|
-
const start = sortedBreaks[i];
|
|
159
|
-
const end = sortedBreaks[i + 1];
|
|
160
|
-
const segment = text.slice(start, end);
|
|
161
|
-
if (!segment) continue;
|
|
162
|
-
|
|
163
|
-
const segmentBaseOffset = baseOffset + start;
|
|
164
|
-
const segmentEndOffset = baseOffset + end;
|
|
165
|
-
|
|
166
|
-
const applicableHints = formatHints.filter(h =>
|
|
167
|
-
h.start <= segmentBaseOffset && h.end >= segmentEndOffset
|
|
168
|
-
);
|
|
169
|
-
|
|
148
|
+
|
|
149
|
+
const breaks = new Set([0, text.length]);
|
|
150
|
+
for (const hint of formatHints) {
|
|
151
|
+
const localStart = Math.max(0, hint.start - baseOffset);
|
|
152
|
+
const localEnd = Math.min(text.length, hint.end - baseOffset);
|
|
153
|
+
if (localStart >= 0 && localStart < text.length) breaks.add(localStart);
|
|
154
|
+
if (localEnd > 0 && localEnd <= text.length) breaks.add(localEnd);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const sortedBreaks = Array.from(breaks).sort((a, b) => a - b);
|
|
158
|
+
const runs = [];
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < sortedBreaks.length - 1; i++) {
|
|
161
|
+
const start = sortedBreaks[i];
|
|
162
|
+
const end = sortedBreaks[i + 1];
|
|
163
|
+
const segment = text.slice(start, end);
|
|
164
|
+
if (!segment) continue;
|
|
165
|
+
|
|
166
|
+
const segmentBaseOffset = baseOffset + start;
|
|
167
|
+
const segmentEndOffset = baseOffset + end;
|
|
168
|
+
|
|
169
|
+
const applicableHints = formatHints.filter(h =>
|
|
170
|
+
h.start <= segmentBaseOffset && h.end >= segmentEndOffset
|
|
171
|
+
);
|
|
172
|
+
|
|
170
173
|
const combinedFormat = { ...extractFormatFromRPr(baseRPr) };
|
|
171
174
|
applicableHints.forEach(h => {
|
|
172
175
|
if (h.format) Object.assign(combinedFormat, h.format);
|
|
@@ -178,20 +181,20 @@ export function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffs
|
|
|
178
181
|
? injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines)
|
|
179
182
|
: baseRPr?.cloneNode(true) || null;
|
|
180
183
|
runs.push(createTextRunWithRPrElement(xmlDoc, segment, formattedRPr, false));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return runs;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Creates a text run with an existing rPr element (no clone).
|
|
188
|
-
*
|
|
189
|
-
* @param {Document} xmlDoc - XML document
|
|
190
|
-
* @param {string} text - Text content
|
|
191
|
-
* @param {Element|null} rPrElement - Run properties element
|
|
192
|
-
* @param {boolean} isDelete - Use `w:delText` instead of `w:t`
|
|
193
|
-
* @returns {Element}
|
|
194
|
-
*/
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return runs;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates a text run with an existing rPr element (no clone).
|
|
191
|
+
*
|
|
192
|
+
* @param {Document} xmlDoc - XML document
|
|
193
|
+
* @param {string} text - Text content
|
|
194
|
+
* @param {Element|null} rPrElement - Run properties element
|
|
195
|
+
* @param {boolean} isDelete - Use `w:delText` instead of `w:t`
|
|
196
|
+
* @returns {Element}
|
|
197
|
+
*/
|
|
195
198
|
export function createTextRunWithRPrElement(xmlDoc, text, rPrElement, isDelete) {
|
|
196
199
|
const run = createWordElement(xmlDoc, 'w:r');
|
|
197
200
|
if (rPrElement) run.appendChild(rPrElement);
|
|
@@ -204,8 +207,8 @@ export function createTextRunWithRPrElement(xmlDoc, text, rPrElement, isDelete)
|
|
|
204
207
|
const textEl = createWordElement(xmlDoc, isDelete ? 'w:delText' : 'w:t');
|
|
205
208
|
textEl.setAttribute('xml:space', 'preserve');
|
|
206
209
|
textEl.textContent = text;
|
|
207
|
-
run.appendChild(textEl);
|
|
208
|
-
|
|
210
|
+
run.appendChild(textEl);
|
|
211
|
+
|
|
209
212
|
return run;
|
|
210
213
|
}
|
|
211
214
|
|
|
@@ -236,115 +239,155 @@ function appendVisibleTextPieces(xmlDoc, run, text) {
|
|
|
236
239
|
run.appendChild(textEl);
|
|
237
240
|
}
|
|
238
241
|
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Creates a new rPr synchronized to the requested core formatting flags.
|
|
242
|
-
*
|
|
243
|
-
* @param {Document} xmlDoc - XML document
|
|
244
|
-
* @param {Element|null} baseRPr - Base run properties
|
|
245
|
-
* @param {Object|null} format - Format flags
|
|
246
|
-
* @param {string} [author] - Change author
|
|
247
|
-
* @param {boolean} [generateRedlines] - Whether to create rPrChange
|
|
248
|
-
* @returns {Element}
|
|
249
|
-
*/
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Creates a new rPr synchronized to the requested core formatting flags.
|
|
245
|
+
*
|
|
246
|
+
* @param {Document} xmlDoc - XML document
|
|
247
|
+
* @param {Element|null} baseRPr - Base run properties
|
|
248
|
+
* @param {Object|null} format - Format flags
|
|
249
|
+
* @param {string} [author] - Change author
|
|
250
|
+
* @param {boolean} [generateRedlines] - Whether to create rPrChange
|
|
251
|
+
* @returns {Element}
|
|
252
|
+
*/
|
|
250
253
|
export function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines) {
|
|
251
254
|
const rPr = createWordElement(xmlDoc, 'w:rPr');
|
|
252
|
-
|
|
253
|
-
if (baseRPr) {
|
|
254
|
-
Array.from(baseRPr.childNodes).forEach(child => {
|
|
255
|
-
if (!['w:b', 'w:bCs', 'w:i', 'w:iCs', 'w:u', 'w:strike', 'w:rPrChange'].includes(child.nodeName)) {
|
|
256
|
-
rPr.appendChild(child.cloneNode(true));
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const activeFormat = format || { bold: false, italic: false, underline: false, strikethrough: false };
|
|
262
|
-
|
|
263
|
-
if (author && generateRedlines) {
|
|
264
|
-
createRPrChange(xmlDoc, rPr, author, baseRPr);
|
|
265
|
-
}
|
|
266
|
-
|
|
255
|
+
|
|
256
|
+
if (baseRPr) {
|
|
257
|
+
Array.from(baseRPr.childNodes).forEach(child => {
|
|
258
|
+
if (!['w:b', 'w:bCs', 'w:i', 'w:iCs', 'w:u', 'w:strike', 'w:rPrChange'].includes(child.nodeName)) {
|
|
259
|
+
rPr.appendChild(child.cloneNode(true));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const activeFormat = format || { bold: false, italic: false, underline: false, strikethrough: false };
|
|
265
|
+
|
|
266
|
+
if (author && generateRedlines) {
|
|
267
|
+
createRPrChange(xmlDoc, rPr, author, baseRPr);
|
|
268
|
+
}
|
|
269
|
+
|
|
267
270
|
const syncElement = (tagName, isOn, valOn = null, valOff = '0') => {
|
|
268
271
|
const el = createWordElement(xmlDoc, tagName);
|
|
269
|
-
if (isOn) {
|
|
270
|
-
if (valOn) el.setAttribute('w:val', valOn);
|
|
271
|
-
} else if (valOff) {
|
|
272
|
-
el.setAttribute('w:val', valOff);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const myIndex = RPR_SCHEMA_ORDER.indexOf(tagName);
|
|
276
|
-
const myPriority = myIndex === -1 ? 999 : myIndex;
|
|
277
|
-
|
|
278
|
-
let inserted = false;
|
|
279
|
-
for (const child of Array.from(rPr.childNodes)) {
|
|
280
|
-
if (child.nodeType !== 1) continue;
|
|
281
|
-
const childIndex = RPR_SCHEMA_ORDER.indexOf(child.nodeName);
|
|
282
|
-
const childPriority = childIndex === -1 ? 999 : childIndex;
|
|
283
|
-
if (childPriority > myPriority) {
|
|
284
|
-
rPr.insertBefore(el, child);
|
|
285
|
-
inserted = true;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
if (!inserted) rPr.appendChild(el);
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
syncElement('w:b', !!activeFormat.bold, '1', '0');
|
|
293
|
-
syncElement('w:bCs', !!activeFormat.bold, '1', '0');
|
|
294
|
-
syncElement('w:i', !!activeFormat.italic, '1', '0');
|
|
295
|
-
syncElement('w:iCs', !!activeFormat.italic, '1', '0');
|
|
296
|
-
syncElement('w:u', !!activeFormat.underline, 'single', 'none');
|
|
297
|
-
syncElement('w:strike', !!activeFormat.strikethrough, '1', '0');
|
|
298
|
-
|
|
299
|
-
return rPr;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Creates and attaches a `w:rPrChange` snapshot.
|
|
304
|
-
*
|
|
305
|
-
* @param {Document} xmlDoc - XML document
|
|
306
|
-
* @param {Element} rPr - Target run properties
|
|
307
|
-
* @param {string} author - Change author
|
|
308
|
-
* @param {string} dateStr - ISO date string
|
|
309
|
-
* @param {Element} [sourceNode] - Optional source for previous state snapshot
|
|
310
|
-
* @returns {Element}
|
|
311
|
-
*/
|
|
272
|
+
if (isOn) {
|
|
273
|
+
if (valOn) el.setAttribute('w:val', valOn);
|
|
274
|
+
} else if (valOff) {
|
|
275
|
+
el.setAttribute('w:val', valOff);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const myIndex = RPR_SCHEMA_ORDER.indexOf(tagName);
|
|
279
|
+
const myPriority = myIndex === -1 ? 999 : myIndex;
|
|
280
|
+
|
|
281
|
+
let inserted = false;
|
|
282
|
+
for (const child of Array.from(rPr.childNodes)) {
|
|
283
|
+
if (child.nodeType !== 1) continue;
|
|
284
|
+
const childIndex = RPR_SCHEMA_ORDER.indexOf(child.nodeName);
|
|
285
|
+
const childPriority = childIndex === -1 ? 999 : childIndex;
|
|
286
|
+
if (childPriority > myPriority) {
|
|
287
|
+
rPr.insertBefore(el, child);
|
|
288
|
+
inserted = true;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!inserted) rPr.appendChild(el);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
syncElement('w:b', !!activeFormat.bold, '1', '0');
|
|
296
|
+
syncElement('w:bCs', !!activeFormat.bold, '1', '0');
|
|
297
|
+
syncElement('w:i', !!activeFormat.italic, '1', '0');
|
|
298
|
+
syncElement('w:iCs', !!activeFormat.italic, '1', '0');
|
|
299
|
+
syncElement('w:u', !!activeFormat.underline, 'single', 'none');
|
|
300
|
+
syncElement('w:strike', !!activeFormat.strikethrough, '1', '0');
|
|
301
|
+
|
|
302
|
+
return rPr;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Creates and attaches a `w:rPrChange` snapshot.
|
|
307
|
+
*
|
|
308
|
+
* @param {Document} xmlDoc - XML document
|
|
309
|
+
* @param {Element} rPr - Target run properties
|
|
310
|
+
* @param {string} author - Change author
|
|
311
|
+
* @param {string} dateStr - ISO date string
|
|
312
|
+
* @param {Element} [sourceNode] - Optional source for previous state snapshot
|
|
313
|
+
* @returns {Element}
|
|
314
|
+
*/
|
|
312
315
|
export function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
|
|
313
316
|
const rPrChange = createWordElement(xmlDoc, 'w:rPrChange');
|
|
314
|
-
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
317
|
+
const metadata = createRevisionMetadata(author, xmlDoc, 'rPrChange');
|
|
315
318
|
rPrChange.setAttribute('w:id', String(metadata.id));
|
|
316
319
|
rPrChange.setAttribute('w:author', metadata.author);
|
|
317
320
|
rPrChange.setAttribute('w:date', dateStr || metadata.date);
|
|
318
|
-
|
|
321
|
+
|
|
319
322
|
const previousRPr = createWordElement(xmlDoc, 'w:rPr');
|
|
320
|
-
const source = sourceNode || rPr;
|
|
321
|
-
|
|
322
|
-
Array.from(source.childNodes).forEach(child => {
|
|
323
|
-
if (child.nodeName !== 'w:rPrChange') {
|
|
324
|
-
previousRPr.appendChild(child.cloneNode(true));
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
rPrChange.appendChild(previousRPr);
|
|
329
|
-
|
|
330
|
-
const existing = getFirstElementByTag(rPr, 'w:rPrChange');
|
|
331
|
-
if (existing) {
|
|
332
|
-
rPr.removeChild(existing);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
rPr.appendChild(rPrChange);
|
|
336
|
-
return rPrChange;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Creates `w:rPrChange` for track formatting changes.
|
|
341
|
-
*
|
|
342
|
-
* @param {Document} xmlDoc - XML document
|
|
343
|
-
* @param {Element} rPr - Run properties target
|
|
344
|
-
* @param {string} author - Change author
|
|
345
|
-
* @param {Element} [previousRPrArg] - Optional explicit previous-state source
|
|
346
|
-
* @returns {void}
|
|
347
|
-
*/
|
|
323
|
+
const source = sourceNode || rPr;
|
|
324
|
+
|
|
325
|
+
Array.from(source.childNodes).forEach(child => {
|
|
326
|
+
if (child.nodeName !== 'w:rPrChange') {
|
|
327
|
+
previousRPr.appendChild(child.cloneNode(true));
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
rPrChange.appendChild(previousRPr);
|
|
332
|
+
|
|
333
|
+
const existing = getFirstElementByTag(rPr, 'w:rPrChange');
|
|
334
|
+
if (existing) {
|
|
335
|
+
rPr.removeChild(existing);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
rPr.appendChild(rPrChange);
|
|
339
|
+
return rPrChange;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Creates `w:rPrChange` for track formatting changes.
|
|
344
|
+
*
|
|
345
|
+
* @param {Document} xmlDoc - XML document
|
|
346
|
+
* @param {Element} rPr - Run properties target
|
|
347
|
+
* @param {string} author - Change author
|
|
348
|
+
* @param {Element} [previousRPrArg] - Optional explicit previous-state source
|
|
349
|
+
* @returns {void}
|
|
350
|
+
*/
|
|
348
351
|
function createRPrChange(xmlDoc, rPr, author, previousRPrArg) {
|
|
349
352
|
snapshotAndAttachRPrChange(xmlDoc, rPr, author, null, previousRPrArg || rPr);
|
|
350
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Creates and attaches a `w:pPrChange` snapshot to paragraph properties.
|
|
357
|
+
*
|
|
358
|
+
* @param {Document} xmlDoc - XML document
|
|
359
|
+
* @param {Element} pPr - Target paragraph properties
|
|
360
|
+
* @param {string} author - Change author
|
|
361
|
+
* @param {string} [dateStr] - ISO date string
|
|
362
|
+
* @param {Element} [sourceNode] - Optional source for previous state snapshot
|
|
363
|
+
* @returns {Element}
|
|
364
|
+
*/
|
|
365
|
+
export function snapshotAndAttachPPrChange(xmlDoc, pPr, author, dateStr, sourceNode) {
|
|
366
|
+
const pPrChange = createWordElement(xmlDoc, 'w:pPrChange');
|
|
367
|
+
const metadata = createRevisionMetadata(author, xmlDoc, 'pPrChange');
|
|
368
|
+
pPrChange.setAttribute('w:id', String(metadata.id));
|
|
369
|
+
pPrChange.setAttribute('w:author', metadata.author);
|
|
370
|
+
pPrChange.setAttribute('w:date', dateStr || metadata.date);
|
|
371
|
+
|
|
372
|
+
const previousPPr = createWordElement(xmlDoc, 'w:pPr');
|
|
373
|
+
const source = sourceNode || pPr;
|
|
374
|
+
|
|
375
|
+
Array.from(source.childNodes).forEach(child => {
|
|
376
|
+
if (child.nodeType === 1 && child.nodeName !== 'w:pPrChange' && child.localName !== 'pPrChange') {
|
|
377
|
+
previousPPr.appendChild(child.cloneNode(true));
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
pPrChange.appendChild(previousPPr);
|
|
382
|
+
|
|
383
|
+
const existing = Array.from(pPr.childNodes).find(
|
|
384
|
+
c => c.nodeType === 1 && (c.nodeName === 'w:pPrChange' || c.localName === 'pPrChange')
|
|
385
|
+
);
|
|
386
|
+
if (existing) {
|
|
387
|
+
pPr.removeChild(existing);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
pPr.appendChild(pPrChange);
|
|
391
|
+
return pPrChange;
|
|
392
|
+
}
|
|
393
|
+
|