Skip to content

Commit 6bf6cea

Browse files
committed
fix(document-api): avoid empty node IDs in markdownToFragment structural fragments
1 parent c9114f4 commit 6bf6cea

3 files changed

Lines changed: 57 additions & 2 deletions

File tree

packages/super-editor/src/document-api-adapters/helpers/sd-projection.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -981,8 +981,9 @@ function projectInlineFallback(pmNode: ProseMirrorNode): SDRun {
981981
// Helpers: node ID
982982
// ---------------------------------------------------------------------------
983983

984-
function resolveNodeId(pmNode: ProseMirrorNode): string {
985-
return pmNode.attrs?.sdBlockId ?? '';
984+
function resolveNodeId(pmNode: ProseMirrorNode): string | undefined {
985+
const id = pmNode.attrs?.sdBlockId;
986+
return typeof id === 'string' && id.length > 0 ? id : undefined;
986987
}
987988

988989
// ---------------------------------------------------------------------------

packages/super-editor/src/document-api-adapters/plan-engine/insert-structured-wrapper.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { insertStructuredWrapper } from './plan-wrappers.js';
77
import { registerBuiltInExecutors } from './register-executors.js';
88
import { clearExecutorRegistry } from './executor-registry.js';
99
import { resolveTextTarget } from '../helpers/adapter-utils.js';
10+
import { nodeAllowsSdBlockIdAttr } from '../../extensions/block-node/block-node.js';
1011

1112
let docData: Awaited<ReturnType<typeof loadTestDataForEditorTests>>;
1213

@@ -273,6 +274,26 @@ describe('insertStructuredWrapper — table separators', () => {
273274
}
274275
}
275276
});
277+
278+
it('assigns sdBlockId to all block nodes that support it after markdown table insert', () => {
279+
const result = insertStructuredWrapper(editor, {
280+
value: '| A | B |\n| --- | --- |\n| foo | bar |',
281+
type: 'markdown',
282+
});
283+
expect(result.success).toBe(true);
284+
285+
const missing: Array<{ type: string; pos: number; id: unknown }> = [];
286+
editor.state.doc.descendants((node, pos) => {
287+
if (!nodeAllowsSdBlockIdAttr(node)) return true;
288+
const id = node.attrs?.sdBlockId;
289+
if (typeof id !== 'string' || id.length === 0) {
290+
missing.push({ type: node.type.name, pos, id });
291+
}
292+
return true;
293+
});
294+
295+
expect(missing).toEqual([]);
296+
});
276297
});
277298

278299
describe('insertStructuredWrapper — list numbering rollback', () => {

packages/super-editor/src/document-api-adapters/structural-write-engine/structural-write-engine.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { registerBuiltInExecutors } from '../plan-engine/register-executors.js';
55
import { clearExecutorRegistry } from '../plan-engine/executor-registry.js';
66
import { insertStructuredWrapper, replaceStructuredWrapper } from '../plan-engine/plan-wrappers.js';
77
import { executePlan } from '../plan-engine/executor.js';
8+
import { markdownToFragmentAdapter } from '../markdown-to-fragment-adapter.js';
89
import { executeStructuralInsert, executeStructuralReplace, materializeFragment } from './index.js';
910
import { enforceNestingPolicy } from './nesting-guard.js';
1011
import { validateDocumentFragment } from '@superdoc/document-api';
@@ -250,6 +251,38 @@ describe('replaceStructuredWrapper', () => {
250251
expect(editor.state.doc.textContent).not.toContain('cell data');
251252
});
252253

254+
it('replaces a table block with markdownToFragment output', () => {
255+
const seed = executeStructuralInsert(editor, {
256+
content: {
257+
type: 'table',
258+
rows: [
259+
{
260+
type: 'tableRow',
261+
cells: [{ type: 'tableCell', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'old' }] }] }],
262+
},
263+
],
264+
},
265+
});
266+
const tableBlockId = seed.insertedBlockIds[0]!;
267+
268+
const parsed = markdownToFragmentAdapter(editor, {
269+
markdown: '| Col A | Col B |\n| --- | --- |\n| foo | bar |',
270+
});
271+
272+
// Regression guard: markdown projection must not emit duplicate empty IDs.
273+
expect(() => validateDocumentFragment(parsed.fragment)).not.toThrow();
274+
275+
const result = replaceStructuredWrapper(editor, {
276+
target: { kind: 'text', blockId: tableBlockId, range: { start: 0, end: 0 } },
277+
content: parsed.fragment,
278+
});
279+
280+
expect(result.success).toBe(true);
281+
expect(editor.state.doc.textContent).toContain('foo');
282+
expect(editor.state.doc.textContent).toContain('bar');
283+
expect(editor.state.doc.textContent).not.toContain('old');
284+
});
285+
253286
it('supports dry-run mode without mutating the document', () => {
254287
const seed = executeStructuralInsert(editor, {
255288
content: { type: 'paragraph', content: [{ type: 'text', text: 'keep me' }] },

0 commit comments

Comments
 (0)