From ced8d390ac7692a3bd0d69b2135fd375ce478caf Mon Sep 17 00:00:00 2001 From: Luccas Correa Date: Mon, 9 Mar 2026 11:04:08 -0300 Subject: [PATCH 1/3] fix: remove syncing of runProperties with paragraph Word performs this only if the paragraph marker is selected, which is not a concept we have currently. Keeping this logic caused weird issues when dealing with lists. --- .../run/calculateInlineRunPropertiesPlugin.js | 51 ------------------- ...calculateInlineRunPropertiesPlugin.test.js | 12 ++--- .../src/extensions/tests/headless.test.js | 4 +- 3 files changed, 8 insertions(+), 59 deletions(-) diff --git a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js index 75513db8a7..fed27882fa 100644 --- a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js +++ b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js @@ -74,8 +74,6 @@ export const calculateInlineRunPropertiesPlugin = (editor) => if (!runPositions.size) return null; const selectionPreserver = createSelectionPreserver(tr, newState.selection); - const firstRunPosByParagraph = new Map(); - const sortedRunPositions = Array.from(runPositions).sort((a, b) => b - a); sortedRunPositions.forEach((pos) => { @@ -97,26 +95,6 @@ export const calculateInlineRunPropertiesPlugin = (editor) => ); const runProperties = firstInlineProps ?? null; - let firstRunPos = firstRunPosByParagraph.get(paragraphPos); - if (firstRunPos === undefined) { - firstRunPos = findFirstRunPosInParagraph(paragraphNode, paragraphPos, runType); - firstRunPosByParagraph.set(paragraphPos, firstRunPos); - } - const isFirstInParagraph = firstRunPos === mappedPos; - - if (isFirstInParagraph) { - // Keep paragraph's default runProperties in sync for the first run. - const currentParagraphRunProperties = paragraphNode.attrs?.paragraphProperties?.runProperties ?? null; - if (!areRunPropertiesEqual(currentParagraphRunProperties, runProperties)) { - const inlineParagraphProperties = carbonCopy(paragraphNode.attrs.paragraphProperties) || {}; - inlineParagraphProperties.runProperties = runProperties; - tr.setNodeMarkup(paragraphPos, paragraphNode.type, { - ...paragraphNode.attrs, - paragraphProperties: inlineParagraphProperties, - }); - } - } - if (segments.length === 1) { if (JSON.stringify(runProperties) === JSON.stringify(runNode.attrs.runProperties)) return; tr.setNodeMarkup(mappedPos, runNode.type, { ...runNode.attrs, runProperties }, runNode.marks); @@ -224,24 +202,6 @@ export function extractTableInfo($pos, depth) { return fallbackInfo; } } -/** - * Find the absolute document position of the first run node inside a paragraph. - * - * @param {import('prosemirror-model').Node} paragraphNode - * @param {number} paragraphPos Absolute position of the paragraph node. - * @param {import('prosemirror-model').NodeType} runType - * @returns {number|null} - */ -function findFirstRunPosInParagraph(paragraphNode, paragraphPos, runType) { - let firstRunPos = null; - paragraphNode.descendants((child, childPos) => { - if (firstRunPos !== null) return false; - if (child.type !== runType) return true; - firstRunPos = paragraphPos + 1 + childPos; - return false; - }); - return firstRunPos; -} /** * Split a run node into segments whose inline runProperties match for adjacent content. @@ -409,17 +369,6 @@ function stableStringifyInlineProps(inlineProps) { return JSON.stringify(sorted); } -/** - * Compare two runProperties objects with stable key ordering. - * - * @param {Record|null} left - * @param {Record|null} right - * @returns {boolean} - */ -function areRunPropertiesEqual(left, right) { - return stableStringifyInlineProps(left) === stableStringifyInlineProps(right); -} - /** * Track and reapply selection across run replacements. * diff --git a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js index c39c9ee1bd..56b24654bf 100644 --- a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js +++ b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js @@ -318,7 +318,7 @@ describe('calculateInlineRunPropertiesPlugin', () => { }); }); - it('keeps paragraph runProperties in sync with the first run', () => { + it('does not sync paragraph runProperties with the first run', () => { const schema = makeSchema(); const doc = paragraphDoc(schema); const state = createState(schema, doc); @@ -328,10 +328,10 @@ describe('calculateInlineRunPropertiesPlugin', () => { const { state: nextState } = state.applyTransaction(tr); const paragraph = nextState.doc.firstChild; - expect(paragraph.attrs.paragraphProperties).toEqual({ runProperties: { bold: true } }); + expect(paragraph.attrs.paragraphProperties).toBeNull(); }); - it('treats the first run inside inline wrappers as the paragraph first run', () => { + it('does not sync paragraph runProperties for first runs inside inline wrappers', () => { const schema = makeSchema(); const doc = schema.node('doc', null, [ schema.node('paragraph', null, [ @@ -347,7 +347,7 @@ describe('calculateInlineRunPropertiesPlugin', () => { const { state: nextState } = state.applyTransaction(tr); const paragraph = nextState.doc.firstChild; - expect(paragraph.attrs.paragraphProperties).toEqual({ runProperties: { bold: true } }); + expect(paragraph.attrs.paragraphProperties).toBeNull(); }); it('does not update paragraph runProperties when a non-first run changes', () => { @@ -372,7 +372,7 @@ describe('calculateInlineRunPropertiesPlugin', () => { expect(firstRun?.attrs.runProperties).toBeNull(); }); - it('updates paragraph runProperties when first run is nested inside an inline container', () => { + it('does not update paragraph runProperties when first run is nested inside an inline container', () => { const schema = makeSchema(); const doc = schema.node('doc', null, [ schema.node('paragraph', null, [ @@ -391,7 +391,7 @@ describe('calculateInlineRunPropertiesPlugin', () => { const { state: nextState } = state.applyTransaction(tr); const paragraph = nextState.doc.firstChild; - expect(paragraph.attrs.paragraphProperties).toEqual({ runProperties: { bold: true } }); + expect(paragraph.attrs.paragraphProperties).toBeNull(); }); it('does not update paragraph runProperties when nested run is not first in paragraph', () => { diff --git a/packages/super-editor/src/extensions/tests/headless.test.js b/packages/super-editor/src/extensions/tests/headless.test.js index 77a5b0a1d1..c01013ab80 100644 --- a/packages/super-editor/src/extensions/tests/headless.test.js +++ b/packages/super-editor/src/extensions/tests/headless.test.js @@ -166,7 +166,7 @@ describe('Headless Mode Optimization', () => { editor.destroy(); }); - it('updates paragraph runProperties for first runs nested in inline wrappers in headless mode', async () => { + it('does not sync paragraph runProperties for first runs nested in inline wrappers in headless mode', async () => { const buffer = await getTestDataAsFileBuffer('blank-doc.docx'); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); @@ -206,7 +206,7 @@ describe('Headless Mode Optimization', () => { editor.dispatch(tr); const updatedParagraph = editor.state.doc.firstChild; - expect(updatedParagraph?.attrs?.paragraphProperties).toEqual({ runProperties: { bold: true } }); + expect(updatedParagraph?.attrs?.paragraphProperties).toBeNull(); expect(hasInvalidParagraphRangeError(logSpy.mock.calls)).toBe(false); } finally { logSpy.mockRestore(); From 92c2ce0aa54150ea6db72057b0dcf00cdf936f52 Mon Sep 17 00:00:00 2001 From: Luccas Correa Date: Wed, 11 Mar 2026 10:40:51 -0300 Subject: [PATCH 2/3] chore: remove unused import --- .../src/extensions/run/calculateInlineRunPropertiesPlugin.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js index fed27882fa..a3da2e605b 100644 --- a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js +++ b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js @@ -6,7 +6,6 @@ import { calculateResolvedParagraphProperties, getResolvedParagraphProperties, } from '@extensions/paragraph/resolvedPropertiesCache.js'; -import { carbonCopy } from '@core/utilities/carbonCopy'; import { collectChangedRangesThroughTransactions } from '@utils/rangeUtils.js'; const RUN_PROPERTIES_DERIVED_FROM_MARKS = new Set([ From 917ecac32a0ed540826ca2660f13a497df2b86d1 Mon Sep 17 00:00:00 2001 From: Luccas Correa Date: Wed, 11 Mar 2026 10:48:23 -0300 Subject: [PATCH 3/3] test: adjust inline run properies computation tests --- ...calculateInlineRunPropertiesPlugin.test.js | 74 ++++++------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js index 56b24654bf..f4d792304b 100644 --- a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js +++ b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js @@ -331,70 +331,40 @@ describe('calculateInlineRunPropertiesPlugin', () => { expect(paragraph.attrs.paragraphProperties).toBeNull(); }); - it('does not sync paragraph runProperties for first runs inside inline wrappers', () => { + it("does not update a paragraph's runProperties using the run's properties", () => { const schema = makeSchema(); + const paragraphRunProperties = { italic: true, styleId: 'ParagraphDefault' }; const doc = schema.node('doc', null, [ - schema.node('paragraph', null, [ - schema.node('bookmarkStart', null, [schema.node('run', null, schema.text('Wrapped'))]), - ]), - ]); - const state = createState(schema, doc); - const [wrappedRunPos] = runPositions(state.doc); - const from = wrappedRunPos + 1; - const to = wrappedRunPos + 3; - - const tr = state.tr.addMark(from, to, schema.marks.bold.create()); - const { state: nextState } = state.applyTransaction(tr); - - const paragraph = nextState.doc.firstChild; - expect(paragraph.attrs.paragraphProperties).toBeNull(); - }); - - it('does not update paragraph runProperties when a non-first run changes', () => { - const schema = makeSchema(); - const doc = schema.node('doc', null, [ - schema.node('paragraph', null, [ - schema.node('run', null, schema.text('First')), - schema.node('run', null, schema.text('Second')), - ]), + schema.node( + 'paragraph', + { + paragraphProperties: { + alignment: 'center', + runProperties: paragraphRunProperties, + }, + }, + [schema.node('run', null, schema.text('Hello')), schema.node('run', null, schema.text('World'))], + ), ]); const state = createState(schema, doc); - const [firstRunPos, secondRunPos] = runPositions(state.doc); - const from = secondRunPos + 1; - const to = secondRunPos + 3; + const [firstRunPos] = runPositions(state.doc); + const { from, to } = runTextRangeAtPos(firstRunPos, 0, 2); const tr = state.tr.addMark(from, to, schema.marks.bold.create()); const { state: nextState } = state.applyTransaction(tr); const paragraph = nextState.doc.firstChild; - expect(paragraph.attrs.paragraphProperties).toBeNull(); - const firstRun = nextState.doc.nodeAt(firstRunPos); - expect(firstRun?.attrs.runProperties).toBeNull(); - }); - - it('does not update paragraph runProperties when first run is nested inside an inline container', () => { - const schema = makeSchema(); - const doc = schema.node('doc', null, [ - schema.node('paragraph', null, [ - schema.node('pageReference', { instruction: 'PAGEREF _Toc123456789 h' }, [ - schema.node('run', null, schema.text('Ref')), - ]), - schema.node('run', null, schema.text(' tail')), - ]), - ]); - const state = createState(schema, doc); - const [nestedRunPos] = runPositions(state.doc); - const from = nestedRunPos + 1; - const to = nestedRunPos + 4; - - const tr = state.tr.addMark(from, to, schema.marks.bold.create()); - const { state: nextState } = state.applyTransaction(tr); + expect(paragraph.attrs.paragraphProperties).toEqual({ + alignment: 'center', + runProperties: paragraphRunProperties, + }); - const paragraph = nextState.doc.firstChild; - expect(paragraph.attrs.paragraphProperties).toBeNull(); + const updatedRuns = runPositions(nextState.doc); + const updatedRun = nextState.doc.nodeAt(updatedRuns[0]); + expect(updatedRun?.attrs.runProperties).toEqual({ bold: true }); }); - it('does not update paragraph runProperties when nested run is not first in paragraph', () => { + it('does not update paragraph runProperties when a nested run changes', () => { const schema = makeSchema(); const doc = schema.node('doc', null, [ schema.node('paragraph', null, [