Skip to content

Commit 661c628

Browse files
feat(blocks): add plain text adapter for mind map element (#9006)
[BS-2163](https://linear.app/affine-design/issue/BS-2163/补齐-mindmap-plain-text-adapter)
1 parent 0f25a1e commit 661c628

File tree

16 files changed

+288
-58
lines changed

16 files changed

+288
-58
lines changed

packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/brush.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
1+
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';
22

3-
export const brushElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
3+
export const brushToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
44
{
55
name: 'brush',
66
match: elementModel => elementModel.type === 'brush',

packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/connector.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import type { DeltaInsert } from '@blocksuite/inline/types';
22

3-
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
3+
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';
44

5-
export const connectorElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
5+
export const connectorToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
66
{
77
name: 'connector',
88
match: elementModel => elementModel.type === 'connector',
99
toAST: elementModel => {
1010
let text = '';
11-
if ('text' in elementModel && elementModel.text) {
11+
if (
12+
'text' in elementModel &&
13+
typeof elementModel.text === 'object' &&
14+
elementModel.text
15+
) {
1216
let delta: DeltaInsert[] = [];
1317
if ('delta' in elementModel.text) {
1418
delta = elementModel.text.delta as DeltaInsert[];

packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/group.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import type { DeltaInsert } from '@blocksuite/inline/types';
22

3-
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
3+
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';
44

5-
export const groupElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
5+
export const groupToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
66
{
77
name: 'group',
88
match: elementModel => elementModel.type === 'group',
99
toAST: elementModel => {
1010
let title = '';
11-
if ('title' in elementModel && elementModel.title) {
11+
if (
12+
'title' in elementModel &&
13+
typeof elementModel.title === 'object' &&
14+
elementModel.title
15+
) {
1216
let delta: DeltaInsert[] = [];
1317
if ('delta' in elementModel.title) {
1418
delta = elementModel.title.delta as DeltaInsert[];
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { brushElementModelToPlainTextAdapterMatcher } from './brush.js';
2-
import { connectorElementModelToPlainTextAdapterMatcher } from './connector.js';
3-
import { groupElementModelToPlainTextAdapterMatcher } from './group.js';
4-
import { shapeElementModelToPlainTextAdapterMatcher } from './shape.js';
5-
import { textElementModelToPlainTextAdapterMatcher } from './text.js';
1+
import { brushToPlainTextAdapterMatcher } from './brush.js';
2+
import { connectorToPlainTextAdapterMatcher } from './connector.js';
3+
import { groupToPlainTextAdapterMatcher } from './group.js';
4+
import { mindmapToPlainTextAdapterMatcher } from './mindmap.js';
5+
import { shapeToPlainTextAdapterMatcher } from './shape.js';
6+
import { textToPlainTextAdapterMatcher } from './text.js';
67

78
export const elementModelToPlainTextAdapterMatchers = [
8-
groupElementModelToPlainTextAdapterMatcher,
9-
shapeElementModelToPlainTextAdapterMatcher,
10-
connectorElementModelToPlainTextAdapterMatcher,
11-
brushElementModelToPlainTextAdapterMatcher,
12-
textElementModelToPlainTextAdapterMatcher,
9+
groupToPlainTextAdapterMatcher,
10+
shapeToPlainTextAdapterMatcher,
11+
connectorToPlainTextAdapterMatcher,
12+
brushToPlainTextAdapterMatcher,
13+
textToPlainTextAdapterMatcher,
14+
mindmapToPlainTextAdapterMatcher,
1315
];
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { MindMapTreeNode } from '../../../types/mindmap.js';
2+
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';
3+
4+
import { buildMindMapTree } from '../../../utils/mindmap.js';
5+
import { getShapeText } from '../../../utils/shape.js';
6+
7+
export const mindmapToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
8+
{
9+
name: 'mindmap',
10+
match: elementModel => elementModel.type === 'mindmap',
11+
toAST: (elementModel, context) => {
12+
let content = '';
13+
const mindMapTree = buildMindMapTree(elementModel);
14+
if (!mindMapTree) {
15+
return { content };
16+
}
17+
// traverse the mindMapTree and construct the content string
18+
// like:
19+
// - Root
20+
// - Child 1
21+
// - Child 1.1
22+
// - Child 1.2
23+
// - Child 2
24+
// - Child 2.1
25+
// - Child 2.2
26+
// - Child 3
27+
// - Child 3.1
28+
// - Child 3.2
29+
const { elements } = context;
30+
let layer = 0;
31+
let mindMapContent = '';
32+
const traverseMindMapTree = (node: MindMapTreeNode, prefix: string) => {
33+
const shapeElement = elements[node.id as string];
34+
const shapeText = getShapeText(shapeElement);
35+
if (shapeElement) {
36+
mindMapContent += `${prefix.repeat(layer * 4)}- ${shapeText}\n`;
37+
}
38+
node.children.forEach(child => {
39+
layer++;
40+
traverseMindMapTree(child, prefix);
41+
layer--;
42+
});
43+
};
44+
traverseMindMapTree(mindMapTree, ' ');
45+
content = `Mind Map with nodes:\n${mindMapContent}`;
46+
return { content };
47+
},
48+
};
Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
1-
import type { DeltaInsert } from '@blocksuite/inline/types';
1+
import type { MindMapTreeNode } from '../../../types/mindmap.js';
2+
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';
23

3-
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
4+
import { getShapeText } from '../../../utils/shape.js';
45

5-
export const shapeElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
6+
export const shapeToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
67
{
78
name: 'shape',
89
match: elementModel => elementModel.type === 'shape',
9-
toAST: elementModel => {
10-
let text = '';
11-
let shapeType = '';
12-
if ('text' in elementModel && elementModel.text) {
13-
let delta: DeltaInsert[] = [];
14-
if ('delta' in elementModel.text) {
15-
delta = elementModel.text.delta as DeltaInsert[];
10+
toAST: (elementModel, context) => {
11+
let content = '';
12+
const { walkerContext } = context;
13+
const mindMapNodeMaps = walkerContext.getGlobalContext(
14+
'surface:mindMap:nodeMapArray'
15+
) as Array<Map<string, MindMapTreeNode>>;
16+
if (mindMapNodeMaps && mindMapNodeMaps.length > 0) {
17+
// Check if the elementModel is a mindMap node
18+
// If it is, we should return { content: '' } directly
19+
// And get the content when we handle the whole mindMap
20+
const isMindMapNode = mindMapNodeMaps.some(nodeMap =>
21+
nodeMap.has(elementModel.id as string)
22+
);
23+
if (isMindMapNode) {
24+
return { content };
1625
}
17-
text = delta.map(d => d.insert).join('');
1826
}
19-
if ('shapeType' in elementModel) {
27+
28+
// If it is not, we should return the text and shapeType
29+
const text = getShapeText(elementModel);
30+
let shapeType = '';
31+
if (
32+
'shapeType' in elementModel &&
33+
typeof elementModel.shapeType === 'string'
34+
) {
2035
shapeType =
2136
elementModel.shapeType.charAt(0).toUpperCase() +
2237
elementModel.shapeType.slice(1);
2338
}
24-
const content = `${shapeType}, with text label "${text}"`;
39+
content = `${shapeType}, with text label "${text}"`;
2540
return { content };
2641
},
2742
};

packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/text.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import type { DeltaInsert } from '@blocksuite/inline/types';
22

3-
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
3+
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';
44

5-
export const textElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
5+
export const textToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
66
{
77
name: 'text',
88
match: elementModel => elementModel.type === 'text',
99
toAST: elementModel => {
1010
let content = '';
11-
if ('text' in elementModel && elementModel.text) {
11+
if (
12+
'text' in elementModel &&
13+
typeof elementModel.text === 'object' &&
14+
elementModel.text
15+
) {
1216
let delta: DeltaInsert[] = [];
1317
if ('delta' in elementModel.text) {
1418
delta = elementModel.text.delta as DeltaInsert[];

packages/affine/block-surface/src/adapters/plain-text/element-adapter/index.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
1-
import type { ElementModelMap } from '../../../element-model/index.js';
2-
import type { ElementModelToPlainTextAdapterMatcher } from './elements/type.js';
1+
import type { TextBuffer } from '@blocksuite/affine-shared/adapters';
32

4-
import { ElementModelAdapter } from '../../type.js';
3+
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
4+
5+
import {
6+
ElementModelAdapter,
7+
type ElementModelAdapterContext,
8+
} from '../../type.js';
59
import { elementModelToPlainTextAdapterMatchers } from './elements/index.js';
610

7-
export class PlainTextElementModelAdapter extends ElementModelAdapter<string> {
11+
export class PlainTextElementModelAdapter extends ElementModelAdapter<
12+
string,
13+
TextBuffer
14+
> {
815
constructor(
916
readonly elementModelMatchers: ElementModelToPlainTextAdapterMatcher[] = elementModelToPlainTextAdapterMatchers
1017
) {
1118
super();
1219
}
1320

14-
fromElementModel(elementModel: ElementModelMap[keyof ElementModelMap]) {
21+
fromElementModel(
22+
element: Record<string, unknown>,
23+
context: ElementModelAdapterContext<TextBuffer>
24+
) {
1525
for (const matcher of this.elementModelMatchers) {
16-
if (matcher.match(elementModel)) {
17-
return matcher.toAST(elementModel).content;
26+
if (matcher.match(element)) {
27+
return matcher.toAST(element, context).content;
1828
}
1929
}
2030
return '';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { TextBuffer } from '@blocksuite/affine-shared/adapters';
22

3-
import type { ElementModelMatcher } from '../../../type.js';
3+
import type { ElementModelMatcher } from '../../type.js';
44

55
export type ElementModelToPlainTextAdapterMatcher =
66
ElementModelMatcher<TextBuffer>;

packages/affine/block-surface/src/adapters/plain-text/plain-text.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import {
33
type BlockPlainTextAdapterMatcher,
44
} from '@blocksuite/affine-shared/adapters';
55

6-
import type { ElementModelMap } from '../../element-model/index.js';
7-
86
import { SurfaceBlockSchema } from '../../surface-model.js';
7+
import { getMindMapNodeMap } from '../utils/mindmap.js';
98
import { PlainTextElementModelAdapter } from './element-adapter/index.js';
109

1110
export const surfaceBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher =
@@ -32,15 +31,29 @@ export const edgelessSurfaceBlockPlainTextAdapterMatcher: BlockPlainTextAdapterM
3231
toBlockSnapshot: {},
3332
fromBlockSnapshot: {
3433
enter: (o, context) => {
34+
const { walkerContext } = context;
3535
const plainTextElementModelAdapter = new PlainTextElementModelAdapter();
3636
if ('elements' in o.node.props) {
37+
const elements = o.node.props.elements as Record<
38+
string,
39+
Record<string, unknown>
40+
>;
41+
// Get all the node maps of mindMap elements
42+
const mindMapArray = Object.entries(elements)
43+
.filter(([_, element]) => element.type === 'mindmap')
44+
.map(([_, element]) => getMindMapNodeMap(element));
45+
walkerContext.setGlobalContext(
46+
'surface:mindMap:nodeMapArray',
47+
mindMapArray
48+
);
49+
3750
Object.entries(
3851
o.node.props.elements as Record<string, Record<string, unknown>>
39-
).forEach(([_, elementModel]) => {
40-
const element =
41-
elementModel as unknown as ElementModelMap[keyof ElementModelMap];
42-
const plainText =
43-
plainTextElementModelAdapter.fromElementModel(element);
52+
).forEach(([_, element]) => {
53+
const plainText = plainTextElementModelAdapter.fromElementModel(
54+
element,
55+
{ walkerContext, elements }
56+
);
4457
if (plainText) {
4558
context.textBuffer.content += plainText + '\n';
4659
}

0 commit comments

Comments
 (0)