diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.regressions.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.regressions.test.ts index e8f2987511..8d956d980a 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.regressions.test.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.regressions.test.ts @@ -208,7 +208,6 @@ function makeTableEditor(options: TableEditorOptions = {}): Editor { attrs: { sdBlockId: 'table-1', tableProperties: {}, - tableGrid: [5000, 5000], grid: [{ col: 1200 }, { col: 3000 }], }, isBlock: true, @@ -317,6 +316,15 @@ function makeTableEditor(options: TableEditorOptions = {}): Editor { }, }, dispatch: vi.fn(), + extensionService: { + attributes: [ + { type: 'table', name: 'sdBlockId', attribute: { keepOnSplit: false } }, + { type: 'table', name: 'paraId', attribute: { keepOnSplit: false } }, + { type: 'table', name: 'textId', attribute: { keepOnSplit: false } }, + { type: 'table', name: 'tableProperties', attribute: { keepOnSplit: true } }, + { type: 'table', name: 'grid', attribute: { keepOnSplit: true } }, + ], + }, commands: {}, can: vi.fn(() => ({})), schema: { marks: {}, nodes: {} }, @@ -380,6 +388,8 @@ describe('tables-adapter regressions', () => { const editor = makeTableEditor(); const tr = editor.state.tr as unknown as { insert: ReturnType }; const tableNode = editor.state.doc.nodeAt(0) as ProseMirrorNode; + (tableNode.attrs as Record).paraId = 'table-para-id'; + (tableNode.attrs as Record).textId = 'table-text-id'; const expectedInsertPos = tableNode.nodeSize; const result = tablesSplitAdapter(editor, { nodeId: 'table-1', rowIndex: 1 }); @@ -395,6 +405,11 @@ describe('tables-adapter regressions', () => { expect(insertedSeparator.type.name).toBe('paragraph'); expect(secondInsertCall[0]).toBe(expectedInsertPos + insertedSeparator.nodeSize); expect(insertedTable.type.name).toBe('table'); + expect(insertedTable.attrs.tableProperties).toEqual({}); + expect(insertedTable.attrs.grid).toEqual([{ col: 1200 }, { col: 3000 }]); + expect(insertedTable.attrs.sdBlockId).toBeUndefined(); + expect(insertedTable.attrs.paraId).toBeUndefined(); + expect(insertedTable.attrs.textId).toBeUndefined(); }); it('SD-2127: inserts a new cell in every row when appending a column to the right of the last column', () => { diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts index 65c35119fd..222cbe65be 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts @@ -97,6 +97,7 @@ import { DocumentApiAdapterError } from './errors.js'; import { toBlockAddress, findBlockById, findBlockByNodeIdOnly } from './helpers/node-address-resolver.js'; import { resolvePreferredNewTableStyleId, isKnownTableStyleId } from '@superdoc/style-engine/ooxml'; import { generateDocxHexId } from '../utils/generateDocxHexId.js'; +import { Attribute, type AttributeValue } from '../core/Attribute.js'; import { readSettingsRoot, ensureSettingsRoot, @@ -2115,11 +2116,11 @@ export function tablesSplitAdapter( tr.delete(rp, rEnd); } - // Build the new table with the same attributes. - const newTableAttrs = { ...(tableNode.attrs as Record) }; - delete newTableAttrs.sdBlockId; // Each table needs a unique ID — let PM assign one. - delete newTableAttrs.paraId; // Never duplicate legacy/imported table paraIds across split tables. - delete newTableAttrs.textId; // Avoid duplicate w14:textId after split. + const newTableAttrs = Attribute.getSplittedAttributes( + editor.extensionService?.attributes ?? [], + tableNode.type.name, + tableNode.attrs as Record, + ); const newTable = schema.nodes.table.create(newTableAttrs, secondTableRows); const separatorParagraph = createSeparatorParagraph(schema); if (!separatorParagraph) { diff --git a/packages/super-editor/src/editors/v1/extensions/paragraph/paragraph.js b/packages/super-editor/src/editors/v1/extensions/paragraph/paragraph.js index b0ef33e3d2..593f599a5f 100644 --- a/packages/super-editor/src/editors/v1/extensions/paragraph/paragraph.js +++ b/packages/super-editor/src/editors/v1/extensions/paragraph/paragraph.js @@ -131,8 +131,8 @@ export const Paragraph = OxmlNode.create({ addAttributes() { return { - paraId: { rendered: false }, - textId: { rendered: false }, + paraId: { rendered: false, keepOnSplit: false }, + textId: { rendered: false, keepOnSplit: false }, rsidR: { rendered: false }, rsidRDefault: { rendered: false }, rsidP: { rendered: false }, diff --git a/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.js b/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.js index ec862f7341..fa5b58a13a 100644 --- a/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.js +++ b/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.js @@ -1,6 +1,7 @@ // @ts-check import { NodeSelection, TextSelection, AllSelection } from 'prosemirror-state'; import { canSplit } from 'prosemirror-transform'; +import { Attribute } from '@core/Attribute.js'; import { defaultBlockAt } from '@core/helpers/defaultBlockAt.js'; import { getSplitRunProperties, syncSplitParagraphRunProperties } from '@core/helpers/splitParagraphRunProperties.js'; import { @@ -94,15 +95,8 @@ export function splitBlockPatch(state, dispatch, editor) { atStart = $from.start(d) == $from.pos - ($from.depth - d); deflt = defaultBlockAt($from.node(d - 1).contentMatchAt($from.indexAfter(d - 1))); const sourceParagraphStyleId = node.attrs?.paragraphProperties?.styleId; - paragraphAttrs = /** @type {Record} */ ({ - ...node.attrs, - // Ensure newly created block gets a fresh ID (block-node plugin assigns one) - sdBlockId: null, - sdBlockRev: null, - // Reset DOCX identifiers on split to avoid duplicate paragraph IDs - paraId: null, - textId: null, - }); + const extensionAttrs = editor?.extensionService?.attributes ?? []; + paragraphAttrs = Attribute.getSplittedAttributes(extensionAttrs, node.type.name, node.attrs); paragraphAttrs = clearInheritedLinkedStyleId(paragraphAttrs, editor, { emptyParagraph: atEnd }); // When splitting at the end (creating an empty new paragraph), store the diff --git a/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.test.js b/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.test.js index 90ac82fd6e..556e4fa051 100644 --- a/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.test.js +++ b/packages/super-editor/src/editors/v1/extensions/run/commands/split-run.test.js @@ -143,6 +143,43 @@ describe('splitRunToParagraph command', () => { expect(paragraphTexts).toEqual(['He', 'llo']); }); + it('uses paragraph split metadata instead of copying DOCX identities', () => { + loadDoc({ + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { + paraId: 'ABCDEF01', + textId: 'ABCDEF02', + sdBlockId: 'block-1', + sdBlockRev: 3, + paragraphProperties: { styleId: 'BodyText' }, + }, + content: [ + { + type: 'run', + content: [{ type: 'text', text: 'Hello' }], + }, + ], + }, + ], + }); + + const start = findTextPos('Hello'); + expect(start).not.toBeNull(); + updateSelection((start ?? 0) + 2); + + expect(editor.commands.splitRunToParagraph()).toBe(true); + + const splitParagraph = editor.view.state.doc.child(1); + expect(splitParagraph.attrs.paraId).toBeNull(); + expect(splitParagraph.attrs.textId).toBeNull(); + expect(splitParagraph.attrs.sdBlockId).toBeNull(); + expect(splitParagraph.attrs.sdBlockRev).toBe(0); + expect(splitParagraph.attrs.paragraphProperties).toEqual({ styleId: 'BodyText' }); + }); + it('preserves explicit stored marks when splitting into a new paragraph', () => { loadDoc(RUN_DOC); diff --git a/packages/super-editor/src/editors/v1/extensions/table/table.test.js b/packages/super-editor/src/editors/v1/extensions/table/table.test.js index a1373acec7..4f01cf3f06 100644 --- a/packages/super-editor/src/editors/v1/extensions/table/table.test.js +++ b/packages/super-editor/src/editors/v1/extensions/table/table.test.js @@ -1232,6 +1232,33 @@ describe('Table commands', async () => { }); }); }); + + it('does not duplicate paraId when splitting the trailing paragraph', async () => { + const { docx, media, mediaFiles, fonts } = cachedBlankDoc; + ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts })); + + const pos = editor.state.doc.content.size; + editor.commands.insertTableAt({ pos, rows: 2, columns: 2 }); + + const doc = editor.state.doc; + const trailingParagraph = doc.child(doc.childCount - 1); + expect(trailingParagraph.type.name).toBe('paragraph'); + expect(trailingParagraph.attrs.paraId).toMatch(/^[0-9A-F]{8}$/); + + const trailingParagraphPos = doc.content.size - trailingParagraph.nodeSize; + editor.view.dispatch(editor.state.tr.setSelection(TextSelection.create(doc, trailingParagraphPos + 1))); + + expect(editor.commands.splitBlock()).toBe(true); + + const paraIds = []; + editor.state.doc.descendants((node) => { + if (node.type.name === 'paragraph' && node.attrs.paraId) { + paraIds.push(node.attrs.paraId); + } + }); + + expect(new Set(paraIds).size).toBe(paraIds.length); + }); }); describe('insertTable trailing separator paragraph', () => {