@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
@@ -7,6 +7,8 @@
7
7
 
8
8
  import { parseOoxml, serializeOoxml } from './oxml-engine.js';
9
9
  import { getDefaultAuthor } from '../adapters/config.js';
10
+ import { createRevisionMetadata } from '../core/types.js';
11
+ import { createWordElement } from '../core/word-xml.js';
10
12
 
11
13
  /**
12
14
  * Removes specific formatting properties from a run properties (w:rPr) element.
@@ -154,7 +156,7 @@ export function injectHighlightIntoRPr(doc, rPr, color = 'yellow', options = {})
154
156
  let rPrElement = rPr;
155
157
  if (!rPrElement) {
156
158
  // Create new rPr element
157
- rPrElement = doc.createElementNS(NS_W, 'w:rPr');
159
+ rPrElement = createWordElement(doc, 'w:rPr');
158
160
  } else {
159
161
  rPrElement = rPr.cloneNode(true);
160
162
  }
@@ -163,7 +165,7 @@ export function injectHighlightIntoRPr(doc, rPr, color = 'yellow', options = {})
163
165
  // Clone the *original* rPr children before we touch them
164
166
  let previousRPrState = null;
165
167
  if (generateRedlines) {
166
- previousRPrState = doc.createElementNS(NS_W, 'w:rPr');
168
+ previousRPrState = createWordElement(doc, 'w:rPr');
167
169
  Array.from(rPrElement.childNodes).forEach(child => {
168
170
  // Don't include existing rPrChange in the "previous" state wrapper usually,
169
171
  // but for simplicity we clone children. Word generally handles nested track changes poorly,
@@ -180,18 +182,19 @@ export function injectHighlightIntoRPr(doc, rPr, color = 'yellow', options = {})
180
182
  Array.from(existingHighlight).forEach(el => el.remove());
181
183
 
182
184
  // Create and add new highlight element
183
- const highlightEl = doc.createElementNS(NS_W, 'w:highlight');
185
+ const highlightEl = createWordElement(doc, 'w:highlight');
184
186
  highlightEl.setAttributeNS(NS_W, 'w:val', ooxmlColor);
185
187
  rPrElement.appendChild(highlightEl);
186
188
 
187
189
  // --- WRAP IN REDLINES IF ENABLED ---
188
190
  if (generateRedlines && previousRPrState) {
189
- const rPrChange = doc.createElementNS(NS_W, 'w:rPrChange');
190
-
191
- // Attributes
192
- rPrChange.setAttributeNS(NS_W, 'w:id', Math.floor(Math.random() * 9999999).toString());
193
- rPrChange.setAttributeNS(NS_W, 'w:author', author);
194
- rPrChange.setAttributeNS(NS_W, 'w:date', new Date().toISOString());
191
+ const rPrChange = createWordElement(doc, 'w:rPrChange');
192
+
193
+ // Attributes
194
+ const metadata = createRevisionMetadata(author);
195
+ rPrChange.setAttribute('w:id', String(metadata.id));
196
+ rPrChange.setAttribute('w:author', metadata.author);
197
+ rPrChange.setAttribute('w:date', metadata.date);
195
198
 
196
199
  // Format: <w:rPrChange ...> <w:rPr>...previous...</w:rPr> </w:rPrChange>
197
200
  rPrChange.appendChild(previousRPrState);
@@ -272,7 +275,7 @@ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow',
272
275
  const tNodes = prefixRun.getElementsByTagNameNS(NS_W, 't');
273
276
  // Simply remove all t nodes and add one with new text to avoid complexity of multiple t nodes
274
277
  Array.from(tNodes).forEach(t => t.remove());
275
- const newT = doc.createElementNS(NS_W, 'w:t');
278
+ const newT = createWordElement(doc, 'w:t');
276
279
  // Preserve xml:space="preserve" if it existed, or just add it usually
277
280
  newT.setAttribute('xml:space', 'preserve');
278
281
  newT.textContent = prefixText;
@@ -286,7 +289,7 @@ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow',
286
289
  // Update text content
287
290
  const tNodes = matchRun.getElementsByTagNameNS(NS_W, 't');
288
291
  Array.from(tNodes).forEach(t => t.remove());
289
- const newT = doc.createElementNS(NS_W, 'w:t');
292
+ const newT = createWordElement(doc, 'w:t');
290
293
  newT.setAttribute('xml:space', 'preserve');
291
294
  newT.textContent = matchText;
292
295
  matchRun.appendChild(newT);
@@ -310,7 +313,7 @@ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow',
310
313
  // Update text content
311
314
  const tNodes = suffixRun.getElementsByTagNameNS(NS_W, 't');
312
315
  Array.from(tNodes).forEach(t => t.remove());
313
- const newT = doc.createElementNS(NS_W, 'w:t');
316
+ const newT = createWordElement(doc, 'w:t');
314
317
  newT.setAttribute('xml:space', 'preserve');
315
318
  newT.textContent = suffixText;
316
319
  suffixRun.appendChild(newT);
@@ -7,11 +7,11 @@
7
7
  import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
8
8
  import { isListTargetLoose } from '../pipeline/list-markers.js';
9
9
  import { ReconciliationPipeline } from '../pipeline/pipeline.js';
10
- import { wrapInDocumentFragment } from '../pipeline/serialization.js';
11
- import {
12
- getElementsByTag,
13
- getXmlParseError
14
- } from '../core/xml-query.js';
10
+ import { wrapInDocumentFragment } from '../pipeline/serialization.js';
11
+ import {
12
+ getElementsByTagNSOrTag,
13
+ getXmlParseError
14
+ } from '../core/xml-query.js';
15
15
  import { createParser, createSerializer, parseXml, serializeXml } from '../adapters/xml-adapter.js';
16
16
  import { log, error } from '../adapters/logger.js';
17
17
  import { extractFormattingFromOoxml } from './format-extraction.js';
@@ -25,6 +25,9 @@ import { applySurgicalMode } from './surgical-mode.js';
25
25
  import { applyReconstructionMode } from './reconstruction-mode.js';
26
26
  import { applyTableReconciliation, applyTextToTableTransformation } from './table-mode.js';
27
27
  import { getDefaultAuthor } from '../adapters/config.js';
28
+ import { containsTrackedChanges, withOoxmlSourceType } from '../core/word-xml.js';
29
+ import { NS_W, seedRevisionIdsFromDocument } from '../core/types.js';
30
+ import { acceptTrackedChangesInOoxml } from '../services/revision-comment-management.js';
28
31
 
29
32
  /**
30
33
  * Applies redline track changes to OOXML by modifying the DOM in-place.
@@ -32,33 +35,87 @@ import { getDefaultAuthor } from '../adapters/config.js';
32
35
  * @param {string} oxml - Original OOXML string
33
36
  * @param {string} originalText - Original plain text
34
37
  * @param {string} modifiedText - New text (may contain markdown)
35
- * @param {Object} [options={}] - Options
36
- * @param {string} [options.author='AI'] - Author for track changes
37
- * @param {string|null} [options.targetParagraphId=null] - Preferred paragraph identity for table wrappers
38
- * @returns {Promise<{ oxml: string, hasChanges: boolean }>}
39
- */
38
+ * @param {Object} [options={}] - Options
39
+ * @param {string} [options.author='AI'] - Author for track changes
40
+ * @param {string|null} [options.targetParagraphId=null] - Preferred paragraph identity for table wrappers
41
+ * @param {'reject-input'|'accept-all-first'} [options.existingRevisions='reject-input'] - Policy for source OOXML with tracked changes
42
+ * @returns {Promise<{ oxml: string, hasChanges: boolean, sourceType?: 'package'|'document'|'fragment', status?: 'ok'|'no-op'|'error', error?: { code: string, message: string } }>}
43
+ */
40
44
  export async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
