refactor(editor): extensionalize plain text adapter (#9330)

This commit is contained in:
donteatfriedrice
2024-12-26 07:27:07 +00:00
parent 45acdbda04
commit cadb9211a6
27 changed files with 226 additions and 120 deletions

View File

@@ -2,6 +2,8 @@ import {
DEFAULT_NOTE_BACKGROUND_COLOR,
NoteDisplayMode,
} from '@blocksuite/affine-model';
import { PlainTextAdapter } from '@blocksuite/affine-shared/adapters';
import { Container } from '@blocksuite/global/di';
import type {
BlockSnapshot,
DocSnapshot,
@@ -9,10 +11,20 @@ import type {
} from '@blocksuite/store';
import { describe, expect, test } from 'vitest';
import { PlainTextAdapter } from '../../_common/adapters/plain-text/plain-text.js';
import { defaultBlockPlainTextAdapterMatchers } from '../../_common/adapters/plain-text/block-matcher.js';
import { inlineDeltaToPlainTextAdapterMatchers } from '../../_common/adapters/plain-text/delta-converter/inline-delta.js';
import { embedSyncedDocMiddleware } from '../../_common/transformers/middlewares.js';
import { createJob } from '../utils/create-job.js';
const container = new Container();
[
...defaultBlockPlainTextAdapterMatchers,
...inlineDeltaToPlainTextAdapterMatchers,
].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();
describe('snapshot to plain text', () => {
test('paragraph', async () => {
const blockSnapshot: BlockSnapshot = {
@@ -157,7 +169,7 @@ describe('snapshot to plain text', () => {
};
const plainText = 'aaabbbccc\nddd\neee\nfff\nggg\n';
const plainTextAdapter = new PlainTextAdapter(createJob());
const plainTextAdapter = new PlainTextAdapter(createJob(), provider);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});
@@ -316,7 +328,7 @@ describe('snapshot to plain text', () => {
const plainText = 'aaa\nbbb\nccc\nddd\neee\n';
const plainTextAdapter = new PlainTextAdapter(createJob());
const plainTextAdapter = new PlainTextAdapter(createJob(), provider);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});
@@ -386,7 +398,7 @@ describe('snapshot to plain text', () => {
};
const plainText = 'aaa\n---\n';
const plainTextAdapter = new PlainTextAdapter(createJob());
const plainTextAdapter = new PlainTextAdapter(createJob(), provider);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});
@@ -449,7 +461,7 @@ describe('snapshot to plain text', () => {
};
const plainText = 'import this\n';
const plainTextAdapter = new PlainTextAdapter(createJob());
const plainTextAdapter = new PlainTextAdapter(createJob(), provider);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});
@@ -568,7 +580,10 @@ describe('snapshot to plain text', () => {
adapterConfigs.set('title:deadbeef', 'test');
adapterConfigs.set('docLinkBaseUrl', 'https://example.com');
};
const plainTextAdapter = new PlainTextAdapter(createJob([middleware]));
const plainTextAdapter = new PlainTextAdapter(
createJob([middleware]),
provider
);
const plainText =
'aaa: https://affine.pro/\ntest: https://example.com/deadbeef?mode=page&blockIds=abc%2C123&elementIds=def%2C456&databaseId=deadbeef&databaseRowId=123\nE=mc^2\n';
@@ -670,7 +685,7 @@ describe('snapshot to plain text', () => {
],
};
const plainTextAdapter = new PlainTextAdapter(createJob());
const plainTextAdapter = new PlainTextAdapter(createJob(), provider);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});
@@ -750,7 +765,10 @@ describe('snapshot to plain text', () => {
};
const plainText =
'test: https://example.com/4T5ObMgEIMII-4Bexyta1?mode=page&blockIds=abc%2C123&elementIds=def%2C456&databaseId=deadbeef&databaseRowId=123\n';
const plainTextAdapter = new PlainTextAdapter(createJob([middleware]));
const plainTextAdapter = new PlainTextAdapter(
createJob([middleware]),
provider
);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapShot,
});
@@ -1146,7 +1164,7 @@ describe('snapshot to plain text', () => {
await job.snapshotToDoc(syncedDocSnapshot);
await job.snapshotToDoc(docSnapShot);
const mdAdapter = new PlainTextAdapter(job);
const mdAdapter = new PlainTextAdapter(job, provider);
const target = await mdAdapter.fromDocSnapshot({
snapshot: docSnapShot,
});
@@ -1180,7 +1198,7 @@ describe('snapshot to plain text', () => {
};
const plainText = 'LaTex, with value: E=mc^2\n';
const plainTextAdapter = new PlainTextAdapter(createJob());
const plainTextAdapter = new PlainTextAdapter(createJob(), provider);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});
@@ -1405,7 +1423,7 @@ describe('snapshot to plain text', () => {
| Task 1 | TODO | 2023-12-15 | 1 | 65 | test1,test2 | test2: https://google.com | https://google.com | true |
| Task 2 | In Progress | 2023-12-20 | | | | test1 | | |
`;
const plainTextAdapter = new PlainTextAdapter(createJob());
const plainTextAdapter = new PlainTextAdapter(createJob(), provider);
const target = await plainTextAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});

View File

@@ -1,4 +1,7 @@
import { HtmlAdapterFactoryExtension } from '@blocksuite/affine-shared/adapters';
import {
HtmlAdapterFactoryExtension,
PlainTextAdapterFactoryExtension,
} from '@blocksuite/affine-shared/adapters';
import type { ExtensionType } from '@blocksuite/block-std';
import { AttachmentAdapterFactoryExtension } from './attachment.js';
@@ -10,12 +13,13 @@ import { MixTextAdapterFactoryExtension } from './mix-text.js';
import { notionHtmlInlineToDeltaMatchers } from './notion-html/delta-converter/html-inline.js';
import { NotionHtmlAdapterFactoryExtension } from './notion-html/notion-html.js';
import { NotionTextAdapterFactoryExtension } from './notion-text.js';
import { PlainTextAdapterFactoryExtension } from './plain-text/plain-text.js';
import { inlineDeltaToPlainTextAdapterMatchers } from './plain-text/delta-converter/inline-delta.js';
export const AdapterFactoryExtensions: ExtensionType[] = [
...htmlInlineToDeltaMatchers,
...inlineDeltaToHtmlAdapterMatchers,
...notionHtmlInlineToDeltaMatchers,
...inlineDeltaToPlainTextAdapterMatchers,
AttachmentAdapterFactoryExtension,
ImageAdapterFactoryExtension,
MarkdownAdapterFactoryExtension,

View File

@@ -5,4 +5,3 @@ 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';

View File

@@ -1,34 +1,33 @@
import { bookmarkBlockPlainTextAdapterMatcher } from '@blocksuite/affine-block-bookmark';
import { BookmarkBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-bookmark';
import {
embedFigmaBlockPlainTextAdapterMatcher,
embedGithubBlockPlainTextAdapterMatcher,
embedLinkedDocBlockPlainTextAdapterMatcher,
embedLoomBlockPlainTextAdapterMatcher,
embedSyncedDocBlockPlainTextAdapterMatcher,
embedYoutubeBlockPlainTextAdapterMatcher,
EmbedFigmaBlockPlainTextAdapterExtension,
EmbedGithubBlockPlainTextAdapterExtension,
EmbedLinkedDocBlockPlainTextAdapterExtension,
EmbedLoomBlockPlainTextAdapterExtension,
EmbedSyncedDocBlockPlainTextAdapterExtension,
EmbedYoutubeBlockPlainTextAdapterExtension,
} from '@blocksuite/affine-block-embed';
import { latexBlockPlainTextAdapterMatcher } from '@blocksuite/affine-block-latex';
import { listBlockPlainTextAdapterMatcher } from '@blocksuite/affine-block-list';
import { paragraphBlockPlainTextAdapterMatcher } from '@blocksuite/affine-block-paragraph';
import type { BlockPlainTextAdapterMatcher } from '@blocksuite/affine-shared/adapters';
import { LatexBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-latex';
import { ListBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-list';
import { ParagraphBlockPlainTextAdapterExtension } from '@blocksuite/affine-block-paragraph';
import type { ExtensionType } from '@blocksuite/block-std';
import { codeBlockPlainTextAdapterMatcher } from '../../../code-block/adapters/plain-text.js';
import { databaseBlockPlainTextAdapterMatcher } from '../../../database-block/adapters/plain-text.js';
import { dividerBlockPlainTextAdapterMatcher } from '../../../divider-block/adapters/plain-text.js';
import { CodeBlockPlainTextAdapterExtension } from '../../../code-block/adapters/plain-text.js';
import { DatabaseBlockPlainTextAdapterExtension } from '../../../database-block/adapters/plain-text.js';
import { DividerBlockPlainTextAdapterExtension } from '../../../divider-block/adapters/plain-text.js';
export const defaultBlockPlainTextAdapterMatchers: BlockPlainTextAdapterMatcher[] =
[
paragraphBlockPlainTextAdapterMatcher,
listBlockPlainTextAdapterMatcher,
dividerBlockPlainTextAdapterMatcher,
codeBlockPlainTextAdapterMatcher,
bookmarkBlockPlainTextAdapterMatcher,
embedFigmaBlockPlainTextAdapterMatcher,
embedGithubBlockPlainTextAdapterMatcher,
embedLoomBlockPlainTextAdapterMatcher,
embedYoutubeBlockPlainTextAdapterMatcher,
embedLinkedDocBlockPlainTextAdapterMatcher,
embedSyncedDocBlockPlainTextAdapterMatcher,
latexBlockPlainTextAdapterMatcher,
databaseBlockPlainTextAdapterMatcher,
];
export const defaultBlockPlainTextAdapterMatchers: ExtensionType[] = [
ParagraphBlockPlainTextAdapterExtension,
ListBlockPlainTextAdapterExtension,
DividerBlockPlainTextAdapterExtension,
CodeBlockPlainTextAdapterExtension,
BookmarkBlockPlainTextAdapterExtension,
EmbedFigmaBlockPlainTextAdapterExtension,
EmbedGithubBlockPlainTextAdapterExtension,
EmbedLoomBlockPlainTextAdapterExtension,
EmbedYoutubeBlockPlainTextAdapterExtension,
EmbedLinkedDocBlockPlainTextAdapterExtension,
EmbedSyncedDocBlockPlainTextAdapterExtension,
LatexBlockPlainTextAdapterExtension,
DatabaseBlockPlainTextAdapterExtension,
];

View File

@@ -1,11 +1,12 @@
import { generateDocUrl } from '@blocksuite/affine-block-embed';
import type {
InlineDeltaToPlainTextAdapterMatcher,
TextBuffer,
import {
InlineDeltaToPlainTextAdapterExtension,
type TextBuffer,
} from '@blocksuite/affine-shared/adapters';
import type { ExtensionType } from '@blocksuite/block-std';
export const referenceDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatcher =
{
export const referenceDeltaMarkdownAdapterMatch =
InlineDeltaToPlainTextAdapterExtension({
name: 'reference',
match: delta => !!delta.attributes?.reference,
toAST: (delta, context) => {
@@ -30,10 +31,10 @@ export const referenceDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMa
content,
};
},
};
});
export const linkDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatcher =
{
export const linkDeltaMarkdownAdapterMatch =
InlineDeltaToPlainTextAdapterExtension({
name: 'link',
match: delta => !!delta.attributes?.link,
toAST: delta => {
@@ -51,10 +52,10 @@ export const linkDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatcher
content,
};
},
};
});
export const latexDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatcher =
{
export const latexDeltaMarkdownAdapterMatch =
InlineDeltaToPlainTextAdapterExtension({
name: 'inlineLatex',
match: delta => !!delta.attributes?.latex,
toAST: delta => {
@@ -68,11 +69,10 @@ export const latexDeltaMarkdownAdapterMatch: InlineDeltaToPlainTextAdapterMatche
content: delta.attributes?.latex,
};
},
};
});
export const inlineDeltaToPlainTextAdapterMatchers: InlineDeltaToPlainTextAdapterMatcher[] =
[
referenceDeltaMarkdownAdapterMatch,
linkDeltaMarkdownAdapterMatch,
latexDeltaMarkdownAdapterMatch,
];
export const inlineDeltaToPlainTextAdapterMatchers: ExtensionType[] = [
referenceDeltaMarkdownAdapterMatch,
linkDeltaMarkdownAdapterMatch,
latexDeltaMarkdownAdapterMatch,
];

View File

@@ -1,321 +0,0 @@
import {
DEFAULT_NOTE_BACKGROUND_COLOR,
NoteDisplayMode,
} from '@blocksuite/affine-model';
import {
type AdapterContext,
AdapterFactoryIdentifier,
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 { 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()
)
),
}));
},
};

View File

@@ -1,4 +1,7 @@
import { HtmlAdapter } from '@blocksuite/affine-shared/adapters';
import {
HtmlAdapter,
PlainTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import {
type BlockComponent,
Clipboard,
@@ -6,7 +9,6 @@ import {
} from '@blocksuite/block-std';
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
import { PlainTextAdapter } from '../../_common/adapters/index.js';
import { pasteMiddleware } from '../../root-block/clipboard/middlewares/index.js';
export class CodeClipboardController {

View File

@@ -103,6 +103,9 @@ export {
HtmlAdapter,
HtmlAdapterFactoryExtension,
HtmlAdapterFactoryIdentifier,
PlainTextAdapter,
PlainTextAdapterFactoryExtension,
PlainTextAdapterFactoryIdentifier,
} from '@blocksuite/affine-shared/adapters';
export * from '@blocksuite/affine-shared/services';
export { scrollbarStyle } from '@blocksuite/affine-shared/styles';