feat(core): support Notion markdown zip imports (#14910)

## What changed
- Markdown zip imports now resolve local `.md` links like `./test/2.md`
into AFFiNE linked-page references when the target document exists in
the same archive.
- Added Notion Markdown `.zip` import support in the desktop import
dialog, including nested zip traversal, Notion title extraction,
hash-stripped folder names, attachments, and folder hierarchy
integration.
- Added i18n entries and adapter coverage for standard Markdown zip
links and Notion Markdown zip imports.

## Why
Markdown and Notion exports often contain links between notes using
relative `.md` paths. Keeping those as plain URLs makes imported
workspaces harder to navigate, so the importer now preassigns document
ids and rewrites resolvable archive-local markdown links into linked
pages.

## Notes
The markdown zip folder hierarchy implementation now comes from latest
`origin/canary`, so this PR only layers relative-link resolution and
Notion Markdown zip support on top of that upstream behavior.

## Validation
- `yarn vitest --run
blocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.ts`
- `yarn tsc -b blocksuite/affine/all/tsconfig.json --verbose`
- `git diff --check`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for importing Notion Markdown exports in `.zip` format,
including subpages, attachments, and nested folders.
* Internal links inside imported Markdown now resolve correctly between
pages, preserving link text when available.
* The import dialog now includes a dedicated “Notion (Markdown, .zip)”
option.

* **Bug Fixes**
* Improved filename handling so non-Latin characters in ZIP imports are
preserved correctly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
Jachin
2026-07-04 16:42:48 -04:00
committed by GitHub
parent 8063af2da8
commit 477015f064
7 changed files with 725 additions and 52 deletions
@@ -231,6 +231,7 @@ type ImportType =
| 'markdown'
| 'markdownZip'
| 'notion'
| 'notionMarkdown'
| 'obsidian'
| 'bear'
| 'snapshot'
@@ -318,6 +319,17 @@ const importOptions = [
testId: 'editor-option-menu-import-notion',
type: 'notion' as ImportType,
},
{
key: 'notionMarkdown',
label: 'com.affine.import.notion-markdown',
prefixIcon: <NotionIcon color={cssVar('black')} width={20} height={20} />,
suffixIcon: (
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
),
suffixTooltip: 'com.affine.import.notion-markdown.tooltip',
testId: 'editor-option-menu-import-notion-markdown',
type: 'notionMarkdown' as ImportType,
},
{
key: 'obsidian',
label: 'com.affine.import.obsidian',
@@ -510,6 +522,42 @@ const importConfigs: Record<ImportType, ImportConfig> = {
};
},
},
notionMarkdown: {
fileOptions: { acceptType: 'Zip', multiple: false },
importFunction: async (
docCollection,
files,
_handleImportAffineFile,
organizeService,
explorerIconService
) => {
const file = files.length === 1 ? files[0] : null;
if (!file) {
throw new Error('Expected a single zip file for notionMarkdown import');
}
const { docIds, folderHierarchy } =
await MarkdownTransformer.importNotionMarkdownZip({
collection: docCollection,
schema: getAFFiNEWorkspaceSchema(),
imported: file,
extensions: getStoreManager().config.init().value.get('store'),
});
const rootFolderId =
folderHierarchy && organizeService
? applyFolderHierarchy(
organizeService,
folderHierarchy,
explorerIconService
)
: undefined;
return {
docIds,
rootFolderId,
};
},
},
obsidian: {
fileOptions: { acceptType: 'Directory', multiple: false },
importFunction: async (