From f9b155ba6e64bfdd0df42dfcf0164f2d2cd04178 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 29 Apr 2026 05:27:39 +0200 Subject: [PATCH] fix: repair malformed editor block entries --- src/hooks/editorBlockRepair.ts | 11 ++++++- src/hooks/useEditorTabSwap.test.ts | 49 ++++++++++++++++++++++++++++++ src/hooks/useEditorTabSwap.ts | 3 +- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/hooks/editorBlockRepair.ts b/src/hooks/editorBlockRepair.ts index 5dbc30f8..2310c068 100644 --- a/src/hooks/editorBlockRepair.ts +++ b/src/hooks/editorBlockRepair.ts @@ -16,8 +16,17 @@ function hasUsableBlockId(block: Record): boolean { return typeof block.id === 'string' && block.id.trim().length > 0 } +function fallbackParagraphBlock(): Record { + return { + id: createEditorBlockId(), + type: 'paragraph', + content: [], + children: [], + } +} + function repairEditorBlock(block: unknown): unknown { - if (!isEditorBlockRecord(block)) return block + if (!isEditorBlockRecord(block)) return fallbackParagraphBlock() const children = Array.isArray(block.children) ? repairMalformedEditorBlocks(block.children) diff --git a/src/hooks/useEditorTabSwap.test.ts b/src/hooks/useEditorTabSwap.test.ts index ae4fed44..762ba5e8 100644 --- a/src/hooks/useEditorTabSwap.test.ts +++ b/src/hooks/useEditorTabSwap.test.ts @@ -542,6 +542,55 @@ describe('useEditorTabSwap raw mode sync', () => { ]) }) + it('replaces non-object malformed parsed blocks before applying them to the editor', async () => { + const tabA = makeTab('a.md', 'Note A') + const malformedTab = { + ...makeTab('malformed.md', 'Malformed'), + content: '---\ntitle: Malformed\n---\n\n# Malformed\n\nRecovered body.', + } + + const { mockEditor, rerenderWith } = await createSwapHarness({ + initialProps: { tabs: [tabA], activeTabPath: 'a.md', rawMode: false }, + setupEditor: (editor) => { + editor.tryParseMarkdownToBlocks.mockReturnValue([ + null, + 'dangling content', + { + type: 'paragraph', + content: [{ type: 'text', text: 'Parent', styles: {} }], + children: [ + undefined, + { + type: 'paragraph', + content: [{ type: 'text', text: 'Child', styles: {} }], + children: [], + }, + ], + }, + ]) + }, + }) + mockEditor.replaceBlocks.mockClear() + + await rerenderWith({ tabs: [malformedTab], activeTabPath: 'malformed.md' }) + + const appliedBlocks = mockEditor.replaceBlocks.mock.calls[0][1] + expect(appliedBlocks).toEqual([ + expect.objectContaining({ id: expect.any(String), type: 'paragraph', content: [], children: [] }), + expect.objectContaining({ id: expect.any(String), type: 'paragraph', content: [], children: [] }), + expect.objectContaining({ + id: expect.any(String), + children: [ + expect.objectContaining({ id: expect.any(String), type: 'paragraph', content: [], children: [] }), + expect.objectContaining({ + id: expect.any(String), + content: [{ type: 'text', text: 'Child', styles: {} }], + }), + ], + }), + ]) + }) + it('ignores editor change events before the pending tab swap applies a new untitled note', async () => { vi.spyOn(document, 'querySelector').mockReturnValue({ scrollTop: 0 } as unknown as Element) vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { cb(0); return 0 }) diff --git a/src/hooks/useEditorTabSwap.ts b/src/hooks/useEditorTabSwap.ts index e56578e4..277b649b 100644 --- a/src/hooks/useEditorTabSwap.ts +++ b/src/hooks/useEditorTabSwap.ts @@ -174,8 +174,9 @@ async function resolveBlocksForTarget( } const parsed = normalizeParsedImageBlocks(await parseMarkdownBlocks(editor, preprocessed)) as EditorBlocks + const parseSafeBlocks = repairMalformedEditorBlocks(parsed) as EditorBlocks const nextState = { - blocks: repairMalformedEditorBlocks(injectEditorMarkdownBlocks(parsed)) as EditorBlocks, + blocks: repairMalformedEditorBlocks(injectEditorMarkdownBlocks(parseSafeBlocks)) as EditorBlocks, scrollTop: 0, sourceContent: content, }