diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/brush.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/brush.ts index c819cf23093f..68fb545d8020 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/brush.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/brush.ts @@ -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', diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/connector.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/connector.ts index aaff2ffe465e..6881ca02fcb0 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/connector.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/connector.ts @@ -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[]; diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/group.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/group.ts index 4ae87ace5582..bd40579c4af7 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/group.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/group.ts @@ -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[]; diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/index.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/index.ts index 434cdf677116..10c454c9246d 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/index.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/index.ts @@ -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, ]; diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/mindmap.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/mindmap.ts new file mode 100644 index 000000000000..588528282513 --- /dev/null +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/mindmap.ts @@ -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 }; + }, + }; diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/shape.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/shape.ts index 6620354efe4a..2daa73be879f 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/shape.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/shape.ts @@ -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>; + 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 }; }, }; diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/text.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/text.ts index ea897f742f24..7ac34969224e 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/text.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/text.ts @@ -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[]; diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/index.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/index.ts index 91d22ea25a92..b6ab6505ce41 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/index.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/index.ts @@ -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 { +export class PlainTextElementModelAdapter extends ElementModelAdapter< + string, + TextBuffer +> { constructor( readonly elementModelMatchers: ElementModelToPlainTextAdapterMatcher[] = elementModelToPlainTextAdapterMatchers ) { super(); } - fromElementModel(elementModel: ElementModelMap[keyof ElementModelMap]) { + fromElementModel( + element: Record, + context: ElementModelAdapterContext + ) { 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 ''; diff --git a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/type.ts b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/type.ts similarity index 72% rename from packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/type.ts rename to packages/affine/block-surface/src/adapters/plain-text/element-adapter/type.ts index 34fed3d4d7cb..274de33ef2f2 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/element-adapter/elements/type.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/element-adapter/type.ts @@ -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; diff --git a/packages/affine/block-surface/src/adapters/plain-text/plain-text.ts b/packages/affine/block-surface/src/adapters/plain-text/plain-text.ts index d3d8d1f58636..82fffaaaf7bd 100644 --- a/packages/affine/block-surface/src/adapters/plain-text/plain-text.ts +++ b/packages/affine/block-surface/src/adapters/plain-text/plain-text.ts @@ -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 = @@ -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 + >; + // 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> - ).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'; } diff --git a/packages/affine/block-surface/src/adapters/type.ts b/packages/affine/block-surface/src/adapters/type.ts index ef209af61704..8c2adf672a6c 100644 --- a/packages/affine/block-surface/src/adapters/type.ts +++ b/packages/affine/block-surface/src/adapters/type.ts @@ -1,16 +1,30 @@ +import type { ASTWalkerContext } from '@blocksuite/store'; + import type { ElementModelMap } from '../element-model/index.js'; +export type ElementModelAdapterContext = { + walkerContext: ASTWalkerContext; + elements: Record>; +}; + export type ElementModelMatcher = { name: keyof ElementModelMap; - match: (elementModel: ElementModelMap[keyof ElementModelMap]) => boolean; - toAST: (elementModel: ElementModelMap[keyof ElementModelMap]) => TNode; + match: (element: Record) => boolean; + toAST: ( + element: Record, + context: ElementModelAdapterContext + ) => TNode; }; -export abstract class ElementModelAdapter { +export abstract class ElementModelAdapter< + AST = unknown, + TNode extends object = never, +> { /** * Convert element model to AST format */ abstract fromElementModel( - elementModel: ElementModelMap[keyof ElementModelMap] + element: Record, + context: ElementModelAdapterContext ): AST; } diff --git a/packages/affine/block-surface/src/adapters/types/mindmap.ts b/packages/affine/block-surface/src/adapters/types/mindmap.ts new file mode 100644 index 000000000000..7fa789759833 --- /dev/null +++ b/packages/affine/block-surface/src/adapters/types/mindmap.ts @@ -0,0 +1,25 @@ +export interface MindMapTreeNode { + id: string; + index: string; + children: MindMapTreeNode[]; +} + +export interface MindMapNode { + index: string; + parent?: string; +} + +export type MindMapJson = Record; + +export interface MindMapElement { + index: string; + seed: number; + children: { + 'affine:surface:ymap': boolean; + json: MindMapJson; + }; + layoutType: number; + style: number; + type: 'mindmap'; + id: string; +} diff --git a/packages/affine/block-surface/src/adapters/utils/mindmap.ts b/packages/affine/block-surface/src/adapters/utils/mindmap.ts new file mode 100644 index 000000000000..6f379363503c --- /dev/null +++ b/packages/affine/block-surface/src/adapters/utils/mindmap.ts @@ -0,0 +1,74 @@ +import type { + MindMapElement, + MindMapJson, + MindMapTreeNode, +} from '../types/mindmap.js'; + +function isMindMapElement(element: unknown): element is MindMapElement { + return ( + typeof element === 'object' && + element !== null && + 'type' in element && + (element as MindMapElement).type === 'mindmap' && + 'children' in element && + typeof (element as MindMapElement).children === 'object' && + 'json' in (element as MindMapElement).children + ); +} + +export function getMindMapChildrenJson( + element: Record +): MindMapJson | null { + if (!isMindMapElement(element)) { + return null; + } + + return element.children.json; +} + +export function getMindMapNodeMap( + element: Record +): Map { + const nodeMap = new Map(); + const childrenJson = getMindMapChildrenJson(element); + if (!childrenJson) { + return nodeMap; + } + + for (const [id, info] of Object.entries(childrenJson)) { + nodeMap.set(id, { + id, + index: info.index, + children: [], + }); + } + + return nodeMap; +} + +export function buildMindMapTree(element: Record) { + let root: MindMapTreeNode | null = null; + + // First traverse to get node map + const nodeMap = getMindMapNodeMap(element); + const childrenJson = getMindMapChildrenJson(element); + if (!childrenJson) { + return root; + } + + // Second traverse to build tree + for (const [id, info] of Object.entries(childrenJson)) { + const node = nodeMap.get(id)!; + + if (info.parent) { + const parentNode = nodeMap.get(info.parent); + if (parentNode) { + parentNode.children.push(node); + } + } else { + root = node; + } + } + + return root; +} diff --git a/packages/affine/block-surface/src/adapters/utils/shape.ts b/packages/affine/block-surface/src/adapters/utils/shape.ts new file mode 100644 index 000000000000..43be183754df --- /dev/null +++ b/packages/affine/block-surface/src/adapters/utils/shape.ts @@ -0,0 +1,17 @@ +import type { DeltaInsert } from '@blocksuite/inline/types'; + +export function getShapeText(elementModel: Record): string { + let 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; +} diff --git a/packages/affine/shared/src/adapters/plain-text/block-adapter.ts b/packages/affine/shared/src/adapters/plain-text/block-adapter.ts index 42406a830f9a..dcf810577788 100644 --- a/packages/affine/shared/src/adapters/plain-text/block-adapter.ts +++ b/packages/affine/shared/src/adapters/plain-text/block-adapter.ts @@ -5,9 +5,9 @@ import { type ServiceIdentifier, } from '@blocksuite/global/di'; -import type { BlockAdapterMatcher } from '../types/adapter.js'; +import type { BlockAdapterMatcher, TextBuffer } from '../types/adapter.js'; -export type BlockPlainTextAdapterMatcher = BlockAdapterMatcher; +export type BlockPlainTextAdapterMatcher = BlockAdapterMatcher; export const BlockPlainTextAdapterMatcherIdentifier = createIdentifier( diff --git a/packages/blocks/src/_common/adapters/plain-text/plain-text.ts b/packages/blocks/src/_common/adapters/plain-text/plain-text.ts index 2cb58f1c51fa..ad74c6299800 100644 --- a/packages/blocks/src/_common/adapters/plain-text/plain-text.ts +++ b/packages/blocks/src/_common/adapters/plain-text/plain-text.ts @@ -65,7 +65,7 @@ export class PlainTextAdapter extends BaseAdapter { const textBuffer: TextBuffer = { content: '', }; - const walker = new ASTWalker<BlockSnapshot, never>(); + const walker = new ASTWalker<BlockSnapshot, TextBuffer>(); walker.setONodeTypeGuard( (node): node is BlockSnapshot => BlockSnapshotSchema.safeParse(node).success @@ -73,7 +73,7 @@ export class PlainTextAdapter extends BaseAdapter<PlainText> { walker.setEnter(async (o, context) => { for (const matcher of this.blockMatchers) { if (matcher.fromMatch(o)) { - const adapterContext: AdapterContext<BlockSnapshot> = { + const adapterContext: AdapterContext<BlockSnapshot, TextBuffer> = { walker, walkerContext: context, configs: this.configs, @@ -88,7 +88,7 @@ export class PlainTextAdapter extends BaseAdapter<PlainText> { walker.setLeave(async (o, context) => { for (const matcher of this.blockMatchers) { if (matcher.fromMatch(o)) { - const adapterContext: AdapterContext<BlockSnapshot, never> = { + const adapterContext: AdapterContext<BlockSnapshot, TextBuffer> = { walker, walkerContext: context, configs: this.configs,