41
45
  const generateRedlines = options.generateRedlines ?? true;
42
46
  const author = options.author || getDefaultAuthor();
43
47
  const parser = createParser();
44
48
  const serializer = createSerializer();
45
- const noChanges = () => ({ oxml, hasChanges: false });
49
+ const finalize = result => {
50
+ const withStatus = { ...result };
51
+ if (!withStatus.status) {
52
+ withStatus.status = withStatus.hasChanges ? 'ok' : 'no-op';
53
+ }
54
+ return withOoxmlSourceType(withStatus);
55
+ };
56
+ const noChanges = () => finalize({ oxml, hasChanges: false });
46
57
 
47
58
  let xmlDoc;
48
59
  try {
49
60
  xmlDoc = parser.parseFromString(oxml, 'text/xml');
50
- } catch (e) {
51
- error('[OxmlEngine] Failed to parse OXML:', e);
52
- return noChanges();
53
- }
54
-
55
- const parseError = getXmlParseError(xmlDoc);
56
- if (parseError) {
57
- error('[OxmlEngine] XML parse error:', parseError.textContent);
58
- return noChanges();
59
- }
60
-
61
- const initialTableCellContext = detectTableCellContext(xmlDoc, originalText, options);
61
+ } catch (e) {
62
+ error('[OxmlEngine] Failed to parse OXML:', e);
63
+ return finalize({
64
+ oxml,
65
+ hasChanges: false,
66
+ status: 'error',
67
+ error: { code: 'PARSE_ERROR', message: 'Could not parse OOXML input.' }
68
+ });
69
+ }
70
+
71
+ const parseError = getXmlParseError(xmlDoc);
72
+ if (parseError) {
73
+ error('[OxmlEngine] XML parse error:', parseError.textContent);
74
+ return finalize({
75
+ oxml,
76
+ hasChanges: false,
77
+ status: 'error',
78
+ error: { code: 'PARSE_ERROR', message: parseError.textContent || 'Could not parse OOXML input.' }
79
+ });
80
+ }
81
+ seedRevisionIdsFromDocument(xmlDoc);
82
+
83
+ if (containsTrackedChanges(xmlDoc)) {
84
+ const existingRevisionsPolicy = options.existingRevisions || 'reject-input';
85
+ if (existingRevisionsPolicy === 'accept-all-first') {
86
+ log('[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining');
87
+ const accepted = acceptTrackedChangesInOoxml(oxml, { allAuthors: true });
88
+ oxml = accepted.oxml;
89
+ xmlDoc = parser.parseFromString(oxml, 'text/xml');
90
+ const acceptedParseError = getXmlParseError(xmlDoc);
91
+ if (acceptedParseError) {
92
+ error('[OxmlEngine] XML parse error after accepting existing revisions:', acceptedParseError.textContent);
93
+ return finalize({
94
+ oxml,
95
+ hasChanges: false,
96
+ status: 'error',
97
+ error: {
98
+ code: 'PARSE_ERROR',
99
+ message: 'Could not parse OOXML after accepting existing revisions.'
100
+ }
101
+ });
102
+ }
103
+ seedRevisionIdsFromDocument(xmlDoc);
104
+ } else {
105
+ log('[OxmlEngine] Existing revisions detected; rejecting input per existingRevisions policy');
106
+ return finalize({
107
+ oxml,
108
+ hasChanges: false,
109
+ status: 'error',
110
+ error: {
111
+ code: 'EXISTING_REVISIONS',
112
+ message: 'Input OOXML contains existing tracked changes. Pass existingRevisions: "accept-all-first" to normalize before redlining.'
113
+ }
114
+ });
115
+ }
116
+ }
117
+
118
+ const initialTableCellContext = detectTableCellContext(xmlDoc, originalText, options);
62
119
  if (initialTableCellContext.hasTableWrapper && initialTableCellContext.targetParagraph && !options._isolatedTableCell) {
63
120
  log('[OxmlEngine] Isolating table-cell paragraph before diff');
64
121
  const isolatedOxml = serializeParagraphOnly(xmlDoc, initialTableCellContext.targetParagraph, serializer);
@@ -74,8 +131,29 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
74
131
  const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
75
132
  const hasFormatHints = formatHints.length > 0;
76
133
 
77
- const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
134
+ const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
78
135
  const hasExistingFormatting = existingFormatHints.length > 0;
136
+ const visibleText = textSpans.map(span => textSpanVisibleText(span)).join('');
137
+ if (
138
+ hasTextChanges
139
+ && typeof originalText === 'string'
140
+ && originalText.trim()
141
+ && !originalText.includes('\n')
142
+ && !visibleText.includes(originalText.trim())
143
+ && !visibleText.replace(/[\t\n\u2011]/g, '').includes(originalText.trim().replace(/[\t\n\u2011]/g, ''))
144
+ && !normalizeTargetText(visibleText).includes(normalizeTargetText(originalText))
145
+ ) {
146
+ log('[OxmlEngine] Target text not found in OOXML');
147
+ return finalize({
148
+ oxml,
149
+ hasChanges: false,
150
+ status: 'error',
151
+ error: {
152
+ code: 'TARGET_NOT_FOUND',
153
+ message: 'Original text was not found in the supplied OOXML.'
154
+ }
155
+ });
156
+ }
79
157
  let paragraphInfos = null;
80
158
  const getParagraphInfos = () => {
81
159
  if (!paragraphInfos) {
@@ -149,13 +227,13 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
149
227
  );
150
228
 
151
229
  if (tableCellCtx.hasTableWrapper && targetParagraph) {
152
- return {
153
- oxml: serializeParagraphOnly(xmlDoc, targetParagraph, serializer),
154
- hasChanges: removalResult.hasChanges
155
- };
156
- }
157
-
158
- return removalResult;
230
+ return finalize({
231
+ oxml: serializeParagraphOnly(xmlDoc, targetParagraph, serializer),
232
+ hasChanges: removalResult.hasChanges
233
+ });
234
+ }
235
+
236
+ return finalize(removalResult);
159
237
  }
