hololake-platform/src/utils/blockNoteDirectMarkdown.test.ts

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'))
})
})