mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
Merge pull request #5880 from yari-dewalt/update-class-diagram
Update class diagram to v3 using new renderer
This commit is contained in:
commit
bdf145ffe3
1037
cypress/integration/rendering/classDiagram-elk-v3.spec.js
Normal file
1037
cypress/integration/rendering/classDiagram-elk-v3.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
1041
cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js
Normal file
1041
cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
1031
cypress/integration/rendering/classDiagram-v3.spec.js
Normal file
1031
cypress/integration/rendering/classDiagram-v3.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
663
cypress/platform/yari.html
Normal file
663
cypress/platform/yari.html
Normal file
@ -0,0 +1,663 @@
|
||||
<html>
|
||||
<body>
|
||||
<h1 class="header">Class Nodes</h1>
|
||||
<div class="node-showcase">
|
||||
<div class="test">
|
||||
<h2>Basic Class</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
htmlLabels: false
|
||||
---
|
||||
classDiagram
|
||||
class _Duck_ {
|
||||
+String beakColor
|
||||
_+_swim_()a_
|
||||
__+quack() test__
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Basic Class</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
htmlLabels: false
|
||||
---
|
||||
classDiagram
|
||||
class Class10:::exClass2 {
|
||||
int[] id
|
||||
List~int~ ids
|
||||
test(List~int~ ids) List~bool~
|
||||
testArray() bool[]
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Basic Class</h2>
|
||||
<pre class="mermaid">
|
||||
flowchart TD
|
||||
Start --> Stop
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Complex Class</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Square~Shape~{
|
||||
int id
|
||||
List~int~ position
|
||||
setPoints(List~int~ points)
|
||||
getPoints() List~int~
|
||||
}
|
||||
|
||||
Square : -List~string~ messages
|
||||
Square : +setMessages(List~string~ messages)
|
||||
Square : +getMessages() List~string~
|
||||
Square : +getDistanceMatrix() List~List~int~~
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>No Attributes</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Duck {
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>No Methods</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Only Class Name</h2>
|
||||
<p>Empty line as attribute</p>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
class:
|
||||
hideEmptyMembersBox: false
|
||||
---
|
||||
classDiagram
|
||||
class Duck {
|
||||
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Visibility and Types</h2>
|
||||
<p>(Further tilde testing)</p>
|
||||
<div class="mermaid">
|
||||
classDiagram class Duck { ~interface~~~ +String beakColor #swim() ~quack()~~~
|
||||
-test()~~~~~~~ +deposit(amount) bool }
|
||||
</div>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Additional Classifiers</h2>
|
||||
<p>(* Abstract | $ Static)</p>
|
||||
<div class="mermaid">
|
||||
classDiagram class Square~Shape~ { int id* List~int~ position* setPoints(List~int~points)*
|
||||
getPoints()* List~int~ } Square : -List~string~ messages$ Square :
|
||||
+setMessages(List~string~ messages)* Square : +getMessages()$ List~string~ Square :
|
||||
+getDistanceMatrix() List~List~int~~$
|
||||
</div>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Label</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Animal~test~["Animal with a label"]
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Spacing</h2>
|
||||
<p>(Fix ensures consistent spacing rules)</p>
|
||||
<p>(No space or single space?)</p>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class ClassName {
|
||||
-attribute:type
|
||||
- attribute : type
|
||||
test
|
||||
|
||||
+ GetAttribute() type
|
||||
+ GetAttribute() type
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Annotation</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape
|
||||
<<interface>> Shape
|
||||
Shape : noOfVertices
|
||||
Shape : draw()
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Long Class Name Text</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class ThisIsATestForALongClassName {
|
||||
<<interface>>
|
||||
noOfLetters
|
||||
delete()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Long Annotation Text</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape
|
||||
<<superlongannotationtext>> Shape
|
||||
Shape : noOfVertices
|
||||
Shape : draw()
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Long Member Text</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape
|
||||
<<interface>> Shape
|
||||
Shape : noOfVertices
|
||||
Shape : longtexttestkeepgoingandgoing
|
||||
Shape : draw()
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Link</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape
|
||||
link Shape "https://www.github.com" "This is a tooltip for a link"
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Click</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Shape
|
||||
click Shape href "https://www.github.com" "This is a tooltip for a link"
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Hand Drawn</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
look: handDrawn
|
||||
htmlLabels: true
|
||||
---
|
||||
classDiagram
|
||||
class Hand {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
style Hand fill:#f9f,stroke:#29f,stroke-width:2px
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Neutral Theme</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
theme: neutral
|
||||
---
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Dark Theme</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
theme: dark
|
||||
---
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Forest Theme</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
theme: forest
|
||||
---
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Base Theme</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
theme: base
|
||||
---
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Custom Theme</h2>
|
||||
<pre class="mermaid">
|
||||
%%{
|
||||
init: {
|
||||
'theme': 'base',
|
||||
'themeVariables': {
|
||||
'primaryColor': '#BB2528',
|
||||
'primaryTextColor': '#fff',
|
||||
'primaryBorderColor': '#7C0000',
|
||||
'lineColor': '#F83d29',
|
||||
'secondaryColor': '#006100',
|
||||
'tertiaryColor': '#fff'
|
||||
}
|
||||
}
|
||||
}%%
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
Duck--Dog
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Styling within Diagram</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
style Duck fill:#f9f,stroke:#333,stroke-width:8px
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Styling with classDef Statement</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Duck:::bold {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
|
||||
class Dog {
|
||||
+int numTeeth
|
||||
+bark()
|
||||
}
|
||||
|
||||
cssClass "Duck,Dog" pink
|
||||
|
||||
classDef pink fill:#f9f
|
||||
classDef default color:#f1e
|
||||
classDef bold stroke:#333,stroke-width:6px,color:#fff
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Styling with Class in Stylesheet</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Duck {
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
class Duck:::styleClass
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="header">Diagram Testing</h1>
|
||||
<div class="diagram-showcase">
|
||||
<div class="test">
|
||||
<h2>Class Nodes Only</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
title: Animal example
|
||||
---
|
||||
classDiagram
|
||||
Animal : +int age
|
||||
Animal : +String gender
|
||||
Animal: +isMammal()
|
||||
Animal: +mate()
|
||||
class Duck{
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
class Fish{
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
class Zebra{
|
||||
+bool is_wild
|
||||
+run()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Class Nodes LR</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
title: Animal example
|
||||
---
|
||||
classDiagram
|
||||
direction LR
|
||||
Animal : +int age
|
||||
Animal : +String gender
|
||||
Animal: +isMammal()
|
||||
Animal: +mate()
|
||||
class Duck{
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
class Fish{
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
class Zebra{
|
||||
+bool is_wild
|
||||
+run()
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Relations</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
classA <|-- classB
|
||||
classC *-- classD
|
||||
classE o-- classF
|
||||
classG <-- classH
|
||||
classI -- classJ
|
||||
classK <.. classL
|
||||
classM <|.. classN
|
||||
classO .. classP
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Two Way Relation</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
class Animal {
|
||||
int size
|
||||
walk()
|
||||
}
|
||||
class Zebra {
|
||||
int size
|
||||
walk()
|
||||
}
|
||||
Animal o--|> Zebra
|
||||
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Relations with Labels</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
classA <|-- classB : implements
|
||||
classC *-- classD : composition
|
||||
classE o-- classF : aggregation
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Cardinality / Multiplicity</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
Customer "1" --> "*" Ticket
|
||||
Student "1" --> "1..*" Course
|
||||
Galaxy --> "many" Star : Contains
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Complex Relations with Theme</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
theme: forest
|
||||
look: handDrawns
|
||||
layout: elk
|
||||
---
|
||||
classDiagram
|
||||
direction RL
|
||||
class Student {
|
||||
-idCard : IdCard
|
||||
}
|
||||
class IdCard{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
class Bike{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
Student "1" o--o "1" IdCard : carries
|
||||
Student "1" o--o "1" Bike : rides
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Notes</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
note "This is a general note"
|
||||
note for MyClass "This is a note for a class"
|
||||
class MyClass
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Namespaces</h2>
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
namespace BaseShapes {
|
||||
class Triangle
|
||||
class Rectangle {
|
||||
double width
|
||||
double height
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Namespaces</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
classDiagram
|
||||
namespace Namespace1 {
|
||||
class C1
|
||||
class C2
|
||||
}
|
||||
C1 --> C2
|
||||
class C3
|
||||
class C4
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Full Example</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
title: Animal example
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
classDiagram
|
||||
note "From Duck till Zebra"
|
||||
Animal <|--|> Duck
|
||||
note for Duck "can fly<br>can swim<br>can dive<br>can help in debugging"
|
||||
Animal <|-- Fish
|
||||
Animal <|--|> Zebra
|
||||
Animal : +int age
|
||||
Animal : +String gender
|
||||
Animal: +isMammal()
|
||||
Animal: +mate()
|
||||
class Duck{
|
||||
+String beakColor
|
||||
+swim()
|
||||
+quack()
|
||||
}
|
||||
class Fish{
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
class Zebra{
|
||||
+bool is_wild
|
||||
+run()
|
||||
}
|
||||
cssClass "Duck" test
|
||||
classDef test fill:#f71
|
||||
%%classDef default fill:#f93
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<h2>Full Example</h2>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
theme: forest
|
||||
look: handDrawn
|
||||
---
|
||||
classDiagram
|
||||
note for Outside "Note testing"
|
||||
namespace Test {
|
||||
class Outside
|
||||
}
|
||||
namespace BaseShapes {
|
||||
class Triangle
|
||||
class Rectangle {
|
||||
double width
|
||||
double height
|
||||
}
|
||||
}
|
||||
Outside <|--|> Rectangle
|
||||
style Triangle fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
look: handDrawn
|
||||
layout: elk
|
||||
---
|
||||
classDiagram
|
||||
Class01 "1" <|--|> "*" AveryLongClass : Cool
|
||||
<<interface>> Class01
|
||||
Class03 "1" *-- "*" Class04
|
||||
Class05 "1" o-- "many" Class06
|
||||
Class07 "1" .. "*" Class08
|
||||
Class09 "1" --> "*" C2 : Where am i?
|
||||
Class09 "*" --* "*" C3
|
||||
Class09 "1" --|> "1" Class07
|
||||
NewClass ()--() Class04
|
||||
Class09 <|--|> AveryLongClass
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : size()
|
||||
Class01 : int chimp
|
||||
Class01 : int gorilla
|
||||
Class08 "1" <--> "*" C2: Cool label
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
test()
|
||||
}
|
||||
Class10 o--o AveryLongClass
|
||||
Class10 <--> Class07
|
||||
</pre>
|
||||
</div>
|
||||
<div class="test">
|
||||
<pre class="mermaid">
|
||||
classDiagram
|
||||
test ()--() test2
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('Mermaid error: ', err);
|
||||
};
|
||||
mermaid.initialize();
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
<style>
|
||||
.header {
|
||||
text-decoration: underline;
|
||||
text-align: center;
|
||||
}
|
||||
.node-showcase {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.test {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.test > h2 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.test > p {
|
||||
margin-top: -6px;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.diagram-showcase {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.styleClass > * > path {
|
||||
fill: #ff0000 !important;
|
||||
stroke: #ffff00 !important;
|
||||
stroke-width: 4px !important;
|
||||
stroke-dasharray: 2 !important;
|
||||
}
|
||||
</style>
|
||||
</html>
|
@ -20,7 +20,7 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/types.ts:125](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L125)
|
||||
[packages/mermaid/src/rendering-util/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L128)
|
||||
|
||||
---
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/types.ts:124](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L124)
|
||||
[packages/mermaid/src/rendering-util/types.ts:127](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L127)
|
||||
|
||||
---
|
||||
|
||||
@ -40,4 +40,4 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/types.ts:123](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L123)
|
||||
[packages/mermaid/src/rendering-util/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L126)
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/defaultConfig.ts:267](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L267)
|
||||
[packages/mermaid/src/defaultConfig.ts:270](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L270)
|
||||
|
||||
---
|
||||
|
||||
|
@ -427,6 +427,51 @@ And `Link` can be one of:
|
||||
| -- | Solid |
|
||||
| .. | Dashed |
|
||||
|
||||
### Lollipop Interfaces
|
||||
|
||||
Classes can also be given a special relation type that defines a lollipop interface on the class. A lollipop interface is defined using the following syntax:
|
||||
|
||||
- `bar ()-- foo`
|
||||
- `foo --() bar`
|
||||
|
||||
The interface (bar) with the lollipop connects to the class (foo).
|
||||
|
||||
Note: Each interface that is defined is unique and is meant to not be shared between classes / have multiple edges connecting to it.
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
bar ()-- foo
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
bar ()-- foo
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Class01 {
|
||||
int amount
|
||||
draw()
|
||||
}
|
||||
Class01 --() bar
|
||||
Class02 --() bar
|
||||
|
||||
foo ()-- Class01
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Class01 {
|
||||
int amount
|
||||
draw()
|
||||
}
|
||||
Class01 --() bar
|
||||
Class02 --() bar
|
||||
|
||||
foo ()-- Class01
|
||||
```
|
||||
|
||||
## Define Namespace
|
||||
|
||||
A namespace groups classes.
|
||||
@ -776,10 +821,12 @@ Beginner's tip—a full example using interactive links in an HTML page:
|
||||
|
||||
## Styling
|
||||
|
||||
### Styling a node (v10.7.0+)
|
||||
### Styling a node
|
||||
|
||||
It is possible to apply specific styles such as a thicker border or a different background color to an individual node using the `style` keyword.
|
||||
|
||||
Note that notes and namespaces cannot be styled individually but do support themes.
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal
|
||||
@ -799,11 +846,102 @@ classDiagram
|
||||
#### Classes
|
||||
|
||||
More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that
|
||||
should have a different look. This is done by predefining classes in css styles that can be applied from the graph definition using the `cssClass` statement or the `:::` short hand.
|
||||
should have a different look.
|
||||
|
||||
A class definition looks like the example below:
|
||||
|
||||
```
|
||||
classDef className fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
Also, it is possible to define style to multiple classes in one statement:
|
||||
|
||||
```
|
||||
classDef firstClassName,secondClassName font-size:12pt;
|
||||
```
|
||||
|
||||
Attachment of a class to a node is done as per below:
|
||||
|
||||
```
|
||||
cssClass "nodeId1" className;
|
||||
```
|
||||
|
||||
It is also possible to attach a class to a list of nodes in one statement:
|
||||
|
||||
```
|
||||
cssClass "nodeId1,nodeId2" className;
|
||||
```
|
||||
|
||||
A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::someclass
|
||||
classDef someclass fill:#f96
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Animal:::someclass
|
||||
classDef someclass fill:#f96
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::someclass {
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
classDef someclass fill:#f96
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Animal:::someclass {
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
classDef someclass fill:#f96
|
||||
```
|
||||
|
||||
### Default class
|
||||
|
||||
If a class is named default it will be applied to all nodes. Specific styles and classes should be defined afterwards to override the applied default styling.
|
||||
|
||||
```
|
||||
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::pink
|
||||
class Mineral
|
||||
|
||||
classDef default fill:#f96,color:red
|
||||
classDef pink color:#f9f
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Animal:::pink
|
||||
class Mineral
|
||||
|
||||
classDef default fill:#f96,color:red
|
||||
classDef pink color:#f9f
|
||||
```
|
||||
|
||||
### CSS Classes
|
||||
|
||||
It is also possible to predefine classes in CSS styles that can be applied from the graph definition as in the example
|
||||
below:
|
||||
|
||||
**Example style**
|
||||
|
||||
```html
|
||||
<style>
|
||||
.styleClass > rect {
|
||||
.styleClass > * > g {
|
||||
fill: #ff0000;
|
||||
stroke: #ffff00;
|
||||
stroke-width: 4px;
|
||||
@ -811,19 +949,7 @@ should have a different look. This is done by predefining classes in css styles
|
||||
</style>
|
||||
```
|
||||
|
||||
Then attaching that class to a specific node:
|
||||
|
||||
```
|
||||
cssClass "nodeId1" styleClass;
|
||||
```
|
||||
|
||||
It is also possible to attach a class to a list of nodes in one statement:
|
||||
|
||||
```
|
||||
cssClass "nodeId1,nodeId2" styleClass;
|
||||
```
|
||||
|
||||
A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
|
||||
**Example definition**
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
@ -835,136 +961,32 @@ classDiagram
|
||||
class Animal:::styleClass
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::styleClass {
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Animal:::styleClass {
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
```
|
||||
|
||||
?> cssClasses cannot be added using this shorthand method at the same time as a relation statement.
|
||||
|
||||
?> Due to limitations with existing markup for class diagrams, it is not currently possible to define css classes within the diagram itself. **_Coming soon!_**
|
||||
|
||||
### Default Styles
|
||||
|
||||
The main styling of the class diagram is done with a preset number of css classes. During rendering these classes are extracted from the file located at src/themes/class.scss. The classes used here are described below:
|
||||
|
||||
| Class | Description |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| g.classGroup text | Styles for general class text |
|
||||
| classGroup .title | Styles for general class title |
|
||||
| g.classGroup rect | Styles for class diagram rectangle |
|
||||
| g.classGroup line | Styles for class diagram line |
|
||||
| .classLabel .box | Styles for class label box |
|
||||
| .classLabel .label | Styles for class label text |
|
||||
| composition | Styles for composition arrow head and arrow line |
|
||||
| aggregation | Styles for aggregation arrow head and arrow line(dashed or solid) |
|
||||
| dependency | Styles for dependency arrow head and arrow line |
|
||||
|
||||
#### Sample stylesheet
|
||||
|
||||
```scss
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
g.classGroup text {
|
||||
fill: $nodeBorder;
|
||||
stroke: none;
|
||||
font-family: 'trebuchet ms', verdana, arial;
|
||||
font-family: var(--mermaid-font-family);
|
||||
font-size: 10px;
|
||||
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
g.classGroup rect {
|
||||
fill: $nodeBkg;
|
||||
stroke: $nodeBorder;
|
||||
}
|
||||
|
||||
g.classGroup line {
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.classLabel .box {
|
||||
stroke: none;
|
||||
stroke-width: 0;
|
||||
fill: $nodeBkg;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.classLabel .label {
|
||||
fill: $nodeBorder;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.relation {
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
@mixin composition {
|
||||
fill: $nodeBorder;
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
#compositionStart {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#compositionEnd {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
@mixin aggregation {
|
||||
fill: $nodeBkg;
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
#aggregationStart {
|
||||
@include aggregation;
|
||||
}
|
||||
|
||||
#aggregationEnd {
|
||||
@include aggregation;
|
||||
}
|
||||
|
||||
#dependencyStart {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#dependencyEnd {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#extensionStart {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#extensionEnd {
|
||||
@include composition;
|
||||
}
|
||||
```
|
||||
> cssClasses cannot be added using this shorthand method at the same time as a relation statement.
|
||||
|
||||
## Configuration
|
||||
|
||||
`Coming soon!`
|
||||
### Members Box
|
||||
|
||||
It is possible to hide the empty members box of a class node.
|
||||
|
||||
This is done by changing the **hideEmptyMembersBox** value of the class diagram configuration. For more information on how to edit the Mermaid configuration see the [configuration page.](https://mermaid.js.org/config/configuration.html)
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
class:
|
||||
hideEmptyMembersBox: true
|
||||
---
|
||||
classDiagram
|
||||
class Duck
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
class:
|
||||
hideEmptyMembersBox: true
|
||||
---
|
||||
classDiagram
|
||||
class Duck
|
||||
```
|
||||
|
@ -319,6 +319,7 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
|
||||
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
|
||||
| Class Box | Class Box | `classBox` | Class Box | `class-box` |
|
||||
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
|
||||
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
|
||||
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
|
||||
|
@ -149,6 +149,7 @@ export const render = async (
|
||||
const clusterNode = JSON.parse(JSON.stringify(node));
|
||||
clusterNode.x = node.offset.posX + node.width / 2;
|
||||
clusterNode.y = node.offset.posY + node.height / 2;
|
||||
clusterNode.width = Math.max(clusterNode.width, node.labelData.width);
|
||||
await insertCluster(subgraphEl, clusterNode);
|
||||
|
||||
log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels);
|
||||
@ -275,6 +276,8 @@ export const render = async (
|
||||
interpolate: undefined;
|
||||
style: undefined;
|
||||
labelType: any;
|
||||
startLabelRight?: string;
|
||||
endLabelLeft?: string;
|
||||
}) {
|
||||
// Identify Link
|
||||
const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end;
|
||||
@ -328,6 +331,9 @@ export const render = async (
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
edgeData.startLabelRight = edge.startLabelRight;
|
||||
edgeData.endLabelLeft = edge.endLabelLeft;
|
||||
|
||||
switch (edge.stroke) {
|
||||
case 'normal':
|
||||
style = 'fill:none;';
|
||||
|
@ -172,6 +172,7 @@ This Markdown should be kept.
|
||||
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
|
||||
| Class Box | Class Box | \`classBox\` | Class Box | \`class-box\` |
|
||||
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
|
||||
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
|
||||
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
|
||||
|
@ -717,6 +717,7 @@ export interface ClassDiagramConfig extends BaseDiagramConfig {
|
||||
*/
|
||||
diagramPadding?: number;
|
||||
htmlLabels?: boolean;
|
||||
hideEmptyMembersBox?: boolean;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for entity relationship diagrams
|
||||
|
@ -53,6 +53,9 @@ const config: RequiredDeep<MermaidConfig> = {
|
||||
};
|
||||
},
|
||||
},
|
||||
class: {
|
||||
hideEmptyMembersBox: false,
|
||||
},
|
||||
gantt: {
|
||||
...defaultConfigJson.gantt,
|
||||
tickInterval: undefined,
|
||||
|
@ -1,9 +1,8 @@
|
||||
import type { Selection } from 'd3';
|
||||
import { select } from 'd3';
|
||||
import { select, type Selection } from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import common from '../common/common.js';
|
||||
import utils from '../../utils.js';
|
||||
import utils, { getEdgeId } from '../../utils.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
@ -21,13 +20,18 @@ import type {
|
||||
ClassMap,
|
||||
NamespaceMap,
|
||||
NamespaceNode,
|
||||
StyleClass,
|
||||
Interface,
|
||||
} from './classTypes.js';
|
||||
import type { Node, Edge } from '../../rendering-util/types.js';
|
||||
|
||||
const MERMAID_DOM_ID_PREFIX = 'classId-';
|
||||
|
||||
let relations: ClassRelation[] = [];
|
||||
let classes = new Map<string, ClassNode>();
|
||||
const styleClasses = new Map<string, StyleClass>();
|
||||
let notes: ClassNote[] = [];
|
||||
let interfaces: Interface[] = [];
|
||||
let classCounter = 0;
|
||||
let namespaces = new Map<string, NamespaceNode>();
|
||||
let namespaceCounter = 0;
|
||||
@ -58,6 +62,8 @@ export const setClassLabel = function (_id: string, label: string) {
|
||||
|
||||
const { className } = splitClassNameAndType(id);
|
||||
classes.get(className)!.label = label;
|
||||
classes.get(className)!.text =
|
||||
`${label}${classes.get(className)!.type ? `<${classes.get(className)!.type}>` : ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -80,7 +86,9 @@ export const addClass = function (_id: string) {
|
||||
id: name,
|
||||
type: type,
|
||||
label: name,
|
||||
cssClasses: [],
|
||||
text: `${name}${type ? `<${type}>` : ''}`,
|
||||
shape: 'classBox',
|
||||
cssClasses: 'default',
|
||||
methods: [],
|
||||
members: [],
|
||||
annotations: [],
|
||||
@ -91,6 +99,16 @@ export const addClass = function (_id: string) {
|
||||
classCounter++;
|
||||
};
|
||||
|
||||
const addInterface = function (label: string, classId: string) {
|
||||
const classInterface: Interface = {
|
||||
id: `interface${interfaces.length}`,
|
||||
label,
|
||||
classId,
|
||||
};
|
||||
|
||||
interfaces.push(classInterface);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to lookup domId from id in the graph definition.
|
||||
*
|
||||
@ -109,6 +127,7 @@ export const clear = function () {
|
||||
relations = [];
|
||||
classes = new Map();
|
||||
notes = [];
|
||||
interfaces = [];
|
||||
functions = [];
|
||||
functions.push(setupToolTips);
|
||||
namespaces = new Map();
|
||||
@ -133,19 +152,50 @@ export const getNotes = function () {
|
||||
return notes;
|
||||
};
|
||||
|
||||
export const addRelation = function (relation: ClassRelation) {
|
||||
log.debug('Adding relation: ' + JSON.stringify(relation));
|
||||
addClass(relation.id1);
|
||||
addClass(relation.id2);
|
||||
export const addRelation = function (classRelation: ClassRelation) {
|
||||
log.debug('Adding relation: ' + JSON.stringify(classRelation));
|
||||
// Due to relationType cannot just check if it is equal to 'none' or it complains, can fix this later
|
||||
const invalidTypes = [
|
||||
relationType.LOLLIPOP,
|
||||
relationType.AGGREGATION,
|
||||
relationType.COMPOSITION,
|
||||
relationType.DEPENDENCY,
|
||||
relationType.EXTENSION,
|
||||
];
|
||||
|
||||
relation.id1 = splitClassNameAndType(relation.id1).className;
|
||||
relation.id2 = splitClassNameAndType(relation.id2).className;
|
||||
if (
|
||||
classRelation.relation.type1 === relationType.LOLLIPOP &&
|
||||
!invalidTypes.includes(classRelation.relation.type2)
|
||||
) {
|
||||
addClass(classRelation.id2);
|
||||
addInterface(classRelation.id1, classRelation.id2);
|
||||
classRelation.id1 = `interface${interfaces.length - 1}`;
|
||||
} else if (
|
||||
classRelation.relation.type2 === relationType.LOLLIPOP &&
|
||||
!invalidTypes.includes(classRelation.relation.type1)
|
||||
) {
|
||||
addClass(classRelation.id1);
|
||||
addInterface(classRelation.id2, classRelation.id1);
|
||||
classRelation.id2 = `interface${interfaces.length - 1}`;
|
||||
} else {
|
||||
addClass(classRelation.id1);
|
||||
addClass(classRelation.id2);
|
||||
}
|
||||
|
||||
relation.relationTitle1 = common.sanitizeText(relation.relationTitle1.trim(), getConfig());
|
||||
classRelation.id1 = splitClassNameAndType(classRelation.id1).className;
|
||||
classRelation.id2 = splitClassNameAndType(classRelation.id2).className;
|
||||
|
||||
relation.relationTitle2 = common.sanitizeText(relation.relationTitle2.trim(), getConfig());
|
||||
classRelation.relationTitle1 = common.sanitizeText(
|
||||
classRelation.relationTitle1.trim(),
|
||||
getConfig()
|
||||
);
|
||||
|
||||
relations.push(relation);
|
||||
classRelation.relationTitle2 = common.sanitizeText(
|
||||
classRelation.relationTitle2.trim(),
|
||||
getConfig()
|
||||
);
|
||||
|
||||
relations.push(classRelation);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -229,11 +279,37 @@ export const setCssClass = function (ids: string, className: string) {
|
||||
}
|
||||
const classNode = classes.get(id);
|
||||
if (classNode) {
|
||||
classNode.cssClasses.push(className);
|
||||
classNode.cssClasses += ' ' + className;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const defineClass = function (ids: string[], style: string[]) {
|
||||
for (const id of ids) {
|
||||
let styleClass = styleClasses.get(id);
|
||||
if (styleClass === undefined) {
|
||||
styleClass = { id, styles: [], textStyles: [] };
|
||||
styleClasses.set(id, styleClass);
|
||||
}
|
||||
|
||||
if (style) {
|
||||
style.forEach(function (s) {
|
||||
if (/color/.exec(s)) {
|
||||
const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
|
||||
styleClass.textStyles.push(newStyle);
|
||||
}
|
||||
styleClass.styles.push(s);
|
||||
});
|
||||
}
|
||||
|
||||
classes.forEach((value) => {
|
||||
if (value.cssClasses.includes(id)) {
|
||||
value.styles.push(...style.flatMap((s) => s.split(',')));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by parser when a tooltip is found, e.g. a clickable element.
|
||||
*
|
||||
@ -472,6 +548,152 @@ export const setCssStyle = function (id: string, styles: string[]) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the arrow marker for a type index
|
||||
*
|
||||
* @param type - The type to look for
|
||||
* @returns The arrow marker
|
||||
*/
|
||||
function getArrowMarker(type: number) {
|
||||
let marker;
|
||||
switch (type) {
|
||||
case 0:
|
||||
marker = 'aggregation';
|
||||
break;
|
||||
case 1:
|
||||
marker = 'extension';
|
||||
break;
|
||||
case 2:
|
||||
marker = 'composition';
|
||||
break;
|
||||
case 3:
|
||||
marker = 'dependency';
|
||||
break;
|
||||
case 4:
|
||||
marker = 'lollipop';
|
||||
break;
|
||||
default:
|
||||
marker = 'none';
|
||||
}
|
||||
return marker;
|
||||
}
|
||||
|
||||
export const getData = () => {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
const config = getConfig();
|
||||
|
||||
for (const namespaceKey of namespaces.keys()) {
|
||||
const namespace = namespaces.get(namespaceKey);
|
||||
if (namespace) {
|
||||
const node: Node = {
|
||||
id: namespace.id,
|
||||
label: namespace.id,
|
||||
isGroup: true,
|
||||
padding: config.class!.padding ?? 16,
|
||||
// parent node must be one of [rect, roundedWithTitle, noteGroup, divider]
|
||||
shape: 'rect',
|
||||
cssStyles: ['fill: none', 'stroke: black'],
|
||||
look: config.look,
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
for (const classKey of classes.keys()) {
|
||||
const classNode = classes.get(classKey);
|
||||
if (classNode) {
|
||||
const node = classNode as unknown as Node;
|
||||
node.parentId = classNode.parent;
|
||||
node.look = config.look;
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
let cnt = 0;
|
||||
for (const note of notes) {
|
||||
cnt++;
|
||||
const noteNode: Node = {
|
||||
id: note.id,
|
||||
label: note.text,
|
||||
isGroup: false,
|
||||
shape: 'note',
|
||||
padding: config.class!.padding ?? 6,
|
||||
cssStyles: [
|
||||
'text-align: left',
|
||||
'white-space: nowrap',
|
||||
`fill: ${config.themeVariables.noteBkgColor}`,
|
||||
`stroke: ${config.themeVariables.noteBorderColor}`,
|
||||
],
|
||||
look: config.look,
|
||||
};
|
||||
nodes.push(noteNode);
|
||||
|
||||
const noteClassId = classes.get(note.class)?.id ?? '';
|
||||
|
||||
if (noteClassId) {
|
||||
const edge: Edge = {
|
||||
id: `edgeNote${cnt}`,
|
||||
start: note.id,
|
||||
end: noteClassId,
|
||||
type: 'normal',
|
||||
thickness: 'normal',
|
||||
classes: 'relation',
|
||||
arrowTypeStart: 'none',
|
||||
arrowTypeEnd: 'none',
|
||||
arrowheadStyle: '',
|
||||
labelStyle: [''],
|
||||
style: ['fill: none'],
|
||||
pattern: 'dotted',
|
||||
look: config.look,
|
||||
};
|
||||
edges.push(edge);
|
||||
}
|
||||
}
|
||||
|
||||
for (const _interface of interfaces) {
|
||||
const interfaceNode: Node = {
|
||||
id: _interface.id,
|
||||
label: _interface.label,
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
cssStyles: ['opacity: 0;'],
|
||||
look: config.look,
|
||||
};
|
||||
nodes.push(interfaceNode);
|
||||
}
|
||||
|
||||
cnt = 0;
|
||||
for (const classRelation of relations) {
|
||||
cnt++;
|
||||
const edge: Edge = {
|
||||
id: getEdgeId(classRelation.id1, classRelation.id2, {
|
||||
prefix: 'id',
|
||||
counter: cnt,
|
||||
}),
|
||||
start: classRelation.id1,
|
||||
end: classRelation.id2,
|
||||
type: 'normal',
|
||||
label: classRelation.title,
|
||||
labelpos: 'c',
|
||||
thickness: 'normal',
|
||||
classes: 'relation',
|
||||
arrowTypeStart: getArrowMarker(classRelation.relation.type1),
|
||||
arrowTypeEnd: getArrowMarker(classRelation.relation.type2),
|
||||
startLabelRight: classRelation.relationTitle1 === 'none' ? '' : classRelation.relationTitle1,
|
||||
endLabelLeft: classRelation.relationTitle2 === 'none' ? '' : classRelation.relationTitle2,
|
||||
arrowheadStyle: '',
|
||||
labelStyle: ['display: inline-block'],
|
||||
style: classRelation.style || '',
|
||||
pattern: classRelation.relation.lineType == 1 ? 'dashed' : 'solid',
|
||||
look: config.look,
|
||||
};
|
||||
edges.push(edge);
|
||||
}
|
||||
|
||||
return { nodes, edges, other: {}, config, direction: getDirection() };
|
||||
};
|
||||
|
||||
export default {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
@ -497,6 +719,7 @@ export default {
|
||||
relationType,
|
||||
setClickEvent,
|
||||
setCssClass,
|
||||
defineClass,
|
||||
setLink,
|
||||
getTooltip,
|
||||
setTooltip,
|
||||
@ -509,4 +732,5 @@ export default {
|
||||
getNamespace,
|
||||
getNamespaces,
|
||||
setCssStyle,
|
||||
getData,
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ describe('class diagram, ', function () {
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').cssClasses[0]).toBe('exClass');
|
||||
expect(parser.yy.getClass('Class01').cssClasses).toBe('default exClass');
|
||||
});
|
||||
|
||||
it('should be possible to apply a css class to a class directly with struct', function () {
|
||||
@ -28,7 +28,7 @@ describe('class diagram, ', function () {
|
||||
parser.parse(str);
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.cssClasses[0]).toBe('exClass');
|
||||
expect(testClass.cssClasses).toBe('default exClass');
|
||||
});
|
||||
|
||||
it('should be possible to apply a css class to a class with relations', function () {
|
||||
@ -36,7 +36,7 @@ describe('class diagram, ', function () {
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').cssClasses[0]).toBe('exClass');
|
||||
expect(parser.yy.getClass('Class01').cssClasses).toBe('default exClass');
|
||||
});
|
||||
|
||||
it('should be possible to apply a cssClass to a class', function () {
|
||||
@ -44,7 +44,7 @@ describe('class diagram, ', function () {
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').cssClasses[0]).toBe('exClass');
|
||||
expect(parser.yy.getClass('Class01').cssClasses).toBe('default exClass');
|
||||
});
|
||||
|
||||
it('should be possible to apply a cssClass to a comma separated list of classes', function () {
|
||||
@ -53,8 +53,8 @@ describe('class diagram, ', function () {
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').cssClasses[0]).toBe('exClass');
|
||||
expect(parser.yy.getClass('Class02').cssClasses[0]).toBe('exClass');
|
||||
expect(parser.yy.getClass('Class01').cssClasses).toBe('default exClass');
|
||||
expect(parser.yy.getClass('Class02').cssClasses).toBe('default exClass');
|
||||
});
|
||||
it('should be possible to apply a style to an individual node', function () {
|
||||
const str =
|
||||
@ -69,5 +69,47 @@ describe('class diagram, ', function () {
|
||||
expect(styleElements[1]).toBe('stroke:#333');
|
||||
expect(styleElements[2]).toBe('stroke-width:4px');
|
||||
});
|
||||
it('should be possible to define and assign a class inside the diagram', function () {
|
||||
const str =
|
||||
'classDiagram\n' + 'class Class01\n cssClass "Class01" pink\n classDef pink fill:#f9f';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').cssClasses).toBe('default pink');
|
||||
});
|
||||
it('should be possible to define and assign a class using shorthand inside the diagram', function () {
|
||||
const str = 'classDiagram\n' + 'class Class01:::pink\n classDef pink fill:#f9f';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').cssClasses).toBe('default pink');
|
||||
});
|
||||
it('should properly assign styles from a class defined inside the diagram', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class01:::pink\n classDef pink fill:#f9f,stroke:#333,stroke-width:6px';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').styles).toStrictEqual([
|
||||
'fill:#f9f',
|
||||
'stroke:#333',
|
||||
'stroke-width:6px',
|
||||
]);
|
||||
});
|
||||
it('should properly assign multiple classes and styles from classes defined inside the diagram', function () {
|
||||
const str =
|
||||
'classDiagram\n' +
|
||||
'class Class01:::pink\n cssClass "Class01" bold\n classDef pink fill:#f9f\n classDef bold stroke:#333,stroke-width:6px';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
expect(parser.yy.getClass('Class01').styles).toStrictEqual([
|
||||
'fill:#f9f',
|
||||
'stroke:#333',
|
||||
'stroke-width:6px',
|
||||
]);
|
||||
expect(parser.yy.getClass('Class01').cssClasses).toBe('default pink bold');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer-v2.js';
|
||||
import renderer from './classRenderer-v3-unified.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
|
@ -246,7 +246,7 @@ describe('given a basic class diagram, ', function () {
|
||||
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.cssClasses[0]).toBe('styleClass');
|
||||
expect(c1.cssClasses).toBe('default styleClass');
|
||||
});
|
||||
|
||||
it('should parse a class with text label and css class', () => {
|
||||
@ -261,7 +261,7 @@ describe('given a basic class diagram, ', function () {
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
|
||||
expect(c1.cssClasses[0]).toBe('styleClass');
|
||||
expect(c1.cssClasses).toBe('default styleClass');
|
||||
});
|
||||
|
||||
it('should parse two classes with text labels and css classes', () => {
|
||||
@ -276,11 +276,11 @@ describe('given a basic class diagram, ', function () {
|
||||
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.cssClasses[0]).toBe('styleClass');
|
||||
expect(c1.cssClasses).toBe('default styleClass');
|
||||
|
||||
const c2 = classDb.getClass('C2');
|
||||
expect(c2.label).toBe('Long long long long long long long long long long label');
|
||||
expect(c2.cssClasses[0]).toBe('styleClass');
|
||||
expect(c2.cssClasses).toBe('default styleClass');
|
||||
});
|
||||
|
||||
it('should parse two classes with text labels and css class shorthands', () => {
|
||||
@ -293,11 +293,11 @@ describe('given a basic class diagram, ', function () {
|
||||
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.cssClasses[0]).toBe('styleClass1');
|
||||
expect(c1.cssClasses).toBe('default styleClass1');
|
||||
|
||||
const c2 = classDb.getClass('C2');
|
||||
expect(c2.label).toBe('Class 2 !@#$%^&*() label');
|
||||
expect(c2.cssClasses[0]).toBe('styleClass2');
|
||||
expect(c2.cssClasses).toBe('default styleClass2');
|
||||
});
|
||||
|
||||
it('should parse multiple classes with same text labels', () => {
|
||||
@ -494,10 +494,32 @@ class C13["With Città foreign language"]
|
||||
],
|
||||
methods: [],
|
||||
annotations: [],
|
||||
cssClasses: [],
|
||||
cssClasses: 'default',
|
||||
});
|
||||
|
||||
expect(classDb.getClasses().size).toBe(3);
|
||||
expect(classDb.getClasses().get('Student')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"annotations": [],
|
||||
"cssClasses": "default",
|
||||
"domId": "classId-Student-141",
|
||||
"id": "Student",
|
||||
"label": "Student",
|
||||
"members": [
|
||||
ClassMember {
|
||||
"classifier": "",
|
||||
"id": "idCard : IdCard",
|
||||
"memberType": "attribute",
|
||||
"text": "\\-idCard : IdCard",
|
||||
"visibility": "-",
|
||||
},
|
||||
],
|
||||
"methods": [],
|
||||
"shape": "classBox",
|
||||
"styles": [],
|
||||
"text": "Student",
|
||||
"type": "",
|
||||
}
|
||||
`);
|
||||
expect(classDb.getRelations().length).toBe(2);
|
||||
expect(classDb.getRelations()).toMatchInlineSnapshot(`
|
||||
[
|
||||
@ -738,7 +760,7 @@ foo()
|
||||
|
||||
const actual = parser.yy.getClass('Class1');
|
||||
expect(actual.link).toBe('google.com');
|
||||
expect(actual.cssClasses[0]).toBe('clickable');
|
||||
expect(actual.cssClasses).toBe('default clickable');
|
||||
});
|
||||
|
||||
it('should handle href link with tooltip', function () {
|
||||
@ -754,7 +776,7 @@ foo()
|
||||
const actual = parser.yy.getClass('Class1');
|
||||
expect(actual.link).toBe('google.com');
|
||||
expect(actual.tooltip).toBe('A Tooltip');
|
||||
expect(actual.cssClasses[0]).toBe('clickable');
|
||||
expect(actual.cssClasses).toBe('default clickable');
|
||||
});
|
||||
|
||||
it('should handle href link with tooltip and target', function () {
|
||||
@ -773,7 +795,7 @@ foo()
|
||||
const actual = parser.yy.getClass('Class1');
|
||||
expect(actual.link).toBe('google.com');
|
||||
expect(actual.tooltip).toBe('A tooltip');
|
||||
expect(actual.cssClasses[0]).toBe('clickable');
|
||||
expect(actual.cssClasses).toBe('default clickable');
|
||||
});
|
||||
|
||||
it('should handle function call', function () {
|
||||
@ -1468,8 +1490,7 @@ describe('given a class diagram with relationships, ', function () {
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
expect(testClass.cssClasses).toBe('default clickable');
|
||||
});
|
||||
|
||||
it('should associate click and href link and css appropriately', function () {
|
||||
@ -1482,8 +1503,7 @@ describe('given a class diagram with relationships, ', function () {
|
||||
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
expect(testClass.cssClasses).toBe('default clickable');
|
||||
});
|
||||
|
||||
it('should associate link with tooltip', function () {
|
||||
@ -1497,8 +1517,7 @@ describe('given a class diagram with relationships, ', function () {
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.tooltip).toBe('A tooltip');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
expect(testClass.cssClasses).toBe('default clickable');
|
||||
});
|
||||
|
||||
it('should associate click and href link with tooltip', function () {
|
||||
@ -1512,8 +1531,7 @@ describe('given a class diagram with relationships, ', function () {
|
||||
const testClass = parser.yy.getClass('Class1');
|
||||
expect(testClass.link).toBe('google.com');
|
||||
expect(testClass.tooltip).toBe('A tooltip');
|
||||
expect(testClass.cssClasses.length).toBe(1);
|
||||
expect(testClass.cssClasses[0]).toBe('clickable');
|
||||
expect(testClass.cssClasses).toBe('default clickable');
|
||||
});
|
||||
|
||||
it('should associate click and href link with tooltip and target appropriately', function () {
|
||||
@ -1770,8 +1788,7 @@ C1 --> C2
|
||||
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.cssClasses.length).toBe(1);
|
||||
expect(c1.cssClasses[0]).toBe('styleClass');
|
||||
expect(c1.cssClasses).toBe('default styleClass');
|
||||
const member = c1.members[0];
|
||||
expect(member.getDisplayDetails().displayText).toBe('+member1');
|
||||
});
|
||||
@ -1787,8 +1804,7 @@ cssClass "C1" styleClass
|
||||
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.cssClasses.length).toBe(1);
|
||||
expect(c1.cssClasses[0]).toBe('styleClass');
|
||||
expect(c1.cssClasses).toBe('default styleClass');
|
||||
const member = c1.members[0];
|
||||
expect(member.getDisplayDetails().displayText).toBe('+member1');
|
||||
});
|
||||
@ -1805,13 +1821,11 @@ cssClass "C1,C2" styleClass
|
||||
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.cssClasses.length).toBe(1);
|
||||
expect(c1.cssClasses[0]).toBe('styleClass');
|
||||
expect(c1.cssClasses).toBe('default styleClass');
|
||||
|
||||
const c2 = classDb.getClass('C2');
|
||||
expect(c2.label).toBe('Long long long long long long long long long long label');
|
||||
expect(c2.cssClasses.length).toBe(1);
|
||||
expect(c2.cssClasses[0]).toBe('styleClass');
|
||||
expect(c2.cssClasses).toBe('default styleClass');
|
||||
});
|
||||
|
||||
it('should parse two classes with text labels and css class shorthands', () => {
|
||||
@ -1825,13 +1839,11 @@ C1 --> C2
|
||||
|
||||
const c1 = classDb.getClass('C1');
|
||||
expect(c1.label).toBe('Class 1 with text label');
|
||||
expect(c1.cssClasses.length).toBe(1);
|
||||
expect(c1.cssClasses[0]).toBe('styleClass1');
|
||||
expect(c1.cssClasses).toBe('default styleClass1');
|
||||
|
||||
const c2 = classDb.getClass('C2');
|
||||
expect(c2.label).toBe('Class 2 !@#$%^&*() label');
|
||||
expect(c2.cssClasses.length).toBe(1);
|
||||
expect(c2.cssClasses[0]).toBe('styleClass2');
|
||||
expect(c2.cssClasses).toBe('default styleClass2');
|
||||
});
|
||||
|
||||
it('should parse multiple classes with same text labels', () => {
|
||||
|
@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer.js';
|
||||
import renderer from './classRenderer-v3-unified.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
|
@ -0,0 +1,79 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
import utils from '../../utils.js';
|
||||
|
||||
/**
|
||||
* Get the direction from the statement items.
|
||||
* Look through all of the documents (docs) in the parsedItems
|
||||
* Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction.
|
||||
* @param parsedItem - the parsed statement item to look through
|
||||
* @param defaultDir - the direction to use if none is found
|
||||
* @returns The direction to use
|
||||
*/
|
||||
export const getDir = (parsedItem: any, defaultDir = 'TB') => {
|
||||
if (!parsedItem.doc) {
|
||||
return defaultDir;
|
||||
}
|
||||
|
||||
let dir = defaultDir;
|
||||
|
||||
for (const parsedItemDoc of parsedItem.doc) {
|
||||
if (parsedItemDoc.stmt === 'dir') {
|
||||
dir = parsedItemDoc.value;
|
||||
}
|
||||
}
|
||||
|
||||
return dir;
|
||||
};
|
||||
|
||||
export const getClasses = function (
|
||||
text: string,
|
||||
diagramObj: any
|
||||
): Map<string, DiagramStyleClassDef> {
|
||||
return diagramObj.db.getClasses();
|
||||
};
|
||||
|
||||
export const draw = async function (text: string, id: string, _version: string, diag: any) {
|
||||
log.info('REF0:');
|
||||
log.info('Drawing class diagram (v3)', id);
|
||||
const { securityLevel, state: conf, layout } = getConfig();
|
||||
// Extracting the data from the parsed structure into a more usable form
|
||||
// Not related to the refactoring, but this is the first step in the rendering process
|
||||
// diag.db.extract(diag.db.getRootDocV2());
|
||||
|
||||
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||
// into the Layout data format
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
|
||||
data4Layout.nodeSpacing = conf?.nodeSpacing || 50;
|
||||
data4Layout.rankSpacing = conf?.rankSpacing || 50;
|
||||
data4Layout.markers = ['aggregation', 'extension', 'composition', 'dependency', 'lollipop'];
|
||||
data4Layout.diagramId = id;
|
||||
await render(data4Layout, svg);
|
||||
const padding = 8;
|
||||
utils.insertTitle(
|
||||
svg,
|
||||
'classDiagramTitleText',
|
||||
conf?.titleTopMargin ?? 25,
|
||||
diag.db.getDiagramTitle()
|
||||
);
|
||||
|
||||
setupViewPortForSVG(svg, padding, 'classDiagram', conf?.useMaxWidth ?? true);
|
||||
};
|
||||
|
||||
export default {
|
||||
getClasses,
|
||||
draw,
|
||||
getDir,
|
||||
};
|
@ -5,7 +5,9 @@ export interface ClassNode {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
cssClasses: string[];
|
||||
shape: string;
|
||||
text: string;
|
||||
cssClasses: string;
|
||||
methods: ClassMember[];
|
||||
members: ClassMember[];
|
||||
annotations: string[];
|
||||
@ -16,6 +18,7 @@ export interface ClassNode {
|
||||
linkTarget?: string;
|
||||
haveCallback?: boolean;
|
||||
tooltip?: string;
|
||||
look?: string;
|
||||
}
|
||||
|
||||
export type Visibility = '#' | '+' | '~' | '-' | '';
|
||||
@ -30,6 +33,7 @@ export class ClassMember {
|
||||
cssStyle!: string;
|
||||
memberType!: 'method' | 'attribute';
|
||||
visibility!: Visibility;
|
||||
text: string;
|
||||
/**
|
||||
* denote if static or to determine which css class to apply to the node
|
||||
* @defaultValue ''
|
||||
@ -50,6 +54,7 @@ export class ClassMember {
|
||||
this.memberType = memberType;
|
||||
this.visibility = '';
|
||||
this.classifier = '';
|
||||
this.text = '';
|
||||
const sanitizedInput = sanitizeText(input, getConfig());
|
||||
this.parseMember(sanitizedInput);
|
||||
}
|
||||
@ -85,7 +90,7 @@ export class ClassMember {
|
||||
this.visibility = detectedVisibility as Visibility;
|
||||
}
|
||||
|
||||
this.id = match[2].trim();
|
||||
this.id = match[2];
|
||||
this.parameters = match[3] ? match[3].trim() : '';
|
||||
potentialClassifier = match[4] ? match[4].trim() : '';
|
||||
this.returnType = match[5] ? match[5].trim() : '';
|
||||
@ -118,6 +123,14 @@ export class ClassMember {
|
||||
}
|
||||
|
||||
this.classifier = potentialClassifier;
|
||||
// Preserve one space only
|
||||
this.id = this.id.startsWith(' ') ? ' ' + this.id.trim() : this.id.trim();
|
||||
|
||||
const combinedText = `${this.visibility ? '\\' + this.visibility : ''}${parseGenericTypes(this.id)}${this.memberType === 'method' ? `(${parseGenericTypes(this.parameters)})${this.returnType ? ' : ' + parseGenericTypes(this.returnType) : ''}` : ''}`;
|
||||
this.text = combinedText.replaceAll('<', '<').replaceAll('>', '>');
|
||||
if (this.text.startsWith('\\<')) {
|
||||
this.text = this.text.replace('\\<', '~');
|
||||
}
|
||||
}
|
||||
|
||||
parseClassifier() {
|
||||
@ -154,6 +167,12 @@ export interface ClassRelation {
|
||||
};
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
id: string;
|
||||
label: string;
|
||||
classId: string;
|
||||
}
|
||||
|
||||
export interface NamespaceNode {
|
||||
id: string;
|
||||
domId: string;
|
||||
@ -161,5 +180,11 @@ export interface NamespaceNode {
|
||||
children: NamespaceMap;
|
||||
}
|
||||
|
||||
export interface StyleClass {
|
||||
id: string;
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
||||
|
||||
export type ClassMap = Map<string, ClassNode>;
|
||||
export type NamespaceMap = Map<string, NamespaceNode>;
|
||||
|
@ -61,6 +61,7 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
|
||||
<string>[^"]* return "STR";
|
||||
<*>["] this.begin("string");
|
||||
"style" return 'STYLE';
|
||||
"classDef" return 'CLASSDEF';
|
||||
|
||||
<INITIAL,namespace>"namespace" { this.begin('namespace'); return 'NAMESPACE'; }
|
||||
<namespace>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
|
||||
@ -265,6 +266,7 @@ statement
|
||||
| styleStatement
|
||||
| cssClassStatement
|
||||
| noteStatement
|
||||
| classDefStatement
|
||||
| direction
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
@ -326,6 +328,15 @@ noteStatement
|
||||
| NOTE noteText { yy.addNote($2); }
|
||||
;
|
||||
|
||||
classDefStatement
|
||||
: CLASSDEF classList stylesOpt {$$ = $CLASSDEF;yy.defineClass($classList,$stylesOpt);}
|
||||
;
|
||||
|
||||
classList
|
||||
: ALPHA { $$ = [$ALPHA]; }
|
||||
| classList COMMA ALPHA = { $$ = $classList.concat([$ALPHA]); }
|
||||
;
|
||||
|
||||
direction
|
||||
: direction_tb
|
||||
{ yy.setDirection('TB');}
|
||||
|
223
packages/mermaid/src/diagrams/class/shapeUtil.ts
Normal file
223
packages/mermaid/src/diagrams/class/shapeUtil.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import { select } from 'd3';
|
||||
import { getConfig } from '../../config.js';
|
||||
import { getNodeClasses } from '../../rendering-util/rendering-elements/shapes/util.js';
|
||||
import { calculateTextWidth, decodeEntities } from '../../utils.js';
|
||||
import type { ClassMember, ClassNode } from './classTypes.js';
|
||||
import { sanitizeText } from '../../diagram-api/diagramAPI.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import { evaluate, hasKatex } from '../common/common.js';
|
||||
import type { Node } from '../../rendering-util/types.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import type { D3Selection } from '../../types.js';
|
||||
|
||||
// Creates the shapeSvg and inserts text
|
||||
export async function textHelper<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: any,
|
||||
config: MermaidConfig,
|
||||
useHtmlLabels: boolean,
|
||||
GAP = config.class!.padding ?? 12
|
||||
) {
|
||||
const TEXT_PADDING = !useHtmlLabels ? 3 : 0;
|
||||
const shapeSvg = parent
|
||||
// @ts-ignore: Ignore error for using .insert on SVGAElement
|
||||
.insert('g')
|
||||
.attr('class', getNodeClasses(node))
|
||||
.attr('id', node.domId || node.id);
|
||||
|
||||
let annotationGroup = null;
|
||||
let labelGroup = null;
|
||||
let membersGroup = null;
|
||||
let methodsGroup = null;
|
||||
|
||||
let annotationGroupHeight = 0;
|
||||
let labelGroupHeight = 0;
|
||||
let membersGroupHeight = 0;
|
||||
|
||||
annotationGroup = shapeSvg.insert('g').attr('class', 'annotation-group text');
|
||||
if (node.annotations.length > 0) {
|
||||
const annotation = node.annotations[0];
|
||||
await addText(annotationGroup, { text: `«${annotation}»` } as unknown as ClassMember, 0);
|
||||
|
||||
const annotationGroupBBox = annotationGroup.node()!.getBBox();
|
||||
annotationGroupHeight = annotationGroupBBox.height;
|
||||
}
|
||||
|
||||
labelGroup = shapeSvg.insert('g').attr('class', 'label-group text');
|
||||
await addText(labelGroup, node, 0, ['font-weight: bolder']);
|
||||
const labelGroupBBox = labelGroup.node()!.getBBox();
|
||||
labelGroupHeight = labelGroupBBox.height;
|
||||
|
||||
membersGroup = shapeSvg.insert('g').attr('class', 'members-group text');
|
||||
let yOffset = 0;
|
||||
for (const member of node.members) {
|
||||
const height = await addText(membersGroup, member, yOffset, [member.parseClassifier()]);
|
||||
yOffset += height + TEXT_PADDING;
|
||||
}
|
||||
membersGroupHeight = membersGroup.node()!.getBBox().height;
|
||||
if (membersGroupHeight <= 0) {
|
||||
membersGroupHeight = GAP / 2;
|
||||
}
|
||||
|
||||
methodsGroup = shapeSvg.insert('g').attr('class', 'methods-group text');
|
||||
let methodsYOffset = 0;
|
||||
for (const method of node.methods) {
|
||||
const height = await addText(methodsGroup, method, methodsYOffset, [method.parseClassifier()]);
|
||||
methodsYOffset += height + TEXT_PADDING;
|
||||
}
|
||||
|
||||
let bbox = shapeSvg.node()!.getBBox();
|
||||
|
||||
// Center annotation
|
||||
if (annotationGroup !== null) {
|
||||
const annotationGroupBBox = annotationGroup.node()!.getBBox();
|
||||
annotationGroup.attr('transform', `translate(${-annotationGroupBBox.width / 2})`);
|
||||
}
|
||||
|
||||
// Adjust label
|
||||
labelGroup.attr('transform', `translate(${-labelGroupBBox.width / 2}, ${annotationGroupHeight})`);
|
||||
|
||||
bbox = shapeSvg.node()!.getBBox();
|
||||
|
||||
membersGroup.attr(
|
||||
'transform',
|
||||
`translate(${0}, ${annotationGroupHeight + labelGroupHeight + GAP * 2})`
|
||||
);
|
||||
bbox = shapeSvg.node()!.getBBox();
|
||||
methodsGroup.attr(
|
||||
'transform',
|
||||
`translate(${0}, ${annotationGroupHeight + labelGroupHeight + (membersGroupHeight ? membersGroupHeight + GAP * 4 : GAP * 2)})`
|
||||
);
|
||||
|
||||
bbox = shapeSvg.node()!.getBBox();
|
||||
|
||||
return { shapeSvg, bbox };
|
||||
}
|
||||
|
||||
// Modified version of labelHelper() to help create and place text for classes
|
||||
async function addText<T extends SVGGraphicsElement>(
|
||||
parentGroup: D3Selection<T>,
|
||||
node: Node | ClassNode | ClassMember,
|
||||
yOffset: number,
|
||||
styles: string[] = []
|
||||
) {
|
||||
const textEl = parentGroup.insert('g').attr('class', 'label').attr('style', styles.join('; '));
|
||||
const config = getConfig();
|
||||
let useHtmlLabels =
|
||||
'useHtmlLabels' in node ? node.useHtmlLabels : (evaluate(config.htmlLabels) ?? true);
|
||||
|
||||
let textContent = '';
|
||||
// Support regular node type (.label) and classNodes (.text)
|
||||
if ('text' in node) {
|
||||
textContent = node.text;
|
||||
} else {
|
||||
textContent = node.label!;
|
||||
}
|
||||
|
||||
// createText() will cause unwanted behavior because of classDiagram syntax so workarounds are needed
|
||||
|
||||
if (!useHtmlLabels && textContent.startsWith('\\')) {
|
||||
textContent = textContent.substring(1);
|
||||
}
|
||||
|
||||
if (hasKatex(textContent)) {
|
||||
useHtmlLabels = true;
|
||||
}
|
||||
|
||||
const text = await createText(
|
||||
textEl,
|
||||
sanitizeText(decodeEntities(textContent)),
|
||||
{
|
||||
width: calculateTextWidth(textContent, config) + 50, // Add room for error when splitting text into multiple lines
|
||||
classes: 'markdown-node-label',
|
||||
useHtmlLabels,
|
||||
},
|
||||
config
|
||||
);
|
||||
let bbox;
|
||||
let numberOfLines = 1;
|
||||
|
||||
if (!useHtmlLabels) {
|
||||
// Undo font-weight normal
|
||||
if (styles.includes('font-weight: bolder')) {
|
||||
select(text).selectAll('tspan').attr('font-weight', '');
|
||||
}
|
||||
|
||||
numberOfLines = text.children.length;
|
||||
|
||||
const textChild = text.children[0];
|
||||
if (text.textContent === '' || text.textContent.includes('>')) {
|
||||
textChild.textContent =
|
||||
textContent[0] +
|
||||
textContent.substring(1).replaceAll('>', '>').replaceAll('<', '<').trim();
|
||||
|
||||
// Text was improperly removed due to spaces (preserve one space if present)
|
||||
const preserveSpace = textContent[1] === ' ';
|
||||
if (preserveSpace) {
|
||||
textChild.textContent = textChild.textContent[0] + ' ' + textChild.textContent.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
// To support empty boxes
|
||||
if (textChild.textContent === 'undefined') {
|
||||
textChild.textContent = '';
|
||||
}
|
||||
|
||||
// Get the bounding box after the text update
|
||||
bbox = text.getBBox();
|
||||
} else {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
|
||||
numberOfLines = div.innerHTML.split('<br>').length;
|
||||
// Katex math support
|
||||
if (div.innerHTML.includes('</math>')) {
|
||||
numberOfLines += div.innerHTML.split('<mrow>').length - 1;
|
||||
}
|
||||
|
||||
// Support images
|
||||
const images = div.getElementsByTagName('img');
|
||||
if (images) {
|
||||
const noImgText = textContent.replace(/<img[^>]*>/g, '').trim() === '';
|
||||
await Promise.all(
|
||||
[...images].map(
|
||||
(img) =>
|
||||
new Promise((res) => {
|
||||
function setupImage() {
|
||||
img.style.display = 'flex';
|
||||
img.style.flexDirection = 'column';
|
||||
|
||||
if (noImgText) {
|
||||
// default size if no text
|
||||
const bodyFontSize =
|
||||
config.fontSize?.toString() ?? window.getComputedStyle(document.body).fontSize;
|
||||
const enlargingFactor = 5;
|
||||
const width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px';
|
||||
img.style.minWidth = width;
|
||||
img.style.maxWidth = width;
|
||||
} else {
|
||||
img.style.width = '100%';
|
||||
}
|
||||
res(img);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (img.complete) {
|
||||
setupImage();
|
||||
}
|
||||
});
|
||||
img.addEventListener('error', setupImage);
|
||||
img.addEventListener('load', setupImage);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
bbox = div.getBoundingClientRect();
|
||||
dv.attr('width', bbox.width);
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
// Center text and offset by yOffset
|
||||
textEl.attr('transform', 'translate(0,' + (-bbox.height / (2 * numberOfLines) + yOffset) + ')');
|
||||
return bbox.height;
|
||||
}
|
@ -20,6 +20,10 @@ const getStyles = (options) =>
|
||||
.label text {
|
||||
fill: ${options.classText};
|
||||
}
|
||||
|
||||
.labelBkg {
|
||||
background: ${options.mainBkg};
|
||||
}
|
||||
.edgeLabel .label span {
|
||||
background: ${options.mainBkg};
|
||||
}
|
||||
|
@ -277,6 +277,34 @@ And `Link` can be one of:
|
||||
| -- | Solid |
|
||||
| .. | Dashed |
|
||||
|
||||
### Lollipop Interfaces
|
||||
|
||||
Classes can also be given a special relation type that defines a lollipop interface on the class. A lollipop interface is defined using the following syntax:
|
||||
|
||||
- `bar ()-- foo`
|
||||
- `foo --() bar`
|
||||
|
||||
The interface (bar) with the lollipop connects to the class (foo).
|
||||
|
||||
Note: Each interface that is defined is unique and is meant to not be shared between classes / have multiple edges connecting to it.
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
bar ()-- foo
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Class01 {
|
||||
int amount
|
||||
draw()
|
||||
}
|
||||
Class01 --() bar
|
||||
Class02 --() bar
|
||||
|
||||
foo ()-- Class01
|
||||
```
|
||||
|
||||
## Define Namespace
|
||||
|
||||
A namespace groups classes.
|
||||
@ -518,10 +546,12 @@ Beginner's tip—a full example using interactive links in an HTML page:
|
||||
|
||||
## Styling
|
||||
|
||||
### Styling a node (v10.7.0+)
|
||||
### Styling a node
|
||||
|
||||
It is possible to apply specific styles such as a thicker border or a different background color to an individual node using the `style` keyword.
|
||||
|
||||
Note that notes and namespaces cannot be styled individually but do support themes.
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal
|
||||
@ -533,11 +563,78 @@ classDiagram
|
||||
#### Classes
|
||||
|
||||
More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that
|
||||
should have a different look. This is done by predefining classes in css styles that can be applied from the graph definition using the `cssClass` statement or the `:::` short hand.
|
||||
should have a different look.
|
||||
|
||||
A class definition looks like the example below:
|
||||
|
||||
```
|
||||
classDef className fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
Also, it is possible to define style to multiple classes in one statement:
|
||||
|
||||
```
|
||||
classDef firstClassName,secondClassName font-size:12pt;
|
||||
```
|
||||
|
||||
Attachment of a class to a node is done as per below:
|
||||
|
||||
```
|
||||
cssClass "nodeId1" className;
|
||||
```
|
||||
|
||||
It is also possible to attach a class to a list of nodes in one statement:
|
||||
|
||||
```
|
||||
cssClass "nodeId1,nodeId2" className;
|
||||
```
|
||||
|
||||
A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::someclass
|
||||
classDef someclass fill:#f96
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::someclass {
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
classDef someclass fill:#f96
|
||||
```
|
||||
|
||||
### Default class
|
||||
|
||||
If a class is named default it will be applied to all nodes. Specific styles and classes should be defined afterwards to override the applied default styling.
|
||||
|
||||
```
|
||||
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::pink
|
||||
class Mineral
|
||||
|
||||
classDef default fill:#f96,color:red
|
||||
classDef pink color:#f9f
|
||||
```
|
||||
|
||||
### CSS Classes
|
||||
|
||||
It is also possible to predefine classes in CSS styles that can be applied from the graph definition as in the example
|
||||
below:
|
||||
|
||||
**Example style**
|
||||
|
||||
```html
|
||||
<style>
|
||||
.styleClass > rect {
|
||||
.styleClass > * > g {
|
||||
fill: #ff0000;
|
||||
stroke: #ffff00;
|
||||
stroke-width: 4px;
|
||||
@ -545,147 +642,29 @@ should have a different look. This is done by predefining classes in css styles
|
||||
</style>
|
||||
```
|
||||
|
||||
Then attaching that class to a specific node:
|
||||
|
||||
```
|
||||
cssClass "nodeId1" styleClass;
|
||||
```
|
||||
|
||||
It is also possible to attach a class to a list of nodes in one statement:
|
||||
|
||||
```
|
||||
cssClass "nodeId1,nodeId2" styleClass;
|
||||
```
|
||||
|
||||
A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
|
||||
**Example definition**
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::styleClass
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
class Animal:::styleClass {
|
||||
-int sizeInFeet
|
||||
-canEat()
|
||||
}
|
||||
```
|
||||
|
||||
?> cssClasses cannot be added using this shorthand method at the same time as a relation statement.
|
||||
|
||||
?> Due to limitations with existing markup for class diagrams, it is not currently possible to define css classes within the diagram itself. **_Coming soon!_**
|
||||
|
||||
### Default Styles
|
||||
|
||||
The main styling of the class diagram is done with a preset number of css classes. During rendering these classes are extracted from the file located at src/themes/class.scss. The classes used here are described below:
|
||||
|
||||
| Class | Description |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| g.classGroup text | Styles for general class text |
|
||||
| classGroup .title | Styles for general class title |
|
||||
| g.classGroup rect | Styles for class diagram rectangle |
|
||||
| g.classGroup line | Styles for class diagram line |
|
||||
| .classLabel .box | Styles for class label box |
|
||||
| .classLabel .label | Styles for class label text |
|
||||
| composition | Styles for composition arrow head and arrow line |
|
||||
| aggregation | Styles for aggregation arrow head and arrow line(dashed or solid) |
|
||||
| dependency | Styles for dependency arrow head and arrow line |
|
||||
|
||||
#### Sample stylesheet
|
||||
|
||||
```scss
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
g.classGroup text {
|
||||
fill: $nodeBorder;
|
||||
stroke: none;
|
||||
font-family: 'trebuchet ms', verdana, arial;
|
||||
font-family: var(--mermaid-font-family);
|
||||
font-size: 10px;
|
||||
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
g.classGroup rect {
|
||||
fill: $nodeBkg;
|
||||
stroke: $nodeBorder;
|
||||
}
|
||||
|
||||
g.classGroup line {
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.classLabel .box {
|
||||
stroke: none;
|
||||
stroke-width: 0;
|
||||
fill: $nodeBkg;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.classLabel .label {
|
||||
fill: $nodeBorder;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.relation {
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
@mixin composition {
|
||||
fill: $nodeBorder;
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
#compositionStart {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#compositionEnd {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
@mixin aggregation {
|
||||
fill: $nodeBkg;
|
||||
stroke: $nodeBorder;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
#aggregationStart {
|
||||
@include aggregation;
|
||||
}
|
||||
|
||||
#aggregationEnd {
|
||||
@include aggregation;
|
||||
}
|
||||
|
||||
#dependencyStart {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#dependencyEnd {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#extensionStart {
|
||||
@include composition;
|
||||
}
|
||||
|
||||
#extensionEnd {
|
||||
@include composition;
|
||||
}
|
||||
```
|
||||
> cssClasses cannot be added using this shorthand method at the same time as a relation statement.
|
||||
|
||||
## Configuration
|
||||
|
||||
`Coming soon!`
|
||||
### Members Box
|
||||
|
||||
It is possible to hide the empty members box of a class node.
|
||||
|
||||
This is done by changing the **hideEmptyMembersBox** value of the class diagram configuration. For more information on how to edit the Mermaid configuration see the [configuration page.](https://mermaid.js.org/config/configuration.html)
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
class:
|
||||
hideEmptyMembersBox: true
|
||||
---
|
||||
classDiagram
|
||||
class Duck
|
||||
```
|
||||
|
@ -779,7 +779,7 @@ graph TD;A--x|text including URL space|B;`)
|
||||
// We have to have both the specific textDiagramType and the expected type name because the expected type may be slightly different than was is put in the diagram text (ex: in -v2 diagrams)
|
||||
const diagramTypesAndExpectations = [
|
||||
{ textDiagramType: 'C4Context', expectedType: 'c4' },
|
||||
{ textDiagramType: 'classDiagram', expectedType: 'classDiagram' },
|
||||
{ textDiagramType: 'classDiagram', expectedType: 'class' },
|
||||
{ textDiagramType: 'classDiagram-v2', expectedType: 'classDiagram' },
|
||||
{ textDiagramType: 'erDiagram', expectedType: 'er' },
|
||||
{ textDiagramType: 'graph', expectedType: 'flowchart-v2' },
|
||||
|
@ -85,6 +85,8 @@ export function markdownToHTML(markdown: string, { markdownAutoWrap }: MermaidCo
|
||||
return '';
|
||||
} else if (node.type === 'html') {
|
||||
return `${node.text}`;
|
||||
} else if (node.type === 'escape') {
|
||||
return node.text;
|
||||
}
|
||||
return `Unsupported markdown: ${node.type}`;
|
||||
}
|
||||
|
@ -463,15 +463,6 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
|
||||
let lineData = points.filter((p) => !Number.isNaN(p.y));
|
||||
lineData = fixCorners(lineData);
|
||||
let lastPoint = lineData[lineData.length - 1];
|
||||
if (lineData.length > 1) {
|
||||
lastPoint = lineData[lineData.length - 1];
|
||||
const secondLastPoint = lineData[lineData.length - 2];
|
||||
const diffX = (lastPoint.x - secondLastPoint.x) / 2;
|
||||
const diffY = (lastPoint.y - secondLastPoint.y) / 2;
|
||||
const midPoint = { x: secondLastPoint.x + diffX, y: secondLastPoint.y + diffY };
|
||||
lineData.splice(-1, 0, midPoint);
|
||||
}
|
||||
let curve = curveBasis;
|
||||
if (edge.curve) {
|
||||
curve = edge.curve;
|
||||
|
@ -57,6 +57,7 @@ import { triangle } from './shapes/triangle.js';
|
||||
import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js';
|
||||
import { waveRectangle } from './shapes/waveRectangle.js';
|
||||
import { windowPane } from './shapes/windowPane.js';
|
||||
import { classBox } from './shapes/classBox.js';
|
||||
import { kanbanItem } from './shapes/kanbanItem.js';
|
||||
|
||||
type ShapeHandler = <T extends SVGGraphicsElement>(
|
||||
@ -448,6 +449,14 @@ export const shapesDefs = [
|
||||
aliases: ['lined-document'],
|
||||
handler: linedWaveEdgedRect,
|
||||
},
|
||||
{
|
||||
semanticName: 'Class Box',
|
||||
name: 'Class Box',
|
||||
shortName: 'classBox',
|
||||
description: 'Class Box',
|
||||
aliases: ['class-box'],
|
||||
handler: classBox,
|
||||
},
|
||||
] as const satisfies ShapeDefinition[];
|
||||
|
||||
const generateShapeMap = () => {
|
||||
|
@ -0,0 +1,207 @@
|
||||
import { updateNodeBounds } from './util.js';
|
||||
import { getConfig } from '../../../diagram-api/diagramAPI.js';
|
||||
import { select } from 'd3';
|
||||
import type { Node } from '../../types.js';
|
||||
import type { ClassNode } from '../../../diagrams/class/classTypes.js';
|
||||
import rough from 'roughjs';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import { textHelper } from '../../../diagrams/class/shapeUtil.js';
|
||||
import { evaluate } from '../../../diagrams/common/common.js';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
|
||||
export async function classBox<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||
const config = getConfig();
|
||||
const PADDING = config.class!.padding ?? 12;
|
||||
const GAP = PADDING;
|
||||
const useHtmlLabels = node.useHtmlLabels ?? evaluate(config.htmlLabels) ?? true;
|
||||
// Treat node as classNode
|
||||
const classNode = node as unknown as ClassNode;
|
||||
classNode.annotations = classNode.annotations ?? [];
|
||||
classNode.members = classNode.members ?? [];
|
||||
classNode.methods = classNode.methods ?? [];
|
||||
|
||||
const { shapeSvg, bbox } = await textHelper(parent, node, config, useHtmlLabels, GAP);
|
||||
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
|
||||
node.cssStyles = classNode.styles || '';
|
||||
|
||||
const styles = classNode.styles?.join(';') || nodeStyles || '';
|
||||
|
||||
if (!node.cssStyles) {
|
||||
node.cssStyles = styles.replaceAll('!important', '').split(';');
|
||||
}
|
||||
|
||||
const renderExtraBox =
|
||||
classNode.members.length === 0 &&
|
||||
classNode.methods.length === 0 &&
|
||||
!config.class?.hideEmptyMembersBox;
|
||||
|
||||
// Setup roughjs
|
||||
// @ts-ignore TODO: Fix rough typings
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
|
||||
if (node.look !== 'handDrawn') {
|
||||
options.roughness = 0;
|
||||
options.fillStyle = 'solid';
|
||||
}
|
||||
|
||||
const w = bbox.width;
|
||||
let h = bbox.height;
|
||||
if (classNode.members.length === 0 && classNode.methods.length === 0) {
|
||||
h += GAP;
|
||||
} else if (classNode.members.length > 0 && classNode.methods.length === 0) {
|
||||
h += GAP * 2;
|
||||
}
|
||||
const x = -w / 2;
|
||||
const y = -h / 2;
|
||||
|
||||
// Create and center rectangle
|
||||
const roughRect = rc.rectangle(
|
||||
x - PADDING,
|
||||
y -
|
||||
PADDING -
|
||||
(renderExtraBox
|
||||
? PADDING
|
||||
: classNode.members.length === 0 && classNode.methods.length === 0
|
||||
? -PADDING / 2
|
||||
: 0),
|
||||
w + 2 * PADDING,
|
||||
h +
|
||||
2 * PADDING +
|
||||
(renderExtraBox
|
||||
? PADDING * 2
|
||||
: classNode.members.length === 0 && classNode.methods.length === 0
|
||||
? -PADDING
|
||||
: 0),
|
||||
options
|
||||
);
|
||||
|
||||
const rect = shapeSvg.insert(() => roughRect, ':first-child');
|
||||
rect.attr('class', 'basic label-container');
|
||||
const rectBBox = rect.node()!.getBBox();
|
||||
|
||||
// Rect is centered so now adjust labels.
|
||||
// TODO: Fix types
|
||||
shapeSvg.selectAll('.text').each((_: any, i: number, nodes: any) => {
|
||||
const text = select<any, unknown>(nodes[i]);
|
||||
// Get the current transform attribute
|
||||
const transform = text.attr('transform');
|
||||
// Initialize variables for the translation values
|
||||
let translateY = 0;
|
||||
// Check if the transform attribute exists
|
||||
if (transform) {
|
||||
const regex = RegExp(/translate\(([^,]+),([^)]+)\)/);
|
||||
const translate = regex.exec(transform);
|
||||
if (translate) {
|
||||
translateY = parseFloat(translate[2]);
|
||||
}
|
||||
}
|
||||
// Add to the y value
|
||||
let newTranslateY =
|
||||
translateY +
|
||||
y +
|
||||
PADDING -
|
||||
(renderExtraBox
|
||||
? PADDING
|
||||
: classNode.members.length === 0 && classNode.methods.length === 0
|
||||
? -PADDING / 2
|
||||
: 0);
|
||||
if (!useHtmlLabels) {
|
||||
// Fix so non html labels are better centered.
|
||||
// BBox of text seems to be slightly different when calculated so we offset
|
||||
newTranslateY -= 4;
|
||||
}
|
||||
let newTranslateX = x;
|
||||
if (
|
||||
text.attr('class').includes('label-group') ||
|
||||
text.attr('class').includes('annotation-group')
|
||||
) {
|
||||
newTranslateX = -text.node()?.getBBox().width / 2 || 0;
|
||||
shapeSvg.selectAll('text').each(function (_: any, i: number, nodes: any) {
|
||||
if (window.getComputedStyle(nodes[i]).textAnchor === 'middle') {
|
||||
newTranslateX = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Set the updated transform attribute
|
||||
text.attr('transform', `translate(${newTranslateX}, ${newTranslateY})`);
|
||||
});
|
||||
|
||||
// Render divider lines.
|
||||
const annotationGroupHeight =
|
||||
(shapeSvg.select('.annotation-group').node() as SVGGraphicsElement).getBBox().height -
|
||||
(renderExtraBox ? PADDING / 2 : 0) || 0;
|
||||
const labelGroupHeight =
|
||||
(shapeSvg.select('.label-group').node() as SVGGraphicsElement).getBBox().height -
|
||||
(renderExtraBox ? PADDING / 2 : 0) || 0;
|
||||
const membersGroupHeight =
|
||||
(shapeSvg.select('.members-group').node() as SVGGraphicsElement).getBBox().height -
|
||||
(renderExtraBox ? PADDING / 2 : 0) || 0;
|
||||
// First line (under label)
|
||||
if (classNode.members.length > 0 || classNode.methods.length > 0 || renderExtraBox) {
|
||||
const roughLine = rc.line(
|
||||
rectBBox.x,
|
||||
annotationGroupHeight + labelGroupHeight + y + PADDING,
|
||||
rectBBox.x + rectBBox.width,
|
||||
annotationGroupHeight + labelGroupHeight + y + PADDING,
|
||||
options
|
||||
);
|
||||
const line = shapeSvg.insert(() => roughLine);
|
||||
line.attr('class', 'divider').attr('style', styles);
|
||||
}
|
||||
|
||||
// Second line (under members)
|
||||
if (renderExtraBox || classNode.members.length > 0 || classNode.methods.length > 0) {
|
||||
const roughLine = rc.line(
|
||||
rectBBox.x,
|
||||
annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING,
|
||||
rectBBox.x + rectBBox.width,
|
||||
annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + PADDING + GAP * 2,
|
||||
options
|
||||
);
|
||||
const line = shapeSvg.insert(() => roughLine);
|
||||
line.attr('class', 'divider').attr('style', styles);
|
||||
}
|
||||
|
||||
/// Apply styles ///
|
||||
if (classNode.look !== 'handDrawn') {
|
||||
shapeSvg.selectAll('path').attr('style', styles);
|
||||
}
|
||||
// Apply other styles like stroke-width and stroke-dasharray to border (not background of shape)
|
||||
rect.select(':nth-child(2)').attr('style', styles);
|
||||
// Divider lines
|
||||
shapeSvg.selectAll('.divider').select('path').attr('style', styles);
|
||||
// Text elements
|
||||
if (node.labelStyle) {
|
||||
shapeSvg.selectAll('span').attr('style', node.labelStyle);
|
||||
} else {
|
||||
shapeSvg.selectAll('span').attr('style', styles);
|
||||
}
|
||||
// SVG text uses fill not color
|
||||
if (!useHtmlLabels) {
|
||||
// We just want to apply color to the text
|
||||
const colorRegex = RegExp(/color\s*:\s*([^;]*)/);
|
||||
const match = colorRegex.exec(styles);
|
||||
if (match) {
|
||||
const colorStyle = match[0].replace('color', 'fill');
|
||||
shapeSvg.selectAll('tspan').attr('style', colorStyle);
|
||||
} else if (labelStyles) {
|
||||
const match = colorRegex.exec(labelStyles);
|
||||
if (match) {
|
||||
const colorStyle = match[0].replace('color', 'fill');
|
||||
shapeSvg.selectAll('tspan').attr('style', colorStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
@ -95,6 +95,9 @@ export interface Edge {
|
||||
stroke?: string;
|
||||
text?: string;
|
||||
type: string;
|
||||
// Class Diagram specific properties
|
||||
startLabelRight?: string;
|
||||
endLabelLeft?: string;
|
||||
// Rendering specific properties
|
||||
curve?: string;
|
||||
labelpos?: string;
|
||||
|
@ -1448,6 +1448,9 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
htmlLabels:
|
||||
type: boolean
|
||||
default: false
|
||||
hideEmptyMembersBox:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
JourneyDiagramConfig:
|
||||
title: Journey Diagram Config
|
||||
|
@ -824,6 +824,7 @@ export const insertTitle = (
|
||||
parent
|
||||
.append('text')
|
||||
.text(title)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('x', bounds.x + bounds.width / 2)
|
||||
.attr('y', -titleTopMargin)
|
||||
.attr('class', cssClass);
|
||||
|
@ -52,18 +52,15 @@ export const getLineFunctionsWithOffset = (
|
||||
data: (Point | [number, number])[]
|
||||
) {
|
||||
let offset = 0;
|
||||
const DIRECTION =
|
||||
pointTransformer(data[0]).x < pointTransformer(data[data.length - 1]).x ? 'left' : 'right';
|
||||
if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
|
||||
// Handle first point
|
||||
// Calculate the angle and delta between the first two points
|
||||
const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]);
|
||||
// Calculate the offset based on the angle and the marker's dimensions
|
||||
offset =
|
||||
markerOffsets[edge.arrowTypeStart as keyof typeof markerOffsets] *
|
||||
Math.cos(angle) *
|
||||
(deltaX >= 0 ? 1 : -1);
|
||||
} else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
|
||||
// Handle last point
|
||||
// Calculate the angle and delta between the last two points
|
||||
const { angle, deltaX } = calculateDeltaAndAngle(
|
||||
data[data.length - 1],
|
||||
data[data.length - 2]
|
||||
@ -73,6 +70,41 @@ export const getLineFunctionsWithOffset = (
|
||||
Math.cos(angle) *
|
||||
(deltaX >= 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
const differenceToEnd = Math.abs(
|
||||
pointTransformer(d).x - pointTransformer(data[data.length - 1]).x
|
||||
);
|
||||
const differenceInYEnd = Math.abs(
|
||||
pointTransformer(d).y - pointTransformer(data[data.length - 1]).y
|
||||
);
|
||||
const differenceToStart = Math.abs(pointTransformer(d).x - pointTransformer(data[0]).x);
|
||||
const differenceInYStart = Math.abs(pointTransformer(d).y - pointTransformer(data[0]).y);
|
||||
const startMarkerHeight = markerOffsets[edge.arrowTypeStart as keyof typeof markerOffsets];
|
||||
const endMarkerHeight = markerOffsets[edge.arrowTypeEnd as keyof typeof markerOffsets];
|
||||
const extraRoom = 1;
|
||||
|
||||
// Adjust the offset if the difference is smaller than the marker height
|
||||
if (
|
||||
differenceToEnd < endMarkerHeight &&
|
||||
differenceToEnd > 0 &&
|
||||
differenceInYEnd < endMarkerHeight
|
||||
) {
|
||||
let adjustment = endMarkerHeight + extraRoom - differenceToEnd;
|
||||
adjustment *= DIRECTION === 'right' ? -1 : 1;
|
||||
// Adjust the offset by the amount needed to fit the marker
|
||||
offset -= adjustment;
|
||||
}
|
||||
|
||||
if (
|
||||
differenceToStart < startMarkerHeight &&
|
||||
differenceToStart > 0 &&
|
||||
differenceInYStart < startMarkerHeight
|
||||
) {
|
||||
let adjustment = startMarkerHeight + extraRoom - differenceToStart;
|
||||
adjustment *= DIRECTION === 'right' ? -1 : 1;
|
||||
offset += adjustment;
|
||||
}
|
||||
|
||||
return pointTransformer(d).x + offset;
|
||||
},
|
||||
y: function (
|
||||
@ -81,8 +113,9 @@ export const getLineFunctionsWithOffset = (
|
||||
i: number,
|
||||
data: (Point | [number, number])[]
|
||||
) {
|
||||
// Same handling as X above
|
||||
let offset = 0;
|
||||
const DIRECTION =
|
||||
pointTransformer(data[0]).y < pointTransformer(data[data.length - 1]).y ? 'down' : 'up';
|
||||
if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
|
||||
const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]);
|
||||
offset =
|
||||
@ -99,6 +132,40 @@ export const getLineFunctionsWithOffset = (
|
||||
Math.abs(Math.sin(angle)) *
|
||||
(deltaY >= 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
const differenceToEnd = Math.abs(
|
||||
pointTransformer(d).y - pointTransformer(data[data.length - 1]).y
|
||||
);
|
||||
const differenceInXEnd = Math.abs(
|
||||
pointTransformer(d).x - pointTransformer(data[data.length - 1]).x
|
||||
);
|
||||
const differenceToStart = Math.abs(pointTransformer(d).y - pointTransformer(data[0]).y);
|
||||
const differenceInXStart = Math.abs(pointTransformer(d).x - pointTransformer(data[0]).x);
|
||||
const startMarkerHeight = markerOffsets[edge.arrowTypeStart as keyof typeof markerOffsets];
|
||||
const endMarkerHeight = markerOffsets[edge.arrowTypeEnd as keyof typeof markerOffsets];
|
||||
const extraRoom = 1;
|
||||
|
||||
// Adjust the offset if the difference is smaller than the marker height
|
||||
if (
|
||||
differenceToEnd < endMarkerHeight &&
|
||||
differenceToEnd > 0 &&
|
||||
differenceInXEnd < endMarkerHeight
|
||||
) {
|
||||
let adjustment = endMarkerHeight + extraRoom - differenceToEnd;
|
||||
adjustment *= DIRECTION === 'up' ? -1 : 1;
|
||||
// Adjust the offset by the amount needed to fit the marker
|
||||
offset -= adjustment;
|
||||
}
|
||||
|
||||
if (
|
||||
differenceToStart < startMarkerHeight &&
|
||||
differenceToStart > 0 &&
|
||||
differenceInXStart < startMarkerHeight
|
||||
) {
|
||||
let adjustment = startMarkerHeight + extraRoom - differenceToStart;
|
||||
adjustment *= DIRECTION === 'up' ? -1 : 1;
|
||||
offset += adjustment;
|
||||
}
|
||||
return pointTransformer(d).y + offset;
|
||||
},
|
||||
};
|
||||
|
@ -9,6 +9,7 @@
|
||||
"./src/**/*.ts",
|
||||
"./package.json",
|
||||
"src/diagrams/gantt/ganttDb.js",
|
||||
"src/diagrams/git/gitGraphRenderer.js"
|
||||
"src/diagrams/git/gitGraphRenderer.js",
|
||||
"src/diagrams/class/classRenderer.js"
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user