160
238
 
161
239
  if (!hasTextChanges && hasFormatHints) {
@@ -173,16 +251,16 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
173
251
  const formatResult = applyFormatOnlyWithOoxmlFallback(precomputedFormatContext);
174
252
 
175
253
  log('[OxmlEngine] Stripping table wrapper for table cell paragraph (format-only)');
176
- return {
254
+ return finalize({
177
255
  oxml: serializeParagraphOnly(xmlDoc, tableCellCtx.targetParagraph, serializer),
178
256
  hasChanges: formatResult.hasChanges
179
- };
257
+ });
180
258
  }
181
259
 
182
- return applyFormatOnlyWithOoxmlFallback(precomputedFormatContext);
260
+ return finalize(applyFormatOnlyWithOoxmlFallback(precomputedFormatContext));
183
261
  }
184
262
 
185
- const tables = getElementsByTag(xmlDoc, 'w:tbl');
263
+ const tables = getElementsByTagNSOrTag(xmlDoc, NS_W, 'tbl');
186
264
  const hasTables = tables.length > 0;
187
265
  const isMarkdownTable = /^\|.+\|/.test(cleanModifiedText.trim()) && cleanModifiedText.includes('\n');
188
266
  const isTargetList = isListTargetLoose(cleanModifiedText);
