diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts index 89634a28c8..eb35be71e6 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/common.ts @@ -6,6 +6,7 @@ import { import { AIChatBlockSpec } from '@affine/core/blocksuite/presets/blocks/ai-chat-block'; import type { ExtensionType } from '@blocksuite/affine/block-std'; import { + AdapterFactoryExtensions, BookmarkBlockSpec, CodeBlockSpec, DatabaseBlockSpec, @@ -48,6 +49,7 @@ const CommonBlockSpecs: ExtensionType[] = [ EmbedLinkedDocBlockSpec, // special CustomAttachmentBlockSpec, + AdapterFactoryExtensions, ].flat(); export const DefaultBlockSpecs: ExtensionType[] = [ diff --git a/packages/frontend/core/src/components/hooks/affine/use-export-page.ts b/packages/frontend/core/src/components/hooks/affine/use-export-page.ts index 090efdebf4..8a38e4811a 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-export-page.ts @@ -6,15 +6,23 @@ import { import { EditorService } from '@affine/core/modules/editor'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; +import type { BlockStdScope } from '@blocksuite/affine/block-std'; import { + createAssetsArchive, + docLinkBaseURLMiddleware, + download, + embedSyncedDocMiddleware, ExportManager, + HtmlAdapterFactoryIdentifier, HtmlTransformer, + MarkdownAdapterFactoryIdentifier, MarkdownTransformer, printToPdf, + titleMiddleware, ZipTransformer, } from '@blocksuite/affine/blocks'; import type { AffineEditorContainer } from '@blocksuite/affine/presets'; -import type { Doc } from '@blocksuite/affine/store'; +import { type Doc, Job } from '@blocksuite/affine/store'; import { useLiveData, useService } from '@toeverything/infra'; import { useSetAtom } from 'jotai'; import { nanoid } from 'nanoid'; @@ -29,6 +37,90 @@ interface ExportHandlerOptions { type: ExportType; } +interface AdapterResult { + file: string; + assetsIds: string[]; +} + +type AdapterFactoryIdentifier = + | typeof HtmlAdapterFactoryIdentifier + | typeof MarkdownAdapterFactoryIdentifier; + +interface AdapterConfig { + identifier: AdapterFactoryIdentifier; + fileExtension: string; // file extension need to be lower case with dot prefix, e.g. '.md', '.txt', '.html' + contentType: string; + indexFileName: string; +} + +async function exportDoc(doc: Doc, std: BlockStdScope, config: AdapterConfig) { + const job = new Job({ + collection: doc.collection, + middlewares: [ + docLinkBaseURLMiddleware, + titleMiddleware, + embedSyncedDocMiddleware('content'), + ], + }); + + const adapterFactory = std.provider.get(config.identifier); + const adapter = adapterFactory.get(job); + const result = (await adapter.fromDoc(doc)) as AdapterResult; + + if (!result || (!result.file && !result.assetsIds.length)) { + return; + } + + const docTitle = doc.meta?.title || 'Untitled'; + const contentBlob = new Blob([result.file], { type: config.contentType }); + + let downloadBlob: Blob; + let name: string; + + if (result.assetsIds.length > 0) { + if (!job.assets) { + throw new Error('No assets found'); + } + const zip = await createAssetsArchive(job.assets, result.assetsIds); + await zip.file(config.indexFileName, contentBlob); + downloadBlob = await zip.generate(); + name = `${docTitle}.zip`; + } else { + downloadBlob = contentBlob; + name = `${docTitle}${config.fileExtension}`; + } + + download(downloadBlob, name); +} + +async function exportToHtml(doc: Doc, std?: BlockStdScope) { + if (!std) { + // If std is not provided, we use the default export method + await HtmlTransformer.exportDoc(doc); + } else { + await exportDoc(doc, std, { + identifier: HtmlAdapterFactoryIdentifier, + fileExtension: '.html', + contentType: 'text/html', + indexFileName: 'index.html', + }); + } +} + +async function exportToMarkdown(doc: Doc, std?: BlockStdScope) { + if (!std) { + // If std is not provided, we use the default export method + await MarkdownTransformer.exportDoc(doc); + } else { + await exportDoc(doc, std, { + identifier: MarkdownAdapterFactoryIdentifier, + fileExtension: '.md', + contentType: 'text/plain', + indexFileName: 'index.md', + }); + } +} + async function exportHandler({ page, type, @@ -40,10 +132,10 @@ async function exportHandler({ }); switch (type) { case 'html': - await HtmlTransformer.exportDoc(page); + await exportToHtml(page, editorRoot?.std); return; case 'markdown': - await MarkdownTransformer.exportDoc(page); + await exportToMarkdown(page, editorRoot?.std); return; case 'snapshot': await ZipTransformer.exportDocs(page.collection, [page]);