From 67b0a46eb77befef5650ededfcbe57a70c10fa83 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 4 Mar 2026 21:29:13 -0800 Subject: [PATCH 1/2] feat(document-api): insert table adds extra separator to match ms word --- .../src/core/commands/insertTableAt.js | 31 ++++++++- .../insert-structured-wrapper.test.ts | 46 +++++++++++++ .../plan-engine/plan-wrappers.ts | 38 ++++++++++ .../tables-adapter.integration.test.ts | 51 ++++++++++++++ .../src/extensions/table/table.js | 46 ++++++++++++- .../src/extensions/table/table.test.js | 69 +++++++++++++++++++ 6 files changed, 279 insertions(+), 2 deletions(-) diff --git a/packages/super-editor/src/core/commands/insertTableAt.js b/packages/super-editor/src/core/commands/insertTableAt.js index 4fdc6d9d06..811dadb793 100644 --- a/packages/super-editor/src/core/commands/insertTableAt.js +++ b/packages/super-editor/src/core/commands/insertTableAt.js @@ -1,3 +1,25 @@ +import { Fragment } from 'prosemirror-model'; + +/** + * Returns true when inserting a table at `pos` would place it adjacent to + * another table or at the document end. + * + * @param {import('prosemirror-model').Node} doc + * @param {number} pos + * @returns {boolean} + */ +function tableWouldBeAdjacent(doc, pos) { + const $pos = doc.resolve(pos); + if ($pos.depth !== 0) return false; + const indexAfter = $pos.index(0); + const nodeAfter = indexAfter < doc.childCount ? doc.child(indexAfter) : null; + const nodeBefore = indexAfter > 0 ? doc.child(indexAfter - 1) : null; + if (!nodeAfter) return true; + if (nodeAfter.type.name === 'table') return true; + if (nodeBefore?.type.name === 'table') return true; + return false; +} + /** * Insert a table node at an absolute document position. * @@ -35,7 +57,14 @@ export const insertTableAt = const tableAttrs = sdBlockId ? { sdBlockId } : undefined; const tableNode = tableType.createChecked(tableAttrs, rowNodes); - const tr = state.tr.insert(pos, tableNode); + let tr; + if (tableWouldBeAdjacent(state.doc, pos)) { + const separatorParagraph = state.schema.nodes.paragraph.createAndFill(); + const fragment = Fragment.from(separatorParagraph ? [tableNode, separatorParagraph] : [tableNode]); + tr = state.tr.insert(pos, fragment); + } else { + tr = state.tr.insert(pos, tableNode); + } if (!dispatch) return true; tr.setMeta('inputType', 'programmatic'); if (tracked === true) tr.setMeta('forceTrackChanges', true); diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/insert-structured-wrapper.test.ts b/packages/super-editor/src/document-api-adapters/plan-engine/insert-structured-wrapper.test.ts index 0a8a04a0ce..5b80265b2c 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/insert-structured-wrapper.test.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/insert-structured-wrapper.test.ts @@ -153,6 +153,52 @@ describe('insertStructuredWrapper — markdown', () => { }); }); +describe('insertStructuredWrapper — table separators', () => { + it('inserts a trailing separator paragraph after a markdown table', () => { + const result = insertStructuredWrapper(editor, { + value: '| A | B |\n| --- | --- |\n| foo | bar |', + type: 'markdown', + }); + + expect(result.success).toBe(true); + + const doc = editor.state.doc; + let foundTable = false; + let nodeAfterTable: import('prosemirror-model').Node | null = null; + for (let i = 0; i < doc.childCount; i++) { + if (doc.child(i).type.name === 'table') { + foundTable = true; + if (i + 1 < doc.childCount) { + nodeAfterTable = doc.child(i + 1); + } + break; + } + } + + expect(foundTable).toBe(true); + expect(nodeAfterTable).not.toBeNull(); + expect(nodeAfterTable!.type.name).toBe('paragraph'); + }); + + it('two consecutive markdown table inserts produce non-adjacent tables', () => { + insertStructuredWrapper(editor, { + value: '| A | B |\n| --- | --- |\n| 1 | 2 |', + type: 'markdown', + }); + insertStructuredWrapper(editor, { + value: '| C | D |\n| --- | --- |\n| 3 | 4 |', + type: 'markdown', + }); + + const doc = editor.state.doc; + for (let i = 0; i < doc.childCount - 1; i++) { + if (doc.child(i).type.name === 'table' && doc.child(i + 1).type.name === 'table') { + throw new Error(`Adjacent tables at children ${i} and ${i + 1}`); + } + } + }); +}); + describe('insertStructuredWrapper — list numbering rollback', () => { it('rolls back numbering allocations when insertContentAt fails after markdown parsing', () => { // This test exercises the actual rollback branch: markdown with list diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts index 1f7271aecd..56dc2d6806 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts @@ -56,6 +56,24 @@ function editorHasDom(editor: Editor): boolean { return !!(opts?.document ?? opts?.mockDocument ?? (typeof document !== 'undefined' ? document : null)); } +/** + * Mutate `jsonNodes` in place so that adjacent table nodes are separated by + * an empty paragraph, and a trailing separator follows the last table. + * This matches Word's requirement that every `` is followed by ``. + */ +function ensureTableSeparators(jsonNodes: Record[]): void { + const makeSeparator = (): Record => ({ type: 'paragraph' }); + + // Walk backwards so spliced indices don't shift remaining work. + for (let i = jsonNodes.length - 1; i >= 0; i--) { + if (jsonNodes[i].type !== 'table') continue; + const next = jsonNodes[i + 1]; + if (!next || next.type === 'table') { + jsonNodes.splice(i + 1, 0, makeSeparator()); + } + } +} + // --------------------------------------------------------------------------- // Locator normalization (same validation as the old adapters) // --------------------------------------------------------------------------- @@ -641,6 +659,26 @@ export function insertStructuredWrapper( const jsonNodes: Record[] = []; fragment.forEach((node) => jsonNodes.push(node.toJSON())); + // Word always separates adjacent tables with a paragraph. Without a + // trailing separator, consecutive markdown inserts produce adjacent + // elements that Word merges into one visual table. + ensureTableSeparators(jsonNodes); + + // insertContentAt replaces empty textblocks when inserting blocks. + // If the first node is a table and we're in an empty paragraph that + // follows a table, prepend a separator so the replacement doesn't + // produce adjacent tables. + if (jsonNodes[0]?.type === 'table' && from === to) { + const $pos = editor.state.doc.resolve(from); + const parent = $pos.parent; + if (parent.isTextblock && !parent.childCount) { + const idx = $pos.index($pos.depth - 1); + if (idx > 0 && $pos.node($pos.depth - 1).child(idx - 1).type.name === 'table') { + jsonNodes.unshift({ type: 'paragraph' }); + } + } + } + const ok = Boolean(editor.commands.insertContentAt({ from, to }, jsonNodes)); if (!ok) { insertFailure = { diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.integration.test.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.integration.test.ts index 686fb8aef0..71ee761beb 100644 --- a/packages/super-editor/src/document-api-adapters/tables-adapter.integration.test.ts +++ b/packages/super-editor/src/document-api-adapters/tables-adapter.integration.test.ts @@ -5,6 +5,9 @@ import { initTestEditor, loadTestDataForEditorTests } from '@tests/helpers/helpe import DocxZipper from '@core/DocxZipper.js'; import type { Editor } from '../core/Editor.js'; import { createTableAdapter, tablesSplitAdapter } from './tables-adapter.js'; +import { insertStructuredWrapper } from './plan-engine/plan-wrappers.js'; +import { clearExecutorRegistry } from './plan-engine/executor-registry.js'; +import { registerBuiltInExecutors } from './plan-engine/register-executors.js'; type LoadedDocData = Awaited>; @@ -38,6 +41,8 @@ describe('tables adapter DOCX integration', () => { beforeAll(async () => { docData = await loadTestDataForEditorTests('blank-doc.docx'); + clearExecutorRegistry(); + registerBuiltInExecutors(); }); afterEach(() => { @@ -45,6 +50,52 @@ describe('tables adapter DOCX integration', () => { editor = undefined; }); + it('two consecutive create.table calls produce non-adjacent tables in DOCX', async () => { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + createTableAdapter(editor, { rows: 2, columns: 2, at: { kind: 'documentEnd' } }, DIRECT_MUTATION_OPTIONS); + createTableAdapter(editor, { rows: 2, columns: 2, at: { kind: 'documentEnd' } }, DIRECT_MUTATION_OPTIONS); + + const exportedFiles = await exportDocxFiles(editor); + const documentXml = exportedFiles['word/document.xml']; + + expect(documentXml).toBeTruthy(); + expect(documentXml).not.toMatch(/<\/w:tbl>\s*/); + expect(documentXml).toMatch(/<\/w:tbl>\s*]*?\/?>\s* { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + insertStructuredWrapper(editor, { + value: '| A | B |\n| --- | --- |\n| foo | bar |', + type: 'markdown', + }); + insertStructuredWrapper(editor, { + value: '| C | D |\n| --- | --- |\n| baz | qux |', + type: 'markdown', + }); + + const exportedFiles = await exportDocxFiles(editor); + const documentXml = exportedFiles['word/document.xml']; + + expect(documentXml).toBeTruthy(); + expect(documentXml).not.toMatch(/<\/w:tbl>\s*/); + expect(documentXml).toMatch(/<\/w:tbl>\s*]*?\/?>\s* { ({ editor } = initTestEditor({ content: docData.docx, diff --git a/packages/super-editor/src/extensions/table/table.js b/packages/super-editor/src/extensions/table/table.js index 619bc181fb..21841f81f2 100644 --- a/packages/super-editor/src/extensions/table/table.js +++ b/packages/super-editor/src/extensions/table/table.js @@ -169,6 +169,8 @@ * @property {import('prosemirror-model').Node[]} rows - Row nodes to append */ +import { v4 as uuidv4 } from 'uuid'; +import { Fragment } from 'prosemirror-model'; import { Node, Attribute } from '@core/index.js'; import { callOrGet } from '@core/utilities/callOrGet.js'; import { getExtensionConfigField } from '@core/helpers/getExtensionConfigField.js'; @@ -221,6 +223,37 @@ import { insertRowAtIndex, } from './tableHelpers/appendRows.js'; +/** + * Returns true when inserting a table at `pos` would place it adjacent to + * another table or at the document end (where a follow-up insert could + * produce adjacency). A trailing separator paragraph is only needed in + * these cases — inserting before/after a paragraph or heading should not + * add an extra blank block. + * + * @param {import('prosemirror-model').Node} doc + * @param {number} pos - Absolute insertion position (between top-level blocks) + * @returns {boolean} + */ +function tableWouldBeAdjacent(doc, pos) { + // pos sits between top-level children. Resolve to find which children + // border the insertion point. + const $pos = doc.resolve(pos); + // Only act at top-level (depth 0 = doc body). Inside nested structures + // the adjacency rule does not apply. + if ($pos.depth !== 0) return false; + + const indexAfter = $pos.index(0); // child that currently starts at pos + const nodeAfter = indexAfter < doc.childCount ? doc.child(indexAfter) : null; + const nodeBefore = indexAfter > 0 ? doc.child(indexAfter - 1) : null; + + // At document end — always add separator so consecutive inserts stay safe. + if (!nodeAfter) return true; + // Adjacent to an existing table on either side. + if (nodeAfter.type.name === 'table') return true; + if (nodeBefore?.type.name === 'table') return true; + return false; +} + const IMPORT_CONTEXT_SELECTOR = '[data-superdoc-import="true"]'; const IMPORT_DEFAULT_TABLE_WIDTH_PCT = 5000; // OOXML percent units where 5000 == 100% @@ -704,7 +737,18 @@ export const Table = Node.create({ const tableNode = tableType.createChecked(tableAttrs, rowNodes); if (dispatch) { - tr.insert(pos, tableNode); + // Only insert a trailing separator paragraph when the table would + // otherwise be adjacent to another table (or at document end where + // a follow-up insert could produce adjacency). + const needsSeparator = tableWouldBeAdjacent(state.doc, pos); + if (needsSeparator) { + const separatorAttrs = { sdBlockId: uuidv4(), paraId: genParaId() }; + const separatorParagraph = state.schema.nodes.paragraph.createAndFill(separatorAttrs); + const fragment = Fragment.from(separatorParagraph ? [tableNode, separatorParagraph] : [tableNode]); + tr.insert(pos, fragment); + } else { + tr.insert(pos, tableNode); + } tr.setMeta('inputType', 'programmatic'); if (tracked === true) tr.setMeta('forceTrackChanges', true); else if (tracked === false) tr.setMeta('skipTrackChanges', true); diff --git a/packages/super-editor/src/extensions/table/table.test.js b/packages/super-editor/src/extensions/table/table.test.js index 72bd73318a..87cf18bb3d 100644 --- a/packages/super-editor/src/extensions/table/table.test.js +++ b/packages/super-editor/src/extensions/table/table.test.js @@ -1083,6 +1083,75 @@ describe('Table commands', async () => { }); }); + describe('insertTableAt trailing separator paragraph', () => { + it('inserts table followed by a 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; + let foundTable = false; + let nodeAfterTable = null; + for (let i = 0; i < doc.childCount; i++) { + if (doc.child(i).type.name === 'table' && !foundTable) { + foundTable = true; + if (i + 1 < doc.childCount) { + nodeAfterTable = doc.child(i + 1); + } + } + } + + expect(foundTable).toBe(true); + expect(nodeAfterTable).not.toBeNull(); + expect(nodeAfterTable.type.name).toBe('paragraph'); + }); + + it('does not insert separator when table is placed between paragraphs', async () => { + const { docx, media, mediaFiles, fonts } = cachedBlankDoc; + ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts })); + + // Insert some text so we have paragraphs in the doc + editor.commands.insertContent('Hello'); + editor.commands.splitBlock(); + editor.commands.insertContent('World'); + + const docBefore = editor.state.doc; + // Find position between the two paragraphs (after first paragraph) + const firstParaEnd = docBefore.child(0).nodeSize; + + editor.commands.insertTableAt({ pos: firstParaEnd, rows: 2, columns: 2 }); + + const doc = editor.state.doc; + // The table should be at index 1 (between the two paragraphs) + // There should NOT be an extra separator paragraph injected + let tableCount = 0; + let paragraphCount = 0; + for (let i = 0; i < doc.childCount; i++) { + if (doc.child(i).type.name === 'table') tableCount++; + if (doc.child(i).type.name === 'paragraph') paragraphCount++; + } + + expect(tableCount).toBe(1); + // Original 2 paragraphs, no extra separator + expect(paragraphCount).toBe(2); + }); + + it('removes both table and separator paragraph on single undo', async () => { + const { docx, media, mediaFiles, fonts } = cachedBlankDoc; + ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts })); + + const docBefore = editor.state.doc; + const pos = editor.state.doc.content.size; + editor.commands.insertTableAt({ pos, rows: 2, columns: 2 }); + + editor.commands.undo(); + + expect(editor.state.doc.toJSON()).toEqual(docBefore.toJSON()); + }); + }); + describe('normalizeNewTableAttrs tblLook (SD-2086)', async () => { it('includes DEFAULT_TBL_LOOK in tableProperties when a style is resolved', async () => { const { docx, media, mediaFiles, fonts } = cachedBlankDoc; From 4a21f0e6f9ccf833274b21a909c6889c707aa9b5 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 4 Mar 2026 21:41:32 -0800 Subject: [PATCH 2/2] chore: review fixes --- .../src/core/commands/insertTableAt.js | 36 +++++++----- .../plan-engine/plan-wrappers.ts | 37 +++++++------ .../src/extensions/table/table.js | 55 +++++++++---------- 3 files changed, 70 insertions(+), 58 deletions(-) diff --git a/packages/super-editor/src/core/commands/insertTableAt.js b/packages/super-editor/src/core/commands/insertTableAt.js index 811dadb793..9eaf708472 100644 --- a/packages/super-editor/src/core/commands/insertTableAt.js +++ b/packages/super-editor/src/core/commands/insertTableAt.js @@ -1,23 +1,23 @@ import { Fragment } from 'prosemirror-model'; /** - * Returns true when inserting a table at `pos` would place it adjacent to - * another table or at the document end. + * Determines which sides of a table inserted at `pos` need a separator + * paragraph to prevent adjacency with an existing table. * * @param {import('prosemirror-model').Node} doc * @param {number} pos - * @returns {boolean} + * @returns {{ before: boolean, after: boolean }} */ -function tableWouldBeAdjacent(doc, pos) { +function tableSeparatorNeeds(doc, pos) { const $pos = doc.resolve(pos); - if ($pos.depth !== 0) return false; + if ($pos.depth !== 0) return { before: false, after: false }; const indexAfter = $pos.index(0); const nodeAfter = indexAfter < doc.childCount ? doc.child(indexAfter) : null; const nodeBefore = indexAfter > 0 ? doc.child(indexAfter - 1) : null; - if (!nodeAfter) return true; - if (nodeAfter.type.name === 'table') return true; - if (nodeBefore?.type.name === 'table') return true; - return false; + return { + before: nodeBefore?.type.name === 'table', + after: !nodeAfter || nodeAfter.type.name === 'table', + }; } /** @@ -57,11 +57,21 @@ export const insertTableAt = const tableAttrs = sdBlockId ? { sdBlockId } : undefined; const tableNode = tableType.createChecked(tableAttrs, rowNodes); + const sep = tableSeparatorNeeds(state.doc, pos); let tr; - if (tableWouldBeAdjacent(state.doc, pos)) { - const separatorParagraph = state.schema.nodes.paragraph.createAndFill(); - const fragment = Fragment.from(separatorParagraph ? [tableNode, separatorParagraph] : [tableNode]); - tr = state.tr.insert(pos, fragment); + if (sep.before || sep.after) { + const makeSep = () => state.schema.nodes.paragraph.createAndFill(); + const nodes = []; + if (sep.before) { + const s = makeSep(); + if (s) nodes.push(s); + } + nodes.push(tableNode); + if (sep.after) { + const s = makeSep(); + if (s) nodes.push(s); + } + tr = state.tr.insert(pos, Fragment.from(nodes)); } else { tr = state.tr.insert(pos, tableNode); } diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts index 56dc2d6806..27d8ac358b 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts @@ -57,19 +57,15 @@ function editorHasDom(editor: Editor): boolean { } /** - * Mutate `jsonNodes` in place so that adjacent table nodes are separated by - * an empty paragraph, and a trailing separator follows the last table. - * This matches Word's requirement that every `` is followed by ``. + * Mutate `jsonNodes` in place so that consecutive table nodes within the + * array are separated by an empty paragraph. Only handles within-fragment + * adjacency — document-context separators (leading/trailing) are handled + * by the caller after inspecting the insertion position. */ function ensureTableSeparators(jsonNodes: Record[]): void { - const makeSeparator = (): Record => ({ type: 'paragraph' }); - - // Walk backwards so spliced indices don't shift remaining work. - for (let i = jsonNodes.length - 1; i >= 0; i--) { - if (jsonNodes[i].type !== 'table') continue; - const next = jsonNodes[i + 1]; - if (!next || next.type === 'table') { - jsonNodes.splice(i + 1, 0, makeSeparator()); + for (let i = jsonNodes.length - 2; i >= 0; i--) { + if (jsonNodes[i].type === 'table' && jsonNodes[i + 1].type === 'table') { + jsonNodes.splice(i + 1, 0, { type: 'paragraph' }); } } } @@ -664,18 +660,25 @@ export function insertStructuredWrapper( // elements that Word merges into one visual table. ensureTableSeparators(jsonNodes); - // insertContentAt replaces empty textblocks when inserting blocks. - // If the first node is a table and we're in an empty paragraph that - // follows a table, prepend a separator so the replacement doesn't - // produce adjacent tables. - if (jsonNodes[0]?.type === 'table' && from === to) { + // insertContentAt replaces empty textblocks when inserting block + // content. Check whether the replaced paragraph's neighbors are tables + // and add separators to prevent adjacency in the result. + if (from === to) { const $pos = editor.state.doc.resolve(from); const parent = $pos.parent; if (parent.isTextblock && !parent.childCount) { + const grandparent = $pos.node($pos.depth - 1); const idx = $pos.index($pos.depth - 1); - if (idx > 0 && $pos.node($pos.depth - 1).child(idx - 1).type.name === 'table') { + const prevIsTable = idx > 0 && grandparent.child(idx - 1).type.name === 'table'; + const nextIsTable = idx + 1 < grandparent.childCount && grandparent.child(idx + 1).type.name === 'table'; + const atEnd = idx + 1 >= grandparent.childCount; + + if (jsonNodes[0]?.type === 'table' && prevIsTable) { jsonNodes.unshift({ type: 'paragraph' }); } + if (jsonNodes[jsonNodes.length - 1]?.type === 'table' && (nextIsTable || atEnd)) { + jsonNodes.push({ type: 'paragraph' }); + } } } diff --git a/packages/super-editor/src/extensions/table/table.js b/packages/super-editor/src/extensions/table/table.js index 21841f81f2..1b6ee9b840 100644 --- a/packages/super-editor/src/extensions/table/table.js +++ b/packages/super-editor/src/extensions/table/table.js @@ -224,34 +224,25 @@ import { } from './tableHelpers/appendRows.js'; /** - * Returns true when inserting a table at `pos` would place it adjacent to - * another table or at the document end (where a follow-up insert could - * produce adjacency). A trailing separator paragraph is only needed in - * these cases — inserting before/after a paragraph or heading should not - * add an extra blank block. + * Determines which sides of a table inserted at `pos` need a separator + * paragraph to prevent adjacency with an existing table. * * @param {import('prosemirror-model').Node} doc * @param {number} pos - Absolute insertion position (between top-level blocks) - * @returns {boolean} + * @returns {{ before: boolean, after: boolean }} */ -function tableWouldBeAdjacent(doc, pos) { - // pos sits between top-level children. Resolve to find which children - // border the insertion point. +function tableSeparatorNeeds(doc, pos) { const $pos = doc.resolve(pos); - // Only act at top-level (depth 0 = doc body). Inside nested structures - // the adjacency rule does not apply. - if ($pos.depth !== 0) return false; + if ($pos.depth !== 0) return { before: false, after: false }; - const indexAfter = $pos.index(0); // child that currently starts at pos + const indexAfter = $pos.index(0); const nodeAfter = indexAfter < doc.childCount ? doc.child(indexAfter) : null; const nodeBefore = indexAfter > 0 ? doc.child(indexAfter - 1) : null; - // At document end — always add separator so consecutive inserts stay safe. - if (!nodeAfter) return true; - // Adjacent to an existing table on either side. - if (nodeAfter.type.name === 'table') return true; - if (nodeBefore?.type.name === 'table') return true; - return false; + return { + before: nodeBefore?.type.name === 'table', + after: !nodeAfter || nodeAfter.type.name === 'table', + }; } const IMPORT_CONTEXT_SELECTOR = '[data-superdoc-import="true"]'; @@ -737,15 +728,23 @@ export const Table = Node.create({ const tableNode = tableType.createChecked(tableAttrs, rowNodes); if (dispatch) { - // Only insert a trailing separator paragraph when the table would - // otherwise be adjacent to another table (or at document end where - // a follow-up insert could produce adjacency). - const needsSeparator = tableWouldBeAdjacent(state.doc, pos); - if (needsSeparator) { - const separatorAttrs = { sdBlockId: uuidv4(), paraId: genParaId() }; - const separatorParagraph = state.schema.nodes.paragraph.createAndFill(separatorAttrs); - const fragment = Fragment.from(separatorParagraph ? [tableNode, separatorParagraph] : [tableNode]); - tr.insert(pos, fragment); + const sep = tableSeparatorNeeds(state.doc, pos); + const makeSep = () => { + const attrs = { sdBlockId: uuidv4(), paraId: genParaId() }; + return state.schema.nodes.paragraph.createAndFill(attrs); + }; + if (sep.before || sep.after) { + const nodes = []; + if (sep.before) { + const s = makeSep(); + if (s) nodes.push(s); + } + nodes.push(tableNode); + if (sep.after) { + const s = makeSep(); + if (s) nodes.push(s); + } + tr.insert(pos, Fragment.from(nodes)); } else { tr.insert(pos, tableNode); }