mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
fix(editor): support relative image reference path when importing zip with images (#12264)
Closes: [BS-3385](https://linear.app/affine-design/issue/BS-3385/markdown类型的导入,支持media文件和md文件不在同目录的情况) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added utility functions to resolve and normalize image file paths in markdown and HTML imports. - Introduced middleware to provide full file path context during file import and transformation. - Added new types for improved asset and file management in zip imports. - **Refactor** - Centralized and simplified image processing logic across HTML, Markdown, and Notion HTML adapters for improved maintainability. - Enhanced type safety and clarity in file and asset handling during zip imports. - **Tests** - Added comprehensive tests for image file path resolution utility. - **Documentation** - Improved inline code comments explaining file path resolution logic. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddleware,
|
||||
fileNameMiddleware,
|
||||
filePathMiddleware,
|
||||
HtmlAdapter,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
@@ -15,6 +16,7 @@ import type {
|
||||
} from '@blocksuite/store';
|
||||
import { extMimeMap, Transformer } from '@blocksuite/store';
|
||||
|
||||
import type { AssetMap, ImportedFileEntry, PathBlobIdMap } from './type.js';
|
||||
import { createAssetsArchive, download, Unzip } from './utils.js';
|
||||
|
||||
type ImportHTMLToDocOptions = {
|
||||
@@ -143,9 +145,9 @@ async function importHTMLZip({
|
||||
await unzip.load(imported);
|
||||
|
||||
const docIds: string[] = [];
|
||||
const pendingAssets = new Map<string, File>();
|
||||
const pendingPathBlobIdMap = new Map<string, string>();
|
||||
const htmlBlobs: [string, Blob][] = [];
|
||||
const pendingAssets: AssetMap = new Map();
|
||||
const pendingPathBlobIdMap: PathBlobIdMap = new Map();
|
||||
const htmlBlobs: ImportedFileEntry[] = [];
|
||||
|
||||
for (const { path, content: blob } of unzip) {
|
||||
if (path.includes('__MACOSX') || path.includes('.DS_Store')) {
|
||||
@@ -154,7 +156,11 @@ async function importHTMLZip({
|
||||
|
||||
const fileName = path.split('/').pop() ?? '';
|
||||
if (fileName.endsWith('.html')) {
|
||||
htmlBlobs.push([fileName, blob]);
|
||||
htmlBlobs.push({
|
||||
filename: fileName,
|
||||
contentBlob: blob,
|
||||
fullPath: path,
|
||||
});
|
||||
} else {
|
||||
const ext = path.split('.').at(-1) ?? '';
|
||||
const mime = extMimeMap.get(ext) ?? '';
|
||||
@@ -165,8 +171,9 @@ async function importHTMLZip({
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
htmlBlobs.map(async ([fileName, blob]) => {
|
||||
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
|
||||
htmlBlobs.map(async htmlFile => {
|
||||
const { filename, contentBlob, fullPath } = htmlFile;
|
||||
const fileNameWithoutExt = filename.replace(/\.[^/.]+$/, '');
|
||||
const job = new Transformer({
|
||||
schema,
|
||||
blobCRUD: collection.blobSync,
|
||||
@@ -179,18 +186,19 @@ async function importHTMLZip({
|
||||
defaultImageProxyMiddleware,
|
||||
fileNameMiddleware(fileNameWithoutExt),
|
||||
docLinkBaseURLMiddleware(collection.id),
|
||||
filePathMiddleware(fullPath),
|
||||
],
|
||||
});
|
||||
const assets = job.assets;
|
||||
const pathBlobIdMap = job.assetsManager.getPathBlobIdMap();
|
||||
for (const [key, value] of pendingAssets.entries()) {
|
||||
assets.set(key, value);
|
||||
}
|
||||
for (const [key, value] of pendingPathBlobIdMap.entries()) {
|
||||
pathBlobIdMap.set(key, value);
|
||||
for (const [assetPath, key] of pendingPathBlobIdMap.entries()) {
|
||||
pathBlobIdMap.set(assetPath, key);
|
||||
if (pendingAssets.get(key)) {
|
||||
assets.set(key, pendingAssets.get(key)!);
|
||||
}
|
||||
}
|
||||
const htmlAdapter = new HtmlAdapter(job, provider);
|
||||
const html = await blob.text();
|
||||
const html = await contentBlob.text();
|
||||
const doc = await htmlAdapter.toDoc({
|
||||
file: html,
|
||||
assets: job.assetsManager,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddleware,
|
||||
fileNameMiddleware,
|
||||
filePathMiddleware,
|
||||
MarkdownAdapter,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
@@ -16,6 +17,7 @@ import type {
|
||||
} from '@blocksuite/store';
|
||||
import { extMimeMap, Transformer } from '@blocksuite/store';
|
||||
|
||||
import type { AssetMap, ImportedFileEntry, PathBlobIdMap } from './type.js';
|
||||
import { createAssetsArchive, download, Unzip } from './utils.js';
|
||||
|
||||
function getProvider(extensions: ExtensionType[]) {
|
||||
@@ -196,19 +198,28 @@ async function importMarkdownZip({
|
||||
await unzip.load(imported);
|
||||
|
||||
const docIds: string[] = [];
|
||||
const pendingAssets = new Map<string, File>();
|
||||
const pendingPathBlobIdMap = new Map<string, string>();
|
||||
const markdownBlobs: [string, Blob][] = [];
|
||||
const pendingAssets: AssetMap = new Map();
|
||||
const pendingPathBlobIdMap: PathBlobIdMap = new Map();
|
||||
const markdownBlobs: ImportedFileEntry[] = [];
|
||||
|
||||
// Iterate over all files in the zip
|
||||
for (const { path, content: blob } of unzip) {
|
||||
// Skip the files that are not markdown files
|
||||
if (path.includes('__MACOSX') || path.includes('.DS_Store')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the file name
|
||||
const fileName = path.split('/').pop() ?? '';
|
||||
// If the file is a markdown file, store it to markdownBlobs
|
||||
if (fileName.endsWith('.md')) {
|
||||
markdownBlobs.push([fileName, blob]);
|
||||
markdownBlobs.push({
|
||||
filename: fileName,
|
||||
contentBlob: blob,
|
||||
fullPath: path,
|
||||
});
|
||||
} else {
|
||||
// If the file is not a markdown file, store it to pendingAssets
|
||||
const ext = path.split('.').at(-1) ?? '';
|
||||
const mime = extMimeMap.get(ext) ?? '';
|
||||
const key = await sha(await blob.arrayBuffer());
|
||||
@@ -218,8 +229,9 @@ async function importMarkdownZip({
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
markdownBlobs.map(async ([fileName, blob]) => {
|
||||
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
|
||||
markdownBlobs.map(async markdownFile => {
|
||||
const { filename, contentBlob, fullPath } = markdownFile;
|
||||
const fileNameWithoutExt = filename.replace(/\.[^/.]+$/, '');
|
||||
const job = new Transformer({
|
||||
schema,
|
||||
blobCRUD: collection.blobSync,
|
||||
@@ -232,18 +244,25 @@ async function importMarkdownZip({
|
||||
defaultImageProxyMiddleware,
|
||||
fileNameMiddleware(fileNameWithoutExt),
|
||||
docLinkBaseURLMiddleware(collection.id),
|
||||
filePathMiddleware(fullPath),
|
||||
],
|
||||
});
|
||||
const assets = job.assets;
|
||||
const pathBlobIdMap = job.assetsManager.getPathBlobIdMap();
|
||||
for (const [key, value] of pendingAssets.entries()) {
|
||||
assets.set(key, value);
|
||||
}
|
||||
for (const [key, value] of pendingPathBlobIdMap.entries()) {
|
||||
pathBlobIdMap.set(key, value);
|
||||
// Iterate over all assets to be imported
|
||||
for (const [assetPath, key] of pendingPathBlobIdMap.entries()) {
|
||||
// Get the relative path of the asset to the markdown file
|
||||
// Store the path to blobId map
|
||||
pathBlobIdMap.set(assetPath, key);
|
||||
// Store the asset to assets, the key is the blobId, the value is the file object
|
||||
// In block adapter, it will use the blobId to get the file object
|
||||
if (pendingAssets.get(key)) {
|
||||
assets.set(key, pendingAssets.get(key)!);
|
||||
}
|
||||
}
|
||||
|
||||
const mdAdapter = new MarkdownAdapter(job, provider);
|
||||
const markdown = await blob.text();
|
||||
const markdown = await contentBlob.text();
|
||||
const doc = await mdAdapter.toDoc({
|
||||
file: markdown,
|
||||
assets: job.assetsManager,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Represents an imported file entry in the zip archive
|
||||
*/
|
||||
export type ImportedFileEntry = {
|
||||
/** The filename of the file (e.g. "document.md", "document.html") */
|
||||
filename: string;
|
||||
/** The blob containing the file content */
|
||||
contentBlob: Blob;
|
||||
/** The full path of the file in the zip archive */
|
||||
fullPath: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of asset hash to File object for all media files in the zip
|
||||
* Key: SHA hash of the file content (blobId)
|
||||
* Value: File object containing the actual media data
|
||||
*/
|
||||
export type AssetMap = Map<string, File>;
|
||||
|
||||
/**
|
||||
* Map of file paths to their corresponding asset hashes
|
||||
* Key: Original file path in the zip
|
||||
* Value: SHA hash of the file content (blobId)
|
||||
*/
|
||||
export type PathBlobIdMap = Map<string, string>;
|
||||
Reference in New Issue
Block a user