@@ -192,12 +270,12 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
192
270
 
193
271
  if (isMarkdownTable && !hasTables) {
194
272
  log('[OxmlEngine] Text-to-table transformation: generating new table from Markdown');
195
- return applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines);
196
- }
197
-
198
- if (hasTables && isMarkdownTable) {
199
- return applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines);
200
- }
273
+ return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
274
+ }
275
+
276
+ if (hasTables && isMarkdownTable) {
277
+ return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, parser, author, generateRedlines));
278
+ }
201
279
  if (hasTables) {
202
280
  const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph
203
281
  ? tableCellContext.targetParagraph
@@ -217,11 +295,11 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
217
295
  surgicalTarget
218
296
  );
219
297
 
220
- if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
221
- log('[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)');
222
- return { oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true };
223
- }
224
- return result;
298
+ if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
299
+ log('[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)');
300
+ return finalize({ oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true });
301
+ }
302
+ return finalize(result);
225
303
  }
226
304
  if (isTargetList) {
227
305
  log('[OxmlEngine] 🎯 Using reconciliation pipeline for list generation');
@@ -235,15 +313,28 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
235
313
  numberingXml: result.numberingXml
236
314
  });
237
315
  log(`[OxmlEngine] ✅ Wrapped OOXML length: ${wrapped.length}`);
