From 4b37f9d42ec99147e2fab23c24746165301210a7 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:54:05 +0800 Subject: [PATCH] feat(editor): improve obsidian import (#15304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #15290 #### PR Dependency Tree * **PR #15304** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit * **New Features** * Improved Obsidian vault imports with more reliable attachment and embedded image matching. * Supports nested vault structures and configured attachment folders. * Preserves imported folder hierarchy and organizes imported content more accurately. * **Bug Fixes** * Fixed asset resolution for attachments with nested paths or duplicate filenames. --- .../__tests__/adapters/markdown.unit.spec.ts | 54 ++++++ .../linked-doc/src/transformers/obsidian.ts | 174 ++++++++++++++++-- .../src/modules/import/services/service.ts | 1 + 3 files changed, 216 insertions(+), 13 deletions(-) diff --git a/blocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.ts b/blocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.ts index 882291b105..8a3cded89c 100644 --- a/blocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.ts +++ b/blocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.ts @@ -826,6 +826,60 @@ Hello world .sort((a, b) => (a ?? '').localeCompare(b ?? '')), entry: snapshotDocByTitle(collection, 'entry', titleById), }).toMatchSnapshot(); + + const nestedCollection = new TestWorkspace(); + nestedCollection.storeExtensions = testStoreExtensions; + nestedCollection.meta.initialize(); + const nestedPlan = await ObsidianTransformer.planObsidianVault({ + collection: nestedCollection, + schema, + importedFiles: [ + withRelativePath( + new File(['![[logo.png]]'], 'entry.md', { + type: 'text/markdown', + }), + 'vault/notes/entry.md' + ), + withRelativePath( + new File( + [JSON.stringify({ attachmentFolderPath: 'attachments' })], + 'app.json', + { type: 'application/json' } + ), + 'vault/.obsidian/app.json' + ), + withRelativePath( + new File([new Uint8Array([137, 80, 78, 71])], 'logo.png', { + type: 'image/png', + }), + 'vault/attachments/logo.png' + ), + withRelativePath( + new File([new Uint8Array([137, 80, 78, 72])], 'logo.png', { + type: 'image/png', + }), + 'vault/other/logo.png' + ), + ], + extensions: testStoreExtensions, + }); + const configuredLogo = nestedPlan.batch.blobs.find( + blob => blob.sourcePath === 'vault/attachments/logo.png' + ); + expect(configuredLogo).toBeTruthy(); + expect(JSON.stringify(nestedPlan.batch.docs)).toContain( + configuredLogo!.blobId + ); + expect(nestedPlan.batch.blobs).toHaveLength(2); + expect(nestedPlan.batch.folders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ path: 'notes', name: 'notes' }), + expect.objectContaining({ + parentPath: 'notes', + pageId: nestedPlan.docIds[0], + }), + ]) + ); }); test('imports notion html zip golden baseline', async () => { diff --git a/blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts b/blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts index 1ce78a4808..f6dbd46383 100644 --- a/blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts +++ b/blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts @@ -23,6 +23,7 @@ import { blobsFromAssets, type ImportBatch, type ImportDoc, + type ImportFolder, } from './import-batch.js'; import { bindImportedAssetsToJob, @@ -397,6 +398,70 @@ type ObsidianAttachmentEmbed = { fileType: string; }; +type ObsidianAssetLookup = { + resolve: ( + targetPath: string, + currentFilePath: string + ) => { blobId: string; path: string } | null; +}; + +function createAssetLookup( + pathBlobIdMap: ReadonlyMap, + vaultRoot: string, + attachmentFolderPath?: string +): ObsidianAssetLookup { + const exact = new Map(); + const byBasename = new Map< + string, + { blobId: string; path: string } | typeof AMBIGUOUS_PAGE_LOOKUP + >(); + + for (const [path, blobId] of pathBlobIdMap) { + const entry = { blobId, path }; + exact.set(normalizeLookupKey(path), entry); + const name = normalizeLookupKey(basename(path)); + const existing = byBasename.get(name); + if (!existing) { + byBasename.set(name, entry); + } else if ( + existing !== AMBIGUOUS_PAGE_LOOKUP && + existing.blobId !== blobId + ) { + byBasename.set(name, AMBIGUOUS_PAGE_LOOKUP); + } + } + + return { + resolve(targetPath, currentFilePath) { + const normalizedTarget = normalizeFilePathReference(targetPath); + const rootPath = vaultRoot + ? `${vaultRoot}/${normalizedTarget}` + : normalizedTarget; + const candidates = [ + getImageFullPath(currentFilePath, normalizedTarget), + rootPath, + ]; + if (attachmentFolderPath) { + candidates.push( + vaultRoot + ? `${vaultRoot}/${attachmentFolderPath}/${basename(normalizedTarget)}` + : `${attachmentFolderPath}/${basename(normalizedTarget)}` + ); + } + + for (const candidate of candidates) { + const resolved = exact.get(normalizeLookupKey(candidate)); + if (resolved) return resolved; + } + + const match = byBasename.get( + normalizeLookupKey(basename(normalizedTarget)) + ); + return match && match !== AMBIGUOUS_PAGE_LOOKUP ? match : null; + }, + }; +} + function createObsidianAttach(embed: ObsidianAttachmentEmbed): string { return `