Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -74,8 +73,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) => {
Expand All @@ -97,26 +94,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);
Expand Down Expand Up @@ -224,24 +201,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.
Expand Down Expand Up @@ -409,17 +368,6 @@ function stableStringifyInlineProps(inlineProps) {
return JSON.stringify(sorted);
}

/**
* Compare two runProperties objects with stable key ordering.
*
* @param {Record<string, any>|null} left
* @param {Record<string, any>|null} right
* @returns {boolean}
*/
function areRunPropertiesEqual(left, right) {
return stableStringifyInlineProps(left) === stableStringifyInlineProps(right);
}

/**
* Track and reapply selection across run replacements.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -327,74 +327,44 @@ describe('calculateInlineRunPropertiesPlugin', () => {
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).toEqual({ runProperties: { bold: true } });
});

it('treats the first run inside inline wrappers as the paragraph first run', () => {
const schema = makeSchema();
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).toEqual({ runProperties: { bold: true } });
});

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')),
]),
]);
const state = createState(schema, doc);
const [firstRunPos, secondRunPos] = runPositions(state.doc);
const from = secondRunPos + 1;
const to = secondRunPos + 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();
const firstRun = nextState.doc.nodeAt(firstRunPos);
expect(firstRun?.attrs.runProperties).toBeNull();
});

it('updates paragraph runProperties when first run is nested inside an inline container', () => {
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('pageReference', { instruction: 'PAGEREF _Toc123456789 h' }, [
schema.node('run', null, schema.text('Ref')),
]),
schema.node('run', null, schema.text(' tail')),
]),
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 [nestedRunPos] = runPositions(state.doc);
const from = nestedRunPos + 1;
const to = nestedRunPos + 4;
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).toEqual({ runProperties: { bold: true } });
expect(paragraph.attrs.paragraphProperties).toEqual({
alignment: 'center',
runProperties: paragraphRunProperties,
});

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, [
Expand Down
4 changes: 2 additions & 2 deletions packages/super-editor/src/extensions/tests/headless.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {});

Expand Down Expand Up @@ -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();
Expand Down
Loading