Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(blocks): add plain text adapter for mind map element #9006

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading