refactor(editor): improve element adapters (#11473)

This commit is contained in:
Saul-Mirone
2025-04-05 09:40:13 +00:00
parent 0fbca31c27
commit aed7f40568
45 changed files with 292 additions and 248 deletions

View File

@@ -0,0 +1,2 @@
export * from './markdown';
export * from './plain-text';

View File

@@ -0,0 +1,18 @@
import { ElementToMarkdownAdapterExtension } from '@blocksuite/affine-block-surface';
export const brushToMarkdownAdapterMatcher = ElementToMarkdownAdapterExtension({
name: 'brush',
match: elementModel => elementModel.type === 'brush',
toAST: () => {
const content = `Brush Stroke`;
return {
type: 'paragraph',
children: [
{
type: 'text',
value: content,
},
],
};
},
});

View File

@@ -0,0 +1,11 @@
import { ElementToPlainTextAdapterExtension } from '@blocksuite/affine-block-surface';
export const brushToPlainTextAdapterMatcher =
ElementToPlainTextAdapterExtension({
name: 'brush',
match: elementModel => elementModel.type === 'brush',
toAST: () => {
const content = `Brush Stroke`;
return { content };
},
});

View File

@@ -1,3 +1,4 @@
export * from './adapter';
export * from './brush-tool';
export * from './element-renderer';
export * from './eraser-tool';

View File

@@ -0,0 +1,2 @@
export * from './markdown';
export * from './plain-text';

View File

@@ -0,0 +1,26 @@
import { ElementToMarkdownAdapterExtension } from '@blocksuite/affine-block-surface';
import { getConnectorText } from '../text';
export const connectorToMarkdownAdapterMatcher =
ElementToMarkdownAdapterExtension({
name: 'connector',
match: elementModel => elementModel.type === 'connector',
toAST: elementModel => {
const text = getConnectorText(elementModel);
if (!text) {
return null;
}
const content = `Connector, with text label "${text}"`;
return {
type: 'paragraph',
children: [
{
type: 'text',
value: content,
},
],
};
},
});

View File

@@ -0,0 +1,14 @@
import { ElementToPlainTextAdapterExtension } from '@blocksuite/affine-block-surface';
import { getConnectorText } from '../text';
export const connectorToPlainTextAdapterMatcher =
ElementToPlainTextAdapterExtension({
name: 'connector',
match: elementModel => elementModel.type === 'connector',
toAST: elementModel => {
const text = getConnectorText(elementModel);
const content = `Connector, with text label "${text}"`;
return { content };
},
});

View File

@@ -0,0 +1,23 @@
import type { DeltaInsert } from '@blocksuite/affine/store';
export function getConnectorText(
elementModel: Record<string, unknown>
): string {
let text = '';
if (elementModel.type !== 'connector') {
return text;
}
if (
'text' in elementModel &&
typeof elementModel.text === 'object' &&
elementModel.text
) {
let delta: DeltaInsert[] = [];
if ('delta' in elementModel.text) {
delta = elementModel.text.delta as DeltaInsert[];
}
text = delta.map(d => d.insert).join('');
}
return text;
}

View File

@@ -1,3 +1,4 @@
export * from './adapter';
export * from './connector-tool';
export * from './element-renderer';
export * from './element-transform';

View File

@@ -0,0 +1,2 @@
export * from './markdown';
export * from './plain-text';

View File

@@ -0,0 +1,25 @@
import { ElementToMarkdownAdapterExtension } from '@blocksuite/affine-block-surface';
import { getGroupTitle } from '../text';
export const groupToMarkdownAdapterMatcher = ElementToMarkdownAdapterExtension({
name: 'group',
match: elementModel => elementModel.type === 'group',
toAST: elementModel => {
const title = getGroupTitle(elementModel);
if (!title) {
return null;
}
const content = `Group, with title "${title}"`;
return {
type: 'paragraph',
children: [
{
type: 'text',
value: content,
},
],
};
},
});

View File

@@ -0,0 +1,14 @@
import { ElementToPlainTextAdapterExtension } from '@blocksuite/affine-block-surface';
import { getGroupTitle } from '../text';
export const groupToPlainTextAdapterMatcher =
ElementToPlainTextAdapterExtension({
name: 'group',
match: elementModel => elementModel.type === 'group',
toAST: elementModel => {
const title = getGroupTitle(elementModel);
const content = `Group, with title "${title}"`;
return { content };
},
});

View File

@@ -0,0 +1,21 @@
import type { DeltaInsert } from '@blocksuite/affine/store';
export function getGroupTitle(elementModel: Record<string, unknown>): string {
let title = '';
if (elementModel.type !== 'group') {
return title;
}
if (
'title' in elementModel &&
typeof elementModel.title === 'object' &&
elementModel.title
) {
let delta: DeltaInsert[] = [];
if ('delta' in elementModel.title) {
delta = elementModel.title.delta as DeltaInsert[];
}
title = delta.map(d => d.insert).join('');
}
return title;
}

View File

@@ -1,3 +1,4 @@
export * from './adapter';
export * from './command';
export * from './element-renderer';
export * from './text/text';

View File

@@ -0,0 +1,2 @@
export * from './markdown';
export * from './plain-text';

View File

@@ -0,0 +1,69 @@
import { ElementToMarkdownAdapterExtension } from '@blocksuite/affine-block-surface';
import { getShapeText } from '@blocksuite/affine-gfx-shape';
import {
buildMindMapTree,
type MindMapTreeNode,
} from '@blocksuite/affine-model';
export const mindmapToMarkdownAdapterMatcher =
ElementToMarkdownAdapterExtension({
name: 'mindmap',
match: elementModel => elementModel.type === 'mindmap',
toAST: (elementModel, context) => {
if (elementModel.type !== 'mindmap') {
return null;
}
const mindMapTree = buildMindMapTree(elementModel);
if (!mindMapTree) {
return null;
}
const { walkerContext, elements } = context;
const traverseMindMapTree = (node: MindMapTreeNode) => {
const shapeElement = elements[node.id as string];
const shapeText = getShapeText(shapeElement);
walkerContext
.openNode(
{
type: 'listItem',
spread: false,
children: [],
},
'children'
)
.openNode(
{
type: 'paragraph',
children: [
{
type: 'text',
value: shapeText,
},
],
},
'children'
)
.closeNode();
node.children.forEach(child => {
traverseMindMapTree(child);
walkerContext.closeNode();
});
};
// First create a list node for the mind map tree
walkerContext.openNode(
{
type: 'list',
ordered: false,
spread: false,
children: [],
},
'children'
);
traverseMindMapTree(mindMapTree);
walkerContext.closeNode().closeNode();
return null;
},
});

View File

@@ -0,0 +1,13 @@
import { ElementToPlainTextAdapterExtension } from '@blocksuite/affine-block-surface';
import { getMindMapTreeText } from '../utils';
export const mindmapToPlainTextAdapterMatcher =
ElementToPlainTextAdapterExtension({
name: 'mindmap',
match: elementModel => elementModel.type === 'mindmap',
toAST: (elementModel, context) => {
const mindMapContent = getMindMapTreeText(elementModel, context.elements);
return { content: mindMapContent };
},
});

View File

@@ -0,0 +1,65 @@
import { getShapeText } from '@blocksuite/affine-gfx-shape';
import {
buildMindMapTree,
type MindMapTreeNode,
} from '@blocksuite/affine-model';
/**
* traverse the mindMapTree and construct the content string
* like:
* - Root
* - Child 1
* - Child 1.1
* - Child 1.2
* - Child 2
* - Child 2.1
* - Child 2.2
* - Child 3
* - Child 3.1
* - Child 3.2
* @param elementModel - the mindmap element model
* @param elements - the elements map
* @returns the mindmap tree text
*/
export function getMindMapTreeText(
elementModel: Record<string, unknown>,
elements: Record<string, Record<string, unknown>>,
options: {
prefix: string;
repeat: number;
} = {
prefix: ' ',
repeat: 2,
}
): string {
let mindMapContent = '';
if (elementModel.type !== 'mindmap') {
return mindMapContent;
}
const mindMapTree = buildMindMapTree(elementModel);
if (!mindMapTree) {
return mindMapContent;
}
let layer = 0;
const traverseMindMapTree = (
node: MindMapTreeNode,
prefix: string,
repeat: number
) => {
const shapeElement = elements[node.id as string];
const shapeText = getShapeText(shapeElement);
if (shapeElement) {
mindMapContent += `${prefix.repeat(layer * repeat)}- ${shapeText}\n`;
}
node.children.forEach(child => {
layer++;
traverseMindMapTree(child, prefix, repeat);
layer--;
});
};
traverseMindMapTree(mindMapTree, options.prefix, options.repeat);
return mindMapContent;
}

View File

@@ -1,3 +1,4 @@
export * from './adapter';
export * from './element-renderer';
export * from './element-transform';
export * from './indicator-overlay';

View File

@@ -0,0 +1,3 @@
export * from './markdown';
export * from './plain-text';
export * from './utils';

View File

@@ -0,0 +1,46 @@
import { ElementToMarkdownAdapterExtension } from '@blocksuite/affine-block-surface';
import type { MindMapTreeNode } from '@blocksuite/affine-model';
import { getShapeText, getShapeType } from '../utils';
export const shapeToMarkdownAdapterMatcher = ElementToMarkdownAdapterExtension({
name: 'shape',
match: elementModel => elementModel.type === 'shape',
toAST: (elementModel, context) => {
let content = '';
const { walkerContext } = context;
const mindMapNodeMaps = walkerContext.getGlobalContext(
'surface:mindMap:nodeMapArray'
) as Array<Map<string, MindMapTreeNode>>;
if (mindMapNodeMaps && mindMapNodeMaps.length > 0) {
// Check if the elementModel is a mindMap node
// If it is, we should return { content: '' } directly
// And get the content when we handle the whole mindMap
const isMindMapNode = mindMapNodeMaps.some(nodeMap =>
nodeMap.has(elementModel.id as string)
);
if (isMindMapNode) {
return null;
}
}
// If it is not, we should return the text and shapeType
const text = getShapeText(elementModel);
const type = getShapeType(elementModel);
if (!text && !type) {
return null;
}
const shapeType = type.charAt(0).toUpperCase() + type.slice(1);
content = `${shapeType}, with text label "${text}"`;
return {
type: 'paragraph',
children: [
{
type: 'text',
value: content,
},
],
};
},
});

View File

@@ -0,0 +1,35 @@
import { ElementToPlainTextAdapterExtension } from '@blocksuite/affine-block-surface';
import type { MindMapTreeNode } from '@blocksuite/affine-model';
import { getShapeText, getShapeType } from '../utils';
export const shapeToPlainTextAdapterMatcher =
ElementToPlainTextAdapterExtension({
name: 'shape',
match: elementModel => elementModel.type === 'shape',
toAST: (elementModel, context) => {
let content = '';
const { walkerContext } = context;
const mindMapNodeMaps = walkerContext.getGlobalContext(
'surface:mindMap:nodeMapArray'
) as Array<Map<string, MindMapTreeNode>>;
if (mindMapNodeMaps && mindMapNodeMaps.length > 0) {
// Check if the elementModel is a mindMap node
// If it is, we should return { content: '' } directly
// And get the content when we handle the whole mindMap
const isMindMapNode = mindMapNodeMaps.some(nodeMap =>
nodeMap.has(elementModel.id as string)
);
if (isMindMapNode) {
return { content };
}
}
// If it is not, we should return the text and shapeType
const text = getShapeText(elementModel);
const type = getShapeType(elementModel);
const shapeType = type.charAt(0).toUpperCase() + type.slice(1);
content = `${shapeType}, with text label "${text}"`;
return { content };
},
});

View File

@@ -0,0 +1,36 @@
import type { DeltaInsert } from '@blocksuite/store';
export function getShapeType(elementModel: Record<string, unknown>): string {
let shapeType = '';
if (elementModel.type !== 'shape') {
return shapeType;
}
if (
'shapeType' in elementModel &&
typeof elementModel.shapeType === 'string'
) {
shapeType = elementModel.shapeType;
}
return shapeType;
}
export function getShapeText(elementModel: Record<string, unknown>): string {
let text = '';
if (elementModel.type !== 'shape') {
return text;
}
if (
'text' in elementModel &&
typeof elementModel.text === 'object' &&
elementModel.text
) {
let delta: DeltaInsert[] = [];
if ('delta' in elementModel.text) {
delta = elementModel.text.delta as DeltaInsert[];
}
text = delta.map(d => d.insert).join('');
}
return text;
}

View File

@@ -1,3 +1,4 @@
export * from './adapter';
export * from './consts';
export * from './draggable';
export * from './element-renderer';

View File

@@ -0,0 +1,2 @@
export * from './markdown';
export * from './plain-text';

View File

@@ -0,0 +1,24 @@
import { ElementToMarkdownAdapterExtension } from '@blocksuite/affine-block-surface';
import { getTextElementText } from '../text';
export const textToMarkdownAdapterMatcher = ElementToMarkdownAdapterExtension({
name: 'text',
match: elementModel => elementModel.type === 'text',
toAST: elementModel => {
const content = getTextElementText(elementModel);
if (!content) {
return null;
}
return {
type: 'paragraph',
children: [
{
type: 'text',
value: content,
},
],
};
},
});

View File

@@ -0,0 +1,14 @@
import { ElementToPlainTextAdapterExtension } from '@blocksuite/affine-block-surface';
import { getTextElementText } from '../text';
export const textToPlainTextAdapterMatcher = ElementToPlainTextAdapterExtension(
{
name: 'text',
match: elementModel => elementModel.type === 'text',
toAST: elementModel => {
const content = getTextElementText(elementModel);
return { content };
},
}
);

View File

@@ -0,0 +1,22 @@
import type { DeltaInsert } from '@blocksuite/affine/store';
export function getTextElementText(
elementModel: Record<string, unknown>
): string {
let text = '';
if (elementModel.type !== 'text') {
return text;
}
if (
'text' in elementModel &&
typeof elementModel.text === 'object' &&
elementModel.text
) {
let delta: DeltaInsert[] = [];
if ('delta' in elementModel.text) {
delta = elementModel.text.delta as DeltaInsert[];
}
text = delta.map(d => d.insert).join('');
}
return text;
}

View File

@@ -1,3 +1,4 @@
export * from './adapter';
export * from './commands';
export * from './edgeless-text-editor';
export * from './element-renderer';