diff --git a/src/hooks/useEditorTabSwap.test.ts b/src/hooks/useEditorTabSwap.test.ts new file mode 100644 index 00000000..58d6e98c --- /dev/null +++ b/src/hooks/useEditorTabSwap.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest' +import { extractEditorBody } from './useEditorTabSwap' + +describe('extractEditorBody', () => { + it('strips frontmatter and title heading from new note content (double newline)', () => { + const content = '---\ntitle: Untitled note\ntype: Note\nstatus: Active\n---\n\n# Untitled note\n\n' + expect(extractEditorBody(content)).toBe('') + }) + + it('strips frontmatter and title heading from content with single newline', () => { + const content = '---\ntitle: Test\n---\n# Test\n\nBody text here.' + expect(extractEditorBody(content)).toBe('Body text here.') + }) + + it('preserves body content after the heading', () => { + const content = '---\ntitle: My Note\ntype: Note\n---\n\n# My Note\n\nFirst paragraph.\n\nSecond paragraph.' + expect(extractEditorBody(content)).toBe('First paragraph.\n\nSecond paragraph.') + }) + + it('handles content without frontmatter', () => { + const content = '# Just a Heading\n\nSome body text.' + expect(extractEditorBody(content)).toBe('Some body text.') + }) + + it('handles content without frontmatter or heading', () => { + const content = 'Just plain text.' + expect(extractEditorBody(content)).toBe('Just plain text.') + }) + + it('handles completely empty content', () => { + expect(extractEditorBody('')).toBe('') + }) + + it('handles frontmatter-only content', () => { + const content = '---\ntitle: Empty\n---\n' + expect(extractEditorBody(content)).toBe('') + }) + + it('preserves wikilinks in body', () => { + const content = '---\ntitle: Test\n---\n\n# Test\n\nSee [[Other Note]] for details.' + expect(extractEditorBody(content)).toBe('See [[Other Note]] for details.') + }) + + it('does not strip heading that is not at the start of body', () => { + const content = '---\ntitle: Test\n---\n\nSome intro text.\n\n# A Heading\n\nMore text.' + expect(extractEditorBody(content)).toBe('Some intro text.\n\n# A Heading\n\nMore text.') + }) + + it('returns empty for buildNoteContent output', () => { + // Exactly what buildNoteContent('My Project', 'Project', 'Active') produces + const content = '---\ntitle: My Project\ntype: Project\nstatus: Active\n---\n\n# My Project\n\n' + expect(extractEditorBody(content)).toBe('') + }) +}) diff --git a/src/hooks/useEditorTabSwap.ts b/src/hooks/useEditorTabSwap.ts index 9f08fde3..dc1f6372 100644 --- a/src/hooks/useEditorTabSwap.ts +++ b/src/hooks/useEditorTabSwap.ts @@ -15,6 +15,13 @@ interface UseEditorTabSwapOptions { onContentChange?: (path: string, content: string) => void } +/** Strip the YAML frontmatter and the title heading (# ...) from raw file + * content, returning only the body that should appear in the editor. */ +export function extractEditorBody(rawFileContent: string): string { + const [, rawBody] = splitFrontmatter(rawFileContent) + return rawBody.trimStart().replace(/^# [^\n]*\n?/, '').trimStart() +} + /** * Manages the tab content-swap machinery for the BlockNote editor. * @@ -151,10 +158,19 @@ export function useEditorTabSwap({ tabs, activeTabPath, editor, onContentChange return } - const [, rawBody] = splitFrontmatter(tab.content) - const body = rawBody.replace(/^# [^\n]*\n?/, '').trimStart() + const body = extractEditorBody(tab.content) const preprocessed = preProcessWikilinks(body) + // Fast path: empty body (e.g. newly created notes). Skip the + // potentially-async markdown parser and set a single empty paragraph + // so the editor is immediately interactive. + if (!preprocessed.trim()) { + const emptyDoc = [{ type: 'paragraph', content: [] }] + cache.set(targetPath, emptyDoc) + applyBlocks(emptyDoc) + return + } + try { const result = editor.tryParseMarkdownToBlocks(preprocessed) // eslint-disable-next-line @typescript-eslint/no-explicit-any -- BlockNote block arrays