@ansonlai/docx-redline-js 0.2.0 → 0.2.1
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/dist/docx-redline-js.esm.js +28 -31
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +57 -57
- package/dist/docx-redline-js.esm.min.js.map +3 -3
- package/engine/formatting-removal.js +14 -8
- package/engine/run-builders.js +14 -10
- package/engine/surgical-diff-application.js +7 -21
- package/package.json +3 -3
|
@@ -9,6 +9,12 @@ import { parseOoxml, serializeOoxml } from './oxml-engine.js';
|
|
|
9
9
|
import { getDefaultAuthor } from '../adapters/config.js';
|
|
10
10
|
import { createRevisionMetadata } from '../core/types.js';
|
|
11
11
|
import { createWordElement } from '../core/word-xml.js';
|
|
12
|
+
|
|
13
|
+
function removeNode(node) {
|
|
14
|
+
if (node?.parentNode) {
|
|
15
|
+
node.parentNode.removeChild(node);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Removes specific formatting properties from a run properties (w:rPr) element.
|
|
@@ -31,7 +37,7 @@ export function removeFormattingFromRPr(rPr, formatTypes = ['all']) {
|
|
|
31
37
|
toRemove.forEach(tag => {
|
|
32
38
|
// Handle both namespaced and non-namespaced versions
|
|
33
39
|
const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace('w:', '')}`);
|
|
34
|
-
elements.forEach(
|
|
40
|
+
elements.forEach(removeNode);
|
|
35
41
|
});
|
|
36
42
|
} else {
|
|
37
43
|
// Remove specific properties
|
|
@@ -55,7 +61,7 @@ export function removeFormattingFromRPr(rPr, formatTypes = ['all']) {
|
|
|
55
61
|
if (tag) {
|
|
56
62
|
// Handle both namespaced and non-namespaced versions
|
|
57
63
|
const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace('w:', '')}`);
|
|
58
|
-
elements.forEach(
|
|
64
|
+
elements.forEach(removeNode);
|
|
59
65
|
}
|
|
60
66
|
});
|
|
61
67
|
}
|
|
@@ -112,7 +118,7 @@ export function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTyp
|
|
|
112
118
|
}
|
|
113
119
|
} else {
|
|
114
120
|
// Remove entire rPr if empty
|
|
115
|
-
rPr
|
|
121
|
+
removeNode(rPr);
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
}
|
|
@@ -179,7 +185,7 @@ export function injectHighlightIntoRPr(doc, rPr, color = 'yellow', options = {})
|
|
|
179
185
|
// --- APPLY CHANGE ---
|
|
180
186
|
// Remove any existing highlight
|
|
181
187
|
const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W, 'highlight');
|
|
182
|
-
Array.from(existingHighlight).forEach(
|
|
188
|
+
Array.from(existingHighlight).forEach(removeNode);
|
|
183
189
|
|
|
184
190
|
// Create and add new highlight element
|
|
185
191
|
const highlightEl = createWordElement(doc, 'w:highlight');
|
|
@@ -201,7 +207,7 @@ export function injectHighlightIntoRPr(doc, rPr, color = 'yellow', options = {})
|
|
|
201
207
|
|
|
202
208
|
// Remove any EXISTING rPrChange to avoid duplicates or nested weirdness
|
|
203
209
|
const existingChange = rPrElement.getElementsByTagNameNS(NS_W, 'rPrChange');
|
|
204
|
-
Array.from(existingChange).forEach(
|
|
210
|
+
Array.from(existingChange).forEach(removeNode);
|
|
205
211
|
|
|
206
212
|
// Append to rPr
|
|
207
213
|
rPrElement.appendChild(rPrChange);
|
|
@@ -274,7 +280,7 @@ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow',
|
|
|
274
280
|
// Update text content
|
|
275
281
|
const tNodes = prefixRun.getElementsByTagNameNS(NS_W, 't');
|
|
276
282
|
// Simply remove all t nodes and add one with new text to avoid complexity of multiple t nodes
|
|
277
|
-
Array.from(tNodes).forEach(
|
|
283
|
+
Array.from(tNodes).forEach(removeNode);
|
|
278
284
|
const newT = createWordElement(doc, 'w:t');
|
|
279
285
|
// Preserve xml:space="preserve" if it existed, or just add it usually
|
|
280
286
|
newT.setAttribute('xml:space', 'preserve');
|
|
@@ -288,7 +294,7 @@ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow',
|
|
|
288
294
|
const matchRun = run.cloneNode(true);
|
|
289
295
|
// Update text content
|
|
290
296
|
const tNodes = matchRun.getElementsByTagNameNS(NS_W, 't');
|
|
291
|
-
Array.from(tNodes).forEach(
|
|
297
|
+
Array.from(tNodes).forEach(removeNode);
|
|
292
298
|
const newT = createWordElement(doc, 'w:t');
|
|
293
299
|
newT.setAttribute('xml:space', 'preserve');
|
|
294
300
|
newT.textContent = matchText;
|
|
@@ -312,7 +318,7 @@ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow',
|
|
|
312
318
|
const suffixRun = run.cloneNode(true);
|
|
313
319
|
// Update text content
|
|
314
320
|
const tNodes = suffixRun.getElementsByTagNameNS(NS_W, 't');
|
|
315
|
-
Array.from(tNodes).forEach(
|
|
321
|
+
Array.from(tNodes).forEach(removeNode);
|
|
316
322
|
const newT = createWordElement(doc, 'w:t');
|
|
317
323
|
newT.setAttribute('xml:space', 'preserve');
|
|
318
324
|
newT.textContent = suffixText;
|
package/engine/run-builders.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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';
|
|
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';
|
|
11
11
|
import { createWordElement } from '../core/word-xml.js';
|
|
@@ -140,8 +140,8 @@ export function createTextRun(xmlDoc, text, rPr, isDelete) {
|
|
|
140
140
|
* @param {boolean} [generateRedlines] - Whether to create rPrChange
|
|
141
141
|
* @returns {Element[]}
|
|
142
142
|
*/
|
|
143
|
-
export function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffset, author, generateRedlines) {
|
|
144
|
-
if (!text) return [];
|
|
143
|
+
export function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffset, author, generateRedlines) {
|
|
144
|
+
if (!text) return [];
|
|
145
145
|
|
|
146
146
|
const breaks = new Set([0, text.length]);
|
|
147
147
|
for (const hint of formatHints) {
|
|
@@ -167,13 +167,17 @@ export function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffs
|
|
|
167
167
|
h.start <= segmentBaseOffset && h.end >= segmentEndOffset
|
|
168
168
|
);
|
|
169
169
|
|
|
170
|
-
const combinedFormat = {};
|
|
171
|
-
applicableHints.forEach(h => {
|
|
172
|
-
if (h.format) Object.assign(combinedFormat, h.format);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
const combinedFormat = { ...extractFormatFromRPr(baseRPr) };
|
|
171
|
+
applicableHints.forEach(h => {
|
|
172
|
+
if (h.format) Object.assign(combinedFormat, h.format);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// During a text edit, missing Markdown is not an instruction to clear
|
|
176
|
+
// the source run's formatting. Only synchronize explicitly hinted spans.
|
|
177
|
+
const formattedRPr = applicableHints.length > 0
|
|
178
|
+
? injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines)
|
|
179
|
+
: baseRPr?.cloneNode(true) || null;
|
|
180
|
+
runs.push(createTextRunWithRPrElement(xmlDoc, segment, formattedRPr, false));
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
return runs;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getApplicableFormatHints } from '../pipeline/markdown-processor.js';
|
|
2
|
-
import { isWordElement } from '../core/word-xml.js';
|
|
3
2
|
import {
|
|
4
3
|
createTrackChange,
|
|
5
4
|
createTextRun,
|
|
@@ -20,30 +19,17 @@ import {
|
|
|
20
19
|
findLastSpanEndingBeforeOrAt,
|
|
21
20
|
forEachOverlappingSpan
|
|
22
21
|
} from './surgical-spans.js';
|
|
22
|
+
import { extractFormatFromRPr } from './rpr-helpers.js';
|
|
23
23
|
|
|
24
24
|
export function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHints, author, generateRedlines) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
25
|
+
// Plain modified text carries no negative formatting instruction. Preserve
|
|
26
|
+
// unchanged source formatting unless Markdown explicitly targets this span.
|
|
27
|
+
if (applicableHints.length === 0) return false;
|
|
29
28
|
|
|
30
29
|
const rPr = span.rPr;
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
};
|
|
30
|
+
const existingFormat = extractFormatFromRPr(rPr);
|
|
31
|
+
const desiredFormat = { ...existingFormat };
|
|
32
|
+
applicableHints.forEach(h => Object.assign(desiredFormat, h.format));
|
|
47
33
|
|
|
48
34
|
const formatsToCheck = ['bold', 'italic', 'underline', 'strikethrough'];
|
|
49
35
|
const changesNeeded = formatsToCheck.some(f => !!desiredFormat[f] !== existingFormat[f]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ansonlai/docx-redline-js",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Host-independent OOXML reconciliation engine for .docx manipulation with track changes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"
|
|
55
|
-
"
|
|
54
|
+
"@xmldom/xmldom": "^0.9.0",
|
|
55
|
+
"esbuild": "^0.28.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "node scripts/build.mjs",
|