@@ -3,6 +3,9 @@ import { ConnectableEntity } from "@/core/stage/stageObject/abstract/Connectable
33import { Entity } from "@/core/stage/stageObject/abstract/StageEntity" ;
44import { TextNode } from "@/core/stage/stageObject/entity/TextNode" ;
55import { EntityDetailsTool } from "../../dataManageService/entityDetailsService/entityDetailsTool" ;
6+ import { CopyEngineUtils } from "../../dataManageService/copyEngine/copyEngineUtils" ;
7+ import { Section } from "@/core/stage/stageObject/entity/Section" ;
8+ import { LineEdge } from "@/core/stage/stageObject/association/LineEdge" ;
69
710/**
811 * 专注于导出各种格式内容的引擎
@@ -63,6 +66,223 @@ export class StageExport {
6366 return this . getTreeTypeString ( textNode , this . getTabText ) ;
6467 }
6568
69+ /**
70+ * 格式:
71+ * ```mermaid
72+ * graph TD
73+ * A --> B
74+ * A --> C
75+ * B -- 连线文字 --> C
76+ * ```
77+ *
78+ * (TD)表示自上而下,LR表示自左而右
79+ * 使用 subgraph ... end 来定义子图。
80+ */
81+ public getMermaidTextByEntites ( entities : Entity [ ] ) : string {
82+ const stageObjects = CopyEngineUtils . getAllStageObjectFromEntities ( this . project , entities ) ;
83+ const allNodes = stageObjects . filter ( ( v ) => v instanceof TextNode || v instanceof Section ) as (
84+ | TextNode
85+ | Section
86+ ) [ ] ;
87+ const allLinks = stageObjects . filter ( ( v ) => v instanceof LineEdge ) as LineEdge [ ] ;
88+
89+ // 创建节点集合,用于快速查找
90+ const nodeSet = new Set ( allNodes . map ( ( n ) => n . uuid ) ) ;
91+
92+ // 过滤出有效的连线(source 和 target 都在节点集合中)
93+ const validLinks = allLinks . filter ( ( link ) => nodeSet . has ( link . source . uuid ) && nodeSet . has ( link . target . uuid ) ) ;
94+
95+ // 生成节点 ID 映射(uuid -> mermaid ID)
96+ const nodeIdMap = new Map < string , string > ( ) ;
97+ const nodeIdCounter = new Map < string , number > ( ) ;
98+
99+ // 生成有效的 Mermaid 节点 ID
100+ const getNodeId = ( node : TextNode | Section ) : string => {
101+ if ( nodeIdMap . has ( node . uuid ) ) {
102+ return nodeIdMap . get ( node . uuid ) ! ;
103+ }
104+
105+ // 基于文本生成 ID,Mermaid 支持中文字符作为节点 ID
106+ let baseId = node . text . trim ( ) ;
107+
108+ // 如果文本为空,使用 uuid 的前8位
109+ if ( ! baseId ) {
110+ baseId = "node_" + node . uuid . substring ( 0 , 8 ) ;
111+ } else {
112+ // 如果以数字开头,在前面加下划线
113+ if ( / ^ [ 0 - 9 ] / . test ( baseId ) ) {
114+ baseId = "_" + baseId ;
115+ }
116+ // Mermaid 支持 Unicode 字符(包括中文),所以不需要过滤中文字符
117+ // 只需要确保不以数字开头即可
118+ }
119+
120+ // 处理重复的 ID
121+ let finalId = baseId ;
122+ if ( nodeIdCounter . has ( baseId ) ) {
123+ const count = nodeIdCounter . get ( baseId ) ! + 1 ;
124+ nodeIdCounter . set ( baseId , count ) ;
125+ finalId = baseId + "_" + count ;
126+ } else {
127+ nodeIdCounter . set ( baseId , 0 ) ;
128+ }
129+
130+ nodeIdMap . set ( node . uuid , finalId ) ;
131+ return finalId ;
132+ } ;
133+
134+ // 转义 Mermaid 文本中的特殊字符
135+ const escapeMermaidText = ( text : string ) : string => {
136+ // Mermaid 中的特殊字符需要转义或使用引号
137+ return text . replace ( / " / g, """ ) . replace ( / \n / g, "<br>" ) ;
138+ } ;
139+
140+ // 找出所有 Section
141+ const sections = allNodes . filter ( ( n ) => n instanceof Section ) as Section [ ] ;
142+
143+ // 找出每个节点所在的 Section(只考虑最内层的 Section,即直接包含它的 Section)
144+ const nodeToSectionMap = new Map < string , Section > ( ) ;
145+
146+ // 找出每个 TextNode 所在的最内层 Section(直接包含它的 Section)
147+ for ( const node of allNodes ) {
148+ if ( node instanceof Section ) continue ;
149+
150+ // 找出所有包含该节点的 Section
151+ const containingSections : Section [ ] = [ ] ;
152+ for ( const section of sections ) {
153+ if ( this . project . sectionMethods . isEntityInSection ( node , section ) ) {
154+ containingSections . push ( section ) ;
155+ }
156+ }
157+
158+ // 找出最内层的 Section(不被其他包含该节点的 Section 包含的 Section)
159+ if ( containingSections . length > 0 ) {
160+ let innermostSection = containingSections [ 0 ] ;
161+ for ( const section of containingSections ) {
162+ // 如果 innermostSection 包含 section,则 section 是更内层的
163+ if ( this . project . sectionMethods . isEntityInSection ( section , innermostSection ) ) {
164+ innermostSection = section ;
165+ }
166+ }
167+ nodeToSectionMap . set ( node . uuid , innermostSection ) ;
168+ }
169+ }
170+
171+ // 找出每个 Section 所在的父 Section(最内层的父 Section,即直接包含它的 Section)
172+ const sectionToParentMap = new Map < Section , Section > ( ) ;
173+ for ( const section of sections ) {
174+ // 找出所有包含该 Section 的父 Section
175+ const parentSections : Section [ ] = [ ] ;
176+ for ( const s of sections ) {
177+ if ( s !== section && this . project . sectionMethods . isEntityInSection ( section , s ) ) {
178+ parentSections . push ( s ) ;
179+ }
180+ }
181+
182+ if ( parentSections . length > 0 ) {
183+ // 找出最内层的父 Section(不被其他父 Section 包含的父 Section)
184+ let innermostParent = parentSections [ 0 ] ;
185+ for ( const s of parentSections ) {
186+ // 如果 innermostParent 包含 s,则 s 是更内层的
187+ if ( this . project . sectionMethods . isEntityInSection ( s , innermostParent ) ) {
188+ innermostParent = s ;
189+ }
190+ }
191+ sectionToParentMap . set ( section , innermostParent ) ;
192+ }
193+ }
194+
195+ // 按 Section 分组节点
196+ const sectionToNodesMap = new Map < Section , ( TextNode | Section ) [ ] > ( ) ;
197+ const nodesWithoutSection : ( TextNode | Section ) [ ] = [ ] ;
198+
199+ for ( const node of allNodes ) {
200+ if ( node instanceof Section ) {
201+ // Section 本身:如果它在其他 Section 内,则放在父 Section 中
202+ const parentSection = sectionToParentMap . get ( node ) ;
203+ if ( parentSection ) {
204+ if ( ! sectionToNodesMap . has ( parentSection ) ) {
205+ sectionToNodesMap . set ( parentSection , [ ] ) ;
206+ }
207+ sectionToNodesMap . get ( parentSection ) ! . push ( node ) ;
208+ } else {
209+ // 最外层的 Section,直接添加到根节点列表
210+ nodesWithoutSection . push ( node ) ;
211+ }
212+ } else {
213+ // TextNode:如果它在 Section 内,则放在对应的 Section 中
214+ const section = nodeToSectionMap . get ( node . uuid ) ;
215+ if ( section ) {
216+ if ( ! sectionToNodesMap . has ( section ) ) {
217+ sectionToNodesMap . set ( section , [ ] ) ;
218+ }
219+ sectionToNodesMap . get ( section ) ! . push ( node ) ;
220+ } else {
221+ // 不在任何 Section 中的节点
222+ nodesWithoutSection . push ( node ) ;
223+ }
224+ }
225+ }
226+
227+ // 生成 Mermaid 字符串
228+ let result = "graph TD\n" ;
229+
230+ // 递归生成节点和子图
231+ const generateNodes = ( nodes : ( TextNode | Section ) [ ] , indent : string = "" ) : void => {
232+ for ( const node of nodes ) {
233+ if ( node instanceof Section ) {
234+ // 生成子图
235+ const sectionId = getNodeId ( node ) ;
236+ const sectionTitle = escapeMermaidText ( node . text || "Section" ) ;
237+ // 如果 ID 和显示文本相同,就不使用别名
238+ if ( sectionId === sectionTitle ) {
239+ result += `${ indent } subgraph ${ sectionId } \n` ;
240+ } else {
241+ result += `${ indent } subgraph ${ sectionId } ["${ sectionTitle } "]\n` ;
242+ }
243+
244+ // 生成子图内的节点
245+ const innerNodes = sectionToNodesMap . get ( node ) || [ ] ;
246+ generateNodes ( innerNodes , indent + " " ) ;
247+
248+ result += `${ indent } end\n` ;
249+ } else {
250+ // 生成普通节点
251+ const nodeId = getNodeId ( node ) ;
252+ const nodeText = escapeMermaidText ( node . text || "" ) ;
253+ if ( nodeText ) {
254+ // 如果 ID 和显示文本相同,就不使用别名
255+ if ( nodeId === nodeText ) {
256+ result += `${ indent } ${ nodeId } \n` ;
257+ } else {
258+ result += `${ indent } ${ nodeId } ["${ nodeText } "]\n` ;
259+ }
260+ } else {
261+ result += `${ indent } ${ nodeId } \n` ;
262+ }
263+ }
264+ }
265+ } ;
266+
267+ // 生成所有节点(包括不在 Section 中的节点和 Section 本身)
268+ generateNodes ( nodesWithoutSection ) ;
269+
270+ // 生成连线
271+ for ( const link of validLinks ) {
272+ const sourceId = getNodeId ( link . source as TextNode | Section ) ;
273+ const targetId = getNodeId ( link . target as TextNode | Section ) ;
274+
275+ if ( link . text && link . text . trim ( ) ) {
276+ const linkText = escapeMermaidText ( link . text . trim ( ) ) ;
277+ result += `${ sourceId } -- "${ linkText } " --> ${ targetId } \n` ;
278+ } else {
279+ result += `${ sourceId } --> ${ targetId } \n` ;
280+ }
281+ }
282+
283+ return result . trim ( ) ;
284+ }
285+
66286 /**
67287 * 树形遍历节点
68288 * @param textNode
0 commit comments