From 8406f9656e2f9f51350bcdd33f96804724bde826 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:35:38 +0800 Subject: [PATCH 1/2] perf(editor): improve bounding box calc caching (#14668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #14668** πŸ‘ˆ This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) --- .../affine/widgets/toolbar/src/toolbar.ts | 5 +- .../src/gfx/model/surface/element-model.ts | 67 ++++++++++++------- .../src/gfx/model/surface/surface-model.ts | 53 +++++++++++++++ .../__tests__/edgeless/surface-model.spec.ts | 24 +++++++ 4 files changed, 121 insertions(+), 28 deletions(-) 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', () => { From 0b1a44863f39d983c040e0fe0f3ae2693721cb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Jim=C3=A9nez?= Date: Mon, 16 Mar 2026 10:49:17 -0600 Subject: [PATCH 2/2] feat(editor): add obsidian vault import support (#14593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #14592 ### Description > πŸ€– **Note:** The code in this Pull Request were developed with the assistance of AI, but have been thoroughly reviewed and manually tested. > I noticed there's a check when opening an issue that asks _"Is your content generated by AI?"_, so I mention it here in case it's a deal breaker. If so I understand, you can close the PR, just wanted to share this in case it's useful anyways. This PR introduces **Obsidian Vault Import Support** to AFFiNE. Previously, users migrating from Obsidian had to rely on the generic Markdown importer, which often resulted in broken cross-links, missing directory structures, and metadata conflicts because Obsidian relies heavily on proprietary structures not supported by standard Markdown. This completely new feature makes migrating to AFFiNE easy. **Key Features & Implementations:** 1. **Vault (Directory) Selection** - Utilizes the `openDirectory` blocksuite utility in the import modal to allow users to select an entire folder directly from their filesystem, maintaining file context rather than forcing `.zip` uploads. 2. **Wikilink Resolution (Two-Pass Import)** - Restructured the `importObsidianVault` process into a two-pass architecture. - **Pass 1:** Discovers all files, assigns new AFFiNE document IDs, and maps them efficiently (by title, alias, and filename) into a high-performance hash map. - **Pass 2:** Processes the generic markdown AST and correctly maps custom `[[wikilinks]]` to the actual pre-registered AFFiNE blocksuite document IDs via `obsidianWikilinkToDeltaMatcher`. - Safely strips leading emojis from wikilink aliases to prevent duplicated page icons rendering mid-sentence. 3. **Emoji Metadata & State Fixes** - Implemented an aggressive, single-pass RegExp to extract multiple leading/combining emojis (`Emoji_Presentation` / `\ufe0f`) from H1 headers and Frontmatter. Emojis are assigned specifically to the page icon metadata property and cleanly stripped from the visual document title. - Fixed a core mutation bug where the loop iterating over existing `docMetas` was aggressively overwriting newly minted IDs for the current import batch. This fully resolves the issue where imported pages (especially re-imports) were incorrectly flagged as `trashed`. - Enforces explicit `trash: false` patch instructions. 4. **Syntax Conversion** - Implemented conversion of Obsidian-style Callouts (`> [!NOTE] Title`) into native AFFiNE block formats (`> πŸ’‘ **Title**`). - Hardened the `blockquote` parser so that nested structures (like `> - list items`) are fully preserved instead of discarded. ### UI Changes - Updated the Import Modal to include the "Import Obsidian Vault" flow utilizing the native filesystem directory picker. - Regenerated and synced `i18n-completenesses.json` correctly up to 100% across all supported locales for the new modal string additions. ### Testing Instructions 1. Navigate to the Workspace sidebar and click "Import". 2. Select "Obsidian" and use the directory picker to define a comprehensive Vault folder. 3. Validate that cross-links between documents automatically resolve to their specific AFFiNE instances. 4. Validate documents containing leading Emojis display exactly one Emoji (in the page icon area), and none duplicated in the actual title header. 5. Validate Callouts are rendered cleanly and correctly, and no documents are incorrectly marked as "Trash". ## Summary by CodeRabbit * **New Features** * Import Obsidian vaults with wikilink resolution, emoji/title preservation, asset handling, and automatic document creation. * Folder-based imports via a Directory Picker (with hidden-input fallback) integrated into the import dialog. * **Localization** * Added Obsidian import label and tooltip translations. * **Tests** * Added end-to-end tests validating Obsidian vault import and asset handling. --------- Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: DarkSky --- .../__snapshots__/markdown.unit.spec.ts.snap | 94 +++ .../adapters/fixtures/obsidian/entry.md | 14 + .../adapters/fixtures/obsidian/linked.md | 1 + .../__tests__/adapters/markdown.unit.spec.ts | 185 ++++- .../attachment/src/adapters/markdown.ts | 8 +- .../affine/shared/src/adapters/attachment.ts | 56 +- .../shared/src/adapters/utils/file-path.ts | 19 +- .../affine/shared/src/utils/file/filesys.ts | 114 ++- .../linked-doc/src/import-doc/import-doc.ts | 46 +- .../linked-doc/src/transformers/index.ts | 1 + .../linked-doc/src/transformers/markdown.ts | 179 +++-- .../linked-doc/src/transformers/obsidian.ts | 732 ++++++++++++++++++ .../linked-doc/src/transformers/type.ts | 9 + .../snapshot-1-importing-1.png | Bin 24488 -> 25785 bytes .../snapshot-2-importing-1.png | Bin 24488 -> 25785 bytes .../src/doc-parser/delta-to-md/utils/url.ts | 4 +- .../core/src/desktop/dialogs/import/index.tsx | 89 ++- .../i18n/src/i18n-completenesses.json | 10 +- packages/frontend/i18n/src/i18n.gen.ts | 8 + packages/frontend/i18n/src/resources/en.json | 2 + packages/frontend/native/index.js | 104 +-- 21 files changed, 1515 insertions(+), 160 deletions(-) create mode 100644 blocksuite/affine/all/src/__tests__/adapters/__snapshots__/markdown.unit.spec.ts.snap create mode 100644 blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/entry.md create mode 100644 blocksuite/affine/all/src/__tests__/adapters/fixtures/obsidian/linked.md create mode 100644 blocksuite/affine/widgets/linked-doc/src/transformers/obsidian.ts 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/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 9b4ec9199ee4e6af8b905b7f93b052b632cddd66..7a55ae1d4ce6dc3ccb3edbea66786bc0e05a4703 100644 GIT binary patch literal 25785 zcmeHQX;f2Z8omUC0E0xWMr8?ADt0WOr$qvR5EVwzYG$SkVp)O*5g9~GQwWfdggVr5 zWT`kU7B#W7Q>+S6qU?}h9Z->^)P*gZf=~&NgoGpzNamsfp}%`NJu~0Gd%oOnd%x#> z-sQfz_tYQv?R0Wj>i|KJQ~0jXKSPkU0D^3;BW>Z9_dZzv2!cGI@X&1$nQ%y=6!QE-61Y{_yVTZ@c{7)em_VU@5{n;Y^*+qGx}l%R6aU8r+^V?pdY z$r+z5n#Y3`YrUvpv%Qs7(5Z(b9`+WiArPPjTg&AU;+G>3naj(2wk=;+Gl9i|GMp^C zu>_trHY(43yc3@DJDa_*8d+=E4ayBiBIV&14%>y#?f>QNh;F|{-4JI{D8X55$I{~0 zQhN2P&GnQ;-E20z>A~_Mm%4c_k^nNkYf(2O5ZwQ+;55Slcd4=*s%uc@kqhM3HT~Ck zgOMUf&29@6N9kbarPDG?dBM5M-Q=-+b~wYvPb1A0L7izbNlRqPDj-M#PEmN_iJPf+O$c}^)VKQOsh_)uabg!1A4yh79Ug2u21f1 zJJy%ciPeiK#J}%=IH>b>%NB^{_!}_sjAO3|>@f7r4DHv<*l|41|BKS5mPhw$Z&@3f z^imfdlgIa$){^`0VM9e0Y($76Kqp(edd0h!sH9D_wlPTq+RZeQ@L6S(t~ZY>l5SK` z`9{fYCI&w(tcsve&ZC?C9Uu0k@n`N1A(?FdmTbL*+9qxCEi8%4CO1wFMsvEQ^%IR` zVZcy+YG1YY^nKl@PA*S+leO6v)O!w%68N0>+TmIRB*G(-b${B$X^tiTyQIy#MqH(O z?!}>h)Tp|hR;b7qQ$+g0#!cE&wknD!*dHU~2YxF#?Nfh5{PDwr1f6>CF-xp@!&s{p z?w@{KIRvrY`YK}^2ARXgE>0jul&T}YUqWI`s)O$2Sgd5DBT_zYA2q5`1h7hsN1r7G zd&!JaNrGmOZwjn-nLjKm#8nckB_>KlPToC8%1aVuPChR|7gA--{w2X<8j&n}fL0^P z=)=xvAEHfW^|XUl!R6x_I^NEr`Ykj&MU6B&rBh~O9^D3u>V;v@-+UvRk-x&?AV&}g zLwCi7EYTr|>1sH{chr7egb$3PjZTCOX-9aXwX&%vAq6~PDQ>)JRv%Ad7>63oCgbTb zenMMnf>AemwsEWLd-H3~y|uxE)z!PkkuRNCEl@UUga&nyGTO5)`Qf)}iBh^{aL^RO zFlUtK7nKH(ycN;6smmV2QDH@X|sbZP1>B2WoFxKk=9Su>t0#B7bjM z1}0OUOIFFhBGS#zPW!UWr__b^vCEczK5v!gG&L-4GW&P(o}KO;_^Bh`_5G_PuIics zmeGx$x-N;Z{rO~NlRi|Kx6H!e<2fwIL}w^Wor*0i40BqohNr0Lt~0hcH;3%Qjow^I z!xKX-Na5Zxl)$SkT@s{#r+O}h6rsf4i&O@cu0Bl)>O4tvofnQ3;!P_n*H7ZEz6%e{ zG-;T)aq^x`Q{$8}rV&w2Q;E|V6gM%jEk;~arMe1pd$Tu*U=igF4 zet%Pu;%2J2mVALC3}~BJP2I3y;V-TCVByA64yOm(s@TD1KQ>H3Z&@3TC=pGE_m!e| zTg*|6Ky>GkBXS?F65uk#TdUJ#%>ya;<8ncFou(yBX4K;R%uk(}tDS?jsxo6oN2)tj zOm3@6J8~;Y#8wZ5n6sW5Nrv0#RVJz~4OS{1$ELyJ6tCAWu~7}4#6Y8~yl2SDHyvjj;SDnH)9+!;(-r-`%*pm?MP{ODFE8%Ecr$78zF+^8g3HnR zJ(Dje?zNS$sz(YVUZhteGWBDU{+=Yg>%`?Geia>NBut%1ZKHzq1M8kOkd&ZP{NR;+ zKF=`Xc1aYZd9WhKp=kZWl07zhuugQRkgu#1oOH&x!`;nE#S)q_I6>#%%(8Zn(* z>h_@_S{^dhq^l< zB;>D`#=37L;J5qYzk=AoM^^EPLFzl16OX$JCqq+Va<82nJl_V}i!C9oZVS6a{RGpK zDIn@GH(q!3cF6S8qA%zL81oB7lG`1auZMO}y2={niF4%pq}(YO=YB9{+)|s2O5b`} zMGYe-hB5ivk;It^)tii%sE;{Bm|4n|n{loCNjd4WJUeK9qJRDtseQz&869)nWcab9 z`KIbXi_6xdhHE=?ITi{T`6UaAOZ2+aejqM>)%SMJ?yfv4nLQ&J9qc&Y_ICM&sC4tA^u?j?1Tscw6m_j0-gjGVGjYS85mvUe1rRd zTxK0LfCGR7fP+`gF5pSP2QPI2*h^p+W^E(D0l)#k!OJKN_=$iIfS(Bb#JNNt-~+%1 zfDeAg2eo@EcFxrTEF?mK4+VTE;6uI6c7vD-asmjzP9P$qPAb-zJ%J#`ng(w|JEA7 zyhzlqan?Z2K+dxS18@Lv0C4czv&BF!fnEZ=1bR7_Cjfi^_yF($-~+%1v*iT<2Xiv` zP1pswxtE);7Jjp&?LkwlXbJ5K>l@7L8i=>!S$k3!r?+bDSIb?RHZRJ9{S#qZ`{>EK zxn0d3aqfARRdc7K#cQ$P(i$)vkR1RD&s&FpR*d03H-@7lEq{;)-7+ zh+5uQ`40TSfSB2D1i*PG5TAiy6a?}hYw=5_Xsiz^27L3x4H7tU1cis~3$5H9|H*#< DEpaol literal 24488 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 DEpaol literal 24488 zcmeI4dsGuw9>*skKzvlOYUxrXRISv~BR&Cz0Jd19R;adJECQiLMVmrEXcNt2pty@& zOIUS%P#&?>N?jjdQDHT&R!Gz+R3Z-_VS*?SK!%X_Yi5IwB;9km=j=cIW6%8~XKrS0 z?)>tb@8|csznjT?{eEKn3ljq-LJ;&q!kV}Z5acC=AW|LK$GKAknU34gKj26r815lOr0cm*<_K_dGF#TG&Y;**A&v9JychdF_P&vraJ(&g_b z75n|BYsNGpku@#L7&O6ifLO>D;4xkyRJ58*RNbs9Pr#TG5;Z&izq3SHq2n@tT)e_ zK4YZNXo8li$nY~~&Tth;X#O-e1a%n%f>7NH50&unZilAlSiG;mXz%DK&~@B&_(ij7 z;8#7+h+UKM7}jmtcJW+RtolxUmhwPwQX)rrVSDx^tEzoFoqu0ze9%0iSAY@5kwJdxE} zP1%Vj2s7<_bRC8mqFLK34~ZAEw4VmxF-H!V*AH~o(rP(x_LiLX9(|XGx&f|x-T94v z%olVQQ%AzN;eCjA`b>$$VZEpCyIEoKuZC%68_F>6)0jT8f9+OfQPK=Qset_Gia1f- zX8b{OO<5EF*WuJVE5^iHzUuI0<&b4U{S?XLmY8?BBR_@SyRr~_&%=7Tw{UWsyp+>4 zft-4&U^}+>;gHhIM_I2R237Sj^Yw!v!HZO6&$z%zl{U0R=ja=-JvVPUeN7!%#YcbP zBg!9a5eAmUz zm61rJe~Rq-IE@)KNq?mT7Ws_clb;ibsA-ptDvSb4Y&N~U6%DJkIuPwDT*PKx!u!|G zE)DLuM3wArV_nqLa@meS!?G_qDO!5 zrTejWkFvNu+E&(!wQ?POei^cbXS2#mn7S;F!FPFE;g>FSwrtHx&1llqO6)7CE9di) zsX~=XrD>_cZ@~Qn%=%=m(By%`uXBS6**TvaV7!@&n%KTEI~@NKSTTpMvb(#xA6rgI z^0?#+CX&@3G?`8!{x8RT6~(J%+I8PGG*EQs%51-W6Ray+QihnQBiT2)E8zxnW9wqx zGOKCDQ3N|XnwKvWcqlKBAo04NzqgsO!)83=RZM~Hc`1gtjo}iUcULUrj^@$ELg%JX zqFIe&wfy9*RvtF1xBQ%ZdPXUd7MR3eF79QUvxQN&l@`-ip)_OD#*%D%xoiB`V}&Rb zp5C0ezDUa!vO}u>PA8!^D;oMbS9eFVG{+1EgJ!V(lulRR(7xAR;?Kr72Kqb-76H4( z5v$gFlSVa&)Cbw<6aC^R$8M z^&8(@5%A;TiNbOkZkmTg&$u0I*?D7* zOWU#PA*Z-IEtY#(@2ii)sg~pYRjW3HOm`6!F`dXGS>LO?wm*15U0zP+qxx7u1n;uVi8Bm65&9$!G{(!F9sqO zp6>&Ht_Wv{ty(_X70tB{U#;}l4SAIXpHG>2wYR()MI;iO+0ofiqpd;=r7;-Vyw{&P zNk45J>K^OY;GjZV&Zoz?MeU&>%c_6aDV&dX3bPrEBonFFnVl?pcxyy)SXmW`moH3jT$Ejf+=WXKt?Gfwa{0=s?uF^X z+au}P^o%nrd61*4=tv*~nOk@dCKUpNk#LDSsIJfU4F zECGP%x7>! z(H`@yeL=>({LP5)i=ldW_s&dAJG7}GC$aM^*R((&lY7i0-=Cw7Ke~3xtxK}I@zeFn zFnmAd1M!YW*UvA2tqWs{d0QRtBMJ8#$%-*DgPs9NV00n@;lbRHu08juZ02Dmd z<3Irk3P?~uf&voM3ZPbSMMAKw=+2O!fPDVLJ(w!E00E)^qHx|)0Fow1ny#1tk|s!+ zV1W%Ruz{=q7TDa85M%|nFTkuB%$mWh`ALQZC;$`y3IK)idpKQxwg3xkE?cZ_Pmlo2%`9mKMTMkNw~5FXn5vM;0@r7r)!Tu2|x)z34j7X0iXa- z04M+y015yFfC4}Ppx}?9zzjzg1dTuOanV)88c2I}v$T-lih^~!$Dfj)usSjB>{}UM{Tp2 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) {