diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/helpers.js index 76d3115be0..9e6c4bede2 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/helpers.js @@ -4,9 +4,17 @@ import { processOutputMarks } from '@converter/exporter.js'; * @param {Array} marks SD node marks. * @returns {Object|undefined} Properties element for trackFormat change or undefined. */ -export const createTrackStyleMark = (marks) => { +export const decodeTrackFormatMark = (marks) => { const trackStyleMark = marks.find((mark) => mark.type === 'trackFormat'); if (trackStyleMark) { + const beforeMarks = Array.isArray(trackStyleMark.attrs.before) ? trackStyleMark.attrs.before : []; + const beforeElements = beforeMarks + .flatMap((mark) => processOutputMarks([mark]) || []) + .filter((element) => element && typeof element === 'object'); + const rPrElement = { + name: 'w:rPr', + elements: beforeElements, + }; return { type: 'element', name: 'w:rPrChange', @@ -16,7 +24,7 @@ export const createTrackStyleMark = (marks) => { 'w:authorEmail': trackStyleMark.attrs.authorEmail, 'w:date': trackStyleMark.attrs.date, }, - elements: trackStyleMark.attrs.before.map((mark) => processOutputMarks([mark])).filter((r) => r !== undefined), + elements: [rPrElement], }; } return undefined; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.js index 11460b4ad8..f89fff6e67 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.js @@ -2,7 +2,7 @@ import { NodeTranslator } from '@translator'; import { createAttributeHandler } from '@converter/v3/handlers/utils.js'; import { exportSchemaToJson } from '@converter/exporter.js'; -import { createTrackStyleMark } from '@converter/v3/handlers/helpers.js'; +import { decodeTrackFormatMark } from '@converter/v3/handlers/helpers.js'; /** @type {import('@translator').XmlNodeName} */ const XML_NODE_NAME = 'w:del'; @@ -73,7 +73,7 @@ function decode(params) { const trackingMarks = ['trackInsert', 'trackFormat', 'trackDelete']; const marks = node.marks; const trackedMark = marks.find((m) => m.type === 'trackDelete'); - const trackStyleMark = createTrackStyleMark(marks); + const trackStyleMark = decodeTrackFormatMark(marks); node.marks = marks.filter((m) => !trackingMarks.includes(m.type)); if (trackStyleMark) { node.marks.push(trackStyleMark); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.test.js index 028b4eafd0..366ecd635c 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/del/del-translator.test.js @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { config, translator } from './del-translator.js'; import { NodeTranslator } from '@translator'; import { exportSchemaToJson } from '@converter/exporter.js'; -import { createTrackStyleMark } from '@converter/v3/handlers/helpers.js'; +import { decodeTrackFormatMark } from '@converter/v3/handlers/helpers.js'; // Mock external modules vi.mock('@converter/exporter.js', () => ({ @@ -10,7 +10,7 @@ vi.mock('@converter/exporter.js', () => ({ })); vi.mock('@converter/v3/handlers/helpers.js', () => ({ - createTrackStyleMark: vi.fn(), + decodeTrackFormatMark: vi.fn(), })); describe('w:del translator', () => { @@ -98,7 +98,7 @@ describe('w:del translator', () => { const mockTranslatedNode = { elements: [mockTextNode] }; exportSchemaToJson.mockReturnValue(mockTranslatedNode); - createTrackStyleMark.mockReturnValue(null); + decodeTrackFormatMark.mockReturnValue(null); const node = { type: 'text', @@ -132,12 +132,12 @@ describe('w:del translator', () => { }; const mockTrackStyleMark = { type: 'trackStyle', attrs: {} }; - createTrackStyleMark.mockReturnValue(mockTrackStyleMark); + decodeTrackFormatMark.mockReturnValue(mockTrackStyleMark); exportSchemaToJson.mockReturnValue({ elements: [{ name: 'w:t' }] }); const result = config.decode({ node }); - expect(createTrackStyleMark).toHaveBeenCalled(); + expect(decodeTrackFormatMark).toHaveBeenCalled(); expect(result).toBeTruthy(); expect(result.elements[0].elements[0].name).toBe('w:delText'); }); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.js index 8805bb2825..1047d93449 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.js @@ -2,7 +2,7 @@ import { NodeTranslator } from '@translator'; import { createAttributeHandler } from '@converter/v3/handlers/utils.js'; import { exportSchemaToJson } from '@converter/exporter.js'; -import { createTrackStyleMark } from '@converter/v3/handlers/helpers.js'; +import { decodeTrackFormatMark } from '@converter/v3/handlers/helpers.js'; /** @type {import('@translator').XmlNodeName} */ const XML_NODE_NAME = 'w:ins'; @@ -72,7 +72,7 @@ function decode(params) { const trackingMarks = ['trackInsert', 'trackFormat', 'trackDelete']; const marks = node.marks; const trackedMark = marks.find((m) => m.type === 'trackInsert'); - const trackStyleMark = createTrackStyleMark(marks); + const trackStyleMark = decodeTrackFormatMark(marks); node.marks = marks.filter((m) => !trackingMarks.includes(m.type)); if (trackStyleMark) { node.marks.push(trackStyleMark); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.test.js index b433da367a..0a1253dbd1 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/ins/ins-translator.test.js @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { config, translator } from './ins-translator.js'; import { NodeTranslator } from '@translator'; import { exportSchemaToJson } from '@converter/exporter.js'; -import { createTrackStyleMark } from '@converter/v3/handlers/helpers.js'; +import { decodeTrackFormatMark } from '@converter/v3/handlers/helpers.js'; // Mock external modules vi.mock('@converter/exporter.js', () => ({ @@ -10,7 +10,7 @@ vi.mock('@converter/exporter.js', () => ({ })); vi.mock('@converter/v3/handlers/helpers.js', () => ({ - createTrackStyleMark: vi.fn(), + decodeTrackFormatMark: vi.fn(), })); describe('w:del translator', () => { @@ -98,7 +98,7 @@ describe('w:del translator', () => { const mockTranslatedNode = { elements: [mockTextNode] }; exportSchemaToJson.mockReturnValue(mockTranslatedNode); - createTrackStyleMark.mockReturnValue(null); + decodeTrackFormatMark.mockReturnValue(null); const node = { type: 'text', @@ -131,12 +131,12 @@ describe('w:del translator', () => { }; const mockTrackStyleMark = { type: 'trackStyle', attrs: {} }; - createTrackStyleMark.mockReturnValue(mockTrackStyleMark); + decodeTrackFormatMark.mockReturnValue(mockTrackStyleMark); exportSchemaToJson.mockReturnValue({ elements: [{ name: 'w:t' }] }); const result = config.decode({ node }); - expect(createTrackStyleMark).toHaveBeenCalled(); + expect(decodeTrackFormatMark).toHaveBeenCalled(); expect(result).toBeTruthy(); expect(result.elements[0].elements[0].name).toBe('w:t'); }); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/numPr/numPr-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/numPr/numPr-translator.test.js index 6a282e12b0..b14e538692 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/numPr/numPr-translator.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/numPr/numPr-translator.test.js @@ -3,7 +3,7 @@ import { translator } from './numPr-translator.js'; vi.mock('@converter/exporter', () => ({ exportSchemaToJson: vi.fn(), - createTrackStyleMark: vi.fn(), + decodeTrackFormatMark: vi.fn(), })); describe('w:numPr translator', () => { diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/r/helpers/track-change-helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/r/helpers/track-change-helpers.js index 4f0bd07748..cfa81555e2 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/r/helpers/track-change-helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/r/helpers/track-change-helpers.js @@ -1,4 +1,4 @@ -import { TrackDeleteMarkName, TrackInsertMarkName } from '@extensions/track-changes/constants.js'; +import { TrackDeleteMarkName, TrackInsertMarkName, TrackFormatMarkName } from '@extensions/track-changes/constants.js'; const cloneMark = (mark) => { if (!mark) return mark; @@ -24,8 +24,9 @@ const cloneRuns = (runs = []) => runs.map((run) => cloneNode(run)); export const prepareRunTrackingContext = (node = {}) => { const marks = Array.isArray(node.marks) ? node.marks : []; - const trackingMarks = marks.filter( - (mark) => mark?.type === TrackInsertMarkName || mark?.type === TrackDeleteMarkName, + + const trackingMarks = marks.filter((mark) => + [TrackInsertMarkName, TrackDeleteMarkName, TrackFormatMarkName].includes(mark?.type), ); if (!trackingMarks.length) { @@ -88,7 +89,8 @@ export const ensureTrackedWrapper = (runs, trackingMarksByType = new Map()) => { if (!trackingMarksByType.size) return runs; - if (trackingMarksByType.has(TrackInsertMarkName)) { + const isTrackInsertMark = trackingMarksByType.has(TrackInsertMarkName); + if (isTrackInsertMark) { const mark = trackingMarksByType.get(TrackInsertMarkName); const clonedRuns = cloneRuns(runs); const wrapper = { @@ -107,7 +109,8 @@ export const ensureTrackedWrapper = (runs, trackingMarksByType = new Map()) => { return [wrapper]; } - if (trackingMarksByType.has(TrackDeleteMarkName)) { + const isTrackDeleteMark = trackingMarksByType.has(TrackDeleteMarkName); + if (isTrackDeleteMark) { const mark = trackingMarksByType.get(TrackDeleteMarkName); const clonedRuns = cloneRuns(runs); clonedRuns.forEach(renameTextElementsForDeletion); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.js index 2f607c6a09..4733b20a8e 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.js @@ -7,7 +7,10 @@ import { translator as wHyperlinkTranslator } from '../hyperlink/hyperlink-trans import { translator as wRPrTranslator } from '../rpr'; import validXmlAttributes from './attributes/index.js'; import { handleStyleChangeMarksV2 } from '../../../../v2/importer/markImporter.js'; -import { encodeMarksFromRPr, resolveRunProperties } from '../../../../styles.js'; +import { encodeMarksFromRPr, resolveRunProperties } from '@converter/styles.js'; +import { TrackFormatMarkName } from '@extensions/track-changes/constants.js'; +import { decodeTrackFormatMark } from '@converter/v3/handlers/helpers.js'; + /** @type {import('@translator').XmlNodeName} */ const XML_NODE_NAME = 'w:r'; @@ -138,6 +141,25 @@ const encode = (params, encodedAttrs = {}) => { return splitRuns; }; +const addRPrChange = (runPropertiesElement, trackingMarksByType) => { + let rPrElement = cloneXmlNode(runPropertiesElement); + const trackFormatMark = trackingMarksByType.get(TrackFormatMarkName); + + if (trackFormatMark) { + const rPrChangeElement = decodeTrackFormatMark([trackFormatMark]); + if (rPrChangeElement) { + if (!rPrElement) { + rPrElement = { name: 'w:rPr', elements: [] }; + } + if (!Array.isArray(rPrElement.elements)) { + rPrElement.elements = []; + } + rPrElement.elements.push(rPrChangeElement); + } + } + return rPrElement; +}; + const decode = (params, decodedAttrs = {}) => { const { node } = params || {}; if (!node) return undefined; @@ -176,6 +198,11 @@ const decode = (params, decodedAttrs = {}) => { node: { attrs: { runProperties: runProperties } }, }); + const trackFormatMark = trackingMarksByType.get(TrackFormatMarkName); + if (trackFormatMark) { + runPropertiesElement = addRPrChange(runPropertiesElement, trackingMarksByType); + } + const runPropsTemplate = runPropertiesElement ? cloneXmlNode(runPropertiesElement) : null; const applyBaseRunProps = (runNode) => applyRunPropertiesTemplate(runNode, runPropsTemplate); const replaceRunProps = (runNode) => { diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.test.js index 730015fbf3..da605ef3c3 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/r/r-translator.test.js @@ -222,4 +222,50 @@ describe('w:r r-translator (node)', () => { }), ); }); + + it('adds w:rPrChange to run properties when a track format mark is present', () => { + const trackFormatMark = { + type: 'trackFormat', + attrs: { + id: 'fmt-1', + author: 'Alice', + authorEmail: 'alice@example.com', + date: '2024-09-04T09:29:00Z', + before: [{ type: 'bold', attrs: { value: true } }], + }, + }; + + const node = { + type: 'run', + marks: [trackFormatMark], + content: [ + { + type: 'text', + text: 'changed text', + marks: [{ type: 'bold', attrs: { value: true } }], + }, + ], + }; + + const result = translator.decode({ + node, + editor: { extensionService: { extensions: [] } }, + }); + + expect(result.name).toBe('w:r'); + const rPr = result.elements?.find((el) => el.name === 'w:rPr'); + expect(rPr).toBeDefined(); + const change = rPr.elements?.find((el) => el.name === 'w:rPrChange'); + expect(change).toBeDefined(); + expect(change.attributes).toMatchObject({ + 'w:id': 'fmt-1', + 'w:author': 'Alice', + 'w:authorEmail': 'alice@example.com', + 'w:date': '2024-09-04T09:29:00Z', + }); + const beforeRPr = change.elements?.find((el) => el.name === 'w:rPr'); + expect(beforeRPr).toBeDefined(); + const boldElement = beforeRPr.elements?.find((el) => el.name === 'w:b'); + expect(boldElement).toBeDefined(); + }); });