202 lines
6.2 KiB
TypeScript
202 lines
6.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
blocksToMarkdownDirect,
|
|
installBlockNoteDirectMarkdown,
|
|
serializeBlockNoteMarkdown,
|
|
type DirectMarkdownCapableSerializer,
|
|
} from './blockNoteDirectMarkdown'
|
|
import { TOLARIA_CALLOUT_BLOCK_TYPE } from './calloutMarkdown'
|
|
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('serializes Tolaria callout blocks as portable Markdown callouts', () => {
|
|
expect(blocksToMarkdownDirect([
|
|
{
|
|
type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
|
props: { kind: 'success' },
|
|
content: [{ type: 'text', text: 'Package is ready', styles: {} }],
|
|
children: [],
|
|
},
|
|
]).markdown).toBe('> [!success] Package is ready')
|
|
})
|
|
|
|
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<object, Map<string, string>>()
|
|
|
|
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'))
|
|
})
|
|
})
|