import { describe, expect, it, vi } from 'vitest' import { blocksToMarkdownDirect, installBlockNoteDirectMarkdown, serializeBlockNoteMarkdown, type DirectMarkdownCapableSerializer, } from './blockNoteDirectMarkdown' import { serializeRichEditorBodyToMarkdown } from './richEditorMarkdown' function makeEditor(document: unknown[]): DirectMarkdownCapableSerializer & { document: unknown[] } { return { document, blocksToMarkdownLossy: vi.fn(() => 'legacy markdown\n'), } } describe('BlockNote direct Markdown serialization', () => { it('serializes common Tolaria BlockNote blocks without the HTML exporter', () => { const blocks = [ { type: 'heading', props: { level: 1 }, content: [{ type: 'text', text: 'Project Alpha', styles: {} }], children: [], }, { type: 'paragraph', content: [ { type: 'text', text: 'See ', styles: {} }, { type: 'wikilink', props: { target: 'Team/Beta|Beta' } }, { type: 'text', text: ' and ', styles: {} }, { type: 'link', props: { href: 'https://example.com' }, content: [{ type: 'text', text: 'docs', styles: {} }] }, { type: 'text', text: '.', styles: {} }, ], children: [], }, { type: 'bulletListItem', content: [{ type: 'text', text: 'Bold item', styles: { bold: true } }], children: [{ type: 'checkListItem', props: { checked: true }, content: [{ type: 'text', text: 'Done', styles: {} }], children: [], }], }, { type: 'codeBlock', props: { language: 'ts' }, content: [{ type: 'text', text: 'const x = 1', styles: {} }], children: [], }, { type: 'table', content: { type: 'tableContent', rows: [ { cells: ['Name', 'Status'] }, { cells: ['Alpha', { content: [{ type: 'text', text: 'Ready', styles: {} }] }] }, ], }, children: [], }, ] expect(blocksToMarkdownDirect(blocks).markdown).toBe([ '# Project Alpha', '', 'See [[Team/Beta|Beta]] and [docs](https://example.com).', '', '- **Bold item**', ' - [x] Done', '', '```ts', 'const x = 1', '```', '', '| Name | Status |', '| --- | --- |', '| Alpha | Ready |', ].join('\n')) }) it('falls back to BlockNote legacy Markdown for unsupported block types', () => { const editor = makeEditor([{ type: 'unsupportedWidget', children: [] }]) installBlockNoteDirectMarkdown(editor) expect(serializeBlockNoteMarkdown(editor, editor.document)).toBe('legacy markdown\n') expect(editor.blocksToMarkdownLossy).toHaveBeenCalledWith(editor.document) expect(editor.__tolariaLastDirectMarkdownMetrics?.fallbackReason).toBe('unsupported:unsupportedWidget') }) it('escapes literal Markdown syntax without double-escaping table pipes', () => { const blocks = [ { type: 'paragraph', content: [{ type: 'text', text: 'Literal *stars* `ticks` #tag!', styles: {} }], children: [], }, { type: 'table', content: { type: 'tableContent', rows: [ { cells: [{ content: [{ type: 'text', text: 'A|B', styles: {} }] }] }, ], }, children: [], }, ] expect(blocksToMarkdownDirect(blocks).markdown).toBe([ 'Literal \\*stars\\* \\`ticks\\` \\#tag\\!', '', '| A\\|B |', '| --- |', ].join('\n')) }) it('caches unchanged block objects across rich-editor body serialization', () => { const block = { type: 'paragraph', content: [{ type: 'text', text: 'Keep [[Project Alpha]] fast.', styles: {} }], children: [], } const editor = makeEditor([block]) installBlockNoteDirectMarkdown(editor) expect(serializeRichEditorBodyToMarkdown(editor as never)).toBe('Keep [[Project Alpha]] fast.\n') expect(serializeRichEditorBodyToMarkdown(editor as never)).toBe('Keep [[Project Alpha]] fast.\n') expect(editor.blocksToMarkdownLossy).not.toHaveBeenCalled() expect(editor.__tolariaLastDirectMarkdownMetrics?.cacheHits).toBeGreaterThan(0) }) it('keeps ordered-list numbering correct when cached blocks are reused in different positions', () => { const item = { type: 'numberedListItem', content: [{ type: 'text', text: 'Step', styles: {} }], children: [], } const cache = new WeakMap>() expect(blocksToMarkdownDirect([item], cache).markdown).toBe('1. Step') expect(blocksToMarkdownDirect([item, item], cache).markdown).toBe('1. Step\n\n2. Step') }) it('resets nested ordered-list numbering for separate parent list items', () => { const blocks = [ { type: 'bulletListItem', content: [{ type: 'text', text: 'First parent', styles: {} }], children: [ { type: 'numberedListItem', content: [{ type: 'text', text: 'First child step', styles: {} }], children: [], }, { type: 'numberedListItem', content: [{ type: 'text', text: 'Second child step', styles: {} }], children: [], }, ], }, { type: 'bulletListItem', content: [{ type: 'text', text: 'Second parent', styles: {} }], children: [ { type: 'numberedListItem', content: [{ type: 'text', text: 'Fresh child step', styles: {} }], children: [], }, ], }, ] expect(blocksToMarkdownDirect(blocks).markdown).toBe([ '- First parent', ' 1. First child step', '', ' 2. Second child step', '', '- Second parent', ' 1. Fresh child step', ].join('\n')) }) })