Skip to content

Commit 4e9c9ca

Browse files
fix(converter): treat w:moveFrom/w:moveTo as tracked-change wrappers in field preservation
1 parent fd84422 commit 4e9c9ca

4 files changed

Lines changed: 56 additions & 6 deletions

File tree

packages/super-editor/src/editors/v1/core/super-converter/field-references/preProcessNodesForFldChar.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
*/
44
import { getInstructionPreProcessor } from './fld-preprocessors';
55
import { carbonCopy } from '@core/utilities/carbonCopy.js';
6+
import { isTrackChangeElement } from '../v2/importer/trackChangeElements.js';
67

78
const SKIP_FIELD_PROCESSING_NODE_NAMES = new Set(['w:drawing', 'w:pict']);
89

910
const shouldSkipFieldProcessing = (node) => SKIP_FIELD_PROCESSING_NODE_NAMES.has(node?.name);
10-
const isTrackChangeWrapper = (node) => node?.name === 'w:del' || node?.name === 'w:ins';
1111
/**
1212
* @typedef {object} FldCharProcessResult
1313
* @property {OpenXmlNode[]} processedNodes - The list of nodes after processing.
@@ -212,7 +212,7 @@ export const preProcessNodesForFldChar = (nodes = [], docx) => {
212212
// A field started in the children, so this node is part of that field.
213213
childResult.unpairedBegin.forEach((pendingField) => {
214214
const fieldInfo = { ...pendingField.fieldInfo };
215-
if (fieldInfo.preserveRaw || isTrackChangeWrapper(node)) {
215+
if (fieldInfo.preserveRaw || isTrackChangeElement(node)) {
216216
fieldInfo.preserveRaw = true;
217217
}
218218
currentFieldStack.push(fieldInfo);
@@ -226,7 +226,7 @@ export const preProcessNodesForFldChar = (nodes = [], docx) => {
226226
});
227227
} else if (childResult.unpairedEnd) {
228228
// A field from this level or higher ended in the children.
229-
const shouldPreserveRaw = childResult.unpairedEndPreserveRaw || isTrackChangeWrapper(node);
229+
const shouldPreserveRaw = childResult.unpairedEndPreserveRaw || isTrackChangeElement(node);
230230
if (collectedNodesStack.length === 0) {
231231
// Preserve the original subtree; child processing may have stripped the fldChar end marker.
232232
processedNodes.push(rawNode);

packages/super-editor/src/editors/v1/core/super-converter/field-references/preProcessNodesForFldChar.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,52 @@ describe('preProcessNodesForFldChar', () => {
418418
expect(docx['word/_rels/document.xml.rels'].elements[0].elements).toEqual([]);
419419
});
420420

421+
it('preserves raw field nodes when an active field ends inside a tracked move wrapper', () => {
422+
const expectedNodes = [
423+
{ name: 'w:r', elements: [{ name: 'w:fldChar', attributes: { 'w:fldCharType': 'begin' } }] },
424+
{
425+
name: 'w:r',
426+
elements: [{ name: 'w:instrText', elements: [{ type: 'text', text: 'HYPERLINK "http://example.com"' }] }],
427+
},
428+
{ name: 'w:r', elements: [{ name: 'w:fldChar', attributes: { 'w:fldCharType': 'separate' } }] },
429+
{ name: 'w:r', elements: [{ name: 'w:t', elements: [{ type: 'text', text: 'link text' }] }] },
430+
{
431+
name: 'w:p',
432+
elements: [
433+
{
434+
name: 'w:moveFrom',
435+
attributes: { 'w:id': '1', 'w:author': 'Repro', 'w:date': '2026-04-30T00:00:00Z' },
436+
elements: [
437+
{ name: 'w:r', elements: [{ name: 'w:fldChar', attributes: { 'w:fldCharType': 'end' } }] },
438+
{
439+
name: 'w:r',
440+
elements: [
441+
{
442+
name: 'w:t',
443+
attributes: { 'xml:space': 'preserve' },
444+
elements: [{ type: 'text', text: 'moved text after field end' }],
445+
},
446+
],
447+
},
448+
],
449+
},
450+
],
451+
},
452+
];
453+
const nodes = structuredClone(expectedNodes);
454+
const docx = {
455+
'word/_rels/document.xml.rels': {
456+
elements: [{ name: 'Relationships', elements: [] }],
457+
},
458+
};
459+
const { processedNodes, unpairedBegin, unpairedEnd } = preProcessNodesForFldChar(nodes, docx);
460+
461+
expect(processedNodes).toEqual(expectedNodes);
462+
expect(unpairedBegin).toBeNull();
463+
expect(unpairedEnd).toBeNull();
464+
expect(docx['word/_rels/document.xml.rels'].elements[0].elements).toEqual([]);
465+
});
466+
421467
it('preserves raw child nodes when an unpaired end bubbles through a non-collecting wrapper', () => {
422468
const expectedNodes = [
423469
{ name: 'w:r', elements: [{ name: 'w:fldChar', attributes: { 'w:fldCharType': 'begin' } }] },
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const TRACK_CHANGE_ELEMENT_NAMES = new Set(['w:del', 'w:ins', 'w:moveFrom', 'w:moveTo']);
2+
const TRANSLATED_TRACK_CHANGE_ELEMENT_NAMES = new Set(['w:del', 'w:ins']);
3+
4+
export const isTrackChangeElement = (node) => TRACK_CHANGE_ELEMENT_NAMES.has(node?.name);
5+
export const isTranslatedTrackChangeElement = (node) => TRANSLATED_TRACK_CHANGE_ELEMENT_NAMES.has(node?.name);

packages/super-editor/src/editors/v1/core/super-converter/v2/importer/trackChangesImporter.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { translator as wDelTranslator } from '@converter/v3/handlers/w/del';
22
import { translator as wInsTranslator } from '@converter/v3/handlers/w/ins';
3-
4-
const isTrackChangeElement = (node) => node?.name === 'w:del' || node?.name === 'w:ins';
3+
import { isTranslatedTrackChangeElement } from './trackChangeElements.js';
54

65
const unwrapTrackChangeNode = (node) => {
76
if (!node) {
87
return null;
98
}
109

11-
if (isTrackChangeElement(node)) {
10+
if (isTranslatedTrackChangeElement(node)) {
1211
return node;
1312
}
1413

0 commit comments

Comments
 (0)