mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +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:
@@ -4,9 +4,9 @@ import remarkParse from 'remark-parse';
|
||||
import { unified } from 'unified';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { remarkGfm } from '../../../adapters/markdown/gfm';
|
||||
import { remarkCallout } from '../../../adapters/markdown/remark-plugins';
|
||||
import type { MarkdownAST } from '../../../adapters/markdown/type';
|
||||
import { remarkGfm } from '../../../../adapters/markdown/gfm';
|
||||
import { remarkCallout } from '../../../../adapters/markdown/remark-plugins/remark-callout';
|
||||
import type { MarkdownAST } from '../../../../adapters/markdown/type';
|
||||
|
||||
describe('remarkCallout plugin', () => {
|
||||
function isBlockQuote(node: MarkdownAST): node is Blockquote {
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getImageFullPath } from '../../../adapters/utils/file-path';
|
||||
|
||||
describe('getImageFullPath', () => {
|
||||
it('should resolve relative image paths correctly', () => {
|
||||
const filePath = 'path/to/markdown/file.md';
|
||||
|
||||
// Test relative path in same directory
|
||||
expect(getImageFullPath(filePath, 'image.png')).toBe(
|
||||
'path/to/markdown/image.png'
|
||||
);
|
||||
|
||||
// Test relative path in subdirectory
|
||||
expect(getImageFullPath(filePath, 'images/photo.jpg')).toBe(
|
||||
'path/to/markdown/images/photo.jpg'
|
||||
);
|
||||
|
||||
// Test relative path in subdirectory
|
||||
expect(getImageFullPath(filePath, './images/photo.jpg')).toBe(
|
||||
'path/to/markdown/images/photo.jpg'
|
||||
);
|
||||
|
||||
// Test relative path with parent directory
|
||||
expect(getImageFullPath(filePath, '../images/photo.jpg')).toBe(
|
||||
'path/to/images/photo.jpg'
|
||||
);
|
||||
|
||||
// Test relative path with multiple parent directories
|
||||
expect(getImageFullPath(filePath, '../../images/photo.jpg')).toBe(
|
||||
'path/images/photo.jpg'
|
||||
);
|
||||
|
||||
// Test relative path with multiple parent directories (which is not supported)
|
||||
expect(getImageFullPath(filePath, '../../../../images/photo.jpg')).toBe(
|
||||
'images/photo.jpg'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle absolute image paths correctly', () => {
|
||||
const filePath = 'path/to/markdown/file.md';
|
||||
|
||||
// Test absolute path
|
||||
expect(getImageFullPath(filePath, '/images/photo.jpg')).toBe(
|
||||
'images/photo.jpg'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle URL-encoded image paths correctly', () => {
|
||||
const filePath = 'path/to/markdown/file.md';
|
||||
|
||||
// Test URL-encoded spaces
|
||||
expect(getImageFullPath(filePath, 'my%20photo.jpg')).toBe(
|
||||
'path/to/markdown/my photo.jpg'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const FULL_FILE_PATH_KEY = 'fullFilePath';
|
||||
|
||||
/**
|
||||
* Middleware to set the full file path of the imported file
|
||||
* @param filePath - The full file path of the imported file
|
||||
* @returns A TransformerMiddleware that sets the full file path of the imported file
|
||||
*/
|
||||
export const filePathMiddleware = (filePath: string): TransformerMiddleware => {
|
||||
return ({ adapterConfigs }) => {
|
||||
adapterConfigs.set(FULL_FILE_PATH_KEY, filePath);
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ export * from './code';
|
||||
export * from './copy';
|
||||
export * from './doc-link';
|
||||
export * from './file-name';
|
||||
export * from './file-path';
|
||||
export * from './paste';
|
||||
export * from './proxy';
|
||||
export * from './replace-id';
|
||||
|
||||
56
blocksuite/affine/shared/src/adapters/utils/file-path.ts
Normal file
56
blocksuite/affine/shared/src/adapters/utils/file-path.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Normalizes a relative path by resolving all relative path segments
|
||||
* @param basePath The base path (markdown file's directory)
|
||||
* @param relativePath The relative path to normalize
|
||||
* @returns The full path
|
||||
*/
|
||||
function resolveFullPath(basePath: string, relativePath: string): string {
|
||||
// Split both paths into segments
|
||||
const baseSegments = basePath.split('/').filter(Boolean);
|
||||
const relativeSegments = relativePath.split('/').filter(Boolean);
|
||||
|
||||
// Handle each segment of the relative path
|
||||
for (const segment of relativeSegments) {
|
||||
if (segment === '.') {
|
||||
// Current directory, do nothing
|
||||
continue;
|
||||
} else if (segment === '..') {
|
||||
// Parent directory, remove last segment from base
|
||||
if (baseSegments.length > 0) {
|
||||
baseSegments.pop();
|
||||
}
|
||||
} else {
|
||||
// Regular directory or file, add to base
|
||||
baseSegments.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// Join segments back into a path
|
||||
return baseSegments.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path of the reference image from the file path and the image reference
|
||||
* @param filePath The full path of the file containing the image reference
|
||||
* @param imageReference The image reference from the file (can be relative or absolute path)
|
||||
* @returns The full path of the reference image
|
||||
*/
|
||||
export function getImageFullPath(
|
||||
filePath: string,
|
||||
imageReference: string
|
||||
): string {
|
||||
// Decode the image reference in case it contains URL-encoded characters
|
||||
const decodedReference = decodeURIComponent(imageReference);
|
||||
|
||||
// Get the directory of the file path
|
||||
const markdownDir = filePath.substring(0, filePath.lastIndexOf('/'));
|
||||
|
||||
// Check if the image reference is a relative path
|
||||
const isRelative = !decodedReference.startsWith('/');
|
||||
|
||||
// If the image reference is a relative path, resolve it against the file path's directory
|
||||
// Otherwise, it is an absolute path, remove the leading slash if it exists
|
||||
return isRelative
|
||||
? resolveFullPath(markdownDir, decodedReference)
|
||||
: decodedReference.replace(/^\//, '');
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './fetch.js';
|
||||
export * from './file-path.js';
|
||||
export * from './hast.js';
|
||||
export * from './text.js';
|
||||
|
||||
Reference in New Issue
Block a user