238
- return { oxml: wrapped, hasChanges: true };
239
- }
240
- return noChanges();
241
- }
242
-
243
- return applyReconstructionMode(xmlDoc, originalText, cleanModifiedText, serializer, author, formatHints, generateRedlines);
244
- }
245
-
246
- /**
316
+ return finalize({ oxml: wrapped, hasChanges: true });
317
+ }
318
+ return noChanges();
319
+ }
320
+
321
+ return finalize(applyReconstructionMode(xmlDoc, originalText, cleanModifiedText, serializer, author, formatHints, generateRedlines));
322
+ }
323
+
324
+ function normalizeTargetText(text) {
325
+ return String(text || '').replace(/[\t\n\u2011]/g, ' ').replace(/\s+/g, ' ').trim();
326
+ }
327
+
328
+ function textSpanVisibleText(span) {
329
+ const node = span?.textElement;
330
+ const localName = String(node?.localName || node?.nodeName || '').replace(/^.*:/, '');
331
+ if (localName === 'tab') return '\t';
332
+ if (localName === 'br' || localName === 'cr') return '\n';
333
+ if (localName === 'noBreakHyphen') return '\u2011';
334
+ return node?.textContent || '';
335
+ }
336
+
337
+ /**
247
338
  * Sanitizes AI response text by removing common prefixes.
248
339
  *
249
340
  * @param {string} text - AI response text
@@ -109,11 +109,12 @@ export function buildReconstructionMapping(xmlDoc, modifiedText) {
109
109
  });
110
110
  });
111
111
 
112
- let processedModifiedText = modifiedText;
113
- tokenToCharMap.forEach((char, tokenString) => {
114
- const escapedToken = tokenString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
115
- processedModifiedText = processedModifiedText.replace(new RegExp(escapedToken, 'g'), char);
116
- });
112
+ let processedModifiedText = modifiedText;
113
+ tokenToCharMap.forEach((char, tokenString) => {
114
+ const escapedToken = tokenString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
115
+ processedModifiedText = processedModifiedText.replace(new RegExp(escapedToken, 'g'), char);
116
+ });
117
+ processedModifiedText = preserveReferencePlaceholders(originalFullText, processedModifiedText, referenceMap);
117
118
 
118
119
  const containerFragments = new Map();
119
120
  uniqueContainers.forEach(container => {
@@ -169,9 +170,35 @@ export function buildReconstructionMapping(xmlDoc, modifiedText) {
169
170
  getPropertySpanLength,
170
171
  isParagraphStart: index => paragraphStarts.has(index)
171
172
  };
172
- }
173
-
174
- function processChildNode(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
173
+ }
174
+
175
+ function preserveReferencePlaceholders(originalFullText, modifiedText, referenceMap) {
176
+ let result = modifiedText;
177
+
178
+ for (const referenceChar of referenceMap.keys()) {
179
+ if (result.includes(referenceChar)) continue;
180
+
181
+ const originalIndex = originalFullText.indexOf(referenceChar);
182
+ if (originalIndex < 0) continue;
183
+
184
+ const prefix = originalFullText.slice(0, originalIndex);
185
+ const suffix = originalFullText.slice(originalIndex + referenceChar.length);
186
+
187
+ if (prefix && result.startsWith(prefix)) {
188
+ result = `${result.slice(0, prefix.length)}${referenceChar}${result.slice(prefix.length)}`;
189
+ continue;
190
+ }
191
+
192
+ if (suffix && result.endsWith(suffix)) {
193
+ const insertAt = result.length - suffix.length;
194
+ result = `${result.slice(0, insertAt)}${referenceChar}${result.slice(insertAt)}`;
195
+ }
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ function processChildNode(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode) {
175
202
  if (child.nodeName === 'w:r') {
176
203
  return processRunForReconstruction(child, originalFullText, propertyMap, sentinelMap, referenceMap, tokenToCharMap, nextCharCode);
177
204
  }
@@ -5,6 +5,7 @@
5
5
  import { computeWordDiffs } from '../pipeline/diff-engine.js';
6
6
  import { buildReconstructionMapping } from './reconstruction-mapper.js';
7
7
  import { applyReconstructionDiffs } from './reconstruction-writer.js';
8
+ import { withOoxmlSourceType } from '../core/word-xml.js';
8
9
 
9
10
  /**
10
11
  * Applies reconstruction mode reconciliation.
@@ -19,20 +20,20 @@ import { applyReconstructionDiffs } from './reconstruction-writer.js';
19
20
  * @returns {{ oxml: string, hasChanges: boolean }}
20
21
  */
21
22
  export function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true) {
22
- const mapping = buildReconstructionMapping(xmlDoc, modifiedText);
23
- if (mapping.paragraphs.length === 0) {
24
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: false };
25
- }
23
+ const mapping = buildReconstructionMapping(xmlDoc, modifiedText);
24
+ if (mapping.paragraphs.length === 0) {
25
+ return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
26
+ }
26
27
 
27
28
  const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText);
28
29
 
29
- return applyReconstructionDiffs(
30
+ return withOoxmlSourceType(applyReconstructionDiffs(
30
31
  xmlDoc,
31
- diffs,
32
- mapping,
33
- serializer,
34
- author,
35
- formatHints,
36
- generateRedlines
37
- );
38
- }
32
+ diffs,
33
+ mapping,
34
+ serializer,
35
+ author,
36
+ formatHints,
37
+ generateRedlines
38
+ ));
39
+ }