mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-26 14:58:55 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { sha } from '@blocksuite/global/utils';
|
||||
import {
|
||||
type AssetsManager,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotPayload,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotPayload,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotPayload,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
type ToBlockSnapshotPayload,
|
||||
type ToDocSnapshotPayload,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import { AdapterFactoryIdentifier } from './type.js';
|
||||
|
||||
export type Attachment = File[];
|
||||
|
||||
type AttachmentToSliceSnapshotPayload = {
|
||||
file: Attachment;
|
||||
assets?: AssetsManager;
|
||||
blockVersions: Record<string, number>;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class AttachmentAdapter extends BaseAdapter<Attachment> {
|
||||
override fromBlockSnapshot(
|
||||
_payload: FromBlockSnapshotPayload
|
||||
): Promise<FromBlockSnapshotResult<Attachment>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'AttachmentAdapter.fromBlockSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override fromDocSnapshot(
|
||||
_payload: FromDocSnapshotPayload
|
||||
): Promise<FromDocSnapshotResult<Attachment>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'AttachmentAdapter.fromDocSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override fromSliceSnapshot(
|
||||
payload: FromSliceSnapshotPayload
|
||||
): Promise<FromSliceSnapshotResult<Attachment>> {
|
||||
const attachments: Attachment = [];
|
||||
for (const contentSlice of payload.snapshot.content) {
|
||||
if (contentSlice.type === 'block') {
|
||||
const { flavour, props } = contentSlice;
|
||||
if (flavour === 'affine:attachment') {
|
||||
const { sourceId } = props;
|
||||
const file = payload.assets?.getAssets().get(sourceId as string) as
|
||||
| File
|
||||
| undefined;
|
||||
if (file) {
|
||||
attachments.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve({ file: attachments, assetsIds: [] });
|
||||
}
|
||||
|
||||
override toBlockSnapshot(
|
||||
_payload: ToBlockSnapshotPayload<Attachment>
|
||||
): Promise<BlockSnapshot> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'AttachmentAdapter.toBlockSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override toDocSnapshot(
|
||||
_payload: ToDocSnapshotPayload<Attachment>
|
||||
): Promise<DocSnapshot> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'AttachmentAdapter.toDocSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override async toSliceSnapshot(
|
||||
payload: AttachmentToSliceSnapshotPayload
|
||||
): Promise<SliceSnapshot | null> {
|
||||
const content: SliceSnapshot['content'] = [];
|
||||
for (const item of payload.file) {
|
||||
const blobId = await sha(await item.arrayBuffer());
|
||||
payload.assets?.getAssets().set(blobId, item);
|
||||
await payload.assets?.writeToBlob(blobId);
|
||||
content.push({
|
||||
type: 'block',
|
||||
flavour: 'affine:attachment',
|
||||
id: nanoid(),
|
||||
props: {
|
||||
name: item.name,
|
||||
size: item.size,
|
||||
type: item.type,
|
||||
embed: false,
|
||||
style: 'horizontalThin',
|
||||
index: 'a0',
|
||||
xywh: '[0,0,0,0]',
|
||||
rotate: 0,
|
||||
sourceId: blobId,
|
||||
},
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
if (content.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'slice',
|
||||
content,
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const AttachmentAdapterFactoryIdentifier =
|
||||
AdapterFactoryIdentifier('Attachment');
|
||||
|
||||
export const AttachmentAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(AttachmentAdapterFactoryIdentifier, () => ({
|
||||
get: (job: Job) => new AttachmentAdapter(job),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import { AttachmentAdapterFactoryExtension } from './attachment.js';
|
||||
import { BlockHtmlAdapterExtensions } from './html-adapter/block-matcher.js';
|
||||
import { HtmlAdapterFactoryExtension } from './html-adapter/html.js';
|
||||
import { ImageAdapterFactoryExtension } from './image.js';
|
||||
import { BlockMarkdownAdapterExtensions } from './markdown/block-matcher.js';
|
||||
import { MarkdownAdapterFactoryExtension } from './markdown/markdown.js';
|
||||
import { MixTextAdapterFactoryExtension } from './mix-text.js';
|
||||
import { BlockNotionHtmlAdapterExtensions } from './notion-html/block-matcher.js';
|
||||
import { NotionHtmlAdapterFactoryExtension } from './notion-html/notion-html.js';
|
||||
import { NotionTextAdapterFactoryExtension } from './notion-text.js';
|
||||
import { BlockPlainTextAdapterExtensions } from './plain-text/block-matcher.js';
|
||||
import { PlainTextAdapterFactoryExtension } from './plain-text/plain-text.js';
|
||||
|
||||
export const AdapterFactoryExtensions: ExtensionType[] = [
|
||||
AttachmentAdapterFactoryExtension,
|
||||
ImageAdapterFactoryExtension,
|
||||
MarkdownAdapterFactoryExtension,
|
||||
PlainTextAdapterFactoryExtension,
|
||||
HtmlAdapterFactoryExtension,
|
||||
NotionTextAdapterFactoryExtension,
|
||||
NotionHtmlAdapterFactoryExtension,
|
||||
MixTextAdapterFactoryExtension,
|
||||
];
|
||||
|
||||
export const BlockAdapterMatcherExtensions: ExtensionType[] = [
|
||||
BlockPlainTextAdapterExtensions,
|
||||
BlockMarkdownAdapterExtensions,
|
||||
BlockHtmlAdapterExtensions,
|
||||
BlockNotionHtmlAdapterExtensions,
|
||||
].flat();
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
EmbedFigmaBlockHtmlAdapterExtension,
|
||||
embedFigmaBlockHtmlAdapterMatcher,
|
||||
EmbedGithubBlockHtmlAdapterExtension,
|
||||
embedGithubBlockHtmlAdapterMatcher,
|
||||
embedLinkedDocBlockHtmlAdapterMatcher,
|
||||
EmbedLinkedDocHtmlAdapterExtension,
|
||||
EmbedLoomBlockHtmlAdapterExtension,
|
||||
embedLoomBlockHtmlAdapterMatcher,
|
||||
EmbedSyncedDocBlockHtmlAdapterExtension,
|
||||
embedSyncedDocBlockHtmlAdapterMatcher,
|
||||
EmbedYoutubeBlockHtmlAdapterExtension,
|
||||
embedYoutubeBlockHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
ListBlockHtmlAdapterExtension,
|
||||
listBlockHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-list';
|
||||
import {
|
||||
ParagraphBlockHtmlAdapterExtension,
|
||||
paragraphBlockHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-paragraph';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import {
|
||||
BookmarkBlockHtmlAdapterExtension,
|
||||
bookmarkBlockHtmlAdapterMatcher,
|
||||
} from '../../../bookmark-block/adapters/html.js';
|
||||
import {
|
||||
CodeBlockHtmlAdapterExtension,
|
||||
codeBlockHtmlAdapterMatcher,
|
||||
} from '../../../code-block/adapters/html.js';
|
||||
import {
|
||||
DatabaseBlockHtmlAdapterExtension,
|
||||
databaseBlockHtmlAdapterMatcher,
|
||||
} from '../../../database-block/adapters/html.js';
|
||||
import {
|
||||
DividerBlockHtmlAdapterExtension,
|
||||
dividerBlockHtmlAdapterMatcher,
|
||||
} from '../../../divider-block/adapters/html.js';
|
||||
import {
|
||||
ImageBlockHtmlAdapterExtension,
|
||||
imageBlockHtmlAdapterMatcher,
|
||||
} from '../../../image-block/adapters/html.js';
|
||||
import {
|
||||
RootBlockHtmlAdapterExtension,
|
||||
rootBlockHtmlAdapterMatcher,
|
||||
} from '../../../root-block/adapters/html.js';
|
||||
|
||||
export const defaultBlockHtmlAdapterMatchers = [
|
||||
listBlockHtmlAdapterMatcher,
|
||||
paragraphBlockHtmlAdapterMatcher,
|
||||
codeBlockHtmlAdapterMatcher,
|
||||
dividerBlockHtmlAdapterMatcher,
|
||||
imageBlockHtmlAdapterMatcher,
|
||||
rootBlockHtmlAdapterMatcher,
|
||||
embedYoutubeBlockHtmlAdapterMatcher,
|
||||
embedFigmaBlockHtmlAdapterMatcher,
|
||||
embedLoomBlockHtmlAdapterMatcher,
|
||||
embedGithubBlockHtmlAdapterMatcher,
|
||||
bookmarkBlockHtmlAdapterMatcher,
|
||||
databaseBlockHtmlAdapterMatcher,
|
||||
embedLinkedDocBlockHtmlAdapterMatcher,
|
||||
embedSyncedDocBlockHtmlAdapterMatcher,
|
||||
];
|
||||
|
||||
export const BlockHtmlAdapterExtensions: ExtensionType[] = [
|
||||
ListBlockHtmlAdapterExtension,
|
||||
ParagraphBlockHtmlAdapterExtension,
|
||||
CodeBlockHtmlAdapterExtension,
|
||||
DividerBlockHtmlAdapterExtension,
|
||||
ImageBlockHtmlAdapterExtension,
|
||||
RootBlockHtmlAdapterExtension,
|
||||
EmbedYoutubeBlockHtmlAdapterExtension,
|
||||
EmbedFigmaBlockHtmlAdapterExtension,
|
||||
EmbedLoomBlockHtmlAdapterExtension,
|
||||
EmbedGithubBlockHtmlAdapterExtension,
|
||||
BookmarkBlockHtmlAdapterExtension,
|
||||
DatabaseBlockHtmlAdapterExtension,
|
||||
EmbedLinkedDocHtmlAdapterExtension,
|
||||
EmbedSyncedDocBlockHtmlAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,234 @@
|
||||
import type {
|
||||
HtmlAST,
|
||||
HtmlASTToDeltaMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { collapseWhiteSpace } from 'collapse-white-space';
|
||||
import type { Element } from 'hast';
|
||||
|
||||
const isElement = (ast: HtmlAST): ast is Element => {
|
||||
return ast.type === 'element';
|
||||
};
|
||||
|
||||
const textLikeElementTags = new Set(['span', 'bdi', 'bdo', 'ins']);
|
||||
const listElementTags = new Set(['ol', 'ul']);
|
||||
const strongElementTags = new Set(['strong', 'b']);
|
||||
const italicElementTags = new Set(['i', 'em']);
|
||||
|
||||
export const htmlTextToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'text',
|
||||
match: ast => ast.type === 'text',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
const { options } = context;
|
||||
options.trim ??= true;
|
||||
|
||||
if (options.pre) {
|
||||
return [{ insert: ast.value }];
|
||||
}
|
||||
|
||||
const value = options.trim
|
||||
? collapseWhiteSpace(ast.value, { trim: options.trim })
|
||||
: collapseWhiteSpace(ast.value);
|
||||
return value ? [{ insert: value }] : [];
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlTextLikeElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'text-like-element',
|
||||
match: ast => isElement(ast) && textLikeElementTags.has(ast.tagName),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false })
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlListToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'list-element',
|
||||
match: ast => isElement(ast) && listElementTags.has(ast.tagName),
|
||||
toDelta: () => {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlStrongElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'strong-element',
|
||||
match: ast => isElement(ast) && strongElementTags.has(ast.tagName),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, bold: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlItalicElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'italic-element',
|
||||
match: ast => isElement(ast) && italicElementTags.has(ast.tagName),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, italic: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
export const htmlCodeElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'code-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'code',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, code: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlDelElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'del-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'del',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, strike: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlUnderlineElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'underline-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'u',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, underline: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlLinkElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'link-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'a',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const href = ast.properties?.href;
|
||||
if (typeof href !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const { configs } = context;
|
||||
const baseUrl = configs.get('docLinkBaseUrl') ?? '';
|
||||
if (baseUrl && href.startsWith(baseUrl)) {
|
||||
const path = href.substring(baseUrl.length);
|
||||
// ^ - /{pageId}?mode={mode}&blockIds={blockIds}&elementIds={elementIds}
|
||||
const match = path.match(/^\/([^?]+)(\?.*)?$/);
|
||||
if (match) {
|
||||
const pageId = match?.[1];
|
||||
const search = match?.[2];
|
||||
const searchParams = search ? new URLSearchParams(search) : undefined;
|
||||
const mode = searchParams?.get('mode');
|
||||
const blockIds = searchParams?.get('blockIds')?.split(',');
|
||||
const elementIds = searchParams?.get('elementIds')?.split(',');
|
||||
|
||||
return [
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId,
|
||||
params: {
|
||||
mode:
|
||||
mode && ['edgeless', 'page'].includes(mode)
|
||||
? (mode as 'edgeless' | 'page')
|
||||
: undefined,
|
||||
blockIds,
|
||||
elementIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
if (href.startsWith('http')) {
|
||||
delta.attributes = {
|
||||
...delta.attributes,
|
||||
link: href,
|
||||
};
|
||||
return delta;
|
||||
}
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlMarkElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'mark-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'mark',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlBrElementToDeltaMatcher: HtmlASTToDeltaMatcher = {
|
||||
name: 'br-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'br',
|
||||
toDelta: () => {
|
||||
return [{ insert: '\n' }];
|
||||
},
|
||||
};
|
||||
|
||||
export const htmlInlineToDeltaMatchers: HtmlASTToDeltaMatcher[] = [
|
||||
htmlTextToDeltaMatcher,
|
||||
htmlTextLikeElementToDeltaMatcher,
|
||||
htmlStrongElementToDeltaMatcher,
|
||||
htmlItalicElementToDeltaMatcher,
|
||||
htmlCodeElementToDeltaMatcher,
|
||||
htmlDelElementToDeltaMatcher,
|
||||
htmlUnderlineElementToDeltaMatcher,
|
||||
htmlLinkElementToDeltaMatcher,
|
||||
htmlMarkElementToDeltaMatcher,
|
||||
htmlBrElementToDeltaMatcher,
|
||||
];
|
||||
@@ -0,0 +1,145 @@
|
||||
import { generateDocUrl } from '@blocksuite/affine-block-embed';
|
||||
import type {
|
||||
InlineDeltaToHtmlAdapterMatcher,
|
||||
InlineHtmlAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const boldDeltaToHtmlAdapterMatcher: InlineDeltaToHtmlAdapterMatcher = {
|
||||
name: 'bold',
|
||||
match: delta => !!delta.attributes?.bold,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'strong',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const italicDeltaToHtmlAdapterMatcher: InlineDeltaToHtmlAdapterMatcher =
|
||||
{
|
||||
name: 'italic',
|
||||
match: delta => !!delta.attributes?.italic,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'em',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const strikeDeltaToHtmlAdapterMatcher: InlineDeltaToHtmlAdapterMatcher =
|
||||
{
|
||||
name: 'strike',
|
||||
match: delta => !!delta.attributes?.strike,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'del',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const inlineCodeDeltaToMarkdownAdapterMatcher: InlineDeltaToHtmlAdapterMatcher =
|
||||
{
|
||||
name: 'inlineCode',
|
||||
match: delta => !!delta.attributes?.code,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'code',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const underlineDeltaToHtmlAdapterMatcher: InlineDeltaToHtmlAdapterMatcher =
|
||||
{
|
||||
name: 'underline',
|
||||
match: delta => !!delta.attributes?.underline,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'u',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const referenceDeltaToHtmlAdapterMatcher: InlineDeltaToHtmlAdapterMatcher =
|
||||
{
|
||||
name: 'reference',
|
||||
match: delta => !!delta.attributes?.reference,
|
||||
toAST: (delta, context) => {
|
||||
let hast: InlineHtmlAST = {
|
||||
type: 'text',
|
||||
value: delta.insert,
|
||||
};
|
||||
const reference = delta.attributes?.reference;
|
||||
if (!reference) {
|
||||
return hast;
|
||||
}
|
||||
|
||||
const { configs } = context;
|
||||
const title = configs.get(`title:${reference.pageId}`);
|
||||
const url = generateDocUrl(
|
||||
configs.get('docLinkBaseUrl') ?? '',
|
||||
String(reference.pageId),
|
||||
reference.params ?? Object.create(null)
|
||||
);
|
||||
if (title) {
|
||||
hast.value = title;
|
||||
}
|
||||
hast = {
|
||||
type: 'element',
|
||||
tagName: 'a',
|
||||
properties: {
|
||||
href: url,
|
||||
},
|
||||
children: [hast],
|
||||
};
|
||||
|
||||
return hast;
|
||||
},
|
||||
};
|
||||
|
||||
export const linkDeltaToHtmlAdapterMatcher: InlineDeltaToHtmlAdapterMatcher = {
|
||||
name: 'link',
|
||||
match: delta => !!delta.attributes?.link,
|
||||
toAST: (delta, _) => {
|
||||
const hast: InlineHtmlAST = {
|
||||
type: 'text',
|
||||
value: delta.insert,
|
||||
};
|
||||
const link = delta.attributes?.link;
|
||||
if (!link) {
|
||||
return hast;
|
||||
}
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'a',
|
||||
properties: {
|
||||
href: link,
|
||||
},
|
||||
children: [hast],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const inlineDeltaToHtmlAdapterMatchers: InlineDeltaToHtmlAdapterMatcher[] =
|
||||
[
|
||||
boldDeltaToHtmlAdapterMatcher,
|
||||
italicDeltaToHtmlAdapterMatcher,
|
||||
strikeDeltaToHtmlAdapterMatcher,
|
||||
underlineDeltaToHtmlAdapterMatcher,
|
||||
inlineCodeDeltaToMarkdownAdapterMatcher,
|
||||
referenceDeltaToHtmlAdapterMatcher,
|
||||
linkDeltaToHtmlAdapterMatcher,
|
||||
];
|
||||
@@ -0,0 +1,385 @@
|
||||
import {
|
||||
DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
type AdapterContext,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
BlockHtmlAdapterMatcherIdentifier,
|
||||
HastUtils,
|
||||
type HtmlAST,
|
||||
HtmlDeltaConverter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
type AssetsManager,
|
||||
ASTWalker,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
BlockSnapshotSchema,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotPayload,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotPayload,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotPayload,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
type ToBlockSnapshotPayload,
|
||||
type ToDocSnapshotPayload,
|
||||
} from '@blocksuite/store';
|
||||
import type { Root } from 'hast';
|
||||
import rehypeParse from 'rehype-parse';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import { unified } from 'unified';
|
||||
|
||||
import { AdapterFactoryIdentifier } from '../type.js';
|
||||
import { defaultBlockHtmlAdapterMatchers } from './block-matcher.js';
|
||||
import { htmlInlineToDeltaMatchers } from './delta-converter/html-inline.js';
|
||||
import { inlineDeltaToHtmlAdapterMatchers } from './delta-converter/inline-delta.js';
|
||||
|
||||
export type Html = string;
|
||||
|
||||
type HtmlToSliceSnapshotPayload = {
|
||||
file: Html;
|
||||
assets?: AssetsManager;
|
||||
blockVersions: Record<string, number>;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class HtmlAdapter extends BaseAdapter<Html> {
|
||||
private _astToHtml = (ast: Root) => {
|
||||
return unified().use(rehypeStringify).stringify(ast);
|
||||
};
|
||||
|
||||
private _traverseHtml = async (
|
||||
html: HtmlAST,
|
||||
snapshot: BlockSnapshot,
|
||||
assets?: AssetsManager
|
||||
) => {
|
||||
const walker = new ASTWalker<HtmlAST, BlockSnapshot>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is HtmlAST =>
|
||||
'type' in (node as object) && (node as HtmlAST).type !== undefined
|
||||
);
|
||||
walker.setEnter(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.toMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
HtmlAST,
|
||||
BlockSnapshot,
|
||||
HtmlDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
};
|
||||
await matcher.toBlockSnapshot.enter?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
walker.setLeave(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.toMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
HtmlAST,
|
||||
BlockSnapshot,
|
||||
HtmlDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
};
|
||||
await matcher.toBlockSnapshot.leave?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
return walker.walk(html, snapshot);
|
||||
};
|
||||
|
||||
private _traverseSnapshot = async (
|
||||
snapshot: BlockSnapshot,
|
||||
html: HtmlAST,
|
||||
assets?: AssetsManager
|
||||
) => {
|
||||
const assetsIds: string[] = [];
|
||||
const walker = new ASTWalker<BlockSnapshot, HtmlAST>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is BlockSnapshot =>
|
||||
BlockSnapshotSchema.safeParse(node).success
|
||||
);
|
||||
walker.setEnter(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.fromMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
BlockSnapshot,
|
||||
HtmlAST,
|
||||
HtmlDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
updateAssetIds: (assetsId: string) => {
|
||||
assetsIds.push(assetsId);
|
||||
},
|
||||
};
|
||||
await matcher.fromBlockSnapshot.enter?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
walker.setLeave(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.fromMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
BlockSnapshot,
|
||||
HtmlAST,
|
||||
HtmlDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
};
|
||||
await matcher.fromBlockSnapshot.leave?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
ast: (await walker.walk(snapshot, html)) as Root,
|
||||
assetsIds,
|
||||
};
|
||||
};
|
||||
|
||||
deltaConverter: HtmlDeltaConverter;
|
||||
|
||||
constructor(
|
||||
job: Job,
|
||||
readonly blockMatchers: BlockHtmlAdapterMatcher[] = defaultBlockHtmlAdapterMatchers
|
||||
) {
|
||||
super(job);
|
||||
this.deltaConverter = new HtmlDeltaConverter(
|
||||
job.adapterConfigs,
|
||||
inlineDeltaToHtmlAdapterMatchers,
|
||||
htmlInlineToDeltaMatchers
|
||||
);
|
||||
}
|
||||
|
||||
private _htmlToAst(html: Html) {
|
||||
return unified().use(rehypeParse).parse(html);
|
||||
}
|
||||
|
||||
override async fromBlockSnapshot(
|
||||
payload: FromBlockSnapshotPayload
|
||||
): Promise<FromBlockSnapshotResult<string>> {
|
||||
const root: Root = {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'doctype',
|
||||
},
|
||||
],
|
||||
};
|
||||
const { ast, assetsIds } = await this._traverseSnapshot(
|
||||
payload.snapshot,
|
||||
root,
|
||||
payload.assets
|
||||
);
|
||||
return {
|
||||
file: this._astToHtml(ast),
|
||||
assetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
override async fromDocSnapshot(
|
||||
payload: FromDocSnapshotPayload
|
||||
): Promise<FromDocSnapshotResult<string>> {
|
||||
const { file, assetsIds } = await this.fromBlockSnapshot({
|
||||
snapshot: payload.snapshot.blocks,
|
||||
assets: payload.assets,
|
||||
});
|
||||
return {
|
||||
file: file.replace(
|
||||
'<!--BlockSuiteDocTitlePlaceholder-->',
|
||||
`<h1>${payload.snapshot.meta.title}</h1>`
|
||||
),
|
||||
assetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
override async fromSliceSnapshot(
|
||||
payload: FromSliceSnapshotPayload
|
||||
): Promise<FromSliceSnapshotResult<string>> {
|
||||
let buffer = '';
|
||||
const sliceAssetsIds: string[] = [];
|
||||
for (const contentSlice of payload.snapshot.content) {
|
||||
const root: Root = {
|
||||
type: 'root',
|
||||
children: [],
|
||||
};
|
||||
const { ast, assetsIds } = await this._traverseSnapshot(
|
||||
contentSlice,
|
||||
root,
|
||||
payload.assets
|
||||
);
|
||||
sliceAssetsIds.push(...assetsIds);
|
||||
buffer += this._astToHtml(ast);
|
||||
}
|
||||
const html = buffer;
|
||||
return {
|
||||
file: html,
|
||||
assetsIds: sliceAssetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
override toBlockSnapshot(
|
||||
payload: ToBlockSnapshotPayload<string>
|
||||
): Promise<BlockSnapshot> {
|
||||
const htmlAst = this._htmlToAst(payload.file);
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
return this._traverseHtml(
|
||||
htmlAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets
|
||||
);
|
||||
}
|
||||
|
||||
override async toDocSnapshot(
|
||||
payload: ToDocSnapshotPayload<string>
|
||||
): Promise<DocSnapshot> {
|
||||
const htmlAst = this._htmlToAst(payload.file);
|
||||
const titleAst = HastUtils.querySelector(htmlAst, 'title');
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
return {
|
||||
type: 'page',
|
||||
meta: {
|
||||
id: nanoid(),
|
||||
title: HastUtils.getTextContent(titleAst, 'Untitled'),
|
||||
createDate: Date.now(),
|
||||
tags: [],
|
||||
},
|
||||
blocks: {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:page',
|
||||
props: {
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: this.deltaConverter.astToDelta(
|
||||
titleAst ?? {
|
||||
type: 'text',
|
||||
value: 'Untitled',
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:surface',
|
||||
props: {
|
||||
elements: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
await this._traverseHtml(
|
||||
htmlAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async toSliceSnapshot(
|
||||
payload: HtmlToSliceSnapshotPayload
|
||||
): Promise<SliceSnapshot | null> {
|
||||
const htmlAst = this._htmlToAst(payload.file);
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
const contentSlice = (await this._traverseHtml(
|
||||
htmlAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets
|
||||
)) as BlockSnapshot;
|
||||
if (contentSlice.children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'slice',
|
||||
content: [contentSlice],
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const HtmlAdapterFactoryIdentifier = AdapterFactoryIdentifier('Html');
|
||||
|
||||
export const HtmlAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(HtmlAdapterFactoryIdentifier, provider => ({
|
||||
get: (job: Job) =>
|
||||
new HtmlAdapter(
|
||||
job,
|
||||
Array.from(
|
||||
provider.getAll(BlockHtmlAdapterMatcherIdentifier).values()
|
||||
)
|
||||
),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
BlockHtmlAdapterExtensions,
|
||||
defaultBlockHtmlAdapterMatchers,
|
||||
} from './block-matcher.js';
|
||||
export {
|
||||
HtmlAdapter,
|
||||
HtmlAdapterFactoryExtension,
|
||||
HtmlAdapterFactoryIdentifier,
|
||||
} from './html.js';
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { sha } from '@blocksuite/global/utils';
|
||||
import {
|
||||
type AssetsManager,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotPayload,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotPayload,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotPayload,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
type ToBlockSnapshotPayload,
|
||||
type ToDocSnapshotPayload,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import { AdapterFactoryIdentifier } from './type.js';
|
||||
|
||||
export type Image = File[];
|
||||
|
||||
type ImageToSliceSnapshotPayload = {
|
||||
file: Image;
|
||||
assets?: AssetsManager;
|
||||
blockVersions: Record<string, number>;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class ImageAdapter extends BaseAdapter<Image> {
|
||||
override fromBlockSnapshot(
|
||||
_payload: FromBlockSnapshotPayload
|
||||
): Promise<FromBlockSnapshotResult<Image>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ImageAdapter.fromBlockSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override fromDocSnapshot(
|
||||
_payload: FromDocSnapshotPayload
|
||||
): Promise<FromDocSnapshotResult<Image>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ImageAdapter.fromDocSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override fromSliceSnapshot(
|
||||
payload: FromSliceSnapshotPayload
|
||||
): Promise<FromSliceSnapshotResult<Image>> {
|
||||
const images: Image = [];
|
||||
for (const contentSlice of payload.snapshot.content) {
|
||||
if (contentSlice.type === 'block') {
|
||||
const { flavour, props } = contentSlice;
|
||||
if (flavour === 'affine:image') {
|
||||
const { sourceId } = props;
|
||||
const file = payload.assets?.getAssets().get(sourceId as string) as
|
||||
| File
|
||||
| undefined;
|
||||
if (file) {
|
||||
images.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve({ file: images, assetsIds: [] });
|
||||
}
|
||||
|
||||
override toBlockSnapshot(
|
||||
_payload: ToBlockSnapshotPayload<Image>
|
||||
): Promise<BlockSnapshot> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ImageAdapter.toBlockSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override toDocSnapshot(
|
||||
_payload: ToDocSnapshotPayload<Image>
|
||||
): Promise<DocSnapshot> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ImageAdapter.toDocSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override async toSliceSnapshot(
|
||||
payload: ImageToSliceSnapshotPayload
|
||||
): Promise<SliceSnapshot | null> {
|
||||
const content: SliceSnapshot['content'] = [];
|
||||
for (const item of payload.file) {
|
||||
const blobId = await sha(await item.arrayBuffer());
|
||||
payload.assets?.getAssets().set(blobId, item);
|
||||
await payload.assets?.writeToBlob(blobId);
|
||||
content.push({
|
||||
type: 'block',
|
||||
flavour: 'affine:image',
|
||||
id: nanoid(),
|
||||
props: {
|
||||
sourceId: blobId,
|
||||
},
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
if (content.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'slice',
|
||||
content,
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageAdapterFactoryIdentifier = AdapterFactoryIdentifier('Image');
|
||||
|
||||
export const ImageAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(ImageAdapterFactoryIdentifier, () => ({
|
||||
get: (job: Job) => new ImageAdapter(job),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './attachment.js';
|
||||
export * from './html-adapter/html.js';
|
||||
export * from './image.js';
|
||||
export * from './markdown/index.js';
|
||||
export * from './mix-text.js';
|
||||
export * from './notion-html/index.js';
|
||||
export * from './notion-text.js';
|
||||
export * from './plain-text/plain-text.js';
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
embedFigmaBlockMarkdownAdapterMatcher,
|
||||
EmbedFigmaMarkdownAdapterExtension,
|
||||
embedGithubBlockMarkdownAdapterMatcher,
|
||||
EmbedGithubMarkdownAdapterExtension,
|
||||
embedLinkedDocBlockMarkdownAdapterMatcher,
|
||||
EmbedLinkedDocMarkdownAdapterExtension,
|
||||
embedLoomBlockMarkdownAdapterMatcher,
|
||||
EmbedLoomMarkdownAdapterExtension,
|
||||
EmbedSyncedDocBlockMarkdownAdapterExtension,
|
||||
embedSyncedDocBlockMarkdownAdapterMatcher,
|
||||
embedYoutubeBlockMarkdownAdapterMatcher,
|
||||
EmbedYoutubeMarkdownAdapterExtension,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
ListBlockMarkdownAdapterExtension,
|
||||
listBlockMarkdownAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-list';
|
||||
import {
|
||||
ParagraphBlockMarkdownAdapterExtension,
|
||||
paragraphBlockMarkdownAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-paragraph';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import {
|
||||
BookmarkBlockMarkdownAdapterExtension,
|
||||
bookmarkBlockMarkdownAdapterMatcher,
|
||||
} from '../../../bookmark-block/adapters/markdown.js';
|
||||
import {
|
||||
CodeBlockMarkdownAdapterExtension,
|
||||
codeBlockMarkdownAdapterMatcher,
|
||||
} from '../../../code-block/adapters/markdown.js';
|
||||
import {
|
||||
DatabaseBlockMarkdownAdapterExtension,
|
||||
databaseBlockMarkdownAdapterMatcher,
|
||||
} from '../../../database-block/adapters/markdown.js';
|
||||
import {
|
||||
DividerBlockMarkdownAdapterExtension,
|
||||
dividerBlockMarkdownAdapterMatcher,
|
||||
} from '../../../divider-block/adapters/markdown.js';
|
||||
import {
|
||||
ImageBlockMarkdownAdapterExtension,
|
||||
imageBlockMarkdownAdapterMatcher,
|
||||
} from '../../../image-block/adapters/markdown.js';
|
||||
import {
|
||||
LatexBlockMarkdownAdapterExtension,
|
||||
latexBlockMarkdownAdapterMatcher,
|
||||
} from '../../../latex-block/adapters/markdown.js';
|
||||
import {
|
||||
RootBlockMarkdownAdapterExtension,
|
||||
rootBlockMarkdownAdapterMatcher,
|
||||
} from '../../../root-block/adapters/markdown.js';
|
||||
|
||||
export const defaultBlockMarkdownAdapterMatchers = [
|
||||
embedFigmaBlockMarkdownAdapterMatcher,
|
||||
embedGithubBlockMarkdownAdapterMatcher,
|
||||
embedLinkedDocBlockMarkdownAdapterMatcher,
|
||||
embedLoomBlockMarkdownAdapterMatcher,
|
||||
embedSyncedDocBlockMarkdownAdapterMatcher,
|
||||
embedYoutubeBlockMarkdownAdapterMatcher,
|
||||
listBlockMarkdownAdapterMatcher,
|
||||
paragraphBlockMarkdownAdapterMatcher,
|
||||
bookmarkBlockMarkdownAdapterMatcher,
|
||||
codeBlockMarkdownAdapterMatcher,
|
||||
databaseBlockMarkdownAdapterMatcher,
|
||||
dividerBlockMarkdownAdapterMatcher,
|
||||
imageBlockMarkdownAdapterMatcher,
|
||||
latexBlockMarkdownAdapterMatcher,
|
||||
rootBlockMarkdownAdapterMatcher,
|
||||
];
|
||||
|
||||
export const BlockMarkdownAdapterExtensions: ExtensionType[] = [
|
||||
EmbedFigmaMarkdownAdapterExtension,
|
||||
EmbedGithubMarkdownAdapterExtension,
|
||||
EmbedLinkedDocMarkdownAdapterExtension,
|
||||
EmbedLoomMarkdownAdapterExtension,
|
||||
EmbedSyncedDocBlockMarkdownAdapterExtension,
|
||||
EmbedYoutubeMarkdownAdapterExtension,
|
||||
ListBlockMarkdownAdapterExtension,
|
||||
ParagraphBlockMarkdownAdapterExtension,
|
||||
BookmarkBlockMarkdownAdapterExtension,
|
||||
CodeBlockMarkdownAdapterExtension,
|
||||
DatabaseBlockMarkdownAdapterExtension,
|
||||
DividerBlockMarkdownAdapterExtension,
|
||||
ImageBlockMarkdownAdapterExtension,
|
||||
LatexBlockMarkdownAdapterExtension,
|
||||
RootBlockMarkdownAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,153 @@
|
||||
import { generateDocUrl } from '@blocksuite/affine-block-embed';
|
||||
import type { InlineDeltaToMarkdownAdapterMatcher } from '@blocksuite/affine-shared/adapters';
|
||||
import type { PhrasingContent } from 'mdast';
|
||||
|
||||
export const boldDeltaToMarkdownAdapterMatcher: InlineDeltaToMarkdownAdapterMatcher =
|
||||
{
|
||||
name: 'bold',
|
||||
match: delta => !!delta.attributes?.bold,
|
||||
toAST: (_, context) => {
|
||||
const { current: currentMdast } = context;
|
||||
return {
|
||||
type: 'strong',
|
||||
children: [currentMdast],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const italicDeltaToMarkdownAdapterMatcher: InlineDeltaToMarkdownAdapterMatcher =
|
||||
{
|
||||
name: 'italic',
|
||||
match: delta => !!delta.attributes?.italic,
|
||||
toAST: (_, context) => {
|
||||
const { current: currentMdast } = context;
|
||||
return {
|
||||
type: 'emphasis',
|
||||
children: [currentMdast],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const strikeDeltaToMarkdownAdapterMatcher: InlineDeltaToMarkdownAdapterMatcher =
|
||||
{
|
||||
name: 'strike',
|
||||
match: delta => !!delta.attributes?.strike,
|
||||
toAST: (_, context) => {
|
||||
const { current: currentMdast } = context;
|
||||
return {
|
||||
type: 'delete',
|
||||
children: [currentMdast],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const inlineCodeDeltaToMarkdownAdapterMatcher: InlineDeltaToMarkdownAdapterMatcher =
|
||||
{
|
||||
name: 'inlineCode',
|
||||
match: delta => !!delta.attributes?.code,
|
||||
toAST: delta => ({
|
||||
type: 'inlineCode',
|
||||
value: delta.insert,
|
||||
}),
|
||||
};
|
||||
|
||||
export const referenceDeltaToMarkdownAdapterMatcher: InlineDeltaToMarkdownAdapterMatcher =
|
||||
{
|
||||
name: 'reference',
|
||||
match: delta => !!delta.attributes?.reference,
|
||||
toAST: (delta, context) => {
|
||||
let mdast: PhrasingContent = {
|
||||
type: 'text',
|
||||
value: delta.insert,
|
||||
};
|
||||
const reference = delta.attributes?.reference;
|
||||
if (!reference) {
|
||||
return mdast;
|
||||
}
|
||||
|
||||
const { configs } = context;
|
||||
const title = configs.get(`title:${reference.pageId}`);
|
||||
const params = reference.params ?? {};
|
||||
const url = generateDocUrl(
|
||||
configs.get('docLinkBaseUrl') ?? '',
|
||||
String(reference.pageId),
|
||||
params
|
||||
);
|
||||
mdast = {
|
||||
type: 'link',
|
||||
url,
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: title ?? '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return mdast;
|
||||
},
|
||||
};
|
||||
|
||||
export const linkDeltaToMarkdownAdapterMatcher: InlineDeltaToMarkdownAdapterMatcher =
|
||||
{
|
||||
name: 'link',
|
||||
match: delta => !!delta.attributes?.link,
|
||||
toAST: (delta, context) => {
|
||||
const mdast: PhrasingContent = {
|
||||
type: 'text',
|
||||
value: delta.insert,
|
||||
};
|
||||
const link = delta.attributes?.link;
|
||||
if (!link) {
|
||||
return mdast;
|
||||
}
|
||||
|
||||
const { current: currentMdast } = context;
|
||||
if ('value' in currentMdast) {
|
||||
if (currentMdast.value === '') {
|
||||
return {
|
||||
type: 'text',
|
||||
value: link,
|
||||
};
|
||||
}
|
||||
if (mdast.value !== link) {
|
||||
return {
|
||||
type: 'link',
|
||||
url: link,
|
||||
children: [currentMdast],
|
||||
};
|
||||
}
|
||||
}
|
||||
return mdast;
|
||||
},
|
||||
};
|
||||
|
||||
export const latexDeltaToMarkdownAdapterMatcher: InlineDeltaToMarkdownAdapterMatcher =
|
||||
{
|
||||
name: 'inlineLatex',
|
||||
match: delta => !!delta.attributes?.latex,
|
||||
toAST: delta => {
|
||||
const mdast: PhrasingContent = {
|
||||
type: 'text',
|
||||
value: delta.insert,
|
||||
};
|
||||
if (delta.attributes?.latex) {
|
||||
return {
|
||||
type: 'inlineMath',
|
||||
value: delta.attributes.latex,
|
||||
};
|
||||
}
|
||||
return mdast;
|
||||
},
|
||||
};
|
||||
|
||||
export const inlineDeltaToMarkdownAdapterMatchers: InlineDeltaToMarkdownAdapterMatcher[] =
|
||||
[
|
||||
referenceDeltaToMarkdownAdapterMatcher,
|
||||
linkDeltaToMarkdownAdapterMatcher,
|
||||
inlineCodeDeltaToMarkdownAdapterMatcher,
|
||||
boldDeltaToMarkdownAdapterMatcher,
|
||||
italicDeltaToMarkdownAdapterMatcher,
|
||||
strikeDeltaToMarkdownAdapterMatcher,
|
||||
latexDeltaToMarkdownAdapterMatcher,
|
||||
];
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { MarkdownASTToDeltaMatcher } from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const markdownTextToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'text',
|
||||
match: ast => ast.type === 'text',
|
||||
toDelta: ast => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return [{ insert: ast.value }];
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownInlineCodeToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'inlineCode',
|
||||
match: ast => ast.type === 'inlineCode',
|
||||
toDelta: ast => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return [{ insert: ast.value, attributes: { code: true } }];
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownStrongToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'strong',
|
||||
match: ast => ast.type === 'strong',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('children' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, bold: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownEmphasisToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'emphasis',
|
||||
match: ast => ast.type === 'emphasis',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('children' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, italic: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownDeleteToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'delete',
|
||||
match: ast => ast.type === 'delete',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('children' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, strike: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownLinkToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'link',
|
||||
match: ast => ast.type === 'link',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('children' in ast) || !('url' in ast)) {
|
||||
return [];
|
||||
}
|
||||
const { configs } = context;
|
||||
const baseUrl = configs.get('docLinkBaseUrl') ?? '';
|
||||
if (baseUrl && ast.url.startsWith(baseUrl)) {
|
||||
const path = ast.url.substring(baseUrl.length);
|
||||
// ^ - /{pageId}?mode={mode}&blockIds={blockIds}&elementIds={elementIds}
|
||||
const match = path.match(/^\/([^?]+)(\?.*)?$/);
|
||||
if (match) {
|
||||
const pageId = match?.[1];
|
||||
const search = match?.[2];
|
||||
const searchParams = search ? new URLSearchParams(search) : undefined;
|
||||
const mode = searchParams?.get('mode');
|
||||
const blockIds = searchParams?.get('blockIds')?.split(',');
|
||||
const elementIds = searchParams?.get('elementIds')?.split(',');
|
||||
|
||||
return [
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId,
|
||||
params: {
|
||||
mode:
|
||||
mode && ['edgeless', 'page'].includes(mode)
|
||||
? (mode as 'edgeless' | 'page')
|
||||
: undefined,
|
||||
blockIds,
|
||||
elementIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, link: ast.url };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownListToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'list',
|
||||
match: ast => ast.type === 'list',
|
||||
toDelta: () => [],
|
||||
};
|
||||
|
||||
export const markdownInlineMathToDeltaMatcher: MarkdownASTToDeltaMatcher = {
|
||||
name: 'inlineMath',
|
||||
match: ast => ast.type === 'inlineMath',
|
||||
toDelta: ast => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return [{ insert: ' ', attributes: { latex: ast.value } }];
|
||||
},
|
||||
};
|
||||
|
||||
export const markdownInlineToDeltaMatchers: MarkdownASTToDeltaMatcher[] = [
|
||||
markdownTextToDeltaMatcher,
|
||||
markdownInlineCodeToDeltaMatcher,
|
||||
markdownStrongToDeltaMatcher,
|
||||
markdownEmphasisToDeltaMatcher,
|
||||
markdownDeleteToDeltaMatcher,
|
||||
markdownLinkToDeltaMatcher,
|
||||
markdownInlineMathToDeltaMatcher,
|
||||
markdownListToDeltaMatcher,
|
||||
];
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
|
||||
|
||||
mdast-util-gfm-autolink-literal is from markdown only.
|
||||
mdast-util-gfm-footnote is not included.
|
||||
*/
|
||||
import { gfmAutolinkLiteralFromMarkdown } from 'mdast-util-gfm-autolink-literal';
|
||||
import {
|
||||
gfmStrikethroughFromMarkdown,
|
||||
gfmStrikethroughToMarkdown,
|
||||
} from 'mdast-util-gfm-strikethrough';
|
||||
import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table';
|
||||
import {
|
||||
gfmTaskListItemFromMarkdown,
|
||||
gfmTaskListItemToMarkdown,
|
||||
} from 'mdast-util-gfm-task-list-item';
|
||||
import { gfmAutolinkLiteral } from 'micromark-extension-gfm-autolink-literal';
|
||||
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough';
|
||||
import { gfmTable } from 'micromark-extension-gfm-table';
|
||||
import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item';
|
||||
import { combineExtensions } from 'micromark-util-combine-extensions';
|
||||
import type { Processor } from 'unified';
|
||||
|
||||
export function gfm() {
|
||||
return combineExtensions([
|
||||
gfmAutolinkLiteral(),
|
||||
gfmStrikethrough(),
|
||||
gfmTable(),
|
||||
gfmTaskListItem(),
|
||||
]);
|
||||
}
|
||||
|
||||
function gfmFromMarkdown() {
|
||||
return [
|
||||
gfmStrikethroughFromMarkdown(),
|
||||
gfmTableFromMarkdown(),
|
||||
gfmTaskListItemFromMarkdown(),
|
||||
gfmAutolinkLiteralFromMarkdown(),
|
||||
];
|
||||
}
|
||||
|
||||
function gfmToMarkdown() {
|
||||
return {
|
||||
extensions: [
|
||||
gfmStrikethroughToMarkdown(),
|
||||
gfmTableToMarkdown(),
|
||||
gfmTaskListItemToMarkdown(),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function remarkGfm(this: Processor) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
const data = self.data();
|
||||
|
||||
const micromarkExtensions =
|
||||
data.micromarkExtensions || (data.micromarkExtensions = []);
|
||||
const fromMarkdownExtensions =
|
||||
data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
|
||||
const toMarkdownExtensions =
|
||||
data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
|
||||
|
||||
micromarkExtensions.push(gfm());
|
||||
fromMarkdownExtensions.push(gfmFromMarkdown());
|
||||
toMarkdownExtensions.push(gfmToMarkdown());
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
BlockMarkdownAdapterExtensions,
|
||||
defaultBlockMarkdownAdapterMatchers,
|
||||
} from './block-matcher.js';
|
||||
export {
|
||||
MarkdownAdapter,
|
||||
MarkdownAdapterFactoryExtension,
|
||||
MarkdownAdapterFactoryIdentifier,
|
||||
} from './markdown.js';
|
||||
@@ -0,0 +1,455 @@
|
||||
import {
|
||||
DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
type AdapterContext,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
type Markdown,
|
||||
type MarkdownAST,
|
||||
MarkdownDeltaConverter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
type AssetsManager,
|
||||
ASTWalker,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
BlockSnapshotSchema,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotPayload,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotPayload,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotPayload,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
type ToBlockSnapshotPayload,
|
||||
type ToDocSnapshotPayload,
|
||||
} from '@blocksuite/store';
|
||||
import type { Root } from 'mdast';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkStringify from 'remark-stringify';
|
||||
import { unified } from 'unified';
|
||||
|
||||
import { AdapterFactoryIdentifier } from '../type.js';
|
||||
import { defaultBlockMarkdownAdapterMatchers } from './block-matcher.js';
|
||||
import { inlineDeltaToMarkdownAdapterMatchers } from './delta-converter/inline-delta.js';
|
||||
import { markdownInlineToDeltaMatchers } from './delta-converter/markdown-inline.js';
|
||||
import { remarkGfm } from './gfm.js';
|
||||
|
||||
type MarkdownToSliceSnapshotPayload = {
|
||||
file: Markdown;
|
||||
assets?: AssetsManager;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class MarkdownAdapter extends BaseAdapter<Markdown> {
|
||||
private _traverseMarkdown = (
|
||||
markdown: MarkdownAST,
|
||||
snapshot: BlockSnapshot,
|
||||
assets?: AssetsManager
|
||||
) => {
|
||||
const walker = new ASTWalker<MarkdownAST, BlockSnapshot>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is MarkdownAST =>
|
||||
!Array.isArray(node) &&
|
||||
'type' in (node as object) &&
|
||||
(node as MarkdownAST).type !== undefined
|
||||
);
|
||||
walker.setEnter(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.toMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
MarkdownAST,
|
||||
BlockSnapshot,
|
||||
MarkdownDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
};
|
||||
await matcher.toBlockSnapshot.enter?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
walker.setLeave(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.toMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
MarkdownAST,
|
||||
BlockSnapshot,
|
||||
MarkdownDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
};
|
||||
await matcher.toBlockSnapshot.leave?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
return walker.walk(markdown, snapshot);
|
||||
};
|
||||
|
||||
private _traverseSnapshot = async (
|
||||
snapshot: BlockSnapshot,
|
||||
markdown: MarkdownAST,
|
||||
assets?: AssetsManager
|
||||
) => {
|
||||
const assetsIds: string[] = [];
|
||||
const walker = new ASTWalker<BlockSnapshot, MarkdownAST>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is BlockSnapshot =>
|
||||
BlockSnapshotSchema.safeParse(node).success
|
||||
);
|
||||
walker.setEnter(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.fromMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
BlockSnapshot,
|
||||
MarkdownAST,
|
||||
MarkdownDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
updateAssetIds: (assetsId: string) => {
|
||||
assetsIds.push(assetsId);
|
||||
},
|
||||
};
|
||||
await matcher.fromBlockSnapshot.enter?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
walker.setLeave(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.fromMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
BlockSnapshot,
|
||||
MarkdownAST,
|
||||
MarkdownDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
};
|
||||
await matcher.fromBlockSnapshot.leave?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
ast: (await walker.walk(snapshot, markdown)) as Root,
|
||||
assetsIds,
|
||||
};
|
||||
};
|
||||
|
||||
deltaConverter: MarkdownDeltaConverter;
|
||||
|
||||
constructor(
|
||||
job: Job,
|
||||
readonly blockMatchers: BlockMarkdownAdapterMatcher[] = defaultBlockMarkdownAdapterMatchers
|
||||
) {
|
||||
super(job);
|
||||
this.deltaConverter = new MarkdownDeltaConverter(
|
||||
job.adapterConfigs,
|
||||
inlineDeltaToMarkdownAdapterMatchers,
|
||||
markdownInlineToDeltaMatchers
|
||||
);
|
||||
}
|
||||
|
||||
private _astToMarkdown(ast: Root) {
|
||||
return unified()
|
||||
.use(remarkGfm)
|
||||
.use(remarkStringify, {
|
||||
resourceLink: true,
|
||||
})
|
||||
.use(remarkMath)
|
||||
.stringify(ast)
|
||||
.replace(/ \n/g, ' \n');
|
||||
}
|
||||
|
||||
private _markdownToAst(markdown: Markdown) {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkMath)
|
||||
.parse(markdown);
|
||||
}
|
||||
|
||||
async fromBlockSnapshot({
|
||||
snapshot,
|
||||
assets,
|
||||
}: FromBlockSnapshotPayload): Promise<FromBlockSnapshotResult<Markdown>> {
|
||||
const root: Root = {
|
||||
type: 'root',
|
||||
children: [],
|
||||
};
|
||||
const { ast, assetsIds } = await this._traverseSnapshot(
|
||||
snapshot,
|
||||
root,
|
||||
assets
|
||||
);
|
||||
return {
|
||||
file: this._astToMarkdown(ast),
|
||||
assetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
async fromDocSnapshot({
|
||||
snapshot,
|
||||
assets,
|
||||
}: FromDocSnapshotPayload): Promise<FromDocSnapshotResult<Markdown>> {
|
||||
let buffer = '';
|
||||
const { file, assetsIds } = await this.fromBlockSnapshot({
|
||||
snapshot: snapshot.blocks,
|
||||
assets,
|
||||
});
|
||||
buffer += file;
|
||||
return {
|
||||
file: buffer,
|
||||
assetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
async fromSliceSnapshot({
|
||||
snapshot,
|
||||
assets,
|
||||
}: FromSliceSnapshotPayload): Promise<FromSliceSnapshotResult<Markdown>> {
|
||||
let buffer = '';
|
||||
const sliceAssetsIds: string[] = [];
|
||||
for (const contentSlice of snapshot.content) {
|
||||
const root: Root = {
|
||||
type: 'root',
|
||||
children: [],
|
||||
};
|
||||
const { ast, assetsIds } = await this._traverseSnapshot(
|
||||
contentSlice,
|
||||
root,
|
||||
assets
|
||||
);
|
||||
sliceAssetsIds.push(...assetsIds);
|
||||
buffer += this._astToMarkdown(ast);
|
||||
}
|
||||
const markdown =
|
||||
buffer.match(/\n/g)?.length === 1 ? buffer.trimEnd() : buffer;
|
||||
return {
|
||||
file: markdown,
|
||||
assetsIds: sliceAssetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
async toBlockSnapshot(
|
||||
payload: ToBlockSnapshotPayload<Markdown>
|
||||
): Promise<BlockSnapshot> {
|
||||
const markdownAst = this._markdownToAst(payload.file);
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
return this._traverseMarkdown(
|
||||
markdownAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets
|
||||
);
|
||||
}
|
||||
|
||||
async toDocSnapshot(
|
||||
payload: ToDocSnapshotPayload<Markdown>
|
||||
): Promise<DocSnapshot> {
|
||||
const markdownAst = this._markdownToAst(payload.file);
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
return {
|
||||
type: 'page',
|
||||
meta: {
|
||||
id: nanoid(),
|
||||
title: 'Untitled',
|
||||
createDate: Date.now(),
|
||||
tags: [],
|
||||
},
|
||||
blocks: {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:page',
|
||||
props: {
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'Untitled',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:surface',
|
||||
props: {
|
||||
elements: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
await this._traverseMarkdown(
|
||||
markdownAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async toSliceSnapshot(
|
||||
payload: MarkdownToSliceSnapshotPayload
|
||||
): Promise<SliceSnapshot | null> {
|
||||
let codeFence = '';
|
||||
payload.file = payload.file
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
if (line.trimStart().startsWith('-')) {
|
||||
return line;
|
||||
}
|
||||
let trimmedLine = line.trimStart();
|
||||
if (!codeFence && trimmedLine.startsWith('```')) {
|
||||
codeFence = trimmedLine.substring(
|
||||
0,
|
||||
trimmedLine.lastIndexOf('```') + 3
|
||||
);
|
||||
if (codeFence.split('').every(c => c === '`')) {
|
||||
return line;
|
||||
}
|
||||
codeFence = '';
|
||||
}
|
||||
if (!codeFence && trimmedLine.startsWith('~~~')) {
|
||||
codeFence = trimmedLine.substring(
|
||||
0,
|
||||
trimmedLine.lastIndexOf('~~~') + 3
|
||||
);
|
||||
if (codeFence.split('').every(c => c === '~')) {
|
||||
return line;
|
||||
}
|
||||
codeFence = '';
|
||||
}
|
||||
if (
|
||||
!!codeFence &&
|
||||
trimmedLine.startsWith(codeFence) &&
|
||||
trimmedLine.lastIndexOf(codeFence) === 0
|
||||
) {
|
||||
codeFence = '';
|
||||
}
|
||||
if (codeFence) {
|
||||
return line;
|
||||
}
|
||||
|
||||
trimmedLine = trimmedLine.trimEnd();
|
||||
if (!trimmedLine.startsWith('<') && !trimmedLine.endsWith('>')) {
|
||||
// check if it is a url link and wrap it with the angle brackets
|
||||
// sometimes the url includes emphasis `_` that will break URL parsing
|
||||
//
|
||||
// eg. /MuawcBMT1Mzvoar09-_66?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_
|
||||
// https://www.markdownguide.org/basic-syntax/#urls-and-email-addresses
|
||||
try {
|
||||
const valid =
|
||||
URL.canParse?.(trimmedLine) ?? Boolean(new URL(trimmedLine));
|
||||
if (valid) {
|
||||
return `<${trimmedLine}>`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
return line.replace(/^ /, ' ');
|
||||
})
|
||||
.join('\n');
|
||||
const markdownAst = this._markdownToAst(payload.file);
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
} as BlockSnapshot;
|
||||
const contentSlice = (await this._traverseMarkdown(
|
||||
markdownAst,
|
||||
blockSnapshotRoot,
|
||||
payload.assets
|
||||
)) as BlockSnapshot;
|
||||
if (contentSlice.children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'slice',
|
||||
content: [contentSlice],
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const MarkdownAdapterFactoryIdentifier =
|
||||
AdapterFactoryIdentifier('Markdown');
|
||||
|
||||
export const MarkdownAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(MarkdownAdapterFactoryIdentifier, provider => ({
|
||||
get: (job: Job) =>
|
||||
new MarkdownAdapter(
|
||||
job,
|
||||
Array.from(
|
||||
provider.getAll(BlockMarkdownAdapterMatcherIdentifier).values()
|
||||
)
|
||||
),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,363 @@
|
||||
import {
|
||||
DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import {
|
||||
type AssetsManager,
|
||||
ASTWalker,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
BlockSnapshotSchema,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotPayload,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotPayload,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotPayload,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
type ToBlockSnapshotPayload,
|
||||
type ToDocSnapshotPayload,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import { MarkdownAdapter } from './markdown/index.js';
|
||||
import { AdapterFactoryIdentifier } from './type.js';
|
||||
|
||||
export type MixText = string;
|
||||
|
||||
type MixTextToSliceSnapshotPayload = {
|
||||
file: MixText;
|
||||
assets?: AssetsManager;
|
||||
blockVersions: Record<string, number>;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class MixTextAdapter extends BaseAdapter<MixText> {
|
||||
private _markdownAdapter: MarkdownAdapter;
|
||||
|
||||
constructor(job: Job) {
|
||||
super(job);
|
||||
this._markdownAdapter = new MarkdownAdapter(job);
|
||||
}
|
||||
|
||||
private _splitDeltas(deltas: DeltaInsert[]): DeltaInsert[][] {
|
||||
const result: DeltaInsert[][] = [[]];
|
||||
const pending: DeltaInsert[] = deltas;
|
||||
while (pending.length > 0) {
|
||||
const delta = pending.shift();
|
||||
if (!delta) {
|
||||
break;
|
||||
}
|
||||
if (delta.insert.includes('\n')) {
|
||||
const splitIndex = delta.insert.indexOf('\n');
|
||||
const line = delta.insert.slice(0, splitIndex);
|
||||
const rest = delta.insert.slice(splitIndex + 1);
|
||||
result[result.length - 1].push({ ...delta, insert: line });
|
||||
result.push([]);
|
||||
if (rest) {
|
||||
pending.unshift({ ...delta, insert: rest });
|
||||
}
|
||||
} else {
|
||||
result[result.length - 1].push(delta);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _traverseSnapshot(
|
||||
snapshot: BlockSnapshot
|
||||
): Promise<{ mixtext: string }> {
|
||||
let buffer = '';
|
||||
const walker = new ASTWalker<BlockSnapshot, never>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is BlockSnapshot =>
|
||||
BlockSnapshotSchema.safeParse(node).success
|
||||
);
|
||||
walker.setEnter(o => {
|
||||
const text = (o.node.props.text ?? { delta: [] }) as {
|
||||
delta: DeltaInsert[];
|
||||
};
|
||||
if (buffer.length > 0) {
|
||||
buffer += '\n';
|
||||
}
|
||||
switch (o.node.flavour) {
|
||||
case 'affine:code': {
|
||||
buffer += text.delta.map(delta => delta.insert).join('');
|
||||
break;
|
||||
}
|
||||
case 'affine:paragraph': {
|
||||
buffer += text.delta.map(delta => delta.insert).join('');
|
||||
break;
|
||||
}
|
||||
case 'affine:list': {
|
||||
buffer += text.delta.map(delta => delta.insert).join('');
|
||||
break;
|
||||
}
|
||||
case 'affine:divider': {
|
||||
buffer += '---';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
await walker.walkONode(snapshot);
|
||||
return {
|
||||
mixtext: buffer,
|
||||
};
|
||||
}
|
||||
|
||||
async fromBlockSnapshot({
|
||||
snapshot,
|
||||
}: FromBlockSnapshotPayload): Promise<FromBlockSnapshotResult<MixText>> {
|
||||
const { mixtext } = await this._traverseSnapshot(snapshot);
|
||||
return {
|
||||
file: mixtext,
|
||||
assetsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
async fromDocSnapshot({
|
||||
snapshot,
|
||||
assets,
|
||||
}: FromDocSnapshotPayload): Promise<FromDocSnapshotResult<MixText>> {
|
||||
let buffer = '';
|
||||
if (snapshot.meta.title) {
|
||||
buffer += `${snapshot.meta.title}\n\n`;
|
||||
}
|
||||
const { file, assetsIds } = await this.fromBlockSnapshot({
|
||||
snapshot: snapshot.blocks,
|
||||
assets,
|
||||
});
|
||||
buffer += file;
|
||||
return {
|
||||
file: buffer,
|
||||
assetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
async fromSliceSnapshot({
|
||||
snapshot,
|
||||
}: FromSliceSnapshotPayload): Promise<FromSliceSnapshotResult<MixText>> {
|
||||
let buffer = '';
|
||||
const sliceAssetsIds: string[] = [];
|
||||
for (const contentSlice of snapshot.content) {
|
||||
const { mixtext } = await this._traverseSnapshot(contentSlice);
|
||||
buffer += mixtext;
|
||||
}
|
||||
const mixtext =
|
||||
buffer.match(/\n/g)?.length === 1 ? buffer.trimEnd() : buffer;
|
||||
return {
|
||||
file: mixtext,
|
||||
assetsIds: sliceAssetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
toBlockSnapshot(payload: ToBlockSnapshotPayload<MixText>): BlockSnapshot {
|
||||
payload.file = payload.file.replaceAll('\r', '');
|
||||
return {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: payload.file.split('\n').map((line): BlockSnapshot => {
|
||||
return {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: line,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
toDocSnapshot(payload: ToDocSnapshotPayload<MixText>): DocSnapshot {
|
||||
payload.file = payload.file.replaceAll('\r', '');
|
||||
return {
|
||||
type: 'page',
|
||||
meta: {
|
||||
id: nanoid(),
|
||||
title: 'Untitled',
|
||||
createDate: Date.now(),
|
||||
tags: [],
|
||||
},
|
||||
blocks: {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:page',
|
||||
props: {
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'Untitled',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:surface',
|
||||
props: {
|
||||
elements: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: payload.file.split('\n').map((line): BlockSnapshot => {
|
||||
return {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: line,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async toSliceSnapshot(
|
||||
payload: MixTextToSliceSnapshotPayload
|
||||
): Promise<SliceSnapshot | null> {
|
||||
if (payload.file.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
payload.file = payload.file.replaceAll('\r', '');
|
||||
const sliceSnapshot = await this._markdownAdapter.toSliceSnapshot({
|
||||
file: payload.file,
|
||||
assets: payload.assets,
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
});
|
||||
if (!sliceSnapshot) {
|
||||
return null;
|
||||
}
|
||||
for (const contentSlice of sliceSnapshot.content) {
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
} as BlockSnapshot;
|
||||
const walker = new ASTWalker<BlockSnapshot, BlockSnapshot>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is BlockSnapshot =>
|
||||
BlockSnapshotSchema.safeParse(node).success
|
||||
);
|
||||
walker.setEnter((o, context) => {
|
||||
switch (o.node.flavour) {
|
||||
case 'affine:note': {
|
||||
break;
|
||||
}
|
||||
case 'affine:paragraph': {
|
||||
if (o.parent?.node.flavour !== 'affine:note') {
|
||||
context.openNode({ ...o.node, children: [] });
|
||||
break;
|
||||
}
|
||||
const text = (o.node.props.text ?? { delta: [] }) as {
|
||||
delta: DeltaInsert[];
|
||||
};
|
||||
const newDeltas = this._splitDeltas(text.delta);
|
||||
for (const [i, delta] of newDeltas.entries()) {
|
||||
context.openNode({
|
||||
...o.node,
|
||||
id: i === 0 ? o.node.id : nanoid(),
|
||||
props: {
|
||||
...o.node.props,
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta,
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
});
|
||||
if (i < newDeltas.length - 1) {
|
||||
context.closeNode();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
context.openNode({ ...o.node, children: [] });
|
||||
}
|
||||
}
|
||||
});
|
||||
walker.setLeave((o, context) => {
|
||||
switch (o.node.flavour) {
|
||||
case 'affine:note': {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
context.closeNode();
|
||||
}
|
||||
}
|
||||
});
|
||||
await walker.walk(contentSlice, blockSnapshotRoot);
|
||||
contentSlice.children = blockSnapshotRoot.children;
|
||||
}
|
||||
return sliceSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
export const MixTextAdapterFactoryIdentifier =
|
||||
AdapterFactoryIdentifier('MixText');
|
||||
|
||||
export const MixTextAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(MixTextAdapterFactoryIdentifier, () => ({
|
||||
get: (job: Job) => new MixTextAdapter(job),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
ListBlockNotionHtmlAdapterExtension,
|
||||
listBlockNotionHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-list';
|
||||
import {
|
||||
ParagraphBlockNotionHtmlAdapterExtension,
|
||||
paragraphBlockNotionHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-paragraph';
|
||||
import type { BlockNotionHtmlAdapterMatcher } from '@blocksuite/affine-shared/adapters';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import {
|
||||
AttachmentBlockNotionHtmlAdapterExtension,
|
||||
attachmentBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../attachment-block/adapters/notion-html.js';
|
||||
import {
|
||||
BookmarkBlockNotionHtmlAdapterExtension,
|
||||
bookmarkBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../bookmark-block/adapters/notion-html.js';
|
||||
import {
|
||||
CodeBlockNotionHtmlAdapterExtension,
|
||||
codeBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../code-block/adapters/notion-html.js';
|
||||
import {
|
||||
DatabaseBlockNotionHtmlAdapterExtension,
|
||||
databaseBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../database-block/adapters/notion-html.js';
|
||||
import {
|
||||
DividerBlockNotionHtmlAdapterExtension,
|
||||
dividerBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../divider-block/adapters/notion-html.js';
|
||||
import {
|
||||
ImageBlockNotionHtmlAdapterExtension,
|
||||
imageBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../image-block/adapters/notion-html.js';
|
||||
import {
|
||||
LatexBlockNotionHtmlAdapterExtension,
|
||||
latexBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../latex-block/adapters/notion-html.js';
|
||||
import {
|
||||
RootBlockNotionHtmlAdapterExtension,
|
||||
rootBlockNotionHtmlAdapterMatcher,
|
||||
} from '../../../root-block/adapters/notion-html.js';
|
||||
|
||||
export const defaultBlockNotionHtmlAdapterMatchers: BlockNotionHtmlAdapterMatcher[] =
|
||||
[
|
||||
listBlockNotionHtmlAdapterMatcher,
|
||||
paragraphBlockNotionHtmlAdapterMatcher,
|
||||
codeBlockNotionHtmlAdapterMatcher,
|
||||
dividerBlockNotionHtmlAdapterMatcher,
|
||||
imageBlockNotionHtmlAdapterMatcher,
|
||||
rootBlockNotionHtmlAdapterMatcher,
|
||||
bookmarkBlockNotionHtmlAdapterMatcher,
|
||||
databaseBlockNotionHtmlAdapterMatcher,
|
||||
attachmentBlockNotionHtmlAdapterMatcher,
|
||||
latexBlockNotionHtmlAdapterMatcher,
|
||||
];
|
||||
|
||||
export const BlockNotionHtmlAdapterExtensions: ExtensionType[] = [
|
||||
ListBlockNotionHtmlAdapterExtension,
|
||||
ParagraphBlockNotionHtmlAdapterExtension,
|
||||
CodeBlockNotionHtmlAdapterExtension,
|
||||
DividerBlockNotionHtmlAdapterExtension,
|
||||
ImageBlockNotionHtmlAdapterExtension,
|
||||
RootBlockNotionHtmlAdapterExtension,
|
||||
BookmarkBlockNotionHtmlAdapterExtension,
|
||||
DatabaseBlockNotionHtmlAdapterExtension,
|
||||
AttachmentBlockNotionHtmlAdapterExtension,
|
||||
LatexBlockNotionHtmlAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,296 @@
|
||||
import {
|
||||
HastUtils,
|
||||
type HtmlAST,
|
||||
type NotionHtmlASTToDeltaMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { collapseWhiteSpace } from 'collapse-white-space';
|
||||
import type { Element, Text } from 'hast';
|
||||
|
||||
const isElement = (ast: HtmlAST): ast is Element => {
|
||||
return ast.type === 'element';
|
||||
};
|
||||
|
||||
const isText = (ast: HtmlAST): ast is Text => {
|
||||
return ast.type === 'text';
|
||||
};
|
||||
|
||||
const listElementTags = new Set(['ol', 'ul']);
|
||||
const strongElementTags = new Set(['strong', 'b']);
|
||||
const italicElementTags = new Set(['i', 'em']);
|
||||
|
||||
const NotionInlineEquationToken = 'notion-text-equation-token';
|
||||
const NotionUnderlineStyleToken = 'border-bottom:0.05em solid';
|
||||
|
||||
export const notionHtmlTextToDeltaMatcher: NotionHtmlASTToDeltaMatcher = {
|
||||
name: 'text',
|
||||
match: ast => isText(ast),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isText(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { options } = context;
|
||||
options.trim ??= true;
|
||||
if (options.pre || ast.value === ' ') {
|
||||
return [{ insert: ast.value }];
|
||||
}
|
||||
if (options.trim) {
|
||||
const value = collapseWhiteSpace(ast.value, { trim: options.trim });
|
||||
if (value) {
|
||||
return [{ insert: value }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (ast.value) {
|
||||
return [{ insert: collapseWhiteSpace(ast.value) }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlSpanElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'span-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'span',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { toDelta, options } = context;
|
||||
if (
|
||||
Array.isArray(ast.properties?.className) &&
|
||||
ast.properties?.className.includes(NotionInlineEquationToken)
|
||||
) {
|
||||
const latex = HastUtils.getTextContent(
|
||||
HastUtils.querySelector(ast, 'annotation')
|
||||
);
|
||||
return [{ insert: ' ', attributes: { latex } }];
|
||||
}
|
||||
|
||||
// Add underline style detection
|
||||
if (
|
||||
typeof ast.properties?.style === 'string' &&
|
||||
ast.properties?.style?.includes(NotionUnderlineStyleToken)
|
||||
) {
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, underline: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return ast.children.flatMap(child => toDelta(child, options));
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlListToDeltaMatcher: NotionHtmlASTToDeltaMatcher = {
|
||||
name: 'list-element',
|
||||
match: ast => isElement(ast) && listElementTags.has(ast.tagName),
|
||||
toDelta: () => {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlStrongElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'strong-element',
|
||||
match: ast => isElement(ast) && strongElementTags.has(ast.tagName),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, bold: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlItalicElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'italic-element',
|
||||
match: ast => isElement(ast) && italicElementTags.has(ast.tagName),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, italic: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
export const notionHtmlCodeElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'code-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'code',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, code: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlDelElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher = {
|
||||
name: 'del-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'del',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, strike: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlUnderlineElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'underline-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'u',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, underline: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlLinkElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'link-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'a',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const href = ast.properties?.href;
|
||||
if (typeof href !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
if (options.pageMap) {
|
||||
const pageId = options.pageMap.get(decodeURIComponent(href));
|
||||
if (pageId) {
|
||||
delta.attributes = {
|
||||
...delta.attributes,
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId,
|
||||
},
|
||||
};
|
||||
delta.insert = ' ';
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
if (href.startsWith('http')) {
|
||||
delta.attributes = {
|
||||
...delta.attributes,
|
||||
link: href,
|
||||
};
|
||||
return delta;
|
||||
}
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlMarkElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'mark-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'mark',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlLiElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher = {
|
||||
name: 'li-element',
|
||||
match: ast =>
|
||||
isElement(ast) &&
|
||||
ast.tagName === 'li' &&
|
||||
!!HastUtils.querySelector(ast, '.checkbox'),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast) || !HastUtils.querySelector(ast, '.checkbox')) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
// Should ignore the children of to do list which is the checkbox and the space following it
|
||||
const checkBox = HastUtils.querySelector(ast, '.checkbox');
|
||||
const checkBoxIndex = ast.children.findIndex(child => child === checkBox);
|
||||
return ast.children
|
||||
.slice(checkBoxIndex + 2)
|
||||
.flatMap(child => toDelta(child, options));
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlBrElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher = {
|
||||
name: 'br-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'br',
|
||||
toDelta: () => {
|
||||
return [{ insert: '\n' }];
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlStyleElementToDeltaMatcher: NotionHtmlASTToDeltaMatcher =
|
||||
{
|
||||
name: 'style-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'style',
|
||||
toDelta: () => {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export const notionHtmlInlineToDeltaMatchers: NotionHtmlASTToDeltaMatcher[] = [
|
||||
notionHtmlTextToDeltaMatcher,
|
||||
notionHtmlSpanElementToDeltaMatcher,
|
||||
notionHtmlStrongElementToDeltaMatcher,
|
||||
notionHtmlItalicElementToDeltaMatcher,
|
||||
notionHtmlCodeElementToDeltaMatcher,
|
||||
notionHtmlDelElementToDeltaMatcher,
|
||||
notionHtmlUnderlineElementToDeltaMatcher,
|
||||
notionHtmlLinkElementToDeltaMatcher,
|
||||
notionHtmlMarkElementToDeltaMatcher,
|
||||
notionHtmlListToDeltaMatcher,
|
||||
notionHtmlLiElementToDeltaMatcher,
|
||||
notionHtmlBrElementToDeltaMatcher,
|
||||
notionHtmlStyleElementToDeltaMatcher,
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
BlockNotionHtmlAdapterExtensions,
|
||||
defaultBlockNotionHtmlAdapterMatchers,
|
||||
} from './block-matcher.js';
|
||||
export {
|
||||
NotionHtmlAdapter,
|
||||
NotionHtmlAdapterFactoryExtension,
|
||||
NotionHtmlAdapterFactoryIdentifier,
|
||||
} from './notion-html.js';
|
||||
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
type AdapterContext,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
BlockNotionHtmlAdapterMatcherIdentifier,
|
||||
HastUtils,
|
||||
type HtmlAST,
|
||||
type NotionHtml,
|
||||
NotionHtmlDeltaConverter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import {
|
||||
type AssetsManager,
|
||||
ASTWalker,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotPayload,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotPayload,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotPayload,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
} from '@blocksuite/store';
|
||||
import rehypeParse from 'rehype-parse';
|
||||
import { unified } from 'unified';
|
||||
|
||||
import { AdapterFactoryIdentifier } from '../type.js';
|
||||
import { defaultBlockNotionHtmlAdapterMatchers } from './block-matcher.js';
|
||||
import { notionHtmlInlineToDeltaMatchers } from './delta-converter/html-inline.js';
|
||||
|
||||
type NotionHtmlToSliceSnapshotPayload = {
|
||||
file: NotionHtml;
|
||||
assets?: AssetsManager;
|
||||
blockVersions: Record<string, number>;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
type NotionHtmlToDocSnapshotPayload = {
|
||||
file: NotionHtml;
|
||||
assets?: AssetsManager;
|
||||
pageId?: string;
|
||||
pageMap?: Map<string, string>;
|
||||
};
|
||||
|
||||
type NotionHtmlToBlockSnapshotPayload = NotionHtmlToDocSnapshotPayload;
|
||||
|
||||
export class NotionHtmlAdapter extends BaseAdapter<NotionHtml> {
|
||||
private _traverseNotionHtml = async (
|
||||
html: HtmlAST,
|
||||
snapshot: BlockSnapshot,
|
||||
assets?: AssetsManager,
|
||||
pageMap?: Map<string, string>
|
||||
) => {
|
||||
const walker = new ASTWalker<HtmlAST, BlockSnapshot>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is HtmlAST =>
|
||||
'type' in (node as object) && (node as HtmlAST).type !== undefined
|
||||
);
|
||||
walker.setEnter(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.toMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
HtmlAST,
|
||||
BlockSnapshot,
|
||||
NotionHtmlDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
pageMap,
|
||||
};
|
||||
await matcher.toBlockSnapshot.enter?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
walker.setLeave(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.toMatch(o)) {
|
||||
const adapterContext: AdapterContext<
|
||||
HtmlAST,
|
||||
BlockSnapshot,
|
||||
NotionHtmlDeltaConverter
|
||||
> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer: { content: '' },
|
||||
assets,
|
||||
pageMap,
|
||||
};
|
||||
await matcher.toBlockSnapshot.leave?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
return walker.walk(html, snapshot);
|
||||
};
|
||||
|
||||
deltaConverter: NotionHtmlDeltaConverter;
|
||||
|
||||
constructor(
|
||||
job: Job,
|
||||
readonly blockMatchers: BlockNotionHtmlAdapterMatcher[] = defaultBlockNotionHtmlAdapterMatchers
|
||||
) {
|
||||
super(job);
|
||||
this.deltaConverter = new NotionHtmlDeltaConverter(
|
||||
job.adapterConfigs,
|
||||
[],
|
||||
notionHtmlInlineToDeltaMatchers
|
||||
);
|
||||
}
|
||||
|
||||
private _htmlToAst(notionHtml: NotionHtml) {
|
||||
return unified().use(rehypeParse).parse(notionHtml);
|
||||
}
|
||||
|
||||
override fromBlockSnapshot(
|
||||
_payload: FromBlockSnapshotPayload
|
||||
): Promise<FromBlockSnapshotResult<NotionHtml>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'NotionHtmlAdapter.fromBlockSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override fromDocSnapshot(
|
||||
_payload: FromDocSnapshotPayload
|
||||
): Promise<FromDocSnapshotResult<NotionHtml>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'NotionHtmlAdapter.fromDocSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override fromSliceSnapshot(
|
||||
_payload: FromSliceSnapshotPayload
|
||||
): Promise<FromSliceSnapshotResult<NotionHtml>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'NotionHtmlAdapter.fromSliceSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override toBlockSnapshot(
|
||||
payload: NotionHtmlToBlockSnapshotPayload
|
||||
): Promise<BlockSnapshot> {
|
||||
const notionHtmlAst = this._htmlToAst(payload.file);
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
return this._traverseNotionHtml(
|
||||
notionHtmlAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets,
|
||||
payload.pageMap
|
||||
);
|
||||
}
|
||||
|
||||
override async toDoc(payload: NotionHtmlToDocSnapshotPayload) {
|
||||
const snapshot = await this.toDocSnapshot(payload);
|
||||
return this.job.snapshotToDoc(snapshot);
|
||||
}
|
||||
|
||||
override async toDocSnapshot(
|
||||
payload: NotionHtmlToDocSnapshotPayload
|
||||
): Promise<DocSnapshot> {
|
||||
const notionHtmlAst = this._htmlToAst(payload.file);
|
||||
const titleAst = HastUtils.querySelector(notionHtmlAst, 'title');
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
return {
|
||||
type: 'page',
|
||||
meta: {
|
||||
id: payload.pageId ?? nanoid(),
|
||||
title: HastUtils.getTextContent(titleAst, ''),
|
||||
createDate: Date.now(),
|
||||
tags: [],
|
||||
},
|
||||
blocks: {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:page',
|
||||
props: {
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: this.deltaConverter.astToDelta(
|
||||
titleAst ?? {
|
||||
type: 'text',
|
||||
value: '',
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:surface',
|
||||
props: {
|
||||
elements: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
await this._traverseNotionHtml(
|
||||
notionHtmlAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets,
|
||||
payload.pageMap
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
override async toSliceSnapshot(
|
||||
payload: NotionHtmlToSliceSnapshotPayload
|
||||
): Promise<SliceSnapshot | null> {
|
||||
const notionHtmlAst = this._htmlToAst(payload.file);
|
||||
const blockSnapshotRoot = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
const contentSlice = (await this._traverseNotionHtml(
|
||||
notionHtmlAst,
|
||||
blockSnapshotRoot as BlockSnapshot,
|
||||
payload.assets
|
||||
)) as BlockSnapshot;
|
||||
if (contentSlice.children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'slice',
|
||||
content: [contentSlice],
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const NotionHtmlAdapterFactoryIdentifier =
|
||||
AdapterFactoryIdentifier('NotionHtml');
|
||||
|
||||
export const NotionHtmlAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(NotionHtmlAdapterFactoryIdentifier, provider => ({
|
||||
get: (job: Job) =>
|
||||
new NotionHtmlAdapter(
|
||||
job,
|
||||
Array.from(
|
||||
provider.getAll(BlockNotionHtmlAdapterMatcherIdentifier).values()
|
||||
)
|
||||
),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import { DEFAULT_NOTE_BACKGROUND_COLOR } from '@blocksuite/affine-model';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import {
|
||||
type AssetsManager,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import { AdapterFactoryIdentifier } from './type.js';
|
||||
|
||||
type NotionEditingStyle = {
|
||||
0: string;
|
||||
};
|
||||
|
||||
type NotionEditing = {
|
||||
0: string;
|
||||
1: Array<NotionEditingStyle>;
|
||||
};
|
||||
|
||||
export type NotionTextSerialized = {
|
||||
blockType: string;
|
||||
editing: Array<NotionEditing>;
|
||||
};
|
||||
|
||||
export type NotionText = string;
|
||||
|
||||
type NotionHtmlToSliceSnapshotPayload = {
|
||||
file: NotionText;
|
||||
assets?: AssetsManager;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class NotionTextAdapter extends BaseAdapter<NotionText> {
|
||||
override fromBlockSnapshot():
|
||||
| FromBlockSnapshotResult<NotionText>
|
||||
| Promise<FromBlockSnapshotResult<NotionText>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'NotionTextAdapter.fromBlockSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override fromDocSnapshot():
|
||||
| FromDocSnapshotResult<NotionText>
|
||||
| Promise<FromDocSnapshotResult<NotionText>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'NotionTextAdapter.fromDocSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override fromSliceSnapshot():
|
||||
| FromSliceSnapshotResult<NotionText>
|
||||
| Promise<FromSliceSnapshotResult<NotionText>> {
|
||||
return {
|
||||
file: JSON.stringify({
|
||||
blockType: 'text',
|
||||
editing: [
|
||||
['Notion Text is not supported to be exported from BlockSuite', []],
|
||||
],
|
||||
}),
|
||||
assetsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
override toBlockSnapshot(): Promise<BlockSnapshot> | BlockSnapshot {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'NotionTextAdapter.toBlockSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override toDocSnapshot(): Promise<DocSnapshot> | DocSnapshot {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'NotionTextAdapter.toDocSnapshot is not implemented.'
|
||||
);
|
||||
}
|
||||
|
||||
override toSliceSnapshot(
|
||||
payload: NotionHtmlToSliceSnapshotPayload
|
||||
): SliceSnapshot | null {
|
||||
const notionText = JSON.parse(payload.file) as NotionTextSerialized;
|
||||
const content: SliceSnapshot['content'] = [];
|
||||
const deltas: DeltaInsert<AffineTextAttributes>[] = [];
|
||||
for (const editing of notionText.editing) {
|
||||
const delta: DeltaInsert<AffineTextAttributes> = {
|
||||
insert: editing[0],
|
||||
attributes: Object.create(null),
|
||||
};
|
||||
for (const styleElement of editing[1]) {
|
||||
switch (styleElement[0]) {
|
||||
case 'b':
|
||||
delta.attributes!.bold = true;
|
||||
break;
|
||||
case 'i':
|
||||
delta.attributes!.italic = true;
|
||||
break;
|
||||
case '_':
|
||||
delta.attributes!.underline = true;
|
||||
break;
|
||||
case 'c':
|
||||
delta.attributes!.code = true;
|
||||
break;
|
||||
case 's':
|
||||
delta.attributes!.strike = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
deltas.push(delta);
|
||||
}
|
||||
|
||||
content.push({
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: 'both',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltas,
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'slice',
|
||||
content,
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const NotionTextAdapterFactoryIdentifier =
|
||||
AdapterFactoryIdentifier('NotionText');
|
||||
|
||||
export const NotionTextAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(NotionTextAdapterFactoryIdentifier, () => ({
|
||||
get: (job: Job) => new NotionTextAdapter(job),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
EmbedFigmaBlockPlainTextAdapterExtension,
|
||||
embedFigmaBlockPlainTextAdapterMatcher,
|
||||
EmbedGithubBlockPlainTextAdapterExtension,
|
||||
embedGithubBlockPlainTextAdapterMatcher,
|
||||
EmbedLinkedDocBlockPlainTextAdapterExtension,
|
||||
embedLinkedDocBlockPlainTextAdapterMatcher,
|
||||
EmbedLoomBlockPlainTextAdapterExtension,
|
||||
embedLoomBlockPlainTextAdapterMatcher,
|
||||
EmbedSyncedDocBlockPlainTextAdapterExtension,
|
||||
embedSyncedDocBlockPlainTextAdapterMatcher,
|
||||
EmbedYoutubeBlockPlainTextAdapterExtension,
|
||||
embedYoutubeBlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
ListBlockPlainTextAdapterExtension,
|
||||
listBlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-list';
|
||||
import {
|
||||
ParagraphBlockPlainTextAdapterExtension,
|
||||
paragraphBlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-block-paragraph';
|
||||
import type { BlockPlainTextAdapterMatcher } from '@blocksuite/affine-shared/adapters';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
|
||||
import {
|
||||
BookmarkBlockPlainTextAdapterExtension,
|
||||
bookmarkBlockPlainTextAdapterMatcher,
|
||||
} from '../../../bookmark-block/adapters/plain-text.js';
|
||||
import {
|
||||
CodeBlockPlainTextAdapterExtension,
|
||||
codeBlockPlainTextAdapterMatcher,
|
||||
} from '../../../code-block/adapters/plain-text.js';
|
||||
import {
|
||||
DividerBlockPlainTextAdapterExtension,
|
||||
dividerBlockPlainTextAdapterMatcher,
|
||||
} from '../../../divider-block/adapters/plain-text.js';
|
||||
import {
|
||||
LatexBlockPlainTextAdapterExtension,
|
||||
latexBlockPlainTextAdapterMatcher,
|
||||
} from '../../../latex-block/adapters/plain-text.js';
|
||||
|
||||
export const defaultBlockPlainTextAdapterMatchers: BlockPlainTextAdapterMatcher[] =
|
||||
[
|
||||
paragraphBlockPlainTextAdapterMatcher,
|
||||
listBlockPlainTextAdapterMatcher,
|
||||
dividerBlockPlainTextAdapterMatcher,
|
||||
codeBlockPlainTextAdapterMatcher,
|
||||
bookmarkBlockPlainTextAdapterMatcher,
|
||||
embedFigmaBlockPlainTextAdapterMatcher,
|
||||
embedGithubBlockPlainTextAdapterMatcher,
|
||||
embedLoomBlockPlainTextAdapterMatcher,
|
||||
embedYoutubeBlockPlainTextAdapterMatcher,
|
||||
embedLinkedDocBlockPlainTextAdapterMatcher,
|
||||
embedSyncedDocBlockPlainTextAdapterMatcher,
|
||||
latexBlockPlainTextAdapterMatcher,
|
||||
];
|
||||
|
||||
export const BlockPlainTextAdapterExtensions: ExtensionType[] = [
|
||||
ParagraphBlockPlainTextAdapterExtension,
|
||||
ListBlockPlainTextAdapterExtension,
|
||||
DividerBlockPlainTextAdapterExtension,
|
||||
CodeBlockPlainTextAdapterExtension,
|
||||
BookmarkBlockPlainTextAdapterExtension,
|
||||
EmbedFigmaBlockPlainTextAdapterExtension,
|
||||
EmbedGithubBlockPlainTextAdapterExtension,
|
||||
EmbedLoomBlockPlainTextAdapterExtension,
|
||||
EmbedYoutubeBlockPlainTextAdapterExtension,
|
||||
EmbedLinkedDocBlockPlainTextAdapterExtension,
|
||||
EmbedSyncedDocBlockPlainTextAdapterExtension,
|
||||
LatexBlockPlainTextAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,78 @@
|
||||
import { generateDocUrl } from '@blocksuite/affine-block-embed';
|
||||
import type {
|
||||
InlineDeltaToPlainTextAdapterMatcher,
|
||||
TextBuffer,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const referenceDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatcher =
|
||||
{
|
||||
name: 'reference',
|
||||
match: delta => !!delta.attributes?.reference,
|
||||
toAST: (delta, context) => {
|
||||
const node: TextBuffer = {
|
||||
content: delta.insert,
|
||||
};
|
||||
const reference = delta.attributes?.reference;
|
||||
if (!reference) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const { configs } = context;
|
||||
const title = configs.get(`title:${reference.pageId}`) ?? '';
|
||||
const url = generateDocUrl(
|
||||
configs.get('docLinkBaseUrl') ?? '',
|
||||
String(reference.pageId),
|
||||
reference.params ?? Object.create(null)
|
||||
);
|
||||
const content = `${title ? `${title}: ` : ''}${url}`;
|
||||
|
||||
return {
|
||||
content,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const linkDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatcher =
|
||||
{
|
||||
name: 'link',
|
||||
match: delta => !!delta.attributes?.link,
|
||||
toAST: delta => {
|
||||
const linkText = delta.insert;
|
||||
const node: TextBuffer = {
|
||||
content: linkText,
|
||||
};
|
||||
const link = delta.attributes?.link;
|
||||
if (!link) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const content = `${linkText ? `${linkText}: ` : ''}${link}`;
|
||||
return {
|
||||
content,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const latexDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatcher =
|
||||
{
|
||||
name: 'inlineLatex',
|
||||
match: delta => !!delta.attributes?.latex,
|
||||
toAST: delta => {
|
||||
const node: TextBuffer = {
|
||||
content: delta.insert,
|
||||
};
|
||||
if (!delta.attributes?.latex) {
|
||||
return node;
|
||||
}
|
||||
return {
|
||||
content: delta.attributes?.latex,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const inlineDeltaToPlainTextAdapterMatchers: InlineDeltaToPlainTextAdapterMatcher[] =
|
||||
[
|
||||
referenceDeltaMarkdownAdapterMatch,
|
||||
linkDeltaMarkdownAdapterMatch,
|
||||
latexDeltaMarkdownAdapterMatch,
|
||||
];
|
||||
@@ -0,0 +1,321 @@
|
||||
import {
|
||||
DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
type AdapterContext,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
BlockPlainTextAdapterMatcherIdentifier,
|
||||
type PlainText,
|
||||
PlainTextDeltaConverter,
|
||||
type TextBuffer,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { ExtensionType } from '@blocksuite/block-std';
|
||||
import {
|
||||
type AssetsManager,
|
||||
ASTWalker,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
BlockSnapshotSchema,
|
||||
type DocSnapshot,
|
||||
type FromBlockSnapshotPayload,
|
||||
type FromBlockSnapshotResult,
|
||||
type FromDocSnapshotPayload,
|
||||
type FromDocSnapshotResult,
|
||||
type FromSliceSnapshotPayload,
|
||||
type FromSliceSnapshotResult,
|
||||
type Job,
|
||||
nanoid,
|
||||
type SliceSnapshot,
|
||||
type ToBlockSnapshotPayload,
|
||||
type ToDocSnapshotPayload,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import { AdapterFactoryIdentifier } from '../type.js';
|
||||
import { defaultBlockPlainTextAdapterMatchers } from './block-matcher.js';
|
||||
import { inlineDeltaToPlainTextAdapterMatchers } from './delta-converter/inline-delta.js';
|
||||
|
||||
type PlainTextToSliceSnapshotPayload = {
|
||||
file: PlainText;
|
||||
assets?: AssetsManager;
|
||||
blockVersions: Record<string, number>;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class PlainTextAdapter extends BaseAdapter<PlainText> {
|
||||
deltaConverter: PlainTextDeltaConverter;
|
||||
|
||||
constructor(
|
||||
job: Job,
|
||||
readonly blockMatchers: BlockPlainTextAdapterMatcher[] = defaultBlockPlainTextAdapterMatchers
|
||||
) {
|
||||
super(job);
|
||||
this.deltaConverter = new PlainTextDeltaConverter(
|
||||
job.adapterConfigs,
|
||||
inlineDeltaToPlainTextAdapterMatchers,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
private async _traverseSnapshot(
|
||||
snapshot: BlockSnapshot
|
||||
): Promise<{ plaintext: string }> {
|
||||
const textBuffer: TextBuffer = {
|
||||
content: '',
|
||||
};
|
||||
const walker = new ASTWalker<BlockSnapshot, TextBuffer>();
|
||||
walker.setONodeTypeGuard(
|
||||
(node): node is BlockSnapshot =>
|
||||
BlockSnapshotSchema.safeParse(node).success
|
||||
);
|
||||
walker.setEnter(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.fromMatch(o)) {
|
||||
const adapterContext: AdapterContext<BlockSnapshot, TextBuffer> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer,
|
||||
};
|
||||
await matcher.fromBlockSnapshot.enter?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
walker.setLeave(async (o, context) => {
|
||||
for (const matcher of this.blockMatchers) {
|
||||
if (matcher.fromMatch(o)) {
|
||||
const adapterContext: AdapterContext<BlockSnapshot, TextBuffer> = {
|
||||
walker,
|
||||
walkerContext: context,
|
||||
configs: this.configs,
|
||||
job: this.job,
|
||||
deltaConverter: this.deltaConverter,
|
||||
textBuffer,
|
||||
};
|
||||
await matcher.fromBlockSnapshot.leave?.(o, adapterContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
await walker.walkONode(snapshot);
|
||||
return {
|
||||
plaintext: textBuffer.content,
|
||||
};
|
||||
}
|
||||
|
||||
async fromBlockSnapshot({
|
||||
snapshot,
|
||||
}: FromBlockSnapshotPayload): Promise<FromBlockSnapshotResult<PlainText>> {
|
||||
const { plaintext } = await this._traverseSnapshot(snapshot);
|
||||
return {
|
||||
file: plaintext,
|
||||
assetsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
async fromDocSnapshot({
|
||||
snapshot,
|
||||
assets,
|
||||
}: FromDocSnapshotPayload): Promise<FromDocSnapshotResult<PlainText>> {
|
||||
let buffer = '';
|
||||
if (snapshot.meta.title) {
|
||||
buffer += `${snapshot.meta.title}\n\n`;
|
||||
}
|
||||
const { file, assetsIds } = await this.fromBlockSnapshot({
|
||||
snapshot: snapshot.blocks,
|
||||
assets,
|
||||
});
|
||||
buffer += file;
|
||||
return {
|
||||
file: buffer,
|
||||
assetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
async fromSliceSnapshot({
|
||||
snapshot,
|
||||
}: FromSliceSnapshotPayload): Promise<FromSliceSnapshotResult<PlainText>> {
|
||||
let buffer = '';
|
||||
const sliceAssetsIds: string[] = [];
|
||||
for (const contentSlice of snapshot.content) {
|
||||
const { plaintext } = await this._traverseSnapshot(contentSlice);
|
||||
buffer += plaintext;
|
||||
}
|
||||
const plaintext =
|
||||
buffer.match(/\n/g)?.length === 1 ? buffer.trimEnd() : buffer;
|
||||
return {
|
||||
file: plaintext,
|
||||
assetsIds: sliceAssetsIds,
|
||||
};
|
||||
}
|
||||
|
||||
toBlockSnapshot(payload: ToBlockSnapshotPayload<PlainText>): BlockSnapshot {
|
||||
payload.file = payload.file.replaceAll('\r', '');
|
||||
return {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: payload.file.split('\n').map((line): BlockSnapshot => {
|
||||
return {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: line,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
toDocSnapshot(payload: ToDocSnapshotPayload<PlainText>): DocSnapshot {
|
||||
payload.file = payload.file.replaceAll('\r', '');
|
||||
return {
|
||||
type: 'page',
|
||||
meta: {
|
||||
id: nanoid(),
|
||||
title: 'Untitled',
|
||||
createDate: Date.now(),
|
||||
tags: [],
|
||||
},
|
||||
blocks: {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:page',
|
||||
props: {
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'Untitled',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:surface',
|
||||
props: {
|
||||
elements: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: payload.file.split('\n').map((line): BlockSnapshot => {
|
||||
return {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: line,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
toSliceSnapshot(
|
||||
payload: PlainTextToSliceSnapshotPayload
|
||||
): SliceSnapshot | null {
|
||||
if (payload.file.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
payload.file = payload.file.replaceAll('\r', '');
|
||||
const contentSlice = {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DEFAULT_NOTE_BACKGROUND_COLOR,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: payload.file.split('\n').map((line): BlockSnapshot => {
|
||||
return {
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: line,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}),
|
||||
} as BlockSnapshot;
|
||||
return {
|
||||
type: 'slice',
|
||||
content: [contentSlice],
|
||||
workspaceId: payload.workspaceId,
|
||||
pageId: payload.pageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const PlainTextAdapterFactoryIdentifier =
|
||||
AdapterFactoryIdentifier('PlainText');
|
||||
|
||||
export const PlainTextAdapterFactoryExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(PlainTextAdapterFactoryIdentifier, provider => ({
|
||||
get: (job: Job) =>
|
||||
new PlainTextAdapter(
|
||||
job,
|
||||
Array.from(
|
||||
provider.getAll(BlockPlainTextAdapterMatcherIdentifier).values()
|
||||
)
|
||||
),
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { BaseAdapter, Job } from '@blocksuite/store';
|
||||
|
||||
export type AdapterFactory = {
|
||||
// TODO(@chen): Make it return the specific adapter type
|
||||
get: (job: Job) => BaseAdapter;
|
||||
};
|
||||
|
||||
export const AdapterFactoryIdentifier =
|
||||
createIdentifier<AdapterFactory>('AdapterFactory');
|
||||
Reference in New Issue
Block a user