mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor(editor): extract mix-text adapter to shared (#9559)
This commit is contained in:
@@ -35,6 +35,7 @@ export {
|
||||
MarkdownDeltaConverter,
|
||||
} from './markdown';
|
||||
export * from './middlewares';
|
||||
export * from './mix-text';
|
||||
export {
|
||||
BlockNotionHtmlAdapterExtension,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
|
||||
360
blocksuite/affine/shared/src/adapters/mix-text.ts
Normal file
360
blocksuite/affine/shared/src/adapters/mix-text.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import type { ServiceProvider } from '@blocksuite/global/di';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import {
|
||||
type AssetsManager,
|
||||
ASTWalker,
|
||||
BaseAdapter,
|
||||
type BlockSnapshot,
|
||||
BlockSnapshotSchema,
|
||||
type DocSnapshot,
|
||||
type ExtensionType,
|
||||
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/markdown';
|
||||
import { AdapterFactoryIdentifier } from './types/adapter';
|
||||
|
||||
export type MixText = string;
|
||||
|
||||
type MixTextToSliceSnapshotPayload = {
|
||||
file: MixText;
|
||||
assets?: AssetsManager;
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export class MixTextAdapter extends BaseAdapter<MixText> {
|
||||
private readonly _markdownAdapter: MarkdownAdapter;
|
||||
|
||||
constructor(job: Job, provider: ServiceProvider) {
|
||||
super(job);
|
||||
this._markdownAdapter = new MarkdownAdapter(job, provider);
|
||||
}
|
||||
|
||||
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: DefaultTheme.noteBackgrounColor,
|
||||
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: DefaultTheme.noteBackgrounColor,
|
||||
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: DefaultTheme.noteBackgrounColor,
|
||||
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, provider => ({
|
||||
get: (job: Job) => new MixTextAdapter(job, provider),
|
||||
}));
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user