mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-31 19:04:16 +01:00 
			
		
		
		
	Compare commits
	
		
			59 Commits
		
	
	
		
			mermaid@11
			...
			mindmap-no
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 9fdd34c55c | ||
|   | 81a43e3d7c | ||
|   | 9a4dc563ed | ||
|   | 9266ea2673 | ||
|   | b48b2a60f5 | ||
| ![autofix-ci[bot]](/assets/img/avatar_default.png)  | f1e64cd175 | ||
|   | 132b028f94 | ||
|   | 39d9c0212a | ||
|   | c1c14e401a | ||
|   | 8b3057f27c | ||
|   | 717d3b3bb2 | ||
|   | 2f8d9ba958 | ||
|   | ace0367afd | ||
|   | b983626587 | ||
|   | 7effdc147b | ||
|   | 6e67515f41 | ||
|   | 09b74f1c29 | ||
|   | 880da21908 | ||
|   | 38191243be | ||
|   | b75dcb8a82 | ||
|   | 4c1e170f4a | ||
|   | d5c4eff251 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5324fd8dfd | ||
|   | bd25b88a01 | ||
|   | d3de3ecbbb | ||
|   | 3964ce0a0f | ||
|   | 4dbabba8e8 | ||
|   | e3ef5e4208 | ||
|   | daeb85bac2 | ||
|   | 2cdaf03ada | ||
|   | f6fa0260e7 | ||
|   | 29aad6d23c | ||
|   | 82ef7b5fdb | ||
|   | 11cd3f1262 | ||
|   | ac4aa94e78 | ||
|   | c40faac80d | ||
|   | c530baed3f | ||
|   | 045699de10 | ||
|   | 1988d24227 | ||
|   | 39f90debe7 | ||
|   | 73e9849f99 | ||
|   | 5a05540a5f | ||
|   | 2b58df9665 | ||
|   | 0b42bdba07 | ||
|   | 74c96db3e2 | ||
|   | bd47c57eaf | ||
|   | 3e5d2db514 | ||
|   | 40990bb096 | ||
|   | 7ca0665764 | ||
|   | 81a6a361ab | ||
|   | 62faacdeeb | ||
|   | 0e40d8e8a8 | ||
|   | e8d6daf4f6 | ||
|   | cb4ed605b2 | ||
|   | ba9db26bfa | ||
|   | 252b1837f7 | ||
|   | 6b9c15d7f0 | ||
|   | fda640c90c | ||
|   | 584a789183 | 
							
								
								
									
										5
									
								
								.changeset/loud-results-melt.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changeset/loud-results-melt.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| --- | ||||
| 'mermaid': minor | ||||
| --- | ||||
|  | ||||
| feat: Add half-arrowheads (solid & stick) and central connection support | ||||
							
								
								
									
										5
									
								
								.changeset/slow-lemons-know.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changeset/slow-lemons-know.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| --- | ||||
| '@mermaid': patch | ||||
| --- | ||||
|  | ||||
| fix: Mindmap breaking in ELK layout | ||||
							
								
								
									
										5
									
								
								.changeset/sweet-games-build.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changeset/sweet-games-build.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| --- | ||||
