Skip to content

Commit

Permalink
feat(blocks): add plain text adapter for mind map element (#9006)
Browse files Browse the repository at this point in the history
  • Loading branch information
donteatfriedrice committed Dec 18, 2024
1 parent 0f25a1e commit 661c628
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';

export const brushElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
export const brushToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
{
name: 'brush',
match: elementModel => elementModel.type === 'brush',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { DeltaInsert } from '@blocksuite/inline/types';

import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';

export const connectorElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
export const connectorToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
{
name: 'connector',
match: elementModel => elementModel.type === 'connector',
toAST: elementModel => {
let text = '';
if ('text' in elementModel && elementModel.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[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { DeltaInsert } from '@blocksuite/inline/types';

import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';

export const groupElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
export const groupToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
{
name: 'group',
match: elementModel => elementModel.type === 'group',
toAST: elementModel => {
let title = '';
if ('title' in elementModel && elementModel.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[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { brushElementModelToPlainTextAdapterMatcher } from './brush.js';
import { connectorElementModelToPlainTextAdapterMatcher } from './connector.js';
import { groupElementModelToPlainTextAdapterMatcher } from './group.js';
import { shapeElementModelToPlainTextAdapterMatcher } from './shape.js';
import { textElementModelToPlainTextAdapterMatcher } from './text.js';
import { brushToPlainTextAdapterMatcher } from './brush.js';
import { connectorToPlainTextAdapterMatcher } from './connector.js';
import { groupToPlainTextAdapterMatcher } from './group.js';
import { mindmapToPlainTextAdapterMatcher } from './mindmap.js';
import { shapeToPlainTextAdapterMatcher } from './shape.js';
import { textToPlainTextAdapterMatcher } from './text.js';

export const elementModelToPlainTextAdapterMatchers = [
groupElementModelToPlainTextAdapterMatcher,
shapeElementModelToPlainTextAdapterMatcher,
connectorElementModelToPlainTextAdapterMatcher,
brushElementModelToPlainTextAdapterMatcher,
textElementModelToPlainTextAdapterMatcher,
groupToPlainTextAdapterMatcher,
shapeToPlainTextAdapterMatcher,
connectorToPlainTextAdapterMatcher,
brushToPlainTextAdapterMatcher,
textToPlainTextAdapterMatcher,
mindmapToPlainTextAdapterMatcher,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { MindMapTreeNode } from '../../../types/mindmap.js';
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';

import { buildMindMapTree } from '../../../utils/mindmap.js';
import { getShapeText } from '../../../utils/shape.js';

export const mindmapToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
{
name: 'mindmap',
match: elementModel => elementModel.type === 'mindmap',
toAST: (elementModel, context) => {
let content = '';
const mindMapTree = buildMindMapTree(elementModel);
if (!mindMapTree) {
return { content };
}
// 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
const { elements } = context;
let layer = 0;
let mindMapContent = '';
const traverseMindMapTree = (node: MindMapTreeNode, prefix: string) => {
const shapeElement = elements[node.id as string];
const shapeText = getShapeText(shapeElement);
if (shapeElement) {
mindMapContent += `${prefix.repeat(layer * 4)}- ${shapeText}\n`;
}
node.children.forEach(child => {
layer++;
traverseMindMapTree(child, prefix);
layer--;
});
};
traverseMindMapTree(mindMapTree, ' ');
content = `Mind Map with nodes:\n${mindMapContent}`;
return { content };
},
};
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import type { DeltaInsert } from '@blocksuite/inline/types';
import type { MindMapTreeNode } from '../../../types/mindmap.js';
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';

import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
import { getShapeText } from '../../../utils/shape.js';

export const shapeElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
export const shapeToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
{
name: 'shape',
match: elementModel => elementModel.type === 'shape',
toAST: elementModel => {
let text = '';
let shapeType = '';
if ('text' in elementModel && elementModel.text) {
let delta: DeltaInsert[] = [];
if ('delta' in elementModel.text) {
delta = elementModel.text.delta as DeltaInsert[];
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 };
}
text = delta.map(d => d.insert).join('');
}
if ('shapeType' in elementModel) {

// If it is not, we should return the text and shapeType
const text = getShapeText(elementModel);
let shapeType = '';
if (
'shapeType' in elementModel &&
typeof elementModel.shapeType === 'string'
) {
shapeType =
elementModel.shapeType.charAt(0).toUpperCase() +
elementModel.shapeType.slice(1);
}
const content = `${shapeType}, with text label "${text}"`;
content = `${shapeType}, with text label "${text}"`;
return { content };
},
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { DeltaInsert } from '@blocksuite/inline/types';

import type { ElementModelToPlainTextAdapterMatcher } from './type.js';
import type { ElementModelToPlainTextAdapterMatcher } from '../type.js';

export const textElementModelToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
export const textToPlainTextAdapterMatcher: ElementModelToPlainTextAdapterMatcher =
{
name: 'text',
match: elementModel => elementModel.type === 'text',
toAST: elementModel => {
let content = '';
if ('text' in elementModel && elementModel.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[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import type { ElementModelMap } from '../../../element-model/index.js';
import type { ElementModelToPlainTextAdapterMatcher } from './elements/type.js';
import type { TextBuffer } from '@blocksuite/affine-shared/adapters';

import { ElementModelAdapter } from '../../type.js';
import type { ElementModelToPlainTextAdapterMatcher } from './type.js';

import {
ElementModelAdapter,
type ElementModelAdapterContext,
} from '../../type.js';
import { elementModelToPlainTextAdapterMatchers } from './elements/index.js';

export class PlainTextElementModelAdapter extends ElementModelAdapter<string> {
export class PlainTextElementModelAdapter extends ElementModelAdapter<
string,
TextBuffer
> {
constructor(
readonly elementModelMatchers: ElementModelToPlainTextAdapterMatcher[] = elementModelToPlainTextAdapterMatchers
) {
super();
}

fromElementModel(elementModel: ElementModelMap[keyof ElementModelMap]) {
fromElementModel(
element: Record<string, unknown>,
context: ElementModelAdapterContext<TextBuffer>
) {
for (const matcher of this.elementModelMatchers) {
if (matcher.match(elementModel)) {
return matcher.toAST(elementModel).content;
if (matcher.match(element)) {
return matcher.toAST(element, context).content;
}
}
return '';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TextBuffer } from '@blocksuite/affine-shared/adapters';

import type { ElementModelMatcher } from '../../../type.js';
import type { ElementModelMatcher } from '../../type.js';

export type ElementModelToPlainTextAdapterMatcher =
ElementModelMatcher<TextBuffer>;
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import {
type BlockPlainTextAdapterMatcher,
} from '@blocksuite/affine-shared/adapters';

import type { ElementModelMap } from '../../element-model/index.js';

import { SurfaceBlockSchema } from '../../surface-model.js';
import { getMindMapNodeMap } from '../utils/mindmap.js';
import { PlainTextElementModelAdapter } from './element-adapter/index.js';

export const surfaceBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher =
Expand All @@ -32,15 +31,29 @@ export const edgelessSurfaceBlockPlainTextAdapterMatcher: BlockPlainTextAdapterM
toBlockSnapshot: {},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext } = context;
const plainTextElementModelAdapter = new PlainTextElementModelAdapter();
if ('elements' in o.node.props) {
const elements = o.node.props.elements as Record<
string,
Record<string, unknown>
>;
// Get all the node maps of mindMap elements
const mindMapArray = Object.entries(elements)
.filter(([_, element]) => element.type === 'mindmap')
.map(([_, element]) => getMindMapNodeMap(element));
walkerContext.setGlobalContext(
'surface:mindMap:nodeMapArray',
mindMapArray
);

Object.entries(
o.node.props.elements as Record<string, Record<string, unknown>>
).forEach(([_, elementModel]) => {
const element =
elementModel as unknown as ElementModelMap[keyof ElementModelMap];
const plainText =
plainTextElementModelAdapter.fromElementModel(element);
).forEach(([_, element]) => {
const plainText = plainTextElementModelAdapter.fromElementModel(
element,
{ walkerContext, elements }
);
if (plainText) {
context.textBuffer.content += plainText + '\n';
}
Expand Down
22 changes: 18 additions & 4 deletions packages/affine/block-surface/src/adapters/type.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import type { ASTWalkerContext } from '@blocksuite/store';

import type { ElementModelMap } from '../element-model/index.js';

export type ElementModelAdapterContext<TNode extends object = never> = {
walkerContext: ASTWalkerContext<TNode>;
elements: Record<string, Record<string, unknown>>;
};

export type ElementModelMatcher<TNode extends object = never> = {
name: keyof ElementModelMap;
match: (elementModel: ElementModelMap[keyof ElementModelMap]) => boolean;
toAST: (elementModel: ElementModelMap[keyof ElementModelMap]) => TNode;
match: (element: Record<string, unknown>) => boolean;
toAST: (
element: Record<string, unknown>,
context: ElementModelAdapterContext<TNode>
) => TNode;
};

export abstract class ElementModelAdapter<AST = unknown> {
export abstract class ElementModelAdapter<
AST = unknown,
TNode extends object = never,
> {
/**
* Convert element model to AST format
*/
abstract fromElementModel(
elementModel: ElementModelMap[keyof ElementModelMap]
element: Record<string, unknown>,
context: ElementModelAdapterContext<TNode>
): AST;
}
25 changes: 25 additions & 0 deletions packages/affine/block-surface/src/adapters/types/mindmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface MindMapTreeNode {
id: string;
index: string;
children: MindMapTreeNode[];
}

export interface MindMapNode {
index: string;
parent?: string;
}

export type MindMapJson = Record<string, MindMapNode>;

export interface MindMapElement {
index: string;
seed: number;
children: {
'affine:surface:ymap': boolean;
json: MindMapJson;
};
layoutType: number;
style: number;
type: 'mindmap';
id: string;
}
Loading

0 comments on commit 661c628

Please sign in to comment.