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 `