@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.
@@ -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(el => el.remove());
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(el => el.remove());
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.remove();
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(el => el.remove());
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(el => el.remove());
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(t => t.remove());
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(t => t.remove());
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(t => t.remove());
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;
@@ -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
- const formattedRPr = injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines);
176
- runs.push(createTextRunWithRPrElement(xmlDoc, segment, formattedRPr, false));
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
- const desiredFormat = {};
26
- if (applicableHints.length > 0) {
27
- applicableHints.forEach(h => Object.assign(desiredFormat, h.format));
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 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
- };
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.0",
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
- "esbuild": "^0.24.0",
55
- "@xmldom/xmldom": "^0.9.0"
54
+ "@xmldom/xmldom": "^0.9.0",
55
+ "esbuild": "^0.28.1"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "node scripts/build.mjs",