// export.service.ts imports the ESM-only @sindresorhus/slugify (not in jest's // transform allowlist). It is irrelevant to the markdown-serialization path under // test (only used for page-mention link slugs on the DB path), so it is mocked // out to keep the module graph loadable under ts-jest (mirrors the import specs). jest.mock('@sindresorhus/slugify', () => ({ __esModule: true, default: (input: string) => String(input), })); import { convertProseMirrorToMarkdown } from '@docmost/prosemirror-markdown'; import { ExportService } from './export.service'; import { ExportFormat } from './dto/export-dto'; /** * STEP 1 golden test for issue #345: server MARKDOWN export runs DIRECTLY through * the canonical converter (`convertProseMirrorToMarkdown`) — no HTML intermediate * and no `@docmost/editor-ext` markdown layer — so the emitted markdown is in the * canonical package forms and is byte-identical to the git-sync vault body. * * These are the goldens the swap has to satisfy: they assert the CANONICAL * surface (callout `> [!type]`, inline footnote `^[…]`, lossless image * ``) rather than the old editor-ext forms (`:::type`, `[^id]`, * lossy `![alt](src)`). * * `exportPage(..., singlePage=false)` takes no DB path (no mention rewriting), so * the service is constructed with null collaborators and only the pure * PM -> Markdown path is exercised. */ function makeService(): ExportService { return new ExportService( null as any, // pageRepo null as any, // pagePermissionRepo null as any, // db null as any, // storageService null as any, // environmentService null as any, // domainService ); } // A representative page exercising the node types whose canonical markdown form // changed with the move off the editor-ext layer: callout, inline footnote, and a // lossless image carrying width/align attrs that the old layer dropped. const REPRESENTATIVE_DOC = { type: 'doc', content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Body ' }, { type: 'footnoteReference', attrs: { id: 'fn-1' } }, { type: 'text', text: ' end.' }, ], }, { type: 'callout', attrs: { type: 'info', icon: null }, content: [ { type: 'paragraph', content: [{ type: 'text', text: 'Heads up' }], }, ], }, { type: 'image', attrs: { src: '/files/pic.png', alt: 'Pic', width: 320, align: 'left', }, }, { type: 'footnotesList', content: [ { type: 'footnoteDefinition', attrs: { id: 'fn-1' }, content: [ { type: 'paragraph', content: [{ type: 'text', text: 'the note' }], }, ], }, ], }, ], }; describe('ExportService — markdown export via the canonical converter (#345)', () => { it('emits canonical callout, inline footnote and lossless image forms', async () => { const service = makeService(); const md = (await service.exportPage(ExportFormat.Markdown, { title: '', content: REPRESENTATIVE_DOC, } as any)) as string; // Callout: Obsidian `> [!type]`, NOT the legacy `:::type`. expect(md).toContain('> [!info]'); expect(md).not.toContain(':::'); // Inline footnote: `^[…]`, NOT the reference `[^id]` form. expect(md).toContain('^[the note]'); expect(md).not.toMatch(/\[\^/); // Lossless image: trailing `` carrying the dropped attrs. expect(md).toContain('![Pic](/files/pic.png)'); expect(md).toContain('