diff --git a/blocksuite/affine/all/src/__tests__/adapters/__snapshots__/markdown.unit.spec.ts.snap b/blocksuite/affine/all/src/__tests__/adapters/__snapshots__/markdown.unit.spec.ts.snap new file mode 100644 index 0000000000000..e8013008ea96f --- /dev/null +++ b/blocksuite/affine/all/src/__tests__/adapters/__snapshots__/markdown.unit.spec.ts.snap @@ -0,0 +1,94 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`snapshot to markdown > imports obsidian vault fixtures 1`] = ` +{ + "entry": { + "children": [ + { + "children": [ + { + "children": [ + { + "delta": [ + { + "insert": "Panel +Body line", + }, + ], + "flavour": "affine:paragraph", + "type": "text", + }, + ], + "emoji": "πŸ’‘", + "flavour": "affine:callout", + }, + { + "flavour": "affine:attachment", + "name": "archive.zip", + "style": "horizontalThin", + }, + { + "delta": [ + { + "footnote": { + "label": "1", + "reference": { + "title": "reference body", + "type": "url", + }, + }, + "insert": " ", + }, + ], + "flavour": "affine:paragraph", + "type": "text", + }, + { + "flavour": "affine:divider", + }, + { + "delta": [ + { + "insert": "after note", + }, + ], + "flavour": "affine:paragraph", + "type": "text", + }, + { + "delta": [ + { + "insert": " ", + "reference": { + "page": "linked", + "type": "LinkedPage", + }, + }, + ], + "flavour": "affine:paragraph", + "type": "text", + }, + { + "delta": [ + { + "insert": "Sources", + }, + ], + "flavour": "affine:paragraph", + "type": "h6", + }, + { + "flavour": "affine:bookmark", + }, + ], + "flavour": "affine:note", + }, + ], + "flavour": "affine:page", + }, + "titles": [ + "entry", + "linked", + ], +} +`; diff --git a/blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/entry.md b/blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/entry.md new file mode 100644 index 0000000000000..f43f9badacb99 --- /dev/null +++ b/blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/entry.md @@ -0,0 +1,14 @@ +> [!custom] Panel +> Body line + +![[archive.zip]] + +[^1] + +--- + +after note + +[[linked]] + +[^1]: reference body diff --git a/blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/linked.md b/blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/linked.md new file mode 100644 index 0000000000000..ab06e6bf5dba3 --- /dev/null +++ b/blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/linked.md @@ -0,0 +1 @@ +plain linked page 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 2fc4a3bba6f8e..6bbbdc39c53d8 100644 --- a/blocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.ts +++ b/blocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.ts @@ -1,4 +1,10 @@ -import { MarkdownTransformer } from '@blocksuite/affine/widgets/linked-doc'; +import { readFileSync } from 'node:fs'; +import { basename, resolve } from 'node:path'; + +import { + MarkdownTransformer, + ObsidianTransformer, +} from '@blocksuite/affine/widgets/linked-doc'; import { DefaultTheme, NoteDisplayMode, @@ -8,13 +14,18 @@ import { CalloutAdmonitionType, CalloutExportStyle, calloutMarkdownExportMiddleware, + docLinkBaseURLMiddleware, embedSyncedDocMiddleware, MarkdownAdapter, + titleMiddleware, } from '@blocksuite/affine-shared/adapters'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { BlockSnapshot, + DeltaInsert, DocSnapshot, SliceSnapshot, + Store, TransformerMiddleware, } from '@blocksuite/store'; import { AssetsManager, MemoryBlobCRUD, Schema } from '@blocksuite/store'; @@ -29,6 +40,138 @@ import { testStoreExtensions } from '../utils/store.js'; const provider = getProvider(); +function withRelativePath(file: File, relativePath: string): File { + Object.defineProperty(file, 'webkitRelativePath', { + value: relativePath, + writable: false, + }); + return file; +} + +function markdownFixture(relativePath: string): File { + return withRelativePath( + new File( + [ + readFileSync( + resolve(import.meta.dirname, 'fixtures/obsidian', relativePath), + 'utf8' + ), + ], + basename(relativePath), + { type: 'text/markdown' } + ), + `vault/${relativePath}` + ); +} + +function exportSnapshot(doc: Store): DocSnapshot { + const job = doc.getTransformer([ + docLinkBaseURLMiddleware(doc.workspace.id), + titleMiddleware(doc.workspace.meta.docMetas), + ]); + const snapshot = job.docToSnapshot(doc); + expect(snapshot).toBeTruthy(); + return snapshot!; +} + +function normalizeDeltaForSnapshot( + delta: DeltaInsert[], + titleById: ReadonlyMap +) { + return delta.map(item => { + const normalized: Record = { + insert: item.insert, + }; + + if (item.attributes?.link) { + normalized.link = item.attributes.link; + } + + if (item.attributes?.reference?.type === 'LinkedPage') { + normalized.reference = { + type: 'LinkedPage', + page: titleById.get(item.attributes.reference.pageId) ?? '', + ...(item.attributes.reference.title + ? { title: item.attributes.reference.title } + : {}), + }; + } + + if (item.attributes?.footnote) { + const reference = item.attributes.footnote.reference; + normalized.footnote = { + label: item.attributes.footnote.label, + reference: + reference.type === 'doc' + ? { + type: 'doc', + page: reference.docId + ? (titleById.get(reference.docId) ?? '') + : '', + } + : { + type: reference.type, + ...(reference.title ? { title: reference.title } : {}), + ...(reference.fileName ? { fileName: reference.fileName } : {}), + }, + }; + } + + return normalized; + }); +} + +function simplifyBlockForSnapshot( + block: BlockSnapshot, + titleById: ReadonlyMap +): Record { + const simplified: Record = { + flavour: block.flavour, + }; + + if (block.flavour === 'affine:paragraph' || block.flavour === 'affine:list') { + simplified.type = block.props.type; + const text = block.props.text as + | { delta?: DeltaInsert[] } + | undefined; + simplified.delta = normalizeDeltaForSnapshot(text?.delta ?? [], titleById); + } + + if (block.flavour === 'affine:callout') { + simplified.emoji = block.props.emoji; + } + + if (block.flavour === 'affine:attachment') { + simplified.name = block.props.name; + simplified.style = block.props.style; + } + + if (block.flavour === 'affine:image') { + simplified.sourceId = ''; + } + + const children = (block.children ?? []) + .filter(child => child.flavour !== 'affine:surface') + .map(child => simplifyBlockForSnapshot(child, titleById)); + if (children.length) { + simplified.children = children; + } + + return simplified; +} + +function snapshotDocByTitle( + collection: TestWorkspace, + title: string, + titleById: ReadonlyMap +) { + const meta = collection.meta.docMetas.find(meta => meta.title === title); + expect(meta).toBeTruthy(); + const doc = collection.getDoc(meta!.id)?.getStore({ id: meta!.id }); + expect(doc).toBeTruthy(); + return simplifyBlockForSnapshot(exportSnapshot(doc!).blocks, titleById); +} + describe('snapshot to markdown', () => { test('code', async () => { const blockSnapshot: BlockSnapshot = { @@ -127,6 +270,46 @@ Hello world expect(meta?.tags).toEqual(['a', 'b']); }); + test('imports obsidian vault fixtures', async () => { + const schema = new Schema().register(AffineSchemas); + const collection = new TestWorkspace(); + collection.storeExtensions = testStoreExtensions; + collection.meta.initialize(); + + const attachment = withRelativePath( + new File([new Uint8Array([80, 75, 3, 4])], 'archive.zip', { + type: 'application/zip', + }), + 'vault/archive.zip' + ); + + const { docIds } = await ObsidianTransformer.importObsidianVault({ + collection, + schema, + importedFiles: [ + markdownFixture('entry.md'), + markdownFixture('linked.md'), + attachment, + ], + extensions: testStoreExtensions, + }); + expect(docIds).toHaveLength(2); + + const titleById = new Map( + collection.meta.docMetas.map(meta => [ + meta.id, + meta.title ?? '', + ]) + ); + + expect({ + titles: collection.meta.docMetas + .map(meta => meta.title) + .sort((a, b) => (a ?? '').localeCompare(b ?? '')), + entry: snapshotDocByTitle(collection, 'entry', titleById), + }).toMatchSnapshot(); + }); + test('paragraph', async () => { const blockSnapshot: BlockSnapshot = { type: 'block', diff --git a/blocksuite/affine/blocks/attachment/src/adapters/markdown.ts b/blocksuite/affine/blocks/attachment/src/adapters/markdown.ts index a7ec4d0e5426a..4e3b74ace8f38 100644 --- a/blocksuite/affine/blocks/attachment/src/adapters/markdown.ts +++ b/blocksuite/affine/blocks/attachment/src/adapters/markdown.ts @@ -5,6 +5,7 @@ import { import { BlockMarkdownAdapterExtension, type BlockMarkdownAdapterMatcher, + createAttachmentBlockSnapshot, FOOTNOTE_DEFINITION_PREFIX, getFootnoteDefinitionText, isFootnoteDefinitionNode, @@ -56,18 +57,15 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher } walkerContext .openNode( - { - type: 'block', + createAttachmentBlockSnapshot({ id: nanoid(), - flavour: AttachmentBlockSchema.model.flavour, props: { name: fileName, sourceId: blobId, footnoteIdentifier, style: 'citation', }, - children: [], - }, + }), 'children' ) .closeNode(); diff --git a/blocksuite/affine/shared/src/adapters/attachment.ts b/blocksuite/affine/shared/src/adapters/attachment.ts index 31f5a0440d84b..2d8843b2ef829 100644 --- a/blocksuite/affine/shared/src/adapters/attachment.ts +++ b/blocksuite/affine/shared/src/adapters/attachment.ts @@ -1,4 +1,7 @@ -import { AttachmentBlockSchema } from '@blocksuite/affine-model'; +import { + type AttachmentBlockProps, + AttachmentBlockSchema, +} from '@blocksuite/affine-model'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { type AssetsManager, @@ -23,6 +26,24 @@ import { AdapterFactoryIdentifier } from './types/adapter'; export type Attachment = File[]; +type CreateAttachmentBlockSnapshotOptions = { + id?: string; + props: Partial & Pick; +}; + +export function createAttachmentBlockSnapshot({ + id = nanoid(), + props, +}: CreateAttachmentBlockSnapshotOptions): BlockSnapshot { + return { + type: 'block', + id, + flavour: AttachmentBlockSchema.model.flavour, + props, + children: [], + }; +} + type AttachmentToSliceSnapshotPayload = { file: Attachment; assets?: AssetsManager; @@ -97,8 +118,6 @@ export class AttachmentAdapter extends BaseAdapter { if (files.length === 0) return null; const content: SliceSnapshot['content'] = []; - const flavour = AttachmentBlockSchema.model.flavour; - for (const blob of files) { const id = nanoid(); const { name, size, type } = blob; @@ -108,22 +127,21 @@ export class AttachmentAdapter extends BaseAdapter { mapInto: sourceId => ({ sourceId }), }); - content.push({ - type: 'block', - flavour, - id, - props: { - name, - size, - type, - embed: false, - style: 'horizontalThin', - index: 'a0', - xywh: '[0,0,0,0]', - rotate: 0, - }, - children: [], - }); + content.push( + createAttachmentBlockSnapshot({ + id, + props: { + name, + size, + type, + embed: false, + style: 'horizontalThin', + index: 'a0', + xywh: '[0,0,0,0]', + rotate: 0, + }, + }) + ); } return { diff --git a/blocksuite/affine/shared/src/adapters/utils/file-path.ts b/blocksuite/affine/shared/src/adapters/utils/file-path.ts index ec8943b70b8bf..8b7a5f544134f 100644 --- a/blocksuite/affine/shared/src/adapters/utils/file-path.ts +++ b/blocksuite/affine/shared/src/adapters/utils/file-path.ts @@ -1,3 +1,20 @@ +function safeDecodePathReference(path: string): string { + try { + return decodeURIComponent(path); + } catch { + return path; + } +} + +export function normalizeFilePathReference(path: string): string { + return safeDecodePathReference(path) + .trim() + .replace(/\\/g, '/') + .replace(/^\.\/+/, '') + .replace(/^\/+/, '') + .replace(/\/+/g, '/'); +} + /** * Normalizes a relative path by resolving all relative path segments * @param basePath The base path (markdown file's directory) @@ -40,7 +57,7 @@ export function getImageFullPath( imageReference: string ): string { // Decode the image reference in case it contains URL-encoded characters - const decodedReference = decodeURIComponent(imageReference); + const decodedReference = safeDecodePathReference(imageReference); // Get the directory of the file path const markdownDir = filePath.substring(0, filePath.lastIndexOf('/')); diff --git a/blocksuite/affine/shared/src/utils/file/filesys.ts b/blocksuite/affine/shared/src/utils/file/filesys.ts index 0258545be6569..65454a7958d8e 100644 --- a/blocksuite/affine/shared/src/utils/file/filesys.ts +++ b/blocksuite/affine/shared/src/utils/file/filesys.ts @@ -20,9 +20,30 @@ declare global { showOpenFilePicker?: ( options?: OpenFilePickerOptions ) => Promise; + // Window API: showDirectoryPicker + showDirectoryPicker?: (options?: { + id?: string; + mode?: 'read' | 'readwrite'; + startIn?: FileSystemHandle | string; + }) => Promise; } } +// Minimal polyfill for FileSystemDirectoryHandle to iterate over files +interface FileSystemDirectoryHandle { + kind: 'directory'; + name: string; + values(): AsyncIterableIterator< + FileSystemFileHandle | FileSystemDirectoryHandle + >; +} + +interface FileSystemFileHandle { + kind: 'file'; + name: string; + getFile(): Promise; +} + // See [Common MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) const FileTypes: NonNullable = [ { @@ -121,21 +142,27 @@ type AcceptTypes = | 'Docx' | 'MindMap'; -export async function openFilesWith( - acceptType: AcceptTypes = 'Any', - multiple: boolean = true -): Promise { - // Feature detection. The API needs to be supported - // and the app not run in an iframe. - const supportsFileSystemAccess = - 'showOpenFilePicker' in window && +function canUseFileSystemAccessAPI( + api: 'showOpenFilePicker' | 'showDirectoryPicker' +) { + return ( + api in window && (() => { try { return window.self === window.top; } catch { return false; } - })(); + })() + ); +} + +export async function openFilesWith( + acceptType: AcceptTypes = 'Any', + multiple: boolean = true +): Promise { + const supportsFileSystemAccess = + canUseFileSystemAccessAPI('showOpenFilePicker'); // If the File System Access API is supported… if (supportsFileSystemAccess && window.showOpenFilePicker) { @@ -194,6 +221,75 @@ export async function openFilesWith( }); } +export async function openDirectory(): Promise { + const supportsFileSystemAccess = canUseFileSystemAccessAPI( + 'showDirectoryPicker' + ); + + if (supportsFileSystemAccess && window.showDirectoryPicker) { + try { + const dirHandle = await window.showDirectoryPicker(); + const files: File[] = []; + + const readDirectory = async ( + directoryHandle: FileSystemDirectoryHandle, + path: string + ) => { + for await (const handle of directoryHandle.values()) { + const relativePath = path ? `${path}/${handle.name}` : handle.name; + if (handle.kind === 'file') { + const fileHandle = handle as FileSystemFileHandle; + if (fileHandle.getFile) { + const file = await fileHandle.getFile(); + Object.defineProperty(file, 'webkitRelativePath', { + value: relativePath, + writable: false, + }); + files.push(file); + } + } else if (handle.kind === 'directory') { + await readDirectory( + handle as FileSystemDirectoryHandle, + relativePath + ); + } + } + }; + + await readDirectory(dirHandle, ''); + return files; + } catch (err) { + console.error(err); + return null; + } + } + + return new Promise(resolve => { + const input = document.createElement('input'); + input.classList.add('affine-upload-input'); + input.style.display = 'none'; + input.type = 'file'; + + input.setAttribute('webkitdirectory', ''); + input.setAttribute('directory', ''); + + document.body.append(input); + + input.addEventListener('change', () => { + input.remove(); + resolve(input.files ? Array.from(input.files) : null); + }); + + input.addEventListener('cancel', () => resolve(null)); + + if ('showPicker' in HTMLInputElement.prototype) { + input.showPicker(); + } else { + input.click(); + } + }); +} + export async function openSingleFileWith( acceptType?: AcceptTypes ): Promise { diff --git a/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts b/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts index a2c8065ad2441..f15c39434636c 100644 --- a/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts +++ b/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts @@ -7,6 +7,7 @@ import { NotionIcon, } from '@blocksuite/affine-components/icons'; import { + openDirectory, openFilesWith, openSingleFileWith, } from '@blocksuite/affine-shared/utils'; @@ -18,11 +19,16 @@ import { query, state } from 'lit/decorators.js'; import { HtmlTransformer } from '../transformers/html.js'; import { MarkdownTransformer } from '../transformers/markdown.js'; import { NotionHtmlTransformer } from '../transformers/notion-html.js'; +import { ObsidianTransformer } from '../transformers/obsidian.js'; import { styles } from './styles.js'; export type OnSuccessHandler = ( pageIds: string[], - options: { isWorkspaceFile: boolean; importedCount: number } + options: { + isWorkspaceFile: boolean; + importedCount: number; + docEmojis?: Map; + } ) => void; export type OnFailHandler = (message: string) => void; @@ -140,6 +146,29 @@ export class ImportDoc extends WithDisposable(LitElement) { }); } + private async _importObsidian() { + const files = await openDirectory(); + if (!files || files.length === 0) return; + const needLoading = + files.reduce((acc, f) => acc + f.size, 0) > SHOW_LOADING_SIZE; + if (needLoading) { + this.hidden = false; + this._loading = true; + } else { + this.abortController.abort(); + } + const { docIds, docEmojis } = await ObsidianTransformer.importObsidianVault( + { + collection: this.collection, + schema: this.schema, + importedFiles: files, + extensions: this.extensions, + } + ); + needLoading && this.abortController.abort(); + this._onImportSuccess(docIds, { docEmojis }); + } + private _onCloseClick(event: MouseEvent) { event.stopPropagation(); this.abortController.abort(); @@ -151,15 +180,21 @@ export class ImportDoc extends WithDisposable(LitElement) { private _onImportSuccess( pageIds: string[], - options: { isWorkspaceFile?: boolean; importedCount?: number } = {} + options: { + isWorkspaceFile?: boolean; + importedCount?: number; + docEmojis?: Map; + } = {} ) { const { isWorkspaceFile = false, importedCount: pagesImportedCount = pageIds.length, + docEmojis, } = options; this.onSuccess?.(pageIds, { isWorkspaceFile, importedCount: pagesImportedCount, + docEmojis, }); } @@ -258,6 +293,13 @@ export class ImportDoc extends WithDisposable(LitElement) { + + ${ExportToMarkdownIcon} + ${NewIcon} diff --git a/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts b/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts index 28f8f826ec8a7..f5581e4c6899d 100644 --- a/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts +++ b/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts @@ -2,6 +2,7 @@ export { DocxTransformer } from './docx.js'; export { HtmlTransformer } from './html.js'; export { MarkdownTransformer } from './markdown.js'; export { NotionHtmlTransformer } from './notion-html.js'; +export { ObsidianTransformer } from './obsidian.js'; export { PdfTransformer } from './pdf.js'; export { createAssetsArchive, download } from './utils.js'; export { ZipTransformer } from './zip.js'; diff --git a/blocksuite/affine/widgets/linked-doc/src/transformers/markdown.ts b/blocksuite/affine/widgets/linked-doc/src/transformers/markdown.ts index bdb3477151639..7d6e3a4341da9 100644 --- a/blocksuite/affine/widgets/linked-doc/src/transformers/markdown.ts +++ b/blocksuite/affine/widgets/linked-doc/src/transformers/markdown.ts @@ -21,8 +21,11 @@ import { extMimeMap, Transformer } from '@blocksuite/store'; import type { AssetMap, ImportedFileEntry, PathBlobIdMap } from './type.js'; import { createAssetsArchive, download, parseMatter, Unzip } from './utils.js'; -type ParsedFrontmatterMeta = Partial< - Pick +export type ParsedFrontmatterMeta = Partial< + Pick< + DocMeta, + 'title' | 'createDate' | 'updatedDate' | 'tags' | 'favorite' | 'trash' + > >; const FRONTMATTER_KEYS = { @@ -150,11 +153,18 @@ function buildMetaFromFrontmatter( } continue; } + if (FRONTMATTER_KEYS.trash.includes(key)) { + const trash = parseBoolean(value); + if (trash !== undefined) { + meta.trash = trash; + } + continue; + } } return meta; } -function parseFrontmatter(markdown: string): { +export function parseFrontmatter(markdown: string): { content: string; meta: ParsedFrontmatterMeta; } { @@ -176,7 +186,7 @@ function parseFrontmatter(markdown: string): { } } -function applyMetaPatch( +export function applyMetaPatch( collection: Workspace, docId: string, meta: ParsedFrontmatterMeta @@ -187,13 +197,14 @@ function applyMetaPatch( if (meta.updatedDate !== undefined) metaPatch.updatedDate = meta.updatedDate; if (meta.tags) metaPatch.tags = meta.tags; if (meta.favorite !== undefined) metaPatch.favorite = meta.favorite; + if (meta.trash !== undefined) metaPatch.trash = meta.trash; if (Object.keys(metaPatch).length) { collection.meta.setDocMeta(docId, metaPatch); } } -function getProvider(extensions: ExtensionType[]) { +export function getProvider(extensions: ExtensionType[]) { const container = new Container(); extensions.forEach(ext => { ext.setup(container); @@ -223,6 +234,103 @@ type ImportMarkdownZipOptions = { extensions: ExtensionType[]; }; +/** + * Filters hidden/system entries that should never participate in imports. + */ +export function isSystemImportPath(path: string) { + return path.includes('__MACOSX') || path.includes('.DS_Store'); +} + +/** + * Creates the doc CRUD bridge used by importer transformers. + */ +export function createCollectionDocCRUD(collection: Workspace) { + return { + create: (id: string) => collection.createDoc(id).getStore({ id }), + get: (id: string) => collection.getDoc(id)?.getStore({ id }) ?? null, + delete: (id: string) => collection.removeDoc(id), + }; +} + +type CreateMarkdownImportJobOptions = { + collection: Workspace; + schema: Schema; + preferredTitle?: string; + fullPath?: string; +}; + +/** + * Creates a markdown import job with the standard collection middlewares. + */ +export function createMarkdownImportJob({ + collection, + schema, + preferredTitle, + fullPath, +}: CreateMarkdownImportJobOptions) { + return new Transformer({ + schema, + blobCRUD: collection.blobSync, + docCRUD: createCollectionDocCRUD(collection), + middlewares: [ + defaultImageProxyMiddleware, + fileNameMiddleware(preferredTitle), + docLinkBaseURLMiddleware(collection.id), + ...(fullPath ? [filePathMiddleware(fullPath)] : []), + ], + }); +} + +type StageImportedAssetOptions = { + pendingAssets: AssetMap; + pendingPathBlobIdMap: PathBlobIdMap; + path: string; + content: Blob; + fileName: string; +}; + +/** + * Hashes a non-markdown import file and stages it into the shared asset maps. + */ +export async function stageImportedAsset({ + pendingAssets, + pendingPathBlobIdMap, + path, + content, + fileName, +}: StageImportedAssetOptions) { + const ext = path.split('.').at(-1) ?? ''; + const mime = extMimeMap.get(ext.toLowerCase()) ?? ''; + const key = await sha(await content.arrayBuffer()); + pendingPathBlobIdMap.set(path, key); + pendingAssets.set(key, new File([content], fileName, { type: mime })); +} + +/** + * Binds previously staged asset files into a transformer job before import. + */ +export function bindImportedAssetsToJob( + job: Transformer, + pendingAssets: AssetMap, + pendingPathBlobIdMap: PathBlobIdMap +) { + const pathBlobIdMap = job.assetsManager.getPathBlobIdMap(); + // 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 + const assetFile = pendingAssets.get(key); + if (assetFile) { + job.assets.set(key, assetFile); + } + } + + return pathBlobIdMap; +} + /** * Exports a doc to a Markdown file or a zip archive containing Markdown and assets. * @param doc The doc to export @@ -329,19 +437,10 @@ async function importMarkdownToDoc({ const { content, meta } = parseFrontmatter(markdown); const preferredTitle = meta.title ?? fileName; const provider = getProvider(extensions); - const job = new Transformer({ + const job = createMarkdownImportJob({ + collection, schema, - blobCRUD: collection.blobSync, - docCRUD: { - create: (id: string) => collection.createDoc(id).getStore({ id }), - get: (id: string) => collection.getDoc(id)?.getStore({ id }) ?? null, - delete: (id: string) => collection.removeDoc(id), - }, - middlewares: [ - defaultImageProxyMiddleware, - fileNameMiddleware(preferredTitle), - docLinkBaseURLMiddleware(collection.id), - ], + preferredTitle, }); const mdAdapter = new MarkdownAdapter(job, provider); const page = await mdAdapter.toDoc({ @@ -381,7 +480,7 @@ async function importMarkdownZip({ // 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')) { + if (isSystemImportPath(path)) { continue; } @@ -395,12 +494,13 @@ async function importMarkdownZip({ 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()); - pendingPathBlobIdMap.set(path, key); - pendingAssets.set(key, new File([blob], fileName, { type: mime })); + await stageImportedAsset({ + pendingAssets, + pendingPathBlobIdMap, + path, + content: blob, + fileName, + }); } } @@ -411,34 +511,13 @@ async function importMarkdownZip({ const markdown = await contentBlob.text(); const { content, meta } = parseFrontmatter(markdown); const preferredTitle = meta.title ?? fileNameWithoutExt; - const job = new Transformer({ + const job = createMarkdownImportJob({ + collection, schema, - blobCRUD: collection.blobSync, - docCRUD: { - create: (id: string) => collection.createDoc(id).getStore({ id }), - get: (id: string) => collection.getDoc(id)?.getStore({ id }) ?? null, - delete: (id: string) => collection.removeDoc(id), - }, - middlewares: [ - defaultImageProxyMiddleware, - fileNameMiddleware(preferredTitle), - docLinkBaseURLMiddleware(collection.id), - filePathMiddleware(fullPath), - ], + preferredTitle, + fullPath, }); - const assets = job.assets; - const pathBlobIdMap = job.assetsManager.getPathBlobIdMap(); - // 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)!); - } - } + bindImportedAssetsToJob(job, pendingAssets, pendingPathBlobIdMap); const mdAdapter = new MarkdownAdapter(job, provider); const doc = await mdAdapter.toDoc({ diff --git a/blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts b/blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts new file mode 100644 index 0000000000000..a528f71f54826 --- /dev/null +++ b/blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts @@ -0,0 +1,732 @@ +import { FootNoteReferenceParamsSchema } from '@blocksuite/affine-model'; +import { + BlockMarkdownAdapterExtension, + createAttachmentBlockSnapshot, + FULL_FILE_PATH_KEY, + getImageFullPath, + MarkdownAdapter, + type MarkdownAST, + MarkdownASTToDeltaExtension, + normalizeFilePathReference, +} from '@blocksuite/affine-shared/adapters'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; +import type { + DeltaInsert, + ExtensionType, + Schema, + Workspace, +} from '@blocksuite/store'; +import { extMimeMap, nanoid } from '@blocksuite/store'; +import type { Html, Text } from 'mdast'; + +import { + applyMetaPatch, + bindImportedAssetsToJob, + createMarkdownImportJob, + getProvider, + isSystemImportPath, + parseFrontmatter, + stageImportedAsset, +} from './markdown.js'; +import type { + AssetMap, + MarkdownFileImportEntry, + PathBlobIdMap, +} from './type.js'; + +const CALLOUT_TYPE_MAP: Record = { + note: 'πŸ’‘', + info: 'ℹ️', + tip: 'πŸ”₯', + hint: 'βœ…', + important: '‼️', + warning: '⚠️', + caution: '⚠️', + attention: '⚠️', + danger: '⚠️', + error: '🚨', + bug: 'πŸ›', + example: 'πŸ“Œ', + quote: 'πŸ’¬', + cite: 'πŸ’¬', + abstract: 'πŸ“‹', + summary: 'πŸ“‹', + todo: 'β˜‘οΈ', + success: 'βœ…', + check: 'βœ…', + done: 'βœ…', + failure: '❌', + fail: '❌', + missing: '❌', + question: '❓', + help: '❓', + faq: '❓', +}; + +const AMBIGUOUS_PAGE_LOOKUP = '__ambiguous__'; +const DEFAULT_CALLOUT_EMOJI = 'πŸ’‘'; +const OBSIDIAN_TEXT_FOOTNOTE_URL_PREFIX = 'data:text/plain;charset=utf-8,'; +const OBSIDIAN_ATTACHMENT_EMBED_TAG = 'obsidian-attachment'; + +function normalizeLookupKey(value: string): string { + return normalizeFilePathReference(value).toLowerCase(); +} + +function stripMarkdownExtension(value: string): string { + return value.replace(/\.md$/i, ''); +} + +function basename(value: string): string { + return normalizeFilePathReference(value).split('/').pop() ?? value; +} + +function parseObsidianTarget(rawTarget: string): { + path: string; + fragment: string | null; +} { + const normalizedTarget = normalizeFilePathReference(rawTarget); + const match = normalizedTarget.match(/^([^#^]+)([#^].*)?$/); + + return { + path: match?.[1]?.trim() ?? normalizedTarget, + fragment: match?.[2] ?? null, + }; +} + +function extractTitleAndEmoji(rawTitle: string): { + title: string; + emoji: string | null; +} { + const SINGLE_LEADING_EMOJI_RE = + /^[\s\u200b]*((?:[\p{Emoji_Presentation}\p{Extended_Pictographic}\u200b]|\u200d|\ufe0f)+)/u; + + let currentTitle = rawTitle; + let extractedEmojiClusters = ''; + let emojiMatch; + + while ((emojiMatch = currentTitle.match(SINGLE_LEADING_EMOJI_RE))) { + const matchedCluster = emojiMatch[1].trim(); + extractedEmojiClusters += + (extractedEmojiClusters ? ' ' : '') + matchedCluster; + currentTitle = currentTitle.slice(emojiMatch[0].length); + } + + return { + title: currentTitle.trim(), + emoji: extractedEmojiClusters || null, + }; +} + +function preprocessTitleHeader(markdown: string): string { + return markdown.replace( + /^(\s*#\s+)(.*)$/m, + (_, headerPrefix, titleContent) => { + const { title: cleanTitle } = extractTitleAndEmoji(titleContent); + return `${headerPrefix}${cleanTitle}`; + } + ); +} + +function preprocessObsidianCallouts(markdown: string): string { + return markdown.replace( + /^(> *)\[!([^\]\n]+)\]([+-]?)([^\n]*)/gm, + (_, prefix, type, _fold, rest) => { + const calloutToken = + CALLOUT_TYPE_MAP[type.trim().toLowerCase()] ?? DEFAULT_CALLOUT_EMOJI; + const title = rest.trim(); + return title + ? `${prefix}[!${calloutToken}] ${title}` + : `${prefix}[!${calloutToken}]`; + } + ); +} + +function isStructuredFootnoteDefinition(content: string): boolean { + try { + return FootNoteReferenceParamsSchema.safeParse(JSON.parse(content.trim())) + .success; + } catch { + return false; + } +} + +function splitFootnoteTextContent(content: string): { + title: string; + description?: string; +} { + const lines = content + .split('\n') + .map(line => line.trim()) + .filter(Boolean); + const title = lines[0] ?? content.trim(); + const description = lines.slice(1).join('\n').trim(); + + return { + title, + ...(description ? { description } : {}), + }; +} + +function createTextFootnoteDefinition(content: string): string { + const normalizedContent = content.trim(); + const { title, description } = splitFootnoteTextContent(normalizedContent); + + return JSON.stringify({ + type: 'url', + url: encodeURIComponent( + `${OBSIDIAN_TEXT_FOOTNOTE_URL_PREFIX}${encodeURIComponent( + normalizedContent + )}` + ), + title, + ...(description ? { description } : {}), + }); +} + +function extractObsidianFootnotes(markdown: string): { + content: string; + footnotes: string[]; +} { + const lines = markdown.split('\n'); + const output: string[] = []; + const footnotes: string[] = []; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const match = line.match(/^\[\^([^\]]+)\]:\s*(.*)$/); + if (!match) { + output.push(line); + continue; + } + + const identifier = match[1]; + const contentLines = [match[2]]; + + while (index + 1 < lines.length) { + const nextLine = lines[index + 1]; + if (/^(?: {1,4}|\t)/.test(nextLine)) { + contentLines.push(nextLine.replace(/^(?: {1,4}|\t)/, '')); + index += 1; + continue; + } + + if ( + nextLine.trim() === '' && + index + 2 < lines.length && + /^(?: {1,4}|\t)/.test(lines[index + 2]) + ) { + contentLines.push(''); + index += 1; + continue; + } + + break; + } + + const content = contentLines.join('\n').trim(); + footnotes.push( + `[^${identifier}]: ${ + !content || isStructuredFootnoteDefinition(content) + ? content + : createTextFootnoteDefinition(content) + }` + ); + } + + return { content: output.join('\n'), footnotes }; +} + +function buildLookupKeys( + targetPath: string, + currentFilePath?: string +): string[] { + const parsedTargetPath = normalizeFilePathReference(targetPath); + if (!parsedTargetPath) { + return []; + } + + const keys = new Set(); + const addPathVariants = (value: string) => { + const normalizedValue = normalizeFilePathReference(value); + if (!normalizedValue) { + return; + } + + keys.add(normalizedValue); + keys.add(stripMarkdownExtension(normalizedValue)); + + const fileName = basename(normalizedValue); + keys.add(fileName); + keys.add(stripMarkdownExtension(fileName)); + + const cleanTitle = extractTitleAndEmoji( + stripMarkdownExtension(fileName) + ).title; + if (cleanTitle) { + keys.add(cleanTitle); + } + }; + + addPathVariants(parsedTargetPath); + + if (currentFilePath) { + addPathVariants(getImageFullPath(currentFilePath, parsedTargetPath)); + } + + return Array.from(keys).map(normalizeLookupKey); +} + +function registerPageLookup( + pageLookupMap: Map, + key: string, + pageId: string +) { + const normalizedKey = normalizeLookupKey(key); + if (!normalizedKey) { + return; + } + + const existing = pageLookupMap.get(normalizedKey); + if (existing && existing !== pageId) { + pageLookupMap.set(normalizedKey, AMBIGUOUS_PAGE_LOOKUP); + return; + } + + pageLookupMap.set(normalizedKey, pageId); +} + +function resolvePageIdFromLookup( + pageLookupMap: Pick, 'get'>, + rawTarget: string, + currentFilePath?: string +): string | null { + const { path } = parseObsidianTarget(rawTarget); + for (const key of buildLookupKeys(path, currentFilePath)) { + const targetPageId = pageLookupMap.get(key); + if (!targetPageId || targetPageId === AMBIGUOUS_PAGE_LOOKUP) { + continue; + } + return targetPageId; + } + + return null; +} + +function resolveWikilinkDisplayTitle( + rawAlias: string | undefined, + pageEmoji: string | undefined +): string | undefined { + if (!rawAlias) { + return undefined; + } + + const { title: aliasTitle, emoji: aliasEmoji } = + extractTitleAndEmoji(rawAlias); + + if (aliasEmoji && aliasEmoji === pageEmoji) { + return aliasTitle; + } + + return rawAlias; +} + +function isImageAssetPath(path: string): boolean { + const extension = path.split('.').at(-1)?.toLowerCase() ?? ''; + return extMimeMap.get(extension)?.startsWith('image/') ?? false; +} + +function encodeMarkdownPath(path: string): string { + return encodeURI(path).replaceAll('(', '%28').replaceAll(')', '%29'); +} + +function escapeMarkdownLabel(label: string): string { + return label.replace(/[[\]\\]/g, '\\$&'); +} + +function isObsidianSizeAlias(alias: string | undefined): boolean { + return !!alias && /^\d+(?:x\d+)?$/i.test(alias.trim()); +} + +function getEmbedLabel( + rawAlias: string | undefined, + targetPath: string, + fallbackToFileName: boolean +): string { + if (!rawAlias || isObsidianSizeAlias(rawAlias)) { + return fallbackToFileName + ? stripMarkdownExtension(basename(targetPath)) + : ''; + } + + return rawAlias.trim(); +} + +type ObsidianAttachmentEmbed = { + blobId: string; + fileName: string; + fileType: string; +}; + +function createObsidianAttach(embed: ObsidianAttachmentEmbed): string { + return ``; +} + +function parseObsidianAttach(value: string): ObsidianAttachmentEmbed | null { + const match = value.match( + new RegExp(`^$`) + ); + if (!match?.[1]) return null; + + try { + const parsed = JSON.parse( + decodeURIComponent(match[1]) + ) as ObsidianAttachmentEmbed; + if (!parsed.blobId || !parsed.fileName) { + return null; + } + return parsed; + } catch { + return null; + } +} + +function preprocessObsidianEmbeds( + markdown: string, + filePath: string, + pageLookupMap: ReadonlyMap, + pathBlobIdMap: ReadonlyMap +): string { + return markdown.replace( + /!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, + (match, rawTarget: string, rawAlias?: string) => { + const targetPageId = resolvePageIdFromLookup( + pageLookupMap, + rawTarget, + filePath + ); + if (targetPageId) { + return `[[${rawTarget}${rawAlias ? `|${rawAlias}` : ''}]]`; + } + + const { path } = parseObsidianTarget(rawTarget); + if (!path) { + return match; + } + + const assetPath = getImageFullPath(filePath, path); + const encodedPath = encodeMarkdownPath(assetPath); + + if (isImageAssetPath(path)) { + const alt = getEmbedLabel(rawAlias, path, false); + return `![${escapeMarkdownLabel(alt)}](${encodedPath})`; + } + + const label = getEmbedLabel(rawAlias, path, true); + const blobId = pathBlobIdMap.get(assetPath); + if (!blobId) return `[${escapeMarkdownLabel(label)}](${encodedPath})`; + + const extension = path.split('.').at(-1)?.toLowerCase() ?? ''; + return createObsidianAttach({ + blobId, + fileName: basename(path), + fileType: extMimeMap.get(extension) ?? '', + }); + } + ); +} + +function preprocessObsidianMarkdown( + markdown: string, + filePath: string, + pageLookupMap: ReadonlyMap, + pathBlobIdMap: ReadonlyMap +): string { + const { content: contentWithoutFootnotes, footnotes: extractedFootnotes } = + extractObsidianFootnotes(markdown); + const content = preprocessObsidianEmbeds( + contentWithoutFootnotes, + filePath, + pageLookupMap, + pathBlobIdMap + ); + const normalizedMarkdown = preprocessTitleHeader( + preprocessObsidianCallouts(content) + ); + + if (extractedFootnotes.length === 0) { + return normalizedMarkdown; + } + + const trimmedMarkdown = normalizedMarkdown.replace(/\s+$/, ''); + return `${trimmedMarkdown}\n\n${extractedFootnotes.join('\n\n')}\n`; +} + +function isObsidianAttachmentEmbedNode(node: MarkdownAST): node is Html { + return node.type === 'html' && !!parseObsidianAttach(node.value); +} + +export const obsidianAttachmentEmbedMarkdownAdapterMatcher = + BlockMarkdownAdapterExtension({ + flavour: 'obsidian:attachment-embed', + toMatch: o => isObsidianAttachmentEmbedNode(o.node), + fromMatch: () => false, + toBlockSnapshot: { + enter: (o, context) => { + if (!isObsidianAttachmentEmbedNode(o.node)) { + return; + } + + const attachment = parseObsidianAttach(o.node.value); + if (!attachment) { + return; + } + + const assetFile = context.assets?.getAssets().get(attachment.blobId); + context.walkerContext + .openNode( + createAttachmentBlockSnapshot({ + id: nanoid(), + props: { + name: attachment.fileName, + size: assetFile?.size ?? 0, + type: + attachment.fileType || + assetFile?.type || + 'application/octet-stream', + sourceId: attachment.blobId, + embed: false, + style: 'horizontalThin', + footnoteIdentifier: null, + }, + }), + 'children' + ) + .closeNode(); + (o.node as unknown as { type: string }).type = + 'obsidianAttachmentEmbed'; + }, + }, + fromBlockSnapshot: {}, + }); + +export const obsidianWikilinkToDeltaMatcher = MarkdownASTToDeltaExtension({ + name: 'obsidian-wikilink', + match: ast => ast.type === 'text', + toDelta: (ast, context) => { + const textNode = ast as Text; + if (!textNode.value) { + return []; + } + + const nodeContent = textNode.value; + const wikilinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; + const deltas: DeltaInsert[] = []; + + let lastProcessedIndex = 0; + let linkMatch; + + while ((linkMatch = wikilinkRegex.exec(nodeContent)) !== null) { + if (linkMatch.index > lastProcessedIndex) { + deltas.push({ + insert: nodeContent.substring(lastProcessedIndex, linkMatch.index), + }); + } + + const targetPageName = linkMatch[1].trim(); + const alias = linkMatch[2]?.trim(); + const currentFilePath = context.configs.get(FULL_FILE_PATH_KEY); + const targetPageId = resolvePageIdFromLookup( + { get: key => context.configs.get(`obsidian:pageId:${key}`) }, + targetPageName, + typeof currentFilePath === 'string' ? currentFilePath : undefined + ); + + if (targetPageId) { + const pageEmoji = context.configs.get( + 'obsidian:pageEmoji:' + targetPageId + ); + const displayTitle = resolveWikilinkDisplayTitle(alias, pageEmoji); + + deltas.push({ + insert: ' ', + attributes: { + reference: { + type: 'LinkedPage', + pageId: targetPageId, + ...(displayTitle ? { title: displayTitle } : {}), + }, + }, + }); + } else { + deltas.push({ insert: linkMatch[0] }); + } + + lastProcessedIndex = wikilinkRegex.lastIndex; + } + + if (lastProcessedIndex < nodeContent.length) { + deltas.push({ insert: nodeContent.substring(lastProcessedIndex) }); + } + + return deltas; + }, +}); + +export type ImportObsidianVaultOptions = { + collection: Workspace; + schema: Schema; + importedFiles: File[]; + extensions: ExtensionType[]; +}; + +export type ImportObsidianVaultResult = { + docIds: string[]; + docEmojis: Map; +}; + +export async function importObsidianVault({ + collection, + schema, + importedFiles, + extensions, +}: ImportObsidianVaultOptions): Promise { + const provider = getProvider([ + obsidianWikilinkToDeltaMatcher, + obsidianAttachmentEmbedMarkdownAdapterMatcher, + ...extensions, + ]); + + const docIds: string[] = []; + const docEmojis = new Map(); + const pendingAssets: AssetMap = new Map(); + const pendingPathBlobIdMap: PathBlobIdMap = new Map(); + const markdownBlobs: MarkdownFileImportEntry[] = []; + const pageLookupMap = new Map(); + + for (const file of importedFiles) { + const filePath = file.webkitRelativePath || file.name; + if (isSystemImportPath(filePath)) continue; + + if (file.name.endsWith('.md')) { + const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, ''); + const markdown = await file.text(); + const { content, meta } = parseFrontmatter(markdown); + + const documentTitleCandidate = meta.title ?? fileNameWithoutExt; + const { title: preferredTitle, emoji: leadingEmoji } = + extractTitleAndEmoji(documentTitleCandidate); + + const newPageId = collection.idGenerator(); + registerPageLookup(pageLookupMap, filePath, newPageId); + registerPageLookup( + pageLookupMap, + stripMarkdownExtension(filePath), + newPageId + ); + registerPageLookup(pageLookupMap, file.name, newPageId); + registerPageLookup(pageLookupMap, fileNameWithoutExt, newPageId); + registerPageLookup(pageLookupMap, documentTitleCandidate, newPageId); + registerPageLookup(pageLookupMap, preferredTitle, newPageId); + + if (leadingEmoji) { + docEmojis.set(newPageId, leadingEmoji); + } + + markdownBlobs.push({ + filename: file.name, + contentBlob: file, + fullPath: filePath, + pageId: newPageId, + preferredTitle, + content, + meta, + }); + } else { + await stageImportedAsset({ + pendingAssets, + pendingPathBlobIdMap, + path: filePath, + content: file, + fileName: file.name, + }); + } + } + + for (const existingDocMeta of collection.meta.docMetas) { + if (existingDocMeta.title) { + registerPageLookup( + pageLookupMap, + existingDocMeta.title, + existingDocMeta.id + ); + } + } + + await Promise.all( + markdownBlobs.map(async markdownFile => { + const { + fullPath, + pageId: predefinedId, + preferredTitle, + content, + meta, + } = markdownFile; + + const job = createMarkdownImportJob({ + collection, + schema, + preferredTitle, + fullPath, + }); + + for (const [lookupKey, id] of pageLookupMap.entries()) { + if (id === AMBIGUOUS_PAGE_LOOKUP) { + continue; + } + job.adapterConfigs.set(`obsidian:pageId:${lookupKey}`, id); + } + for (const [id, emoji] of docEmojis.entries()) { + job.adapterConfigs.set('obsidian:pageEmoji:' + id, emoji); + } + + const pathBlobIdMap = bindImportedAssetsToJob( + job, + pendingAssets, + pendingPathBlobIdMap + ); + + const preprocessedMarkdown = preprocessObsidianMarkdown( + content, + fullPath, + pageLookupMap, + pathBlobIdMap + ); + const mdAdapter = new MarkdownAdapter(job, provider); + const snapshot = await mdAdapter.toDocSnapshot({ + file: preprocessedMarkdown, + assets: job.assetsManager, + }); + + if (snapshot) { + snapshot.meta.id = predefinedId; + const doc = await job.snapshotToDoc(snapshot); + if (doc) { + applyMetaPatch(collection, doc.id, { + ...meta, + title: preferredTitle, + trash: false, + }); + docIds.push(doc.id); + } + } + }) + ); + + return { docIds, docEmojis }; +} + +export const ObsidianTransformer = { + importObsidianVault, +}; diff --git a/blocksuite/affine/widgets/linked-doc/src/transformers/type.ts b/blocksuite/affine/widgets/linked-doc/src/transformers/type.ts index 464d84d24fb98..fd9e61eaa1398 100644 --- a/blocksuite/affine/widgets/linked-doc/src/transformers/type.ts +++ b/blocksuite/affine/widgets/linked-doc/src/transformers/type.ts @@ -1,3 +1,5 @@ +import type { ParsedFrontmatterMeta } from './markdown.js'; + /** * Represents an imported file entry in the zip archive */ @@ -10,6 +12,13 @@ export type ImportedFileEntry = { fullPath: string; }; +export type MarkdownFileImportEntry = ImportedFileEntry & { + pageId: string; + preferredTitle: string; + content: string; + meta: ParsedFrontmatterMeta; +}; + /** * Map of asset hash to File object for all media files in the zip * Key: SHA hash of the file content (blobId) diff --git a/blocksuite/affine/widgets/toolbar/src/toolbar.ts b/blocksuite/affine/widgets/toolbar/src/toolbar.ts index 92ee9d99a5738..8ce6201de37eb 100644 --- a/blocksuite/affine/widgets/toolbar/src/toolbar.ts +++ b/blocksuite/affine/widgets/toolbar/src/toolbar.ts @@ -162,10 +162,11 @@ export class AffineToolbarWidget extends WidgetComponent { } setReferenceElementWithElements(gfx: GfxController, elements: GfxModel[]) { + const surfaceBounds = getCommonBoundWithRotation(elements); + const getBoundingClientRect = () => { - const bounds = getCommonBoundWithRotation(elements); const { x: offsetX, y: offsetY } = this.getBoundingClientRect(); - const [x, y, w, h] = gfx.viewport.toViewBound(bounds).toXYWH(); + const [x, y, w, h] = gfx.viewport.toViewBound(surfaceBounds).toXYWH(); const rect = new DOMRect(x + offsetX, y + offsetY, w, h); return rect; }; diff --git a/blocksuite/framework/std/src/gfx/model/surface/element-model.ts b/blocksuite/framework/std/src/gfx/model/surface/element-model.ts index d0d66530880f8..bd107c8278ead 100644 --- a/blocksuite/framework/std/src/gfx/model/surface/element-model.ts +++ b/blocksuite/framework/std/src/gfx/model/surface/element-model.ts @@ -103,8 +103,9 @@ export abstract class GfxPrimitiveElementModel< } get deserializedXYWH() { - if (!this._lastXYWH || this.xywh !== this._lastXYWH) { - const xywh = this.xywh; + const xywh = this.xywh; + + if (!this._lastXYWH || xywh !== this._lastXYWH) { this._local.set('deserializedXYWH', deserializeXYWH(xywh)); this._lastXYWH = xywh; } @@ -386,6 +387,8 @@ export abstract class GfxGroupLikeElementModel< { private _childIds: string[] = []; + private _xywhDirty = true; + private readonly _mutex = createMutex(); abstract children: Y.Map; @@ -420,24 +423,9 @@ export abstract class GfxGroupLikeElementModel< get xywh() { this._mutex(() => { - const curXYWH = - (this._local.get('xywh') as SerializedXYWH) ?? '[0,0,0,0]'; - const newXYWH = this._getXYWH().serialize(); - - if (curXYWH !== newXYWH || !this._local.has('xywh')) { - this._local.set('xywh', newXYWH); - - if (curXYWH !== newXYWH) { - this._onChange({ - props: { - xywh: newXYWH, - }, - oldValues: { - xywh: curXYWH, - }, - local: true, - }); - } + if (this._xywhDirty || !this._local.has('xywh')) { + this._local.set('xywh', this._getXYWH().serialize()); + this._xywhDirty = false; } }); @@ -457,15 +445,41 @@ export abstract class GfxGroupLikeElementModel< bound = bound ? bound.unite(child.elementBound) : child.elementBound; }); - if (bound) { - this._local.set('xywh', bound.serialize()); - } else { - this._local.delete('xywh'); - } - return bound ?? new Bound(0, 0, 0, 0); } + invalidateXYWH() { + this._xywhDirty = true; + this._local.delete('deserializedXYWH'); + } + + refreshXYWH(local: boolean) { + this._mutex(() => { + const oldXYWH = + (this._local.get('xywh') as SerializedXYWH) ?? '[0,0,0,0]'; + const nextXYWH = this._getXYWH().serialize(); + + this._xywhDirty = false; + + if (oldXYWH === nextXYWH && this._local.has('xywh')) { + return; + } + + this._local.set('xywh', nextXYWH); + this._local.delete('deserializedXYWH'); + + this._onChange({ + props: { + xywh: nextXYWH, + }, + oldValues: { + xywh: oldXYWH, + }, + local, + }); + }); + } + abstract addChild(element: GfxModel): void; /** @@ -496,6 +510,7 @@ export abstract class GfxGroupLikeElementModel< setChildIds(value: string[], fromLocal: boolean) { const oldChildIds = this.childIds; this._childIds = value; + this.invalidateXYWH(); this._onChange({ props: { diff --git a/blocksuite/framework/std/src/gfx/model/surface/surface-model.ts b/blocksuite/framework/std/src/gfx/model/surface/surface-model.ts index 5cc92fbc86bbd..dae831be1d040 100644 --- a/blocksuite/framework/std/src/gfx/model/surface/surface-model.ts +++ b/blocksuite/framework/std/src/gfx/model/surface/surface-model.ts @@ -52,6 +52,12 @@ export type MiddlewareCtx = { export type SurfaceMiddleware = (ctx: MiddlewareCtx) => void; export class SurfaceBlockModel extends BlockModel { + private static readonly _groupBoundImpactKeys = new Set([ + 'xywh', + 'rotate', + 'hidden', + ]); + protected _decoratorState = createDecoratorState(); protected _elementCtorMap: Record< @@ -308,6 +314,42 @@ export class SurfaceBlockModel extends BlockModel { Object.keys(payload.props).forEach(key => { model.propsUpdated.next({ key }); }); + + this._refreshParentGroupBoundsForElement(model, payload); + } + + private _refreshParentGroupBounds(id: string, local: boolean) { + const group = this.getGroup(id); + + if (group instanceof GfxGroupLikeElementModel) { + group.refreshXYWH(local); + } + } + + private _refreshParentGroupBoundsForElement( + model: GfxPrimitiveElementModel, + payload: ElementUpdatedData + ) { + if ( + model instanceof GfxGroupLikeElementModel && + ('childIds' in payload.props || 'childIds' in payload.oldValues) + ) { + model.refreshXYWH(payload.local); + return; + } + + const affectedKeys = new Set([ + ...Object.keys(payload.props), + ...Object.keys(payload.oldValues), + ]); + + if ( + Array.from(affectedKeys).some(key => + SurfaceBlockModel._groupBoundImpactKeys.has(key) + ) + ) { + this._refreshParentGroupBounds(model.id, payload.local); + } } private _initElementModels() { @@ -458,6 +500,10 @@ export class SurfaceBlockModel extends BlockModel { ); } + if (payload.model instanceof BlockModel) { + this._refreshParentGroupBounds(payload.id, payload.isLocal); + } + break; case 'delete': if (isGfxGroupCompatibleModel(payload.model)) { @@ -482,6 +528,13 @@ export class SurfaceBlockModel extends BlockModel { } } + if ( + payload.props.key && + SurfaceBlockModel._groupBoundImpactKeys.has(payload.props.key) + ) { + this._refreshParentGroupBounds(payload.id, payload.isLocal); + } + break; } }); diff --git a/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts index 6ae5d6d2c585c..61b6ce1e4b8c7 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts @@ -4,6 +4,7 @@ import type { ConnectorElementModel, GroupElementModel, } from '@blocksuite/affine/model'; +import { serializeXYWH } from '@blocksuite/global/gfx'; import { beforeEach, describe, expect, test } from 'vitest'; import { wait } from '../utils/common.js'; @@ -138,6 +139,29 @@ describe('group', () => { expect(group.childIds).toEqual([id]); }); + + test('group xywh should update when child xywh changes', () => { + const shapeId = model.addElement({ + type: 'shape', + xywh: serializeXYWH(0, 0, 100, 100), + }); + const groupId = model.addElement({ + type: 'group', + children: { + [shapeId]: true, + }, + }); + + const group = model.getElementById(groupId) as GroupElementModel; + + expect(group.xywh).toBe(serializeXYWH(0, 0, 100, 100)); + + model.updateElement(shapeId, { + xywh: serializeXYWH(50, 60, 100, 100), + }); + + expect(group.xywh).toBe(serializeXYWH(50, 60, 100, 100)); + }); }); describe('connector', () => { diff --git a/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-1-importing-1.png b/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-1-importing-1.png index 9b4ec9199ee4e..7a55ae1d4ce6d 100644 Binary files a/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-1-importing-1.png and b/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-1-importing-1.png differ diff --git a/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-2-importing-1.png b/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-2-importing-1.png index 9b4ec9199ee4e..7a55ae1d4ce6d 100644 Binary files a/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-2-importing-1.png and b/blocksuite/integration-test/src/__tests__/main/__screenshots__/snapshot.spec.ts/snapshot-2-importing-1.png differ diff --git a/packages/common/reader/src/doc-parser/delta-to-md/utils/url.ts b/packages/common/reader/src/doc-parser/delta-to-md/utils/url.ts index f5ff37e2495c4..624b61e9c3b0f 100644 --- a/packages/common/reader/src/doc-parser/delta-to-md/utils/url.ts +++ b/packages/common/reader/src/doc-parser/delta-to-md/utils/url.ts @@ -1,5 +1,5 @@ export const encodeLink = (link: string) => encodeURI(link) - .replace(/\(/g, '%28') - .replace(/\)/g, '%29') + .replaceAll('(', '%28') + .replaceAll(')', '%29') .replace(/(\?|&)response-content-disposition=attachment.*$/, ''); diff --git a/packages/frontend/core/src/desktop/dialogs/import/index.tsx b/packages/frontend/core/src/desktop/dialogs/import/index.tsx index 0e1367fba00d1..780fa1da131d1 100644 --- a/packages/frontend/core/src/desktop/dialogs/import/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/import/index.tsx @@ -1,4 +1,10 @@ -import { Button, IconButton, IconType, Modal } from '@affine/component'; +import { + Button, + IconButton, + type IconData, + IconType, + Modal, +} from '@affine/component'; import { getStoreManager } from '@affine/core/blocksuite/manager/store'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; @@ -18,13 +24,14 @@ import { import { DebugLogger } from '@affine/debug'; import { useI18n } from '@affine/i18n'; import track from '@affine/track'; -import { openFilesWith } from '@blocksuite/affine/shared/utils'; +import { openDirectory, openFilesWith } from '@blocksuite/affine/shared/utils'; import type { Workspace } from '@blocksuite/affine/store'; import { DocxTransformer, HtmlTransformer, MarkdownTransformer, NotionHtmlTransformer, + ObsidianTransformer, ZipTransformer, } from '@blocksuite/affine/widgets/linked-doc'; import { @@ -112,10 +119,10 @@ function createFolderStructure( logger.debug('Icon data:', child.icon); try { - let iconData; + let iconData: IconData | undefined; if (child.icon.type === 'emoji') { iconData = { - type: IconType.Emoji as const, + type: IconType.Emoji, unicode: child.icon.content, }; logger.debug('Created emoji icon data:', iconData); @@ -185,11 +192,12 @@ type ImportType = | 'markdown' | 'markdownZip' | 'notion' + | 'obsidian' | 'snapshot' | 'html' | 'docx' | 'dotaffinefile'; -type AcceptType = 'Markdown' | 'Zip' | 'Html' | 'Docx' | 'Skip'; // Skip is used for dotaffinefile +type AcceptType = 'Markdown' | 'Zip' | 'Html' | 'Docx' | 'Directory' | 'Skip'; // Skip is used for dotaffinefile type Status = 'idle' | 'importing' | 'success' | 'error'; type ImportResult = { docIds: string[]; @@ -198,6 +206,10 @@ type ImportResult = { rootFolderId?: string; }; +type ImportedWorkspacePayload = { + workspace: WorkspaceMetadata; +}; + type ImportConfig = { fileOptions: { acceptType: AcceptType; multiple: boolean }; importFunction: ( @@ -264,6 +276,19 @@ const importOptions = [ testId: 'editor-option-menu-import-notion', type: 'notion' as ImportType, }, + { + key: 'obsidian', + label: 'com.affine.import.obsidian', + prefixIcon: ( + + ), + suffixIcon: ( + + ), + suffixTooltip: 'com.affine.import.obsidian.tooltip', + testId: 'editor-option-menu-import-obsidian', + type: 'obsidian' as ImportType, + }, { key: 'docx', label: 'com.affine.import.docx', @@ -445,6 +470,36 @@ const importConfigs: Record = { }; }, }, + obsidian: { + fileOptions: { acceptType: 'Directory', multiple: false }, + importFunction: async ( + docCollection, + files, + _handleImportAffineFile, + _organizeService, + explorerIconService + ) => { + const { docIds, docEmojis } = + await ObsidianTransformer.importObsidianVault({ + collection: docCollection, + schema: getAFFiNEWorkspaceSchema(), + importedFiles: files, + extensions: getStoreManager().config.init().value.get('store'), + }); + + if (explorerIconService) { + for (const [id, emoji] of docEmojis.entries()) { + explorerIconService.setIcon({ + where: 'doc', + id, + icon: { type: IconType.Emoji, unicode: emoji }, + }); + } + } + + return { docIds }; + }, + }, docx: { fileOptions: { acceptType: 'Docx', multiple: false }, importFunction: async (docCollection, file) => { @@ -482,7 +537,7 @@ const importConfigs: Record = { file ) ) - .filter(doc => doc !== undefined) + .filter((doc): doc is NonNullable => doc !== undefined) .map(doc => doc.id); return { @@ -713,14 +768,18 @@ export const ImportDialog = ({ }); return new Promise((resolve, reject) => { - globalDialogService.open('import-workspace', undefined, payload => { - if (payload) { - handleCreatedWorkspace({ metadata: payload.workspace }); - resolve(payload.workspace); - } else { - reject(new Error('No workspace imported')); + globalDialogService.open( + 'import-workspace', + undefined, + (payload?: ImportedWorkspacePayload) => { + if (payload) { + handleCreatedWorkspace({ metadata: payload.workspace }); + resolve(payload.workspace); + } else { + reject(new Error('No workspace imported')); + } } - }); + ); }); }; }, [globalDialogService, handleCreatedWorkspace]); @@ -735,7 +794,9 @@ export const ImportDialog = ({ const files = acceptType === 'Skip' ? [] - : await openFilesWith(acceptType, multiple); + : acceptType === 'Directory' + ? await openDirectory() + : await openFilesWith(acceptType, multiple); if (!files || (files.length === 0 && acceptType !== 'Skip')) { throw new Error( diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 5ac38a9fc25cf..5fb6bcb216dd1 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,6 +1,6 @@ { - "ar": 96, - "ca": 98, + "ar": 100, + "ca": 97, "da": 4, "de": 100, "el-GR": 96, @@ -11,16 +11,16 @@ "fa": 96, "fr": 100, "hi": 1, - "it": 98, + "it": 97, "ja": 96, "ko": 97, "nb-NO": 47, "pl": 98, "pt-BR": 96, - "ru": 98, + "ru": 97, "sv-SE": 96, "uk": 96, "ur": 2, "zh-Hans": 98, - "zh-Hant": 97 + "zh-Hant": 96 } diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index b1aa6dbae09b4..ad38936bcca34 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -2494,6 +2494,14 @@ export function useAFFiNEI18N(): { * `Import your Notion data. Supported import formats: HTML with subpages.` */ ["com.affine.import.notion.tooltip"](): string; + /** + * `Obsidian Vault` + */ + ["com.affine.import.obsidian"](): string; + /** + * `Import an Obsidian vault. Select a folder to import all notes, images, and assets with wikilinks resolved.` + */ + ["com.affine.import.obsidian.tooltip"](): string; /** * `Snapshot` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 66f3d6281be55..019cae2228717 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -622,6 +622,8 @@ "com.affine.import.modal.tip": "If you'd like to request support for additional file types, feel free to let us know on", "com.affine.import.notion": "Notion", "com.affine.import.notion.tooltip": "Import your Notion data. Supported import formats: HTML with subpages.", + "com.affine.import.obsidian": "Obsidian Vault", + "com.affine.import.obsidian.tooltip": "Import an Obsidian vault. Select a folder to import all notes, images, and assets with wikilinks resolved.", "com.affine.import.snapshot": "Snapshot", "com.affine.import.snapshot.tooltip": "Import your AFFiNE workspace and page snapshot file.", "com.affine.import.dotaffinefile": ".affine file", diff --git a/packages/frontend/native/index.js b/packages/frontend/native/index.js index 60139c9ed4123..8100c5c02b60c 100644 --- a/packages/frontend/native/index.js +++ b/packages/frontend/native/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('@affine/native-android-arm64') const bindingPackageVersion = require('@affine/native-android-arm64/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('@affine/native-android-arm-eabi') const bindingPackageVersion = require('@affine/native-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('@affine/native-win32-x64-gnu') const bindingPackageVersion = require('@affine/native-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('@affine/native-win32-x64-msvc') const bindingPackageVersion = require('@affine/native-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('@affine/native-win32-ia32-msvc') const bindingPackageVersion = require('@affine/native-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('@affine/native-win32-arm64-msvc') const bindingPackageVersion = require('@affine/native-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('@affine/native-darwin-universal') const bindingPackageVersion = require('@affine/native-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('@affine/native-darwin-x64') const bindingPackageVersion = require('@affine/native-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('@affine/native-darwin-arm64') const bindingPackageVersion = require('@affine/native-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('@affine/native-freebsd-x64') const bindingPackageVersion = require('@affine/native-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('@affine/native-freebsd-arm64') const bindingPackageVersion = require('@affine/native-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-x64-musl') const bindingPackageVersion = require('@affine/native-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-x64-gnu') const bindingPackageVersion = require('@affine/native-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-arm64-musl') const bindingPackageVersion = require('@affine/native-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-arm64-gnu') const bindingPackageVersion = require('@affine/native-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-arm-musleabihf') const bindingPackageVersion = require('@affine/native-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-arm-gnueabihf') const bindingPackageVersion = require('@affine/native-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-loong64-musl') const bindingPackageVersion = require('@affine/native-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-loong64-gnu') const bindingPackageVersion = require('@affine/native-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-riscv64-musl') const bindingPackageVersion = require('@affine/native-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-riscv64-gnu') const bindingPackageVersion = require('@affine/native-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-ppc64-gnu') const bindingPackageVersion = require('@affine/native-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('@affine/native-linux-s390x-gnu') const bindingPackageVersion = require('@affine/native-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('@affine/native-openharmony-arm64') const bindingPackageVersion = require('@affine/native-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('@affine/native-openharmony-x64') const bindingPackageVersion = require('@affine/native-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('@affine/native-openharmony-arm') const bindingPackageVersion = require('@affine/native-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.26.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.26.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.26.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.26.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) {