| 'mermaid': patch | ||||
| --- | ||||
|  | ||||
| fix(er-diagram): prevent syntax error when using 'u', numbers, and decimals in node names | ||||
							
								
								
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							| @@ -26,8 +26,8 @@ jobs: | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         language: ['javascript'] | ||||
|         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] | ||||
|         language: ['javascript', 'actions'] | ||||
|         # CodeQL supports [ 'actions', 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] | ||||
|         # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support | ||||
|  | ||||
|     steps: | ||||
|   | ||||
| @@ -369,4 +369,92 @@ ORDER ||--|{ LINE-ITEM : contains | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Special characters and numbers syntax', () => { | ||||
|     it('should render ER diagram with numeric entity names', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|         erDiagram | ||||
|           1 ||--|| ORDER : places | ||||
|           ORDER ||--|{ 2 : contains | ||||
|           2 ||--o{ 3.5 : references | ||||
|         `, | ||||
|         { logLevel: 1 } | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render ER diagram with "u" character in entity names and cardinality', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|         erDiagram | ||||
|           CUSTOMER ||--|| u : has | ||||
|           u ||--|| ORDER : places | ||||
|           PROJECT u--o{ TEAM_MEMBER : "parent" | ||||
|         `, | ||||
|         { logLevel: 1 } | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render ER diagram with decimal numbers in relationships', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|         erDiagram | ||||
|           2.5 ||--|| 1.5 : has | ||||
|           CUSTOMER ||--o{ 3.14 : references | ||||
|           1.0 ||--|{ ORDER : contains | ||||
|         `, | ||||
|         { logLevel: 1 } | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render ER diagram with numeric entity names and attributes', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|         erDiagram | ||||
|           1 { | ||||
|             string name | ||||
|             int value | ||||
|           } | ||||
|           1 ||--|| ORDER : places | ||||
|           ORDER { | ||||
|             float price | ||||
|             string description | ||||
|           } | ||||
|         `, | ||||
|         { logLevel: 1 } | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render complex ER diagram with mixed special entity names', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|         erDiagram | ||||
|           CUSTOMER ||--o{ 1 : places | ||||
|           1 ||--|{ u : contains | ||||
|           1.5 | ||||
|           u ||--|| 2.5 : processes | ||||
|           2.5 { | ||||
|             string id | ||||
|             float value | ||||
|           } | ||||
|           u { | ||||
|             varchar(50) name | ||||
|             int count | ||||
|           } | ||||
|         `, | ||||
|         { logLevel: 1 } | ||||
|       ); | ||||
|     }); | ||||
|     it('should render ER diagram with numeric entity names and attributes', () => { | ||||
|       imgSnapshotTest( | ||||
|         `erDiagram | ||||
|          PRODUCT ||--o{ ORDER-ITEM : has | ||||
|          1.5 | ||||
|          u | ||||
|          1 | ||||
|         `, | ||||
|         { logLevel: 1 } | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -655,5 +655,126 @@ describe('Sequence Diagram Special Cases', () => { | ||||
|         expect(svg).to.not.have.attr('style'); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     describe('Central Connection Rendering Tests', () => { | ||||
|       it('should render central connection circles on actor vertical lines', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         participant Charlie | ||||
|         Alice ()->>() Bob: Central connection | ||||
|         Bob ()-->> Charlie: Reverse central connection | ||||
|         Charlie ()<<-->>() Alice: Dual central connection`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should render central connections with different arrow types', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()->>() Bob: Solid open arrow | ||||
|         Alice ()-->>() Bob: Dotted open arrow | ||||
|         Alice ()-x() Bob: Solid cross | ||||
|         Alice ()--x() Bob: Dotted cross | ||||
|         Alice ()->() Bob: Solid arrow`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should render central connections with bidirectional arrows', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()<<->>() Bob: Bidirectional solid | ||||
|         Alice ()<<-->>() Bob: Bidirectional dotted`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should render central connections with activations', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         participant Charlie | ||||
|         Alice ()->>() Bob: Activate Bob | ||||
|         activate Bob | ||||
|         Bob ()-->> Charlie: Message to Charlie | ||||
|         Bob ()->>() Alice: Response to Alice | ||||
|         deactivate Bob`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should render central connections mixed with normal messages', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         participant Charlie | ||||
|         Alice ->> Bob: Normal message | ||||
|         Bob ()->>() Charlie: Central connection | ||||
|         Charlie -->> Alice: Normal dotted message | ||||
|         Alice ()<<-->>() Bob: Dual central connection | ||||
|         Bob -x Charlie: Normal cross message`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should render central connections with notes', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         participant Charlie | ||||
|         Alice ()->>() Bob: Central connection | ||||
|         Note over Alice,Bob: Central connection note | ||||
|         Bob ()-->> Charlie: Reverse central connection | ||||
|         Note right of Charlie: Response note | ||||
|         Charlie ()<<-->>() Alice: Dual central connection`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should render central connections with loops and alternatives', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         participant Charlie | ||||
|         loop Every minute | ||||
|             Alice ()->>() Bob: Central heartbeat | ||||
|             Bob ()-->> Charlie: Forward heartbeat | ||||
|         end | ||||
|         alt Success | ||||
|             Charlie ()<<-->>() Alice: Success response | ||||
|         else Failure | ||||
|             Charlie ()-x() Alice: Failure response | ||||
|         end`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should render central connections with different participant types', () => { | ||||
|         imgSnapshotTest( | ||||
|           `sequenceDiagram | ||||
|           participant Alice | ||||
|           actor Bob | ||||
|           participant Charlie@{"type":"boundary"} | ||||
|           participant David@{"type":"control"} | ||||
|           participant Eve@{"type":"entity"} | ||||
|           Alice ()->>() Bob: To actor | ||||
|           Bob ()-->> Charlie: To boundary | ||||
|           Charlie ()->>() David: To control | ||||
|           David ()<<-->>() Eve: To entity | ||||
|           Eve ()-x() Alice: Back to participant`, | ||||
|           { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } | ||||
|         ); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1053,4 +1053,167 @@ describe('Sequence diagram', () => { | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
|   describe('render new arrow type', () => { | ||||
|     it('should render Solid half arrow top', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|     sequenceDiagram | ||||
|       Alice -|\\  John: Hello John, how are you?  | ||||
|       Alice-|\\  John: Hi Alice, I can hear you! | ||||
|       Alice -|\\  John: Test | ||||
|       ` | ||||
|       ); | ||||
|     }); | ||||
|     it('should render Solid half arrow bottom', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|     sequenceDiagram | ||||
|       Alice-|/John: Hello John, how are you? | ||||
|       Alice-|/John: Hi Alice, I can hear you! | ||||
|       Alice-|/John: Test | ||||
|       ` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Stick half arrow top ', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|      sequenceDiagram | ||||
|       Alice-\\\\John: Hello John, how are you? | ||||
|       Alice-\\\\John: Hi Alice, I can hear you! | ||||
|       Alice-\\\\John: Test | ||||
|       ` | ||||
|       ); | ||||
|     }); | ||||
|     it('should render Stick half arrow bottom ', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|        sequenceDiagram | ||||
|       Alice-//John: Hello John, how are you? | ||||
|       Alice-//John: Hi Alice, I can hear you! | ||||
|       Alice-//John: Test | ||||
|       ` | ||||
|       ); | ||||
|     }); | ||||
|     it('should render Solid half arrow top reverse ', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|        sequenceDiagram | ||||
|       Alice/|-John: Hello Alice, how are you? | ||||
|       Alice/|-John: Hi Alice, I can hear you! | ||||
|       Alice/|-John: Test | ||||
|  | ||||
|       ` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Solid half arrow bottom reverse ', () => { | ||||
|       imgSnapshotTest( | ||||
|         `sequenceDiagram | ||||
|         Alice \\|- John: Hello Alice, how are you? | ||||
|         Alice \\|- John: Hi Alice, I can hear you! | ||||
|         Alice \\|- John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Stick half arrow top reverse ', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|       sequenceDiagram | ||||
|       Alice //-John: Hello Alice, how are you? | ||||
|       Alice //-John: Hi Alice, I can hear you! | ||||
|       Alice //-John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Stick half arrow bottom reverse ', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|        sequenceDiagram | ||||
|       Alice \\\\-John: Hello Alice, how are you? | ||||
|       Alice \\\\-John: Hi Alice, I can hear you! | ||||
|       Alice \\\\-John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Solid half arrow top dotted', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|      sequenceDiagram | ||||
|       Alice --|\\John: Hello John, how are you? | ||||
|       Alice --|\\John: Hi Alice, I can hear you! | ||||
|       Alice --|\\John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Solid half arrow bottom dotted', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|      sequenceDiagram | ||||
|       Alice --|/John: Hello John, how are you? | ||||
|       Alice --|/John: Hi Alice, I can hear you! | ||||
|       Alice --|/John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Stick half arrow top dotted', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|      sequenceDiagram | ||||
|       Alice--\\\\John: Hello John, how are you? | ||||
|       Alice--\\\\John: Hi Alice, I can hear you! | ||||
|       Alice--\\\\John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Stick half arrow bottom dotted', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|      sequenceDiagram | ||||
|       Alice--//John: Hello John, how are you? | ||||
|       Alice--//John: Hi Alice, I can hear you! | ||||
|       Alice--//John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Solid half arrow top reverse dotted', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|   sequenceDiagram | ||||
|       Alice/|--John: Hello Alice, how are you? | ||||
|       Alice/|--John: Hi Alice, I can hear you! | ||||
|       Alice/|--John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Solid half arrow bottom reverse dotted', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|   sequenceDiagram | ||||
|       Alice\\|--John: Hello Alice, how are you? | ||||
|       Alice\\|--John: Hi Alice, I can hear you! | ||||
|       Alice\\|--John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Stick half arrow top reverse dotted ', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|   sequenceDiagram | ||||
|       Alice//--John: Hello Alice, how are you? | ||||
|       Alice//--John: Hi Alice, I can hear you! | ||||
|       Alice//--John: Test` | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should render Stick half arrow bottom reverse dotted ', () => { | ||||
|       imgSnapshotTest( | ||||
|         ` | ||||
|   sequenceDiagram | ||||
|       Alice\\\\--John: Hello Alice, how are you? | ||||
|       Alice\\\\--John: Hi Alice, I can hear you! | ||||
|       Alice\\\\--John: Test` | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -6,6 +6,14 @@ | ||||
|       href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap" | ||||
|       rel="stylesheet" | ||||
|     /> | ||||
|     <link | ||||
|       rel="stylesheet" | ||||
|       href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" | ||||
|     /> | ||||
|     <link | ||||
|       href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" | ||||
|       rel="stylesheet" | ||||
|     /> | ||||
|     <style> | ||||
|       svg:not(svg svg) { | ||||
|         border: 2px solid darkred; | ||||
|   | ||||
| @@ -110,6 +110,48 @@ | ||||
|       config: | ||||
|         layout: elk | ||||
|       --- | ||||
|       mindmap | ||||
|       root((mindmap)) | ||||
|         Origins | ||||
|           Long history | ||||
|           ::icon(fa fa-book) | ||||
|           Popularisation | ||||
|             British popular psychology author Tony Buzan | ||||
|         Research | ||||
|           On effectiveness<br/>and features | ||||
|           On Automatic creation | ||||
|             Uses | ||||
|                 Creative techniques | ||||
|                 Strategic planning | ||||
|                 Argument mapping | ||||
|         Tools | ||||
|               id)I am a cloud( | ||||
|                   id))I am a bang(( | ||||
|                     Tools | ||||
|     </pre> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
|       --- | ||||
|       config: | ||||
|         layout: elk | ||||
|       --- | ||||
|       flowchart | ||||
|        aid0 | ||||
|   </pre | ||||
|     > | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
|       --- | ||||
|       config: | ||||
|         layout: elk | ||||
|       --- | ||||
|       mindmap | ||||
|       aid0 | ||||
|  | ||||
|     </pre> | ||||
|     <pre id="diagram4" class="mermaid"> | ||||
|       --- | ||||
|       config: | ||||
|         layout: ogdc | ||||
|       --- | ||||
|       flowchart-elk TB | ||||
|       c1-->a2 | ||||
|       subgraph one | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
|  | ||||
| # Interface: LayoutData | ||||
|  | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L168) | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L169) | ||||
|  | ||||
| ## Indexable | ||||
|  | ||||
| @@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.co | ||||
|  | ||||
| > **config**: [`MermaidConfig`](MermaidConfig.md) | ||||
|  | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171) | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:172](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L172) | ||||
|  | ||||
| --- | ||||
|  | ||||
| @@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.co | ||||
|  | ||||
| > **edges**: `Edge`\[] | ||||
|  | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170) | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171) | ||||
|  | ||||
| --- | ||||
|  | ||||
| @@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.co | ||||
|  | ||||
| > **nodes**: `Node`\[] | ||||
|  | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L169) | ||||
| Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170) | ||||
|   | ||||
| @@ -329,7 +329,11 @@ Messages can be of two displayed either solid or with a dotted line. | ||||
| [Actor][Arrow][Actor]:Message text | ||||
| ``` | ||||
|  | ||||
| There are ten types of arrows currently supported: | ||||
| Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows. | ||||
|  | ||||
| #### Supported Arrow Types | ||||
|  | ||||
| **Standard Arrow Types** | ||||
|  | ||||
| | Type     | Description                                          | | ||||
| | -------- | ---------------------------------------------------- | | ||||
| @@ -344,6 +348,58 @@ There are ten types of arrows currently supported: | ||||
| | `-)`     | Solid line with an open arrow at the end (async)     | | ||||
| | `--)`    | Dotted line with a open arrow at the end (async)     | | ||||
|  | ||||
| **Half-Arrows (v\<MERMAID_RELEASE_VERSION>+)** | ||||
|  | ||||
| The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`). | ||||
|  | ||||
| --- | ||||
|  | ||||
| | Type    | Description                                          | | ||||
| | ------- | ---------------------------------------------------- | | ||||
| | `-\|\`  | Solid line with top half arrowhead                   | | ||||
| | `--\|\` | Dotted line with top half arrowhead                  | | ||||
| | `-\|/`  | Solid line with bottom half arrowhead                | | ||||
| | `--\|/` | Dotted line with bottom half arrowhead               | | ||||
| | `/\|-`  | Solid line with reverse top half arrowhead           | | ||||
| | `/\|--` | Dotted line with reverse top half arrowhead          | | ||||
| | `\\-`   | Solid line with reverse bottom half arrowhead        | | ||||
| | `\\--`  | Dotted line with reverse bottom half arrowhead       | | ||||
| | `-\\`   | Solid line with top stick half arrowhead             | | ||||
| | `--\\`  | Dotted line with top stick half arrowhead            | | ||||
| | `-//`   | Solid line with bottom stick half arrowhead          | | ||||
| | `--//`  | Dotted line with bottom stick half arrowhead         | | ||||
| | `//-`   | Solid line with reverse top stick half arrowhead     | | ||||
| | `//--`  | Dotted line with reverse top stick half arrowhead    | | ||||
| | `\\-`   | Solid line with reverse bottom stick half arrowhead  | | ||||
| | `\\--`  | Dotted line with reverse bottom stick half arrowhead | | ||||
|  | ||||
| ## Central Connections (v\<MERMAID_RELEASE_VERSION>+) | ||||
|  | ||||
| Mermaid sequence diagrams support **central lifeline connections** using a `()`. | ||||
| This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another. | ||||
|  | ||||
| To indicate a central connection, append `()` to the arrow syntax. | ||||
|  | ||||
| #### Basic Syntax | ||||
|  | ||||
| ```mermaid-example | ||||
| sequenceDiagram | ||||
|     participant Alice | ||||
|     participant John | ||||
|     Alice->>()John: Hello John | ||||
|     Alice()->>John: How are you? | ||||
|     John()->>()Alice: Great! | ||||
| ``` | ||||
|  | ||||
| ```mermaid | ||||
| sequenceDiagram | ||||
|     participant Alice | ||||
|     participant John | ||||
|     Alice->>()John: Hello John | ||||
|     Alice()->>John: How are you? | ||||
|     John()->>()Alice: Great! | ||||
| ``` | ||||
|  | ||||
| ## Activations | ||||
|  | ||||
| It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations: | ||||
|   | ||||
							
								
								
									
										36
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								package.json
									
									
									
									
									
								
							| @@ -63,12 +63,12 @@ | ||||
|     ] | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@applitools/eyes-cypress": "^3.44.9", | ||||
|     "@applitools/eyes-cypress": "^3.55.2", | ||||
|     "@argos-ci/cypress": "^6.1.1", | ||||
|     "@changesets/changelog-github": "^0.5.1", | ||||
|     "@changesets/cli": "^2.27.12", | ||||
|     "@changesets/cli": "^2.29.7", | ||||
|     "@cspell/eslint-plugin": "^8.19.4", | ||||
|     "@cypress/code-coverage": "^3.12.49", | ||||
|     "@cypress/code-coverage": "^3.14.6", | ||||
|     "@eslint/js": "^9.26.0", | ||||
|     "@rollup/plugin-typescript": "^12.1.4", | ||||
|     "@types/cors": "^2.8.19", | ||||
| @@ -77,22 +77,22 @@ | ||||
|     "@types/jsdom": "^21.1.7", | ||||
|     "@types/lodash": "^4.17.20", | ||||
|     "@types/mdast": "^4.0.4", | ||||
|     "@types/node": "^22.13.17", | ||||
|     "@types/node": "^22.18.6", | ||||
|     "@types/rollup-plugin-visualizer": "^5.0.3", | ||||
|     "@vitest/coverage-v8": "^3.0.9", | ||||
|     "@vitest/spy": "^3.0.9", | ||||
|     "@vitest/ui": "^3.0.9", | ||||
|     "@vitest/coverage-v8": "^3.2.4", | ||||
|     "@vitest/spy": "^3.2.4", | ||||
|     "@vitest/ui": "^3.2.4", | ||||
|     "ajv": "^8.17.1", | ||||
|     "chokidar": "3.6.0", | ||||
|     "concurrently": "^9.1.2", | ||||
|     "concurrently": "^9.2.1", | ||||
|     "cors": "^2.8.5", | ||||
|     "cpy-cli": "^5.0.0", | ||||
|     "cross-env": "^7.0.3", | ||||
|     "cspell": "^9.1.5", | ||||
|     "cspell": "^9.2.1", | ||||
|     "cypress": "^14.5.4", | ||||
|     "cypress-image-snapshot": "^4.0.1", | ||||
|     "cypress-split": "^1.24.21", | ||||
|     "esbuild": "^0.25.9", | ||||
|     "cypress-split": "^1.24.23", | ||||
|     "esbuild": "^0.25.10", | ||||
|     "eslint": "^9.26.0", | ||||
|     "eslint-config-prettier": "^10.1.8", | ||||
|     "eslint-plugin-cypress": "^4.3.0", | ||||
| @@ -106,10 +106,10 @@ | ||||
|     "eslint-plugin-tsdoc": "^0.4.0", | ||||
|     "eslint-plugin-unicorn": "^59.0.1", | ||||
|     "express": "^5.1.0", | ||||
|     "globals": "^16.0.0", | ||||
|     "globals": "^16.4.0", | ||||
|     "globby": "^14.1.0", | ||||
|     "husky": "^9.1.7", | ||||
|     "jest": "^30.0.5", | ||||
|     "jest": "^30.1.3", | ||||
|     "jison": "^0.4.18", | ||||
|     "js-yaml": "^4.1.0", | ||||
|     "jsdom": "^26.1.0", | ||||
| @@ -118,18 +118,18 @@ | ||||
|     "markdown-table": "^3.0.4", | ||||
|     "nyc": "^17.1.0", | ||||
|     "path-browserify": "^1.0.1", | ||||
|     "prettier": "^3.5.3", | ||||
|     "prettier": "^3.6.2", | ||||
|     "prettier-plugin-jsdoc": "^1.3.3", | ||||
|     "rimraf": "^6.0.1", | ||||
|     "rollup-plugin-visualizer": "^6.0.3", | ||||
|     "start-server-and-test": "^2.0.13", | ||||
|     "start-server-and-test": "^2.1.2", | ||||
|     "tslib": "^2.8.1", | ||||
|     "tsx": "^4.7.3", | ||||
|     "tsx": "^4.20.5", | ||||
|     "typescript": "~5.7.3", | ||||
|     "typescript-eslint": "^8.38.0", | ||||
|     "vite": "^7.0.6", | ||||
|     "vite": "^7.0.7", | ||||
|     "vite-plugin-istanbul": "^7.0.0", | ||||
|     "vitest": "^3.0.9" | ||||
|     "vitest": "^3.2.4" | ||||
|   }, | ||||
|   "nyc": { | ||||
|     "report-dir": "coverage/cypress" | ||||
|   | ||||
| @@ -42,7 +42,7 @@ | ||||
|     "khroma": "^2.1.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "concurrently": "^9.1.2", | ||||
|     "concurrently": "^9.2.1", | ||||
|     "mermaid": "workspace:*", | ||||
|     "rimraf": "^6.0.1" | ||||
|   }, | ||||
|   | ||||
| @@ -67,7 +67,22 @@ export const render = async ( | ||||
|  | ||||
|     // Add the element to the DOM | ||||
|     if (!node.isGroup) { | ||||
|       const child = node as NodeWithVertex; | ||||
|       // const child = node as NodeWithVertex; | ||||
|       const child: NodeWithVertex = { | ||||
|         id: node.id, | ||||
|         width: node.width, | ||||
|         height: node.height, | ||||
|         // Store the original node data for later use | ||||
|         label: node.label, | ||||
|         isGroup: node.isGroup, | ||||
|         shape: node.shape, | ||||
|         padding: node.padding, | ||||
|         cssClasses: node.cssClasses, | ||||
|         cssStyles: node.cssStyles, | ||||
|         look: node.look, | ||||
|         // Include parentId for subgraph processing | ||||
|         parentId: node.parentId, | ||||
|       }; | ||||
|       graph.children.push(child); | ||||
|       nodeDb[node.id] = node; | ||||
|  | ||||
| @@ -150,7 +165,7 @@ export const render = async ( | ||||
|         domId: { node: () => any; attr: (arg0: string, arg1: string) => void }; | ||||
|       }) { | ||||
|         if (node) { | ||||
|           nodeDb[node.id] = node; | ||||
|           nodeDb[node.id] ??= {}; | ||||
|           nodeDb[node.id].offset = { | ||||
|             posX: node.x + relX, | ||||
|             posY: node.y + relY, | ||||
| @@ -860,11 +875,13 @@ export const render = async ( | ||||
|     log.info('APA01 layout result:', JSON.stringify(g, null, 2)); | ||||
|   } catch (error) { | ||||
|     log.error('APA01 ELK layout error:', error); | ||||
|     log.error('APA01 elkGraph that caused error:', JSON.stringify(elkGraph, null, 2)); | ||||
|     throw error; | ||||
|   } | ||||
|  | ||||
|   // debugger; | ||||
|   await drawNodes(0, 0, g.children, svg, subGraphsEl, 0); | ||||
|  | ||||
|   g.edges?.map( | ||||
|     (edge: { | ||||
|       sources: (string | number)[]; | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|   ], | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "@zenuml/core": "^3.35.2" | ||||
|     "@zenuml/core": "^3.41.4" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "mermaid": "workspace:^" | ||||
|   | ||||
| @@ -68,10 +68,10 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@braintree/sanitize-url": "^7.1.1", | ||||
|     "@iconify/utils": "^3.0.1", | ||||
|     "@iconify/utils": "^3.0.2", | ||||
|     "@mermaid-js/parser": "workspace:^", | ||||
|     "@types/d3": "^7.4.3", | ||||
|     "cytoscape": "^3.29.3", | ||||
|     "cytoscape": "^3.33.1", | ||||
|     "cytoscape-cose-bilkent": "^4.1.0", | ||||
|     "cytoscape-fcose": "^2.2.0", | ||||
|     "d3": "^7.9.0", | ||||
| @@ -82,7 +82,7 @@ | ||||
|     "katex": "^0.16.22", | ||||
|     "khroma": "^2.1.0", | ||||
|     "lodash-es": "^4.17.21", | ||||
|     "marked": "^16.2.1", | ||||
|     "marked": "^16.3.0", | ||||
|     "roughjs": "^4.6.6", | ||||
|     "stylis": "^4.3.6", | ||||
|     "ts-dedent": "^2.2.0", | ||||
| @@ -105,9 +105,9 @@ | ||||
|     "@types/stylis": "^4.2.7", | ||||
|     "@types/uuid": "^10.0.0", | ||||
|     "ajv": "^8.17.1", | ||||
|     "canvas": "^3.1.2", | ||||
|     "canvas": "^3.2.0", | ||||
|     "chokidar": "3.6.0", | ||||
|     "concurrently": "^9.1.2", | ||||
|     "concurrently": "^9.2.1", | ||||
|     "csstree-validator": "^4.0.1", | ||||
|     "globby": "^14.1.0", | ||||
|     "jison": "^0.4.18", | ||||
| @@ -116,14 +116,14 @@ | ||||
|     "json-schema-to-typescript": "^15.0.4", | ||||
|     "micromatch": "^4.0.8", | ||||
|     "path-browserify": "^1.0.1", | ||||
|     "prettier": "^3.5.3", | ||||
|     "prettier": "^3.6.2", | ||||
|     "remark": "^15.0.1", | ||||
|     "remark-frontmatter": "^5.0.0", | ||||
|     "remark-gfm": "^4.0.1", | ||||
|     "rimraf": "^6.0.1", | ||||
|     "start-server-and-test": "^2.0.13", | ||||
|     "type-fest": "^4.35.0", | ||||
|     "typedoc": "^0.28.12", | ||||
|     "start-server-and-test": "^2.1.2", | ||||
|     "type-fest": "^4.41.0", | ||||
|     "typedoc": "^0.28.13", | ||||
|     "typedoc-plugin-markdown": "^4.8.1", | ||||
|     "typescript": "~5.7.3", | ||||
|     "unist-util-flatmap": "^1.0.0", | ||||
|   | ||||
| @@ -66,12 +66,15 @@ accDescr\s*"{"\s*                                { this.begin("acc_descr_multili | ||||
| \}\|                            return 'ONE_OR_MORE'; | ||||
| "one"                           return 'ONLY_ONE'; | ||||
| "only one"                      return 'ONLY_ONE'; | ||||
| "1"                             return 'ONLY_ONE'; | ||||
| [0-9]+\.[0-9]+                  return 'DECIMAL_NUM'; | ||||
| "1"(?=\s+[A-Za-z_"'])           return 'ONLY_ONE'; | ||||
| "1"                             return 'ENTITY_ONE'; | ||||
| [0-9]+                          return 'NUM'; | ||||
| \|\|                            return 'ONLY_ONE'; | ||||
| o\|                             return 'ZERO_OR_ONE'; | ||||
| o\{                             return 'ZERO_OR_MORE'; | ||||
| \|\{                            return 'ONE_OR_MORE'; | ||||
| \s*u                            return 'MD_PARENT'; | ||||
| u(?=[\.\-\|])                   return 'MD_PARENT'; | ||||
| \.\.                            return 'NON_IDENTIFYING'; | ||||
| \-\-                            return 'IDENTIFYING'; | ||||
| "to"                            return 'IDENTIFYING'; | ||||
| @@ -80,13 +83,15 @@ o\{                             return 'ZERO_OR_MORE'; | ||||
| \-\.                            return 'NON_IDENTIFYING'; | ||||
| <style>([^\x00-\x7F]|\w|\-|\*)+ return 'STYLE_TEXT'; | ||||
| <style>';'                      return 'SEMI'; | ||||
| ([^\x00-\x7F]|\w|\-|\*)+        return 'UNICODE_TEXT'; | ||||
| [0-9]                           return 'NUM'; | ||||
| ([^\x00-\x7F]|\w|\-|\*|\.)+      return 'UNICODE_TEXT'; | ||||
| .                               return yytext[0]; | ||||
| <<EOF>>                         return 'EOF'; | ||||
|  | ||||
| /lex | ||||
|  | ||||
| %left 'ONLY_ONE' | ||||
| %left 'ZERO_OR_ONE' 'ZERO_OR_MORE' 'ONE_OR_MORE' 'MD_PARENT' | ||||
|  | ||||
| %start start | ||||
| %% /* language grammar */ | ||||
|  | ||||
| @@ -228,6 +233,9 @@ styleComponent: STYLE_TEXT | NUM | COLON | BRKT; | ||||
| entityName | ||||
|     : 'ENTITY_NAME'      { $$ = $1.replace(/"/g, ''); } | ||||
|     | 'UNICODE_TEXT' { $$ = $1; } | ||||
|     | 'NUM' { $$ = $1; } | ||||
|     | 'DECIMAL_NUM' { $$ = $1; } | ||||
|     | 'ENTITY_ONE' { $$ = $1; } | ||||
|     ; | ||||
|  | ||||
| attributes | ||||
|   | ||||
| @@ -1001,4 +1001,90 @@ describe('when parsing ER diagram it...', function () { | ||||
|       } | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   describe('syntax fixes for special characters and numbers', function () { | ||||
|     describe('standalone entity names', function () { | ||||
|       it('should allow number "1" as standalone entity', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n1`); | ||||
|       }); | ||||
|  | ||||
|       it('should allow character "u" as standalone entity', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\nu`); | ||||
|       }); | ||||
|  | ||||
|       it('should allow decimal numbers as standalone entities', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n2.5`); | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n1.5`); | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n0.1`); | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER }|..|{ DELIVERY-ADDRESS : has\n99.99`); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     describe('entity names with attributes', function () { | ||||
|       it('should allow "u" as entity name with attributes', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\nu {\nstring name\nint id\n}`); | ||||
|       }); | ||||
|  | ||||
|       it('should allow number "1" as entity name with attributes', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\n1 {\nstring name\nint id\n}`); | ||||
|       }); | ||||
|  | ||||
|       it('should allow decimal numbers as entity names with attributes', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\n2.5 {\nstring name\nint id\n}`); | ||||
|         erDiagram.parser.parse(`erDiagram\n1.5 {\nstring value\n}`); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     describe('entity names in relationships', function () { | ||||
|       it('should allow "u" in relationships', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER ||--|| u : has`); | ||||
|         erDiagram.parser.parse(`erDiagram\nu ||--|| ORDER : places`); | ||||
|         erDiagram.parser.parse(`erDiagram\nu ||--|| v : connects`); | ||||
|       }); | ||||
|  | ||||
|       it('should allow numbers in relationships', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER ||--|| 1 : has`); | ||||
|         erDiagram.parser.parse(`erDiagram\n1 ||--|| ORDER : places`); | ||||
|         erDiagram.parser.parse(`erDiagram\n1 ||--|| 2 : connects`); | ||||
|       }); | ||||
|  | ||||
|       it('should allow decimal numbers in relationships', function () { | ||||
|         erDiagram.parser.parse(`erDiagram\nCUSTOMER ||--|| 2.5 : has`); | ||||
|         erDiagram.parser.parse(`erDiagram\n1.5 ||--|| ORDER : places`); | ||||
|         erDiagram.parser.parse(`erDiagram\n2.5 ||--|| 5.5 : connects`); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     describe('mixed scenarios', function () { | ||||
|       it('should handle complex diagram with special entity names', function () { | ||||
|         erDiagram.parser.parse( | ||||
|           `erDiagram | ||||
|               CUSTOMER ||--o{ 1 : places | ||||
|               1 ||--|{ u : contains | ||||
|               u { | ||||
|                 string name | ||||
|                 int quantity | ||||
|               } | ||||
|               "2.5" ||--|| ORDER : processes | ||||
|               ORDER { | ||||
|                 int id | ||||
|                 date created | ||||
|               } | ||||
|         ` | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('should handle attributes with numbers in names (but not starting)', function () { | ||||
|         erDiagram.parser.parse( | ||||
|           `erDiagram | ||||
|               ENTITY { | ||||
|                 string name1 | ||||
|                 int value2 | ||||
|                 float point3_5 | ||||
|               } | ||||
|         ` | ||||
|         ); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										222
									
								
								packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | ||||
| import { log } from '../../logger.js'; | ||||
| import type { D3Selection } from '../../types.js'; | ||||
| import type { Node } from '../../rendering-util/types.js'; | ||||
| import { getIconSVG, isIconAvailable } from '../../rendering-util/icons.js'; | ||||
|  | ||||
| export interface MindmapIconConfig { | ||||
|   iconSize: number; | ||||
|   iconPadding: number; | ||||
|   shapeType: 'circle' | 'rect' | 'rounded' | 'bang' | 'cloud' | 'hexagon' | 'default'; | ||||
| } | ||||
|  | ||||
| export interface MindmapDimensions { | ||||
|   width: number; | ||||
|   height: number; | ||||
|   labelOffset: { x: number; y: number }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get icon configuration for different mindmap shape types | ||||
|  */ | ||||
| export function getMindmapIconConfig(shapeType: string): MindmapIconConfig { | ||||
|   const baseConfig = { | ||||
|     iconSize: 30, | ||||
|     iconPadding: 15, | ||||
|     shapeType: shapeType as MindmapIconConfig['shapeType'], | ||||
|   }; | ||||
|  | ||||
|   switch (shapeType) { | ||||
|     case 'bang': | ||||
|       return { ...baseConfig, iconPadding: 1 }; | ||||
|     case 'rect': | ||||
|     case 'default': | ||||
|       return { ...baseConfig, iconPadding: 10 }; | ||||
|     default: | ||||
|       return baseConfig; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Calculate dimensions and label positioning for mindmap nodes with icons | ||||
|  */ | ||||
| export function calculateMindmapDimensions( | ||||
|   node: Node, | ||||
|   bbox: any, | ||||
|   baseWidth: number, | ||||
|   baseHeight: number, | ||||
|   basePadding: number, | ||||
|   config: MindmapIconConfig | ||||
| ): MindmapDimensions { | ||||
|   const hasIcon = Boolean(node.icon); | ||||
|  | ||||
|   if (!hasIcon) { | ||||
|     return { | ||||
|       width: baseWidth, | ||||
|       height: baseHeight, | ||||
|       labelOffset: { x: -bbox.width / 2, y: -bbox.height / 2 }, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   const { iconSize, iconPadding, shapeType } = config; | ||||
|   let width = baseWidth; | ||||
|   let height = baseHeight; | ||||
|   let labelXOffset = -bbox.width / 2; | ||||
|   const labelYOffset = -bbox.height / 2; | ||||
|  | ||||
|   switch (shapeType) { | ||||
|     case 'circle': { | ||||
|       const totalWidthNeeded = bbox.width + iconSize + iconPadding * 2; | ||||
|       const minRadiusWithIcon = totalWidthNeeded / 2 + basePadding; | ||||
|       const radius = Math.max(baseWidth / 2, minRadiusWithIcon); | ||||
|       width = radius * 2; | ||||
|       height = radius * 2; | ||||
|       labelXOffset = -radius + iconSize + iconPadding; | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case 'rect': | ||||
|     case 'rounded': | ||||
|     case 'default': { | ||||
|       const minWidthWithIcon = bbox.width + iconSize + iconPadding * 2 + basePadding * 2; | ||||
|       width = Math.max(baseWidth, minWidthWithIcon); | ||||
|       height = Math.max(baseHeight, iconSize + basePadding * 2); | ||||
|  | ||||
|       const availableTextSpace = width - iconSize - iconPadding * 2; | ||||
|       labelXOffset = -width / 2 + iconSize + iconPadding + availableTextSpace / 2 - bbox.width / 2; | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case 'bang': | ||||
|     case 'cloud': { | ||||
|       const minWidthWithIcon = bbox.width + iconSize + iconPadding * 2 + basePadding; | ||||
|       width = Math.max(baseWidth, minWidthWithIcon); | ||||
|       height = Math.max(baseHeight, iconSize + basePadding); | ||||
|  | ||||
|       const availableTextSpace = width - iconSize - iconPadding * 2; | ||||
|       labelXOffset = -width / 2 + iconSize + iconPadding + availableTextSpace / 2 - bbox.width / 2; | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     default: { | ||||
|       const minWidthWithIcon = bbox.width + iconSize + iconPadding * 2 + basePadding * 2; | ||||
|       width = Math.max(baseWidth, minWidthWithIcon); | ||||
|       height = Math.max(baseHeight, iconSize + basePadding * 2); | ||||
|  | ||||
|       const availableTextSpace = width - iconSize - iconPadding * 2; | ||||
|       labelXOffset = -width / 2 + iconSize + iconPadding + availableTextSpace / 2 - bbox.width / 2; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     width, | ||||
|     height, | ||||
|     labelOffset: { x: labelXOffset, y: labelYOffset }, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Insert mindmap icon into the shape SVG element | ||||
|  */ | ||||
| export async function insertMindmapIcon( | ||||
|   parentElement: D3Selection<SVGGraphicsElement>, | ||||
|   node: Node, | ||||
|   config: MindmapIconConfig | ||||
| ): Promise<void> { | ||||
|   if (!node.icon) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const { iconSize, iconPadding, shapeType } = config; | ||||
|   const section = node.section ?? 0; | ||||
|  | ||||
|   let iconName = node.icon; | ||||
|   const isCssFormat = iconName.includes(' '); | ||||
|  | ||||
|   if (isCssFormat) { | ||||
|     iconName = iconName.replace(' ', ':'); | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     if (await isIconAvailable(iconName)) { | ||||
|       const iconSvg = await getIconSVG( | ||||
|         iconName, | ||||
|         { | ||||
|           height: iconSize, | ||||
|           width: iconSize, | ||||
|         }, | ||||
|         { class: 'label-icon' } | ||||
|       ); | ||||
|  | ||||
|       const iconElem = parentElement.append('g'); | ||||
|       iconElem.html(`<g>${iconSvg}</g>`); | ||||
|  | ||||
|       let iconX = 0; | ||||
|       let iconY = 0; | ||||
|  | ||||
|       switch (shapeType) { | ||||
|         case 'circle': { | ||||
|           const nodeWidth = node.width || 100; | ||||
|           const radius = nodeWidth / 2; | ||||
|           iconX = -radius + iconSize / 2 + iconPadding; | ||||
|           iconY = 0; | ||||
|           break; | ||||
|         } | ||||
|         default: { | ||||
|           const nodeWidth = node.width || 100; | ||||
|           iconX = -nodeWidth / 2 + iconSize / 2 + iconPadding; | ||||
|           iconY = 0; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       iconElem.attr('transform', `translate(${iconX}, ${iconY})`); | ||||
|       iconElem.attr('style', 'color: currentColor;'); | ||||
|       return; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     log.debug('SVG icon rendering failed, falling back to CSS:', error); | ||||
|   } | ||||
|  | ||||
|   // Fallback to CSS approach (original mindmap behavior) | ||||
|   const iconClass = isCssFormat ? node.icon : node.icon.replace(':', ' '); | ||||
|  | ||||
|   let iconX = 0; | ||||
|   const iconY = -iconSize / 2; | ||||
|  | ||||
|   switch (shapeType) { | ||||
|     case 'circle': { | ||||
|       const nodeWidth = node.width || 100; | ||||
|       const radius = nodeWidth / 2; | ||||
|       iconX = -radius + iconPadding; | ||||
|       break; | ||||
|     } | ||||
|     default: { | ||||
|       const nodeWidth = node.width || 100; | ||||
|       iconX = -nodeWidth / 2 + iconPadding; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const icon = parentElement | ||||
|     .append('foreignObject') | ||||
|     .attr('height', `${iconSize}px`) | ||||
|     .attr('width', `${iconSize}px`) | ||||
|     .attr('x', iconX) | ||||
|     .attr('y', iconY) | ||||
|     .attr( | ||||
|       'style', | ||||
|       'text-align: center; display: flex; align-items: center; justify-content: center;' | ||||
|     ); | ||||
|  | ||||
|   icon | ||||
|     .append('div') | ||||
|     .attr('class', 'icon-container') | ||||
|     .attr( | ||||
|       'style', | ||||
|       'width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;' | ||||
|     ) | ||||
|     .append('i') | ||||
|     .attr('class', `node-icon-${section} ${iconClass}`) | ||||
|     .attr('style', `font-size: ${iconSize}px;`); | ||||
| } | ||||
| @@ -7,6 +7,7 @@ import type { LayoutData } from '../../rendering-util/types.js'; | ||||
| import type { FilledMindMapNode } from './mindmapTypes.js'; | ||||
| import defaultConfig from '../../defaultConfig.js'; | ||||
| import type { MindmapDB } from './mindmapDb.js'; | ||||
| import { getMindmapIconConfig, insertMindmapIcon } from './mindmapIconHelper.js'; | ||||
|  | ||||
| /** | ||||
|  * Update the layout data with actual node dimensions after drawing | ||||
| @@ -71,6 +72,28 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj) => { | ||||
|  | ||||
|   // Use the unified rendering system | ||||
|   await render(data4Layout, svg); | ||||
|   const genericShapes = ['hexagon', 'rect', 'rounded', 'squareRect']; | ||||
|   const nodesWithIcons = data4Layout.nodes.filter( | ||||
|     (node) => node.icon && genericShapes.includes(node.shape || '') | ||||
|   ); | ||||
|  | ||||
|   if (nodesWithIcons.length > 0) { | ||||
|     for (const node of nodesWithIcons) { | ||||
|       const nodeId = node.domId || node.id; | ||||
|       const nodeElement = svg.select(`g[id="${nodeId}"]`); | ||||
|  | ||||
|       if (!nodeElement.empty()) { | ||||
|         try { | ||||
|           const iconConfig = getMindmapIconConfig(node.shape || 'default'); | ||||
|           await insertMindmapIcon(nodeElement, node, iconConfig); | ||||
|         } catch (error) { | ||||
|           log.warn(`Failed to add icon to ${nodeId}:`, error); | ||||
|         } | ||||
|       } else { | ||||
|         log.warn(`Could not find DOM element for node ${nodeId}`); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Setup the view box and size of the svg element using config from data4Layout | ||||
|   setupViewPortForSVG( | ||||
|   | ||||
| @@ -78,7 +78,7 @@ accDescr\s*"{"\s*                                { this.begin("acc_descr_multili | ||||
| "off"															return 'off'; | ||||
| ","                                                             return ','; | ||||
| ";"                                                             return 'NEWLINE'; | ||||
| [^+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+<\->\->:\n,;]+)*             { yytext = yytext.trim(); return 'ACTOR'; } | ||||
| [^\/\\\+\()\+<\->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)|\-\|\\|\-\\|\-\/|\-\/\/|\-\|\/|\/\|\-|\\\|\-|\/\/\-|\\\\\-|\/\|\-|\-\-\|\\|\-\-|\(\)))[\-]*[^\+<\->\->:\n,;]+)*             { yytext = yytext.trim(); return 'ACTOR'; } //final_4.11 | ||||
| "->>"                                                           return 'SOLID_ARROW'; | ||||
| "<<->>"                                                           return 'BIDIRECTIONAL_SOLID_ARROW'; | ||||
| "-->>"                                                          return 'DOTTED_ARROW'; | ||||
| @@ -89,10 +89,36 @@ accDescr\s*"{"\s*                                { this.begin("acc_descr_multili | ||||
| \-\-[x]                                                         return 'DOTTED_CROSS'; | ||||
| \-[\)]                                                          return 'SOLID_POINT'; | ||||
| \-\-[\)]                                                        return 'DOTTED_POINT'; | ||||
|  | ||||
| //normal-dotted | ||||
| \-\-\|\\                                                        return 'SOLID_ARROW_TOP_DOTTED'; | ||||
| \-\-\|\/                                                        return 'SOLID_ARROW_BOTTOM_DOTTED'; | ||||
| \-\-\\\\                                                        return 'STICK_ARROW_TOP_DOTTED'; | ||||
| \-\-\/\/                                                        return 'STICK_ARROW_BOTTOM_DOTTED'; | ||||
|  | ||||
| //reverse-dotted | ||||
| \/\|\-\-                                                          return 'SOLID_ARROW_TOP_REVERSE_DOTTED'; | ||||
| \\\|\-\-                                                          return 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED'; | ||||
| \/\/\-\-                                                          return 'STICK_ARROW_TOP_REVERSE_DOTTED'; | ||||
| \\\\\-\-                                                          return 'STICK_ARROW_BOTTOM_REVERSE_DOTTED'; | ||||
|  | ||||
| //normal | ||||
| \-\|\\                                                          return 'SOLID_ARROW_TOP'; | ||||
| \-\|\/                                                          return 'SOLID_ARROW_BOTTOM'; | ||||
| \-\\\\                                                          return 'STICK_ARROW_TOP'; | ||||
| \-\/\/                                                          return 'STICK_ARROW_BOTTOM'; | ||||
|  | ||||
| //reverse | ||||
| \/\|\-                                                          return 'SOLID_ARROW_TOP_REVERSE'; | ||||
| \\\|\-                                                          return 'SOLID_ARROW_BOTTOM_REVERSE'; | ||||
| \/\/\-                                                          return 'STICK_ARROW_TOP_REVERSE'; | ||||
| \\\\\-                                                          return 'STICK_ARROW_BOTTOM_REVERSE'; | ||||
|  | ||||
| ":"(?:(?:no)?wrap:)?[^#\n;]*                                    return 'TXT'; | ||||
| ":"                             								return 'TXT'; | ||||
| "+"                                                             return '+'; | ||||
| "-"                                                             return '-'; | ||||
| "()"                                                            return '()'; | ||||
| <<EOF>>                                                         return 'NEWLINE'; | ||||
| .                                                               return 'INVALID'; | ||||
|  | ||||
| @@ -304,6 +330,20 @@ signal | ||||
| 	{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5}, | ||||
| 	             {type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $1.actor} | ||||
| 	             ]} | ||||
|     | actor signaltype '()' actor text2 | ||||
| 	{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5, activate: true, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION}, | ||||
| 	              {type: 'centralConnection', signalType: yy.LINETYPE.CENTRAL_CONNECTION, actor: $4.actor, } | ||||
| 	             ]} | ||||
|      | ||||
| 	| actor '()' signaltype actor text2 | ||||
| 	{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$3, msg:$5, activate: false, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE}, | ||||
| 	              {type: 'centralConnectionReverse', signalType: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE, actor: $1.actor} | ||||
| 	             ]} | ||||
| 	| actor '()' signaltype '()' actor text2 | ||||
| 	{ $$ = [$1,$5,{type: 'addMessage', from:$1.actor, to:$5.actor, signalType:$3, msg:$6, activate: true, centralConnection: yy.LINETYPE.CENTRAL_CONNECTION_DUAL}, | ||||
| 	 			 {type: 'centralConnection', signalType: yy.LINETYPE.CENTRAL_CONNECTION, actor: $5.actor, }, | ||||
| 				 {type: 'centralConnectionReverse', signalType: yy.LINETYPE.CENTRAL_CONNECTION_REVERSE, actor: $1.actor} | ||||
| 	             ]} | ||||
| 	| actor signaltype actor text2 | ||||
| 	{ $$ = [$1,$3,{type: 'addMessage', from:$1.actor, to:$3.actor, signalType:$2, msg:$4}]} | ||||
| 	; | ||||
| @@ -337,7 +377,28 @@ signaltype | ||||
| 	: SOLID_OPEN_ARROW  { $$ = yy.LINETYPE.SOLID_OPEN; } | ||||
| 	| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; } | ||||
| 	| SOLID_ARROW       { $$ = yy.LINETYPE.SOLID; } | ||||
| 	| BIDIRECTIONAL_SOLID_ARROW       { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; } | ||||
| 	 | ||||
| 	| SOLID_ARROW_TOP    { $$ = yy.LINETYPE.SOLID_TOP; } | ||||
| 	| SOLID_ARROW_BOTTOM { $$ = yy.LINETYPE.SOLID_BOTTOM; } | ||||
| 	| STICK_ARROW_TOP    { $$ = yy.LINETYPE.STICK_TOP; } | ||||
| 	| STICK_ARROW_BOTTOM { $$ = yy.LINETYPE.STICK_BOTTOM; } | ||||
|  | ||||
| 	| SOLID_ARROW_TOP_DOTTED    { $$ = yy.LINETYPE.SOLID_TOP_DOTTED; }	 | ||||
| 	| SOLID_ARROW_BOTTOM_DOTTED { $$ = yy.LINETYPE.SOLID_BOTTOM_DOTTED; } | ||||
| 	| STICK_ARROW_TOP_DOTTED    { $$ = yy.LINETYPE.STICK_TOP_DOTTED; } | ||||
| 	| STICK_ARROW_BOTTOM_DOTTED { $$ = yy.LINETYPE.STICK_BOTTOM_DOTTED; } | ||||
|  | ||||
| 	| SOLID_ARROW_TOP_REVERSE    { $$ = yy.LINETYPE.SOLID_ARROW_TOP_REVERSE; } | ||||
| 	| SOLID_ARROW_BOTTOM_REVERSE { $$ = yy.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE; } | ||||
| 	| STICK_ARROW_TOP_REVERSE    { $$ = yy.LINETYPE.STICK_ARROW_TOP_REVERSE; } | ||||
| 	| STICK_ARROW_BOTTOM_REVERSE { $$ = yy.LINETYPE.STICK_ARROW_BOTTOM_REVERSE; } | ||||
|  | ||||
| 	| SOLID_ARROW_TOP_REVERSE_DOTTED    { $$ = yy.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED; } | ||||
| 	| SOLID_ARROW_BOTTOM_REVERSE_DOTTED { $$ = yy.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED; } | ||||
| 	| STICK_ARROW_TOP_REVERSE_DOTTED    { $$ = yy.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED; } | ||||
| 	| STICK_ARROW_BOTTOM_REVERSE_DOTTED { $$ = yy.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED; } | ||||
|  | ||||
|   | BIDIRECTIONAL_SOLID_ARROW       { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; } | ||||
| 	| DOTTED_ARROW      { $$ = yy.LINETYPE.DOTTED; } | ||||
| 	| BIDIRECTIONAL_DOTTED_ARROW      { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; } | ||||
| 	| SOLID_CROSS       { $$ = yy.LINETYPE.SOLID_CROSS; } | ||||
|   | ||||
| @@ -64,6 +64,30 @@ const LINETYPE = { | ||||
|   PAR_OVER_START: 32, | ||||
|   BIDIRECTIONAL_SOLID: 33, | ||||
|   BIDIRECTIONAL_DOTTED: 34, | ||||
|  | ||||
|   SOLID_TOP: 41, | ||||
|   SOLID_BOTTOM: 42, | ||||
|   STICK_TOP: 43, | ||||
|   STICK_BOTTOM: 44, | ||||
|  | ||||
|   SOLID_ARROW_TOP_REVERSE: 45, | ||||
|   SOLID_ARROW_BOTTOM_REVERSE: 46, | ||||
|   STICK_ARROW_TOP_REVERSE: 47, | ||||
|   STICK_ARROW_BOTTOM_REVERSE: 48, | ||||
|  | ||||
|   SOLID_TOP_DOTTED: 51, | ||||
|   SOLID_BOTTOM_DOTTED: 52, | ||||
|   STICK_TOP_DOTTED: 53, | ||||
|   STICK_BOTTOM_DOTTED: 54, | ||||
|  | ||||
|   SOLID_ARROW_TOP_REVERSE_DOTTED: 55, | ||||
|   SOLID_ARROW_BOTTOM_REVERSE_DOTTED: 56, | ||||
|   STICK_ARROW_TOP_REVERSE_DOTTED: 57, | ||||
|   STICK_ARROW_BOTTOM_REVERSE_DOTTED: 58, | ||||
|  | ||||
|   CENTRAL_CONNECTION: 59, | ||||
|   CENTRAL_CONNECTION_REVERSE: 60, | ||||
|   CENTRAL_CONNECTION_DUAL: 61, | ||||
| } as const; | ||||
|  | ||||
| const ARROWTYPE = { | ||||
| @@ -244,7 +268,8 @@ export class SequenceDB implements DiagramDB { | ||||
|     idTo?: Message['to'], | ||||
|     message?: { text: string; wrap: boolean }, | ||||
|     messageType?: number, | ||||
|     activate = false | ||||
|     activate = false, | ||||
|     centralConnection?: number | ||||
|   ) { | ||||
|     if (messageType === this.LINETYPE.ACTIVE_END) { | ||||
|       const cnt = this.activationCount(idFrom ?? ''); | ||||
| @@ -271,6 +296,7 @@ export class SequenceDB implements DiagramDB { | ||||
|       wrap: message?.wrap ?? this.autoWrap(), | ||||
|       type: messageType, | ||||
|       activate, | ||||
|       centralConnection: centralConnection ?? 0, | ||||
|     }); | ||||
|     return true; | ||||
|   } | ||||
| @@ -563,6 +589,12 @@ export class SequenceDB implements DiagramDB { | ||||
|         case 'activeStart': | ||||
|           this.addSignal(param.actor, undefined, undefined, param.signalType); | ||||
|           break; | ||||
|         case 'centralConnection': | ||||
|           this.addSignal(param.actor, undefined, undefined, param.signalType); | ||||
|           break; | ||||
|         case 'centralConnectionReverse': | ||||
|           this.addSignal(param.actor, undefined, undefined, param.signalType); | ||||
|           break; | ||||
|         case 'activeEnd': | ||||
|           this.addSignal(param.actor, undefined, undefined, param.signalType); | ||||
|           break; | ||||
| @@ -606,7 +638,14 @@ export class SequenceDB implements DiagramDB { | ||||
|               this.state.records.lastDestroyed = undefined; | ||||
|             } | ||||
|           } | ||||
|           this.addSignal(param.from, param.to, param.msg, param.signalType, param.activate); | ||||
|           this.addSignal( | ||||
|             param.from, | ||||
|             param.to, | ||||
|             param.msg, | ||||
|             param.signalType, | ||||
|             param.activate, | ||||
|             param.centralConnection | ||||
|           ); | ||||
|           break; | ||||
|         case 'boxStart': | ||||
|           this.addBox(param.boxData); | ||||
|   | ||||
| @@ -104,6 +104,7 @@ describe('more than one sequence diagram', () => { | ||||
|       [ | ||||
|         { | ||||
|           "activate": false, | ||||
|           "centralConnection": 0, | ||||
|           "from": "Alice", | ||||
|           "id": "0", | ||||
|           "message": "Hello Bob, how are you?", | ||||
| @@ -113,6 +114,7 @@ describe('more than one sequence diagram', () => { | ||||
|         }, | ||||
|         { | ||||
|           "activate": false, | ||||
|           "centralConnection": 0, | ||||
|           "from": "Bob", | ||||
|           "id": "1", | ||||
|           "message": "I am good thanks!", | ||||
| @@ -131,6 +133,7 @@ describe('more than one sequence diagram', () => { | ||||
|       [ | ||||
|         { | ||||
|           "activate": false, | ||||
|           "centralConnection": 0, | ||||
|           "from": "Alice", | ||||
|           "id": "0", | ||||
|           "message": "Hello Bob, how are you?", | ||||
| @@ -140,6 +143,7 @@ describe('more than one sequence diagram', () => { | ||||
|         }, | ||||
|         { | ||||
|           "activate": false, | ||||
|           "centralConnection": 0, | ||||
|           "from": "Bob", | ||||
|           "id": "1", | ||||
|           "message": "I am good thanks!", | ||||
| @@ -160,6 +164,7 @@ describe('more than one sequence diagram', () => { | ||||
|       [ | ||||
|         { | ||||
|           "activate": false, | ||||
|           "centralConnection": 0, | ||||
|           "from": "Alice", | ||||
|           "id": "0", | ||||
|           "message": "Hello John, how are you?", | ||||
| @@ -169,6 +174,7 @@ describe('more than one sequence diagram', () => { | ||||
|         }, | ||||
|         { | ||||
|           "activate": false, | ||||
|           "centralConnection": 0, | ||||
|           "from": "John", | ||||
|           "id": "1", | ||||
|           "message": "I am good thanks!", | ||||
| @@ -181,6 +187,254 @@ describe('more than one sequence diagram', () => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| describe('Central Connection Parsing', () => { | ||||
|   describe('when parsing central connection syntax', () => { | ||||
|     it('should parse actor ()->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()->>() Bob: Hello Bob, how are you? | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse | ||||
|  | ||||
|       // Find the actual message (type: 'addMessage') | ||||
|       const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessage).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         message: 'Hello Bob, how are you?', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL | ||||
|         activate: true, | ||||
|         type: 0, // SOLID (based on test output) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should parse actor ()-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()-->>() Bob: Hello Bob, how are you? | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse | ||||
|  | ||||
|       const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessage).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         message: 'Hello Bob, how are you?', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL | ||||
|         activate: true, | ||||
|         type: 1, // DOTTED (based on test output) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should parse actor ->>() actor syntax as CENTRAL_CONNECTION', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ->>() Bob: Hello Bob, how are you? | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(2); // addMessage + centralConnection (no activation for this pattern) | ||||
|  | ||||
|       const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessage).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         message: 'Hello Bob, how are you?', | ||||
|         centralConnection: 59, // CENTRAL_CONNECTION | ||||
|         activate: true, | ||||
|         type: 0, // SOLID (based on actual parsing) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should parse actor ()-->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()-->> Bob: Hello Bob, how are you? | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse | ||||
|  | ||||
|       const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessage).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         message: 'Hello Bob, how are you?', | ||||
|         centralConnection: 60, // CENTRAL_CONNECTION_REVERSE | ||||
|         activate: false, | ||||
|         type: 1, // DOTTED (based on test output) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should parse actor ()->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()->> Bob: Hello Bob, how are you? | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse | ||||
|  | ||||
|       const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessage).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         message: 'Hello Bob, how are you?', | ||||
|         centralConnection: 60, // CENTRAL_CONNECTION_REVERSE | ||||
|         activate: false, | ||||
|         type: 0, // SOLID (based on test output) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should parse actor ()<<-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()<<-->>() Bob: Hello Bob, how are you? | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse | ||||
|  | ||||
|       const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessage).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         message: 'Hello Bob, how are you?', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL | ||||
|         activate: true, | ||||
|         type: 34, // BIDIRECTIONAL_DOTTED | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should parse actor ()<<->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()<<->>() Bob: Hello Bob, how are you? | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse | ||||
|  | ||||
|       const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessage).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         message: 'Hello Bob, how are you?', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL | ||||
|         activate: true, | ||||
|         type: 33, // BIDIRECTIONAL_SOLID | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should handle multiple central connection types in one diagram', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         participant Charlie | ||||
|         Alice ()->>() Bob: Message 1 | ||||
|         Bob ()-->> Charlie: Message 2 | ||||
|         Charlie ()<<-->>() Alice: Message 3 | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(8); // 3 addMessages + 5 central connection markers | ||||
|  | ||||
|       // Filter to get only the actual messages | ||||
|       const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessages).toHaveLength(3); | ||||
|  | ||||
|       expect(actualMessages[0]).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()->>()) | ||||
|       }); | ||||
|  | ||||
|       expect(actualMessages[1]).toMatchObject({ | ||||
|         from: 'Bob', | ||||
|         to: 'Charlie', | ||||
|         centralConnection: 60, // CENTRAL_CONNECTION_REVERSE (()-->>) | ||||
|       }); | ||||
|  | ||||
|       expect(actualMessages[2]).toMatchObject({ | ||||
|         from: 'Charlie', | ||||
|         to: 'Alice', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()<<-->>()) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should handle central connections with different arrow types', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ()-x() Bob: Cross message | ||||
|         Alice ()--x() Bob: Dotted cross message | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(6); // 2 addMessages + 4 central connection markers | ||||
|  | ||||
|       const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to); | ||||
|       expect(actualMessages).toHaveLength(2); | ||||
|  | ||||
|       expect(actualMessages[0]).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()-x()) | ||||
|         type: 3, // SOLID_CROSS | ||||
|       }); | ||||
|  | ||||
|       expect(actualMessages[1]).toMatchObject({ | ||||
|         from: 'Alice', | ||||
|         to: 'Bob', | ||||
|         centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()--x()) | ||||
|         type: 4, // DOTTED_CROSS | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should not break existing parsing without central connections', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|         sequenceDiagram | ||||
|         participant Alice | ||||
|         participant Bob | ||||
|         Alice ->> Bob: Normal message | ||||
|         Bob -->> Alice: Normal dotted message | ||||
|         Alice -x Bob: Normal cross message | ||||
|       `); | ||||
|  | ||||
|       const messages = diagram.db.getMessages(); | ||||
|       expect(messages).toHaveLength(3); | ||||
|  | ||||
|       messages.forEach((msg) => { | ||||
|         expect(msg.centralConnection).toBe(0); // No central connection | ||||
|       }); | ||||
|  | ||||
|       expect(messages[0].type).toBe(0); // SOLID (based on actual parsing) | ||||
|       expect(messages[1].type).toBe(1); // DOTTED (based on actual parsing) | ||||
|       expect(messages[2].type).toBe(3); // SOLID_CROSS | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| describe('when parsing a sequenceDiagram', function () { | ||||
|   let diagram; | ||||
|   beforeEach(async function () { | ||||
| @@ -2058,6 +2312,36 @@ Bob->>Alice:Got it! | ||||
|     expect(messages[0].from).toBe('Alice'); | ||||
|     expect(messages[0].to).toBe('Bob'); | ||||
|   }); | ||||
|  | ||||
|   it('1 should parse ', async () => { | ||||
|     const diagram = await Diagram.fromText(` | ||||
|     sequenceDiagram | ||||
|     actor Bob | ||||
|     actor Alice | ||||
|         Bob -|\\ Alice: Hello Alice, how are you? | ||||
|         Bob -|/ Alice: Hello Alice, how are you? | ||||
|         Bob -// Alice: Hello Alice, how are you? | ||||
|         Bob -\\\\ Alice: Hello Alice, how are you? | ||||
|          | ||||
|         Bob \\|- Alice: Hello Alice, how are you? | ||||
|         Bob /|- Alice: Hello Alice, how are you? | ||||
|         Bob //- Alice: Hello Alice, how are you? | ||||
|         Bob \\\\- Alice: Hello Alice, how are you?         | ||||
|     `); | ||||
|  | ||||
|     const messages = diagram.db.getMessages(); | ||||
|   }); | ||||
|  | ||||
|   it('2 should parse ', async () => { | ||||
|     const diagram = await Diagram.fromText(` | ||||
|     sequenceDiagram | ||||
|     actor Bob | ||||
|     actor Alice | ||||
|       Alice ()<<->>() Bob: hey? | ||||
|     `); | ||||
|  | ||||
|     const messages = diagram.db.getMessages(); | ||||
|   }); | ||||
|   describe('when parsing extended participant syntax', () => { | ||||
|     it('should parse participants with different quote styles and whitespace', async () => { | ||||
|       const diagram = await Diagram.fromText(` | ||||
|   | ||||
| @@ -282,6 +282,49 @@ const drawNote = async function (elem: any, noteModel: NoteModel) { | ||||
|   bounds.models.addNote(noteModel); | ||||
| }; | ||||
|  | ||||
| const drawCentralConnection = function ( | ||||
|   elem: any, | ||||
|   msg: any, | ||||
|   msgModel: any, | ||||
|   diagObj: Diagram, | ||||
|   startx: number, | ||||
|   stopx: number, | ||||
|   lineStartY: number | ||||
| ) { | ||||
|   const actors = diagObj.db.getActors(); | ||||
|   const fromActor = actors.get(msg.from); | ||||
|   const toActor = actors.get(msg.to); | ||||
|   const fromCenter = fromActor.x + fromActor.width / 2; | ||||
|   const toCenter = toActor.x + toActor.width / 2; | ||||
|  | ||||
|   const g = elem.append('g'); | ||||
|  | ||||
|   const drawCircle = (cx: number) => { | ||||
|     g.append('circle') | ||||
|       .attr('cx', cx) | ||||
|       .attr('cy', lineStartY) | ||||
|       .attr('r', 5) | ||||
|       .attr('width', 10) | ||||
|       .attr('height', 10); | ||||
|   }; | ||||
|  | ||||
|   const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } = | ||||
|     diagObj.db.LINETYPE; | ||||
|  | ||||
|   switch (msg.centralConnection) { | ||||
|     case CENTRAL_CONNECTION: | ||||
|       drawCircle(toCenter); | ||||
|       break; | ||||
|     case CENTRAL_CONNECTION_REVERSE: | ||||
|       drawCircle(fromCenter); | ||||
|       break; | ||||
|     case CENTRAL_CONNECTION_DUAL: | ||||
|       drawCircle(fromCenter); | ||||
|       drawCircle(toCenter); | ||||
|       break; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const messageFont = (cnf) => { | ||||
|   return { | ||||
|     fontFamily: cnf.messageFontFamily, | ||||
| @@ -367,7 +410,7 @@ async function boundMessage(_diagram, msgModel): Promise<number> { | ||||
|  * @param lineStartY - The Y coordinate at which the message line starts | ||||
|  * @param diagObj - The diagram object. | ||||
|  */ | ||||
| const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { | ||||
| const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram, msg) { | ||||
|   const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; | ||||
|   const textDims = utils.calculateTextDimensions(message, messageFont(conf)); | ||||
|   const textObj = svgDrawCommon.getTextObj(); | ||||
| @@ -433,6 +476,9 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO | ||||
|     line.attr('y1', lineStartY); | ||||
|     line.attr('x2', stopx); | ||||
|     line.attr('y2', lineStartY); | ||||
|     if (hasCentralConnection(msg, diagObj)) { | ||||
|       drawCentralConnection(diagram, msg, msgModel, diagObj, startx, stopx, lineStartY); | ||||
|     } | ||||
|   } | ||||
|   // Make an SVG Container | ||||
|   // Draw the line | ||||
| @@ -441,7 +487,15 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO | ||||
|     type === diagObj.db.LINETYPE.DOTTED_CROSS || | ||||
|     type === diagObj.db.LINETYPE.DOTTED_POINT || | ||||
|     type === diagObj.db.LINETYPE.DOTTED_OPEN || | ||||
|     type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED | ||||
|     type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.SOLID_TOP_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.STICK_TOP_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED || | ||||
|     type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED | ||||
|   ) { | ||||
|     line.style('stroke-dasharray', '3, 3'); | ||||
|     line.attr('class', 'messageLine1'); | ||||
| @@ -457,6 +511,51 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO | ||||
|   line.attr('stroke-width', 2); | ||||
|   line.attr('stroke', 'none'); // handled by theme/css anyway | ||||
|   line.style('fill', 'none'); // remove any fill colour | ||||
|  | ||||
|   if (type === diagObj.db.LINETYPE.SOLID_TOP || type === diagObj.db.LINETYPE.SOLID_TOP_DOTTED) { | ||||
|     line.attr('marker-end', 'url(' + url + '#solidTopArrowHead)'); | ||||
|   } | ||||
|   if ( | ||||
|     type === diagObj.db.LINETYPE.SOLID_BOTTOM || | ||||
|     type === diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED | ||||
|   ) { | ||||
|     line.attr('marker-end', 'url(' + url + '#solidBottomArrowHead)'); | ||||
|   } | ||||
|   if (type === diagObj.db.LINETYPE.STICK_TOP || type === diagObj.db.LINETYPE.STICK_TOP_DOTTED) { | ||||
|     line.attr('marker-end', 'url(' + url + '#stickTopArrowHead)'); | ||||
|   } | ||||
|   if ( | ||||
|     type === diagObj.db.LINETYPE.STICK_BOTTOM || | ||||
|     type === diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED | ||||
|   ) { | ||||
|     line.attr('marker-end', 'url(' + url + '#stickBottomArrowHead)'); | ||||
|   } | ||||
|  | ||||
|   if ( | ||||
|     type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE || | ||||
|     type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED | ||||
|   ) { | ||||
|     line.attr('marker-start', 'url(' + url + '#solidBottomArrowHead)'); | ||||
|   } | ||||
|   if ( | ||||
|     type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE || | ||||
|     type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED | ||||
|   ) { | ||||
|     line.attr('marker-start', 'url(' + url + '#solidTopArrowHead)'); | ||||
|   } | ||||
|   if ( | ||||
|     type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE || | ||||
|     type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED | ||||
|   ) { | ||||
|     line.attr('marker-start', 'url(' + url + '#stickBottomArrowHead)'); | ||||
|   } | ||||
|   if ( | ||||
|     type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE || | ||||
|     type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED | ||||
|   ) { | ||||
|     line.attr('marker-start', 'url(' + url + '#stickTopArrowHead)'); | ||||
|   } | ||||
|  | ||||
|   if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) { | ||||
|     line.attr('marker-end', 'url(' + url + '#arrowhead)'); | ||||
|   } | ||||
| @@ -481,7 +580,18 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO | ||||
|       type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID || | ||||
|       type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED; | ||||
|  | ||||
|     if (isBidirectional) { | ||||
|     const isReverseArrowType = | ||||
|       type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE || | ||||
|       type === diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED || | ||||
|       type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE || | ||||
|       type === diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED || | ||||
|       type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE || | ||||
|       type === diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED || | ||||
|       type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE || | ||||
|       type === diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED; | ||||
|  | ||||
|     let x = 0; | ||||
|     if (isBidirectional || isReverseArrowType) { | ||||
|       const SEQUENCE_NUMBER_RADIUS = 6; | ||||
|  | ||||
|       if (startx < stopx) { | ||||
| @@ -489,6 +599,7 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO | ||||
|       } else { | ||||
|         line.attr('x1', startx + SEQUENCE_NUMBER_RADIUS); | ||||
|       } | ||||
|       x = 3.5; | ||||
|     } | ||||
|  | ||||
|     diagram | ||||
| @@ -498,7 +609,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO | ||||
|       .attr('x2', startx) | ||||
|       .attr('y2', lineStartY) | ||||
|       .attr('stroke-width', 0) | ||||
|       .attr('marker-start', 'url(' + url + '#sequencenumber)'); | ||||
|       .attr('marker-start', 'url(' + url + '#sequencenumber)') | ||||
|       .attr('transform', `translate(-${x}, 0)`); | ||||
|  | ||||
|     diagram | ||||
|       .append('text') | ||||
| @@ -508,7 +620,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO | ||||
|       .attr('font-size', '12px') | ||||
|       .attr('text-anchor', 'middle') | ||||
|       .attr('class', 'sequenceNumber') | ||||
|       .text(sequenceIndex); | ||||
|       .text(sequenceIndex) | ||||
|       .attr('transform', `translate(-${x}, 0)`); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| @@ -857,6 +970,10 @@ export const draw = async function (_text: string, id: string, _version: string, | ||||
|   svgDraw.insertArrowCrossHead(diagram); | ||||
|   svgDraw.insertArrowFilledHead(diagram); | ||||
|   svgDraw.insertSequenceNumber(diagram); | ||||
|   svgDraw.insertSolidTopArrowHead(diagram); | ||||
|   svgDraw.insertSolidBottomArrowHead(diagram); | ||||
|   svgDraw.insertStickTopArrowHead(diagram); | ||||
|   svgDraw.insertStickBottomArrowHead(diagram); | ||||
|  | ||||
|   /** | ||||
|    * @param msg - The message to draw. | ||||
| @@ -897,6 +1014,12 @@ export const draw = async function (_text: string, id: string, _version: string, | ||||
|       case diagObj.db.LINETYPE.ACTIVE_START: | ||||
|         bounds.newActivation(msg, diagram, actors); | ||||
|         break; | ||||
|       case diagObj.db.LINETYPE.CENTRAL_CONNECTION: | ||||
|         bounds.newActivation(msg, diagram, actors); | ||||
|         break; | ||||
|       case diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE: | ||||
|         bounds.newActivation(msg, diagram, actors); | ||||
|         break; | ||||
|       case diagObj.db.LINETYPE.ACTIVE_END: | ||||
|         activeEnd(msg, bounds.getVerticalPos()); | ||||
|         break; | ||||
| @@ -1055,7 +1178,7 @@ export const draw = async function (_text: string, id: string, _version: string, | ||||
|             createdActors, | ||||
|             destroyedActors | ||||
|           ); | ||||
|           messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY }); | ||||
|           messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY, msg }); | ||||
|           bounds.models.addMessage(msgModel); | ||||
|         } catch (e) { | ||||
|           log.error('error while drawing message', e); | ||||
| @@ -1068,6 +1191,27 @@ export const draw = async function (_text: string, id: string, _version: string, | ||||
|         diagObj.db.LINETYPE.SOLID_OPEN, | ||||
|         diagObj.db.LINETYPE.DOTTED_OPEN, | ||||
|         diagObj.db.LINETYPE.SOLID, | ||||
|  | ||||
|         diagObj.db.LINETYPE.SOLID_TOP, | ||||
|         diagObj.db.LINETYPE.SOLID_BOTTOM, | ||||
|         diagObj.db.LINETYPE.STICK_TOP, | ||||
|         diagObj.db.LINETYPE.STICK_BOTTOM, | ||||
|  | ||||
|         diagObj.db.LINETYPE.SOLID_TOP_DOTTED, | ||||
|         diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED, | ||||
|         diagObj.db.LINETYPE.STICK_TOP_DOTTED, | ||||
|         diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED, | ||||
|  | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE, | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE, | ||||
|  | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED, | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED, | ||||
|  | ||||
|         diagObj.db.LINETYPE.DOTTED, | ||||
|         diagObj.db.LINETYPE.SOLID_CROSS, | ||||
|         diagObj.db.LINETYPE.DOTTED_CROSS, | ||||
| @@ -1087,7 +1231,7 @@ export const draw = async function (_text: string, id: string, _version: string, | ||||
|   await drawActors(diagram, actors, actorKeys, false); | ||||
|  | ||||
|   for (const e of messagesToDraw) { | ||||
|     await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj); | ||||
|     await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj, e.msg); | ||||
|   } | ||||
|   if (conf.mirrorActors) { | ||||
|     await drawActors(diagram, actors, actorKeys, true); | ||||
| @@ -1461,12 +1605,85 @@ const buildNoteModel = async function (msg, actors, diagObj) { | ||||
|   return noteModel; | ||||
| }; | ||||
|  | ||||
| // Central connection positioning constants | ||||
| const CENTRAL_CONNECTION_BASE_OFFSET = 4; | ||||
| const CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET = 6; | ||||
|  | ||||
| /** | ||||
|  * Check if a message has central connection | ||||
|  * @param msg - The message object | ||||
|  * @param diagObj - The diagram object containing LINETYPE constants | ||||
|  * @returns True if the message has any type of central connection | ||||
|  */ | ||||
| const hasCentralConnection = function (msg, diagObj) { | ||||
|   const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } = | ||||
|     diagObj.db.LINETYPE; | ||||
|   return [CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL].includes( | ||||
|     msg.centralConnection | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Calculate the positioning offset for central connection arrows | ||||
|  * @param msg - The message object | ||||
|  * @param diagObj - The diagram object containing LINETYPE constants | ||||
|  * @param isArrowToRight - Whether the arrow is pointing to the right | ||||
|  * @returns The offset to apply to startx position | ||||
|  */ | ||||
| const calculateCentralConnectionOffset = function (msg, diagObj, isArrowToRight) { | ||||
|   const { | ||||
|     CENTRAL_CONNECTION_REVERSE, | ||||
|     CENTRAL_CONNECTION_DUAL, | ||||
|     BIDIRECTIONAL_SOLID, | ||||
|     BIDIRECTIONAL_DOTTED, | ||||
|   } = diagObj.db.LINETYPE; | ||||
|  | ||||
|   let offset = 0; | ||||
|  | ||||
|   if ( | ||||
|     msg.centralConnection === CENTRAL_CONNECTION_REVERSE || | ||||
|     msg.centralConnection === CENTRAL_CONNECTION_DUAL | ||||
|   ) { | ||||
|     offset += CENTRAL_CONNECTION_BASE_OFFSET; | ||||
|   } | ||||
|  | ||||
|   if ( | ||||
|     msg.centralConnection === CENTRAL_CONNECTION_DUAL && | ||||
|     (msg.type === BIDIRECTIONAL_SOLID || msg.type === BIDIRECTIONAL_DOTTED) | ||||
|   ) { | ||||
|     offset += isArrowToRight ? 0 : -CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET; | ||||
|   } | ||||
|  | ||||
|   return offset; | ||||
| }; | ||||
|  | ||||
| const buildMessageModel = function (msg, actors, diagObj) { | ||||
|   if ( | ||||
|     ![ | ||||
|       diagObj.db.LINETYPE.SOLID_OPEN, | ||||
|       diagObj.db.LINETYPE.DOTTED_OPEN, | ||||
|       diagObj.db.LINETYPE.SOLID, | ||||
|  | ||||
|       diagObj.db.LINETYPE.SOLID_TOP, | ||||
|       diagObj.db.LINETYPE.SOLID_BOTTOM, | ||||
|       diagObj.db.LINETYPE.STICK_TOP, | ||||
|       diagObj.db.LINETYPE.STICK_BOTTOM, | ||||
|  | ||||
|       diagObj.db.LINETYPE.SOLID_TOP_DOTTED, | ||||
|       diagObj.db.LINETYPE.SOLID_BOTTOM_DOTTED, | ||||
|       diagObj.db.LINETYPE.STICK_TOP_DOTTED, | ||||
|       diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED, | ||||
|  | ||||
|       diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, | ||||
|       diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, | ||||
|       diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE, | ||||
|       diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE, | ||||
|  | ||||
|       diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, | ||||
|       diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, | ||||
|       diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED, | ||||
|       diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED, | ||||
|  | ||||
|       diagObj.db.LINETYPE.DOTTED, | ||||
|       diagObj.db.LINETYPE.SOLID_CROSS, | ||||
|       diagObj.db.LINETYPE.DOTTED_CROSS, | ||||
| @@ -1484,6 +1701,8 @@ const buildMessageModel = function (msg, actors, diagObj) { | ||||
|   let startx = isArrowToRight ? fromRight : fromLeft; | ||||
|   let stopx = isArrowToRight ? toLeft : toRight; | ||||
|  | ||||
|   // Apply central connection positioning adjustments | ||||
|   startx += calculateCentralConnectionOffset(msg, diagObj, isArrowToRight); | ||||
|   // As the line width is considered, the left and right values will be off by 2. | ||||
|   const isArrowToActivation = Math.abs(toLeft - toRight) > 2; | ||||
|  | ||||
| @@ -1517,7 +1736,30 @@ const buildMessageModel = function (msg, actors, diagObj) { | ||||
|      * Shorten the length of arrow at the end and move the marker forward (using refX) to have a clean arrowhead | ||||
|      * This is not required for open arrows that don't have arrowheads | ||||
|      */ | ||||
|     if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) { | ||||
|     if ( | ||||
|       ![ | ||||
|         diagObj.db.LINETYPE.SOLID_OPEN, | ||||
|         diagObj.db.LINETYPE.DOTTED_OPEN, | ||||
|  | ||||
|         diagObj.db.LINETYPE.STICK_TOP, | ||||
|         diagObj.db.LINETYPE.STICK_BOTTOM, | ||||
|  | ||||
|         diagObj.db.LINETYPE.STICK_TOP_DOTTED, | ||||
|         diagObj.db.LINETYPE.STICK_BOTTOM_DOTTED, | ||||
|  | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, | ||||
|  | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE, | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE, | ||||
|  | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED, | ||||
|         diagObj.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED, | ||||
|  | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, | ||||
|       ].includes(msg.type) | ||||
|     ) { | ||||
|       stopx += adjustValue(3); | ||||
|     } | ||||
|  | ||||
| @@ -1525,9 +1767,14 @@ const buildMessageModel = function (msg, actors, diagObj) { | ||||
|      * Shorten start position of bidirectional arrow to accommodate for second arrowhead | ||||
|      */ | ||||
|     if ( | ||||
|       [diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes( | ||||
|         msg.type | ||||
|       ) | ||||
|       [ | ||||
|         diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, | ||||
|         diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_TOP_REVERSE, | ||||
|         diagObj.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE, | ||||
|       ].includes(msg.type) | ||||
|     ) { | ||||
|       startx -= adjustValue(3); | ||||
|     } | ||||
|   | ||||
| @@ -1709,6 +1709,77 @@ const _drawMenuItemTextCandidateFunc = (function () { | ||||
|   }; | ||||
| })(); | ||||
|  | ||||
| /** | ||||
|  * Setup arrow head and define the marker. The result is appended to the svg. | ||||
|  * | ||||
|  * @param elem | ||||
|  */ | ||||
| export const insertSolidTopArrowHead = function (elem) { | ||||
|   elem | ||||
|     .append('defs') | ||||
|     .append('marker') | ||||
|     .attr('id', 'solidTopArrowHead') | ||||
|     .attr('refX', 7.9) | ||||
|     .attr('refY', 7.25) | ||||
|     .attr('markerUnits', 'userSpaceOnUse') | ||||
|     .attr('markerWidth', 12) | ||||
|     .attr('markerHeight', 12) | ||||
|     .attr('orient', 'auto-start-reverse') | ||||
|     .append('path') | ||||
|     .attr('d', 'M 0 0 L 10 8 L 0 8 z'); // this is actual shape for arrowhead | ||||
| }; | ||||
|  | ||||
| export const insertSolidBottomArrowHead = function (elem) { | ||||
|   elem | ||||
|     .append('defs') | ||||
|     .append('marker') | ||||
|     .attr('id', 'solidBottomArrowHead') | ||||
|     .attr('refX', 7.9) | ||||
|     .attr('refY', 0.75) | ||||
|     .attr('markerUnits', 'userSpaceOnUse') | ||||
|     .attr('markerWidth', 12) | ||||
|     .attr('markerHeight', 12) | ||||
|     .attr('orient', 'auto-start-reverse') | ||||
|     .append('path') | ||||
|     .attr('d', 'M 0 0 L 10 0 L 0 8 z'); | ||||
| }; | ||||
|  | ||||
| export const insertStickTopArrowHead = function (elem) { | ||||
|   elem | ||||
|     .append('defs') | ||||
|     .append('marker') | ||||
|     .attr('id', 'stickTopArrowHead') | ||||
|     .attr('refX', 7.5) | ||||
|     .attr('refY', 7) | ||||
|     .attr('markerUnits', 'userSpaceOnUse') | ||||
|     .attr('markerWidth', 12) | ||||
|     .attr('markerHeight', 12) | ||||
|     .attr('orient', 'auto-start-reverse') | ||||
|     .append('path') | ||||
|     .attr('d', 'M 0 0 L 7 7') | ||||
|     .attr('stroke', 'black') | ||||
|     .attr('stroke-width', 1.5) | ||||
|     .attr('fill', 'none'); | ||||
| }; | ||||
|  | ||||
| export const insertStickBottomArrowHead = function (elem) { | ||||
|   elem | ||||
|     .append('defs') | ||||
|     .append('marker') | ||||
|     .attr('id', 'stickBottomArrowHead') | ||||
|     .attr('refX', 7.5) | ||||
|     .attr('refY', 0) | ||||
|     .attr('markerUnits', 'userSpaceOnUse') | ||||
|     .attr('markerWidth', 12) | ||||
|     .attr('markerHeight', 12) | ||||
|     .attr('orient', 'auto-start-reverse') | ||||
|     .append('path') | ||||
|     .attr('d', 'M 0 7 L 7 0') | ||||
|     .attr('stroke', 'black') | ||||
|     .attr('stroke-width', 1.5) | ||||
|     .attr('fill', 'none'); | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|   drawRect, | ||||
|   drawText, | ||||
| @@ -1731,4 +1802,8 @@ export default { | ||||
|   getNoteRect, | ||||
|   fixLifeLineHeights, | ||||
|   sanitizeUrl, | ||||
|   insertSolidTopArrowHead, | ||||
|   insertSolidBottomArrowHead, | ||||
|   insertStickTopArrowHead, | ||||
|   insertStickBottomArrowHead, | ||||
| }; | ||||
|   | ||||
| @@ -35,6 +35,7 @@ export interface Message { | ||||
|   type?: number; | ||||
|   activate?: boolean; | ||||
|   placement?: string; | ||||
|   centralConnection?: number; | ||||
| } | ||||
|  | ||||
| export interface AddMessageParams { | ||||
| @@ -50,6 +51,8 @@ export interface AddMessageParams { | ||||
|     | 'destroyParticipant' | ||||
|     | 'activeStart' | ||||
|     | 'activeEnd' | ||||
|     | 'centralConnection' | ||||
|     | 'centralConnectionReverse' | ||||
|     | 'addNote' | ||||
|     | 'addLinks' | ||||
|     | 'addALink' | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@mdi/font": "^7.4.47", | ||||
|     "@vueuse/core": "^13.1.0", | ||||
|     "@vueuse/core": "^13.9.0", | ||||
|     "font-awesome": "^4.7.0", | ||||
|     "jiti": "^2.4.2", | ||||
|     "mermaid": "workspace:^", | ||||
| @@ -25,17 +25,17 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@iconify-json/carbon": "^1.2.13", | ||||
|     "@unocss/reset": "^66.0.0", | ||||
|     "@unocss/reset": "^66.5.1", | ||||
|     "@vite-pwa/vitepress": "^1.0.0", | ||||
|     "@vitejs/plugin-vue": "^6.0.1", | ||||
|     "fast-glob": "^3.3.3", | ||||
|     "https-localhost": "^4.7.1", | ||||
|     "pathe": "^2.0.3", | ||||
|     "unocss": "^66.4.2", | ||||
|     "unplugin-vue-components": "^28.4.0", | ||||
|     "vite": "^7.0.0", | ||||
|     "vite-plugin-pwa": "^1.0.0", | ||||
|     "vitepress": "1.6.3", | ||||
|     "unocss": "^66.5.1", | ||||
|     "unplugin-vue-components": "^28.4.1", | ||||
|     "vite": "^7.0.7", | ||||
|     "vite-plugin-pwa": "^1.0.3", | ||||
|     "vitepress": "1.6.4", | ||||
|     "workbox-window": "^7.3.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -216,7 +216,11 @@ Messages can be of two displayed either solid or with a dotted line. | ||||
| [Actor][Arrow][Actor]:Message text | ||||
| ``` | ||||
|  | ||||
| There are ten types of arrows currently supported: | ||||
| Lines can be solid or dotted, and can end with various types of arrowheads, crosses, or open arrows. | ||||
|  | ||||
| #### Supported Arrow Types | ||||
|  | ||||
| **Standard Arrow Types** | ||||
|  | ||||
| | Type     | Description                                          | | ||||
| | -------- | ---------------------------------------------------- | | ||||
| @@ -231,6 +235,49 @@ There are ten types of arrows currently supported: | ||||
| | `-)`     | Solid line with an open arrow at the end (async)     | | ||||
| | `--)`    | Dotted line with a open arrow at the end (async)     | | ||||
|  | ||||
| **Half-Arrows (v<MERMAID_RELEASE_VERSION>+)** | ||||
|  | ||||
| The following half-arrow types are supported for more expressive sequence diagrams. Both solid and dotted variants are available by increasing the number of dashes (`-` → `--`). | ||||
|  | ||||
| --- | ||||
|  | ||||
| | Type    | Description                                          | | ||||
| | ------- | ---------------------------------------------------- | | ||||
| | `-\|\`  | Solid line with top half arrowhead                   | | ||||
| | `--\|\` | Dotted line with top half arrowhead                  | | ||||
| | `-\|/`  | Solid line with bottom half arrowhead                | | ||||
| | `--\|/` | Dotted line with bottom half arrowhead               | | ||||
| | `/\|-`  | Solid line with reverse top half arrowhead           | | ||||
| | `/\|--` | Dotted line with reverse top half arrowhead          | | ||||
| | `\\-`   | Solid line with reverse bottom half arrowhead        | | ||||
| | `\\--`  | Dotted line with reverse bottom half arrowhead       | | ||||
| | `-\\`   | Solid line with top stick half arrowhead             | | ||||
| | `--\\`  | Dotted line with top stick half arrowhead            | | ||||
| | `-//`   | Solid line with bottom stick half arrowhead          | | ||||
| | `--//`  | Dotted line with bottom stick half arrowhead         | | ||||
| | `//-`   | Solid line with reverse top stick half arrowhead     | | ||||
| | `//--`  | Dotted line with reverse top stick half arrowhead    | | ||||
| | `\\-`   | Solid line with reverse bottom stick half arrowhead  | | ||||
| | `\\--`  | Dotted line with reverse bottom stick half arrowhead | | ||||
|  | ||||
| ## Central Connections (v<MERMAID_RELEASE_VERSION>+) | ||||
|  | ||||
| Mermaid sequence diagrams support **central lifeline connections** using a `()`. | ||||
| This is useful to represent messages or signals that connect to a central point, rather than from one actor directly to another. | ||||
|  | ||||
| To indicate a central connection, append `()` to the arrow syntax. | ||||
|  | ||||
| #### Basic Syntax | ||||
|  | ||||
| ```mermaid-example | ||||
| sequenceDiagram | ||||
|     participant Alice | ||||
|     participant John | ||||
|     Alice->>()John: Hello John | ||||
|     Alice()->>John: How are you? | ||||
|     John()->>()Alice: Great! | ||||
| ``` | ||||
|  | ||||
| ## Activations | ||||
|  | ||||
| It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations: | ||||
|   | ||||
| @@ -207,7 +207,7 @@ describe('when using mermaid and ', () => { | ||||
|         [Error: Parse error on line 2: | ||||
|         ...equenceDiagramAlice:->Bob: Hello Bob, h... | ||||
|         ----------------------^ | ||||
|         Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT'] | ||||
|         Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT'] | ||||
|       `); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,11 @@ import rough from 'roughjs'; | ||||
| import type { D3Selection } from '../../../types.js'; | ||||
| import { handleUndefinedAttr } from '../../../utils.js'; | ||||
| import type { Bounds, Point } from '../../../types.js'; | ||||
| import { | ||||
|   calculateMindmapDimensions, | ||||
|   getMindmapIconConfig, | ||||
|   insertMindmapIcon, | ||||
| } from '../../../diagrams/mindmap/mindmapIconHelper.js'; | ||||
|  | ||||
| export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) { | ||||
|   const { labelStyles, nodeStyles } = styles2String(node); | ||||
| @@ -17,20 +22,34 @@ export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, | ||||
|     getNodeClasses(node) | ||||
|   ); | ||||
|  | ||||
|   const w = bbox.width + 10 * halfPadding; | ||||
|   const h = bbox.height + 8 * halfPadding; | ||||
|   const r = 0.15 * w; | ||||
|   const { cssStyles } = node; | ||||
|   const baseWidth = bbox.width + 10 * halfPadding; | ||||
|   const baseHeight = bbox.height + 8 * halfPadding; | ||||
|  | ||||
|   const iconConfig = getMindmapIconConfig('bang'); | ||||
|   const dimensions = calculateMindmapDimensions( | ||||
|     node, | ||||
|     bbox, | ||||
|     baseWidth, | ||||
|     baseHeight, | ||||
|     halfPadding, | ||||
|     iconConfig | ||||
|   ); | ||||
|  | ||||
|   const w = dimensions.width; | ||||
|   const h = dimensions.height; | ||||
|  | ||||
|   node.width = w; | ||||
|   node.height = h; | ||||
|  | ||||
|   const minWidth = bbox.width + 20; | ||||
|   const minHeight = bbox.height + 20; | ||||
|   const effectiveWidth = Math.max(w, minWidth); | ||||
|   const effectiveHeight = Math.max(h, minHeight); | ||||
|  | ||||
|   label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`); | ||||
|   label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); | ||||
|  | ||||
|   let bangElem; | ||||
|   const path = `M0 0  | ||||
|   const r = 0.15 * effectiveWidth; | ||||
|   const path = `M0 0 | ||||
|     a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${-1 * effectiveHeight * 0.1} | ||||
|     a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} | ||||
|     a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} | ||||
| @@ -50,13 +69,16 @@ export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, | ||||
|     a${r},${r} 1 0,0 ${effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33} | ||||
|   H0 V0 Z`; | ||||
|  | ||||
|   let bangElem; | ||||
|   if (node.look === 'handDrawn') { | ||||
|     // @ts-expect-error -- Passing a D3.Selection seems to work for some reason | ||||
|     const rc = rough.svg(shapeSvg); | ||||
|     const options = userNodeOverrides(node, {}); | ||||
|     const roughNode = rc.path(path, options); | ||||
|     bangElem = shapeSvg.insert(() => roughNode, ':first-child'); | ||||
|     bangElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles)); | ||||
|     bangElem | ||||
|       .attr('class', 'basic label-container') | ||||
|       .attr('style', handleUndefinedAttr(node.cssStyles)); | ||||
|   } else { | ||||
|     bangElem = shapeSvg | ||||
|       .insert('path', ':first-child') | ||||
| @@ -68,6 +90,10 @@ export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, | ||||
|   // Translate the path (center the shape) | ||||
|   bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`); | ||||
|  | ||||
|   if (node.icon) { | ||||
|     await insertMindmapIcon(shapeSvg, node, iconConfig); | ||||
|   } | ||||
|  | ||||
|   updateNodeBounds(node, bangElem); | ||||
|   node.calcIntersect = function (bounds: Bounds, point: Point) { | ||||
|     return intersect.rect(bounds, point); | ||||
|   | ||||
| @@ -14,9 +14,25 @@ export async function circle<T extends SVGGraphicsElement>( | ||||
| ) { | ||||
|   const { labelStyles, nodeStyles } = styles2String(node); | ||||
|   node.labelStyle = labelStyles; | ||||
|   const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|  | ||||
|   const { shapeSvg, bbox, halfPadding, label } = await labelHelper( | ||||
|     parent, | ||||
|     node, | ||||
|     getNodeClasses(node) | ||||
|   ); | ||||
|  | ||||
|   const padding = options?.padding ?? halfPadding; | ||||
|  | ||||
|   const radius = bbox.width / 2 + padding; | ||||
|  | ||||
|   node.width = radius * 2; | ||||
|   node.height = radius * 2; | ||||
|  | ||||
|   const labelXOffset = -bbox.width / 2; | ||||
|   const labelYOffset = -bbox.height / 2; | ||||
|   if (node.icon) { | ||||
|     label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); | ||||
|   } | ||||
|   let circleElem; | ||||
|   const { cssStyles } = node; | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,11 @@ import type { Node } from '../../types.js'; | ||||
| import intersect from '../intersect/index.js'; | ||||
| import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; | ||||
| import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; | ||||
| import { | ||||
|   getMindmapIconConfig, | ||||
|   calculateMindmapDimensions, | ||||
|   insertMindmapIcon, | ||||
| } from '../../../diagrams/mindmap/mindmapIconHelper.js'; | ||||
|  | ||||
| export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) { | ||||
|   const { labelStyles, nodeStyles } = styles2String(node); | ||||
| @@ -17,8 +22,26 @@ export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T> | ||||
|     getNodeClasses(node) | ||||
|   ); | ||||
|  | ||||
|   const w = bbox.width + 2 * halfPadding; | ||||
|   const h = bbox.height + 2 * halfPadding; | ||||
|   const baseWidth = bbox.width + 2 * halfPadding; | ||||
|   const baseHeight = bbox.height + 2 * halfPadding; | ||||
|  | ||||
|   const iconConfig = getMindmapIconConfig('cloud'); | ||||
|   const dimensions = calculateMindmapDimensions( | ||||
|     node, | ||||
|     bbox, | ||||
|     baseWidth, | ||||
|     baseHeight, | ||||
|     halfPadding, | ||||
|     iconConfig | ||||
|   ); | ||||
|  | ||||
|   const w = dimensions.width; | ||||
|   const h = dimensions.height; | ||||
|  | ||||
|   node.width = w; | ||||
|   node.height = h; | ||||
|  | ||||
|   label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); | ||||
|  | ||||
|   // Cloud radii | ||||
|   const r1 = 0.15 * w; | ||||
| @@ -61,11 +84,13 @@ export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T> | ||||
|       .attr('d', path); | ||||
|   } | ||||
|  | ||||
|   label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`); | ||||
|  | ||||
|   // Center the shape | ||||
|   cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`); | ||||
|  | ||||
|   if (node.icon) { | ||||
|     await insertMindmapIcon(shapeSvg, node, iconConfig); | ||||
|   } | ||||
|  | ||||
|   updateNodeBounds(node, cloudElem); | ||||
|  | ||||
|   node.calcIntersect = function (bounds: Bounds, point: Point) { | ||||
|   | ||||
| @@ -3,6 +3,11 @@ import type { Node } from '../../types.js'; | ||||
| import intersect from '../intersect/index.js'; | ||||
| import { styles2String } from './handDrawnShapeStyles.js'; | ||||
| import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; | ||||
| import { | ||||
|   getMindmapIconConfig, | ||||
|   calculateMindmapDimensions, | ||||
|   insertMindmapIcon, | ||||
| } from '../../../diagrams/mindmap/mindmapIconHelper.js'; | ||||
|  | ||||
| export async function defaultMindmapNode<T extends SVGGraphicsElement>( | ||||
|   parent: D3Selection<T>, | ||||
| @@ -17,22 +22,38 @@ export async function defaultMindmapNode<T extends SVGGraphicsElement>( | ||||
|     getNodeClasses(node) | ||||
|   ); | ||||
|  | ||||
|   const w = bbox.width + 8 * halfPadding; | ||||
|   const h = bbox.height + 2 * halfPadding; | ||||
|   const rd = 5; | ||||
|   const baseWidth = bbox.width + 8 * halfPadding; | ||||
|   const baseHeight = bbox.height + 2 * halfPadding; | ||||
|  | ||||
|   const rectPath = ` | ||||
|     M${-w / 2} ${h / 2 - rd} | ||||
|     v${-h + 2 * rd} | ||||
|     q0,-${rd} ${rd},-${rd} | ||||
|     h${w - 2 * rd} | ||||
|     q${rd},0 ${rd},${rd} | ||||
|     v${h - 2 * rd} | ||||
|     q0,${rd} -${rd},${rd} | ||||
|     h${-w + 2 * rd} | ||||
|     q-${rd},0 -${rd},-${rd} | ||||
|     Z | ||||
|   `; | ||||
|   const iconConfig = getMindmapIconConfig('default'); | ||||
|   const dimensions = calculateMindmapDimensions( | ||||
|     node, | ||||
|     bbox, | ||||
|     baseWidth, | ||||
|     baseHeight, | ||||
|     halfPadding, | ||||
|     iconConfig | ||||
|   ); | ||||
|  | ||||
|   const w = dimensions.width; | ||||
|   const h = dimensions.height; | ||||
|  | ||||
|   node.width = w; | ||||
|   node.height = h; | ||||
|  | ||||
|   label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); | ||||
|  | ||||
|   const RD = 5; | ||||
|   const rectPath = `M${-w / 2} ${h / 2 - RD}  | ||||
|     v${-h + 2 * RD}  | ||||
|     q0,-${RD} ${RD},-${RD}  | ||||
|     h${w - 2 * RD}  | ||||
|     q${RD},0 ${RD},${RD}  | ||||
|     v${h - 2 * RD}  | ||||
|     q0,${RD} -${RD},${RD}  | ||||
|     h${-w + 2 * RD}  | ||||
|     q-${RD},0 -${RD},-${RD}  | ||||
|     Z`; | ||||
|  | ||||
|   const bg = shapeSvg | ||||
|     .append('path') | ||||
| @@ -49,9 +70,12 @@ export async function defaultMindmapNode<T extends SVGGraphicsElement>( | ||||
|     .attr('x2', w / 2) | ||||
|     .attr('y2', h / 2); | ||||
|  | ||||
|   label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`); | ||||
|   shapeSvg.append(() => label.node()); | ||||
|  | ||||
|   if (node.icon) { | ||||
|     await insertMindmapIcon(shapeSvg, node, iconConfig); | ||||
|   } | ||||
|  | ||||
|   updateNodeBounds(node, bg); | ||||
|   node.calcIntersect = function (bounds: Bounds, point: Point) { | ||||
|     return intersect.rect(bounds, point); | ||||
|   | ||||
| @@ -15,11 +15,21 @@ export async function drawRect<T extends SVGGraphicsElement>( | ||||
| ) { | ||||
|   const { labelStyles, nodeStyles } = styles2String(node); | ||||
|   node.labelStyle = labelStyles; | ||||
|   // console.log('IPI labelStyles:', labelStyles); | ||||
|   const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|  | ||||
|   const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|  | ||||
|   const totalWidth = Math.max(bbox.width + options.labelPaddingX * 2, node?.width || 0); | ||||
|   const totalHeight = Math.max(bbox.height + options.labelPaddingY * 2, node?.height || 0); | ||||
|  | ||||
|   node.width = totalWidth; | ||||
|   node.height = totalHeight; | ||||
|  | ||||
|   const labelXOffset = -bbox.width / 2; | ||||
|  | ||||
|   const labelYOffset = -bbox.height / 2; | ||||
|   if (node.icon) { | ||||
|     label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); | ||||
|   } | ||||
|   const x = -totalWidth / 2; | ||||
|   const y = -totalHeight / 2; | ||||
|  | ||||
|   | ||||
| @@ -26,10 +26,22 @@ export const createHexagonPathD = ( | ||||
| export async function hexagon<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) { | ||||
|   const { labelStyles, nodeStyles } = styles2String(node); | ||||
|   node.labelStyle = labelStyles; | ||||
|   const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|  | ||||
|   const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|  | ||||
|   const h = bbox.height + (node.padding ?? 0); | ||||
|   const w = bbox.width + (node.padding ?? 0) * 2.5; | ||||
|  | ||||
|   node.width = w; | ||||
|   node.height = h; | ||||
|  | ||||
|   const labelXOffset = -bbox.width / 2; | ||||
|  | ||||
|   const labelYOffset = -bbox.height / 2; | ||||
|  | ||||
|   if (node.icon) { | ||||
|     label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); | ||||
|   } | ||||
|   const { cssStyles } = node; | ||||
|   // @ts-expect-error -- Passing a D3.Selection seems to work for some reason | ||||
|   const rc = rough.svg(shapeSvg); | ||||
| @@ -74,9 +86,6 @@ export async function hexagon<T extends SVGGraphicsElement>(parent: D3Selection< | ||||
|     polygon.selectChildren('path').attr('style', nodeStyles); | ||||
|   } | ||||
|  | ||||
|   node.width = w; | ||||
|   node.height = h; | ||||
|  | ||||
|   updateNodeBounds(node, polygon); | ||||
|  | ||||
|   node.intersect = function (point) { | ||||
|   | ||||
| @@ -1,13 +1,57 @@ | ||||
| import { circle } from './circle.js'; | ||||
| import type { Node, MindmapOptions } from '../../types.js'; | ||||
| import type { Node } from '../../types.js'; | ||||
| import type { D3Selection } from '../../../types.js'; | ||||
| import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; | ||||
| import { styles2String } from './handDrawnShapeStyles.js'; | ||||
| import intersect from '../intersect/index.js'; | ||||
| import { | ||||
|   getMindmapIconConfig, | ||||
|   calculateMindmapDimensions, | ||||
|   insertMindmapIcon, | ||||
| } from '../../../diagrams/mindmap/mindmapIconHelper.js'; | ||||
|  | ||||
| export async function mindmapCircle<T extends SVGGraphicsElement>( | ||||
|   parent: D3Selection<T>, | ||||
|   node: Node | ||||
| ) { | ||||
|   const options = { | ||||
|     padding: node.padding ?? 0, | ||||
|   } as MindmapOptions; | ||||
|   return circle(parent, node, options); | ||||
|   const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|   const halfPadding = (node.padding ?? 0) / 2; | ||||
|  | ||||
|   const iconConfig = getMindmapIconConfig('circle'); | ||||
|   const baseRadius = bbox.width / 2 + halfPadding; | ||||
|   const baseDiameter = baseRadius * 2; | ||||
|  | ||||
|   const dimensions = calculateMindmapDimensions( | ||||
|     node, | ||||
|     bbox, | ||||
|     baseDiameter, | ||||
|     baseDiameter, | ||||
|     halfPadding, | ||||
|     iconConfig | ||||
|   ); | ||||
|  | ||||
|   const radius = dimensions.width / 2; | ||||
|   node.width = dimensions.width; | ||||
|   node.height = dimensions.height; | ||||
|  | ||||
|   label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); | ||||
|  | ||||
|   const circle = shapeSvg | ||||
|     .insert('circle', ':first-child') | ||||
|     .attr('r', radius) | ||||
|     .attr('class', 'basic label-container') | ||||
|     .attr('style', styles2String(node).nodeStyles); | ||||
|  | ||||
|   shapeSvg.append(() => label.node()); | ||||
|  | ||||
|   if (node.icon) { | ||||
|     await insertMindmapIcon(shapeSvg, node, iconConfig); | ||||
|   } | ||||
|  | ||||
|   updateNodeBounds(node, circle); | ||||
|  | ||||
|   node.intersect = function (point) { | ||||
|     return intersect.circle(node, radius, point); | ||||
|   }; | ||||
|  | ||||
|   return shapeSvg; | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,8 @@ import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } f | ||||
| import intersect from '../intersect/index.js'; | ||||
| import type { Node } from '../../types.js'; | ||||
| import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; | ||||
| import rough from 'roughjs'; | ||||
| import type { D3Selection } from '../../../types.js'; | ||||
| import rough from 'roughjs'; | ||||
|  | ||||
| /** | ||||
|  * Generates evenly spaced points along an elliptical arc connecting two points. | ||||
| @@ -91,13 +91,20 @@ export async function roundedRect<T extends SVGGraphicsElement>( | ||||
| ) { | ||||
|   const { labelStyles, nodeStyles } = styles2String(node); | ||||
|   node.labelStyle = labelStyles; | ||||
|   const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|   const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); | ||||
|  | ||||
|   const labelPaddingX = node?.padding ?? 0; | ||||
|   const labelPaddingY = node?.padding ?? 0; | ||||
|  | ||||
|   const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2; | ||||
|   const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2; | ||||
|  | ||||
|   const labelXOffset = -bbox.width / 2; | ||||
|  | ||||
|   if (node.icon) { | ||||
|     label.attr('transform', `translate(${labelXOffset}, ${-bbox.height / 2})`); | ||||
|   } | ||||
|  | ||||
|   const radius = node.radius || 5; | ||||
|   const taper = node.taper || 5; // Taper width for the rounded corners | ||||
|   const { cssStyles } = node; | ||||
|   | ||||
| @@ -84,6 +84,7 @@ interface BaseNode { | ||||
|   radius?: number; | ||||
|   taper?: number; | ||||
|   stroke?: string; | ||||
|   section?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
							
								
								
									
										1013
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1013
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user