Merge branch 'develop' into feature/3508_color-user-journey-title

This commit is contained in:
Shahir Ahmed
2025-03-18 15:15:45 -04:00
48 changed files with 3093 additions and 360 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
Fixes for consistent edge id creation & handling edge cases for animate edge feature

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
Fix for issue #6195 - allowing @ signs inside node labels

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: revert state db to resolve getData returning empty nodes and edges

View File

@@ -1,8 +0,0 @@
---
'mermaid': minor
---
Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
`mermaidAPI.getDiagramFromText()` now returns a new db instance on each call for state diagrams

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
Added versioning to StateDB and updated tests and diagrams to use it.

View File

@@ -1,5 +0,0 @@
---
'mermaid': minor
---
Adding support for animation of flowchart edges

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages.

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config.

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: `getDirection` and `setDirection` in `stateDb` refactored to return and set actual direction

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
`mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram

View File

@@ -0,0 +1,652 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
const testOptions = [
{ description: '', options: { logLevel: 1 } },
{ description: 'ELK: ', options: { logLevel: 1, layout: 'elk' } },
{ description: 'HD: ', options: { logLevel: 1, look: 'handDrawn' } },
];
describe('Entity Relationship Diagram Unified', () => {
testOptions.forEach(({ description, options }) => {
it(`${description}should render a simple ER diagram`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
options
);
});
it(`${description}should render a simple ER diagram without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render an ER diagram with a recursive relationship`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||..o{ CUSTOMER : refers
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
options
);
});
it(`${description}should render an ER diagram with multiple relationships between the same two entities`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER ||--|{ ADDRESS : "invoiced at"
CUSTOMER ||--|{ ADDRESS : "receives goods at"
`,
options
);
});
it(`${description}should render a cyclical ER diagram`, () => {
imgSnapshotTest(
`
erDiagram
A ||--|{ B : likes
B ||--|{ C : likes
C ||--|{ A : likes
`,
options
);
});
it(`${description}should render a not-so-simple ER diagram`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ INVOICE : "liable for"
DELIVERY-ADDRESS ||--o{ ORDER : receives
INVOICE ||--|{ ORDER : covers
ORDER ||--|{ ORDER-ITEM : includes
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
PRODUCT ||--o{ ORDER-ITEM : "ordered in"
`,
options
);
});
it(`${description}should render a not-so-simple ER diagram without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ INVOICE : "liable for"
DELIVERY-ADDRESS ||--o{ ORDER : receives
INVOICE ||--|{ ORDER : covers
ORDER ||--|{ ORDER-ITEM : includes
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
PRODUCT ||--o{ ORDER-ITEM : "ordered in"
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render multiple ER diagrams`, () => {
imgSnapshotTest(
[
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
`
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
],
options
);
});
it(`${description}should render an ER diagram with blank or empty labels`, () => {
imgSnapshotTest(
`
erDiagram
BOOK }|..|{ AUTHOR : ""
BOOK }|..|{ GENRE : " "
AUTHOR }|..|{ GENRE : " "
`,
options
);
});
it(`${description}should render entities that have no relationships`, () => {
renderGraph(
`
erDiagram
DEAD_PARROT
HERMIT
RECLUSE
SOCIALITE }o--o{ SOCIALITE : "interacts with"
RECLUSE }o--o{ SOCIALITE : avoids
`,
options
);
});
it(`${description}should render entities with and without attributes`, () => {
renderGraph(
`
erDiagram
BOOK { string title }
AUTHOR }|..|{ BOOK : writes
BOOK { float price }
`,
options
);
});
it(`${description}should render entities with generic and array attributes`, () => {
renderGraph(
`
erDiagram
BOOK {
string title
string[] authors
type~T~ type
}
`,
options
);
});
it(`${description}should render entities with generic and array attributes without htmlLabels`, () => {
renderGraph(
`
erDiagram
BOOK {
string title
string[] authors
type~T~ type
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with length in attributes type`, () => {
renderGraph(
`
erDiagram
CLUSTER {
varchar(99) name
string(255) description
}
`,
options
);
});
it(`${description}should render entities with length in attributes type without htmlLabels`, () => {
renderGraph(
`
erDiagram
CLUSTER {
varchar(99) name
string(255) description
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities and attributes with big and small entity names`, () => {
renderGraph(
`
erDiagram
PRIVATE_FINANCIAL_INSTITUTION {
string name
int turnover
}
PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs
EMPLOYEE { bool officer_of_firm }
`,
options
);
});
it(`${description}should render entities and attributes with big and small entity names without htmlLabels`, () => {
renderGraph(
`
erDiagram
PRIVATE_FINANCIAL_INSTITUTION {
string name
int turnover
}
PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs
EMPLOYEE { bool officer_of_firm }
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with attributes that begin with asterisk`, () => {
imgSnapshotTest(
`
erDiagram
BOOK {
int *id
string name
varchar(99) summary
}
BOOK }o..o{ STORE : soldBy
STORE {
int *id
string name
varchar(50) address
}
`,
options
);
});
it(`${description}should render entities with attributes that begin with asterisk without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
BOOK {
int *id
string name
varchar(99) summary
}
BOOK }o..o{ STORE : soldBy
STORE {
int *id
string name
varchar(50) address
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with keys`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
float price
string author FK
string title PK
}
`,
options
);
});
it(`${description}should render entities with keys without htmlLabels`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
float price
string author FK
string title PK
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with comments`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string author
string title "author comment"
float price "price comment"
}
`,
options
);
});
it(`${description}should render entities with comments without htmlLabels`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string author
string title "author comment"
float price "price comment"
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with keys and comments`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string description
float price "price comment"
string title PK "title comment"
string author FK
}
`,
options
);
});
it(`${description}should render entities with keys and comments without htmlLabels`, () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string description
float price "price comment"
string title PK "title comment"
string author FK
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with aliases`, () => {
renderGraph(
`
erDiagram
T1 one or zero to one or more T2 : test
T2 one or many optionally to zero or one T3 : test
T3 zero or more to zero or many T4 : test
T4 many(0) to many(1) T5 : test
T5 many optionally to one T6 : test
T6 only one optionally to only one T1 : test
T4 0+ to 1+ T6 : test
T1 1 to 1 T3 : test
`,
options
);
});
it(`${description}should render a simple ER diagram with a title`, () => {
imgSnapshotTest(
`---
title: simple ER diagram
---
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
`,
options
);
});
it(`${description}should render entities with entity name aliases`, () => {
imgSnapshotTest(
`
erDiagram
p[Person] {
varchar(64) firstName
varchar(64) lastName
}
c["Customer Account"] {
varchar(128) email
}
p ||--o| c : has
`,
options
);
});
it(`${description}should render relationship labels with line breaks`, () => {
imgSnapshotTest(
`
erDiagram
p[Person] {
string firstName
string lastName
}
a["Customer Account"] {
string email
}
b["Customer Account Secondary"] {
string email
}
c["Customer Account Tertiary"] {
string email
}
d["Customer Account Nth"] {
string email
}
p ||--o| a : "has<br />one"
p ||--o| b : "has<br />one<br />two"
p ||--o| c : "has<br />one<br/>two<br />three"
p ||--o| d : "has<br />one<br />two<br/>three<br />...<br/>Nth"
`,
options
);
});
it(`${description}should render an ER diagram with unicode text`, () => {
imgSnapshotTest(
`
erDiagram
_**testẽζØ😀㌕ぼ**_ {
*__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
*string(99)~T~~~~~~* firstName "Only __99__ <br>characters are a<br>llowed dsfsdfsdfsdfs"
string last*Name*
string __phone__ UK
int _age_
}
`,
options
);
});
it(`${description}should render an ER diagram with unicode text without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
_**testẽζØ😀㌕ぼ**_ {
*__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***"
*string(99)~T~~~~~~* firstName "Only __99__ <br>characters are a<br>llowed dsfsdfsdfsdfs"
string last*Name*
string __phone__ UK
int _age_
}
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render an ER diagram with relationships with unicode text`, () => {
imgSnapshotTest(
`
erDiagram
person[😀] {
string *first*Name
string _**last**Name_
}
a["*Customer Account*"] {
**string** ema*i*l
}
person ||--o| a : __hẽ😀__
`,
options
);
});
it(`${description}should render an ER diagram with relationships with unicode text without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
person[😀] {
string *first*Name
string _**last**Name_
}
a["*Customer Account*"] {
**string** ema*i*l
}
person ||--o| a : __hẽ😀__
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render an ER diagram with TB direction`, () => {
imgSnapshotTest(
`
erDiagram
direction TB
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render an ER diagram with BT direction`, () => {
imgSnapshotTest(
`
erDiagram
direction BT
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render an ER diagram with LR direction`, () => {
imgSnapshotTest(
`
erDiagram
direction LR
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render an ER diagram with RL direction`, () => {
imgSnapshotTest(
`
erDiagram
direction RL
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
`,
options
);
});
it(`${description}should render entities with styles applied from style statement`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]
style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
`,
options
);
});
it(`${description}should render entities with styles applied from style statement without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]
style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with styles applied from class statement`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]:::blue
classDef bold font-size:24px, font-weight: bold
classDef blue stroke:lightblue, color: #0000FF
class c,p bold
`,
options
);
});
it(`${description}should render entities with styles applied from class statement without htmlLabels`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]:::blue
classDef bold font-size:24px, font-weight: bold
classDef blue stroke:lightblue, color: #0000FF
class c,p bold
`,
{ ...options, htmlLabels: false }
);
});
it(`${description}should render entities with styles applied from the default class and other styles`, () => {
imgSnapshotTest(
`
erDiagram
c[CUSTOMER]
p[PERSON]:::blue
classDef blue stroke:lightblue, color: #0000FF
classDef default fill:pink
style c color:green
`,
{ ...options }
);
});
});
});

View File

@@ -109,8 +109,8 @@ describe('Entity Relationship Diagram', () => {
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
// use within because the absolute value can be slightly different depending on the environment ±5%
expect(maxWidthValue).to.be.within(140 * 0.95, 140 * 1.05);
// use within because the absolute value can be slightly different depending on the environment ±6%
expect(maxWidthValue).to.be.within(140 * 0.96, 140 * 1.06);
});
});
@@ -125,8 +125,8 @@ describe('Entity Relationship Diagram', () => {
);
cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width'));
// use within because the absolute value can be slightly different depending on the environment ±5%
expect(width).to.be.within(140 * 0.95, 140 * 1.05);
// use within because the absolute value can be slightly different depending on the environment ±6%
expect(width).to.be.within(140 * 0.96, 140 * 1.06);
// expect(svg).to.have.attr('height', '465');
expect(svg).to.not.have.attr('style');
});

View File

@@ -917,4 +917,21 @@ graph TD
}
);
});
it('#6369: edge color should affect arrow head', () => {
imgSnapshotTest(
`
flowchart LR
A --> B
A --> C
C --> D
linkStyle 0 stroke:#D50000
linkStyle 2 stroke:#D50000
`,
{
flowchart: { htmlLabels: true },
securityLevel: 'loose',
}
);
});
});

337
cypress/platform/yari2.html Normal file
View File

@@ -0,0 +1,337 @@
<html>
<body>
<h1 class="header">Nodes</h1>
<div class="node-showcase">
<div class="test">
<h2>Basic ErNode</h2>
<pre class="mermaid">
---
config:
htmlLabels: false
look: handDrawn
theme: forest
---
erDiagram
_**hiØ**_[*test*] {
*__List~List~int~~sdfds__* __driversLicense__ PK "***The l😀icense #***"
*string(99)~T~~~~~~* firstName "Only 99 <br>characters are a<br>llowed dsfsdfsdfsdfs"
~str ing~ lastName
string phone UK
int age
}
style PERSON color:red, stroke:blue,fill:#f9f
classDef test,test2 stroke:red
class PERSON test,test2
</pre>
</div>
<div class="test">
<h2>Basic ErNode</h2>
<pre class="mermaid">
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
CAR:::someclass
PERSON:::anotherclass,someclass
classDef someclass fill:#f96
classDef anotherclass color:blue
</pre>
</div>
</div>
<h1 class="header">Diagram Testing</h1>
<div class="diagram-showcase">
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
htmlLabels: false
layout: elk
look: handDrawn
theme: forest
---
erDiagram
"hi" }o..o{ ORDER : places
style hi fill:lightblue
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
htmlLabels: false
look: handDrawn
layout: elk
---
erDiagram
CAR ||--|{ NAMED-DRIVER : allows
PERSON ||..o{ NAMED-DRIVER : is
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
htmlLabels: true
look: handDrawn
theme: forest
---
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
CAR {
test test PK "comment"
string make
string model
string[] parts
}
PERSON ||--o{ NAMED-DRIVER : is
PERSON ||--o{ CAR : is
PERSON {
string driversLicense PK "The license #"
string(99) firstName "Only 99 characters are allowed"
string lastName
string phone UK
int age
}
NAMED-DRIVER {
string carRegistrationNumber PK, FK
string driverLicence PK, FK
}
MANUFACTURER only one to zero or more CAR : makes
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
title: simple ER diagram
config:
theme: forest
---
erDiagram
direction TB
p[Pers😀on] {
string firstName
string lastName
}
a["Customer Account"] {
string email
}
p ||--o| a : has
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
layout: elk
---
erDiagram
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ INVOICE : "liable for"
DELIVERY-ADDRESS ||--o{ ORDER : receives
INVOICE ||--|{ ORDER : covers
ORDER ||--|{ ORDER-ITEM : includes
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
PRODUCT ||--o{ ORDER-ITEM : "ordered in"
</pre>
</div>
<div class="test">
<h2>Basic Relationship</h2>
<pre class="mermaid">
---
config:
layout: elk
---
erDiagram
rental{
~timestamp with time zone~ rental_date "NN"
~integer~ inventory_id "NN"
~integer~ customer_id "NN"
~timestamp with time zone~ return_date
~integer~ staff_id "NN"
~integer~ rental_id "NN"
~timestamp with time zone~ last_update "NN"
}
film_actor{
~integer~ actor_id "NN"
~integer~ film_id "NN"
~timestamp with time zone~ last_update "NN"
}
film{
~text~ title "NN"
~text~ description
~public.year~ release_year
~integer~ language_id "NN"
~integer~ original_language_id
~smallint~ length
~text[]~ special_features
~tsvector~ fulltext "NN"
~integer~ film_id "NN"
~smallint~ rental_duration "NN"
~numeric(4,2)~ rental_rate "NN"
~numeric(5,2)~ replacement_cost "NN"
~public.mpaa_rating~ rating
~timestamp with time zone~ last_update "NN"
}
customer{
~integer~ store_id "NN"
~text~ first_name "NN"
~text~ last_name "NN"
~text~ email
~integer~ address_id "NN"
~integer~ active
~integer~ customer_id "NN"
~boolean~ activebool "NN"
~date~ create_date "NN"
~timestamp with time zone~ last_update
}
film_category{
~integer~ film_id "NN"
~integer~ category_id "NN"
~timestamp with time zone~ last_update "NN"
}
actor{
~text~ first_name "NN"
~text~ last_name "NN"
~integer~ actor_id "NN"
~timestamp with time zone~ last_update "NN"
}
store{
~integer~ manager_staff_id "NN"
~integer~ address_id "NN"
~integer~ store_id "NN"
~timestamp with time zone~ last_update "NN"
}
city{
~text~ city "NN"
~integer~ country_id "NN"
~integer~ city_id "NN"
~timestamp with time zone~ last_update "NN"
}
language{
~character(20)~ name "NN"
~integer~ language_id "NN"
~timestamp with time zone~ last_update "NN"
}
payment{
~integer~ customer_id "NN"
~integer~ staff_id "NN"
~integer~ rental_id "NN"
~numeric(5,2)~ amount "NN"
~timestamp with time zone~ payment_date "NN"
~integer~ payment_id "NN"
}
category{
~text~ name "NN"
~integer~ category_id "NN"
~timestamp with time zone~ last_update "NN"
}
inventory{
~integer~ film_id "NN"
~integer~ store_id "NN"
~integer~ inventory_id "NN"
~timestamp with time zone~ last_update "NN"
}
address{
~text~ address "NN"
~text~ address2
~text~ district "NN"
~integer~ city_id "NN"
~text~ postal_code
~text~ phone "NN"
~integer~ address_id "NN"
~timestamp with time zone~ last_update "NN"
}
staff{
~text~ first_name "NN"
~text~ last_name "NN"
~integer~ address_id "NN"
~text~ email
~integer~ store_id "NN"
~text~ username "NN"
~text~ password
~bytea~ picture
~integer~ staff_id "NN"
~boolean~ active "NN"
~timestamp with time zone~ last_update "NN"
}
country{
~text~ country "NN"
~integer~ country_id "NN"
~timestamp with time zone~ last_update "NN"
}
film_actor }|..|| film : film_actor_film_id_fkey
film_actor }|..|| actor : film_actor_actor_id_fkey
address }|..|| city : address_city_id_fkey
city }|..|| country : city_country_id_fkey
customer }|..|| store : customer_store_id_fkey
customer }|..|| address : customer_address_id_fkey
film }|..|| language : film_original_language_id_fkey
film }|..|| language : film_language_id_fkey
film_category }|..|| film : film_category_film_id_fkey
film_category }|..|| category : film_category_category_id_fkey
inventory }|..|| store : inventory_store_id_fkey
</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;
}
</style>
</html>

View File

@@ -70,6 +70,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Notion](https://notion.so) ✅
- [Observable](https://observablehq.com/@observablehq/mermaid) ✅
- [Obsidian](https://help.obsidian.md/Editing+and+formatting/Advanced+formatting+syntax#Diagram) ✅
- [Outline](https://docs.getoutline.com/s/guide/doc/diagrams-KQiKoT4wzK) ✅
- [Redmine](https://redmine.org)
- [Mermaid Macro](https://www.redmine.org/plugins/redmine_mermaid_macro)
- [Markdown for mermaid plugin](https://github.com/jamieh-mongolian/markdown-for-mermaid-plugin)

View File

@@ -92,7 +92,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to
Where:
- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v10.5.0+), and may also contain digits and hyphens.
- `first-entity` is the name of an entity. Names support any unicode characters and can include spaces if surrounded by double quotes (e.g. "name with space").
- `relationship` describes the way that both entities inter-relate. See below.
- `second-entity` is the name of the other entity.
- `relationship-label` describes the relationship from the perspective of the first entity.
@@ -107,6 +107,34 @@ This statement can be read as _a property contains one or more rooms, and a room
Only the `first-entity` part of a statement is mandatory. This makes it possible to show an entity with no relationships, which can be useful during iterative construction of diagrams. If any other parts of a statement are specified, then all parts are mandatory.
#### Unicode text
Entity names, relationships, and attributes all support unicode text.
```mermaid-example
erDiagram
"This ❤ Unicode"
```
```mermaid
erDiagram
"This ❤ Unicode"
```
#### Markdown formatting
Markdown formatting and text is also supported.
```mermaid-example
erDiagram
"This **is** _Markdown_"
```
```mermaid
erDiagram
"This **is** _Markdown_"
```
### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components:
@@ -145,6 +173,11 @@ Cardinality is a property that describes how many elements of another entity can
Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line:
| Value | Alias for |
| :---: | :---------------: |
| -- | _identifying_ |
| .. | _non-identifying_ |
**Aliases**
| Value | Alias for |
@@ -155,13 +188,25 @@ Relationships may be classified as either _identifying_ or _non-identifying_ and
```mermaid-example
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
PERSON ||--o{ NAMED-DRIVER : is
PERSON }o..o{ NAMED-DRIVER : is
```
```mermaid
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
PERSON ||--o{ NAMED-DRIVER : is
PERSON }o..o{ NAMED-DRIVER : is
```
```mermaid-example
erDiagram
CAR 1 to zero or more NAMED-DRIVER : allows
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
```mermaid
erDiagram
CAR 1 to zero or more NAMED-DRIVER : allows
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
### Attributes
@@ -202,9 +247,9 @@ erDiagram
The `type` values must begin with an alphabetic character and may contain digits, hyphens, underscores, parentheses and square brackets. The `name` values follow a similar format to `type`, but may start with an asterisk as another option to indicate an attribute is a primary key. Other than that, there are no restrictions, and there is no implicit set of valid data types.
### Entity Name Aliases (v10.5.0+)
### Entity Name Aliases
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name.
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. Alias names follow all of the same rules as entity names.
```mermaid-example
erDiagram
@@ -232,7 +277,7 @@ erDiagram
#### Attribute Keys and Comments
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key (markdown formatting and unicode is not supported for keys). To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
```mermaid-example
erDiagram
@@ -282,35 +327,318 @@ erDiagram
MANUFACTURER only one to zero or more CAR : makes
```
### Other Things
### Direction
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
- (v11.1.0+) If you want a multi-line label on a relationship, use `<br />` between the two lines (`"first line<br />second line"`)
The direction statement declares the direction of the diagram.
## Styling
This declares that the diagram is oriented from top to bottom (`TB`). This can be reversed to be oriented from bottom to top (`BT`).
### Config options
```mermaid-example
erDiagram
direction TB
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
For simple color customization:
```mermaid
erDiagram
direction TB
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
| Name | Used as |
| :------- | :------------------------------------------------------------------- |
| `fill` | Background color of an entity or attribute |
| `stroke` | Border color of an entity or attribute, line color of a relationship |
This declares that the diagram is oriented from left to right (`LR`). This can be reversed to be oriented from right to left (`RL`).
### Classes used
```mermaid-example
erDiagram
direction LR
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
The following CSS class selectors are available for richer styling:
```mermaid
erDiagram
direction LR
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
| Selector | Description |
| :------------------------- | :---------------------------------------------------- |
| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows |
| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows |
| `.er.entityBox` | The box representing an entity |
| `.er.entityLabel` | The label for an entity |
| `.er.relationshipLabel` | The label for a relationship |
| `.er.relationshipLabelBox` | The box surrounding a relationship label |
| `.er.relationshipLine` | The line representing a relationship between entities |
Possible diagram orientations are:
- TB - Top to bottom
- BT - Bottom to top
- RL - Right to left
- LR - Left to right
### Styling a node
It is possible to apply specific styles such as a thicker border or a different background color to a node.
```mermaid-example
erDiagram
id1||--||id2 : label
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
```
```mermaid
erDiagram
id1||--||id2 : label
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
```
It is also possible to attach styles to a list of nodes in one statement:
```
style nodeId1,nodeId2 styleList
```
#### 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.
A class definition looks like the example below:
```
classDef className fill:#f9f,stroke:#333,stroke-width:4px
```
It is also possible to define multiple classes in one statement:
```
classDef firstClassName,secondClassName font-size:12pt
```
Attachment of a class to a node is done as per below:
```
class nodeId1 className
```
It is also possible to attach a class to a list of nodes in one statement:
```
class nodeId1,nodeId2 className
```
Multiple classes can be attached at the same time as well:
```
class nodeId1,nodeId2 className1,className2
```
A shorter form of adding a class is to attach the classname to the node using the `:::`operator as per below:
```mermaid-example
erDiagram
direction TB
CAR:::someclass {
string registrationNumber
string make
string model
}
PERSON:::someclass {
string firstName
string lastName
int age
}
HOUSE:::someclass
classDef someclass fill:#f96
```
```mermaid
erDiagram
direction TB
CAR:::someclass {
string registrationNumber
string make
string model
}
PERSON:::someclass {
string firstName
string lastName
int age
}
HOUSE:::someclass
classDef someclass fill:#f96
```
This form can be used when declaring relationships between entities:
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
```mermaid
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
Similar to the class statement, the shorthand syntax can also apply multiple classes at once:
```
nodeId:::className1,className2
```
### Default class
If a class is named default it will be assigned to all classes without specific class definitions.
```
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
```
> **Note:** Custom styles from style or other class statements take priority and will overwrite the default styles. (e.g. The `default` class gives nodes a background color of pink but the `blue` class will give that node a background color of blue if applied.)
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef default fill:#f9f,stroke-width:4px
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
```mermaid
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef default fill:#f9f,stroke-width:4px
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
## Configuration
### Renderer
The layout of the diagram is done with the renderer. The default renderer is dagre.
You can opt to use an alternate renderer named elk by editing the configuration. The elk renderer is better for larger and/or more complex diagrams.
```
---
config:
layout: elk
---
```
> **Note**
> Note that the site needs to use mermaid version 9.4+ for this to work and have this featured enabled in the lazy-loading configuration.
<!--- cspell:locale en,en-gb --->

View File

@@ -1,5 +1,49 @@
# mermaid
## 11.5.0
### Minor Changes
- [#6187](https://github.com/mermaid-js/mermaid/pull/6187) [`7809b5a`](https://github.com/mermaid-js/mermaid/commit/7809b5a93fae127f45727071f5ff14325222c518) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Flowchart new syntax for node metadata bugs
- Incorrect label mapping for nodes when using `&`
- Syntax error when `}` with trailing spaces before new line
- [#6136](https://github.com/mermaid-js/mermaid/pull/6136) [`ec0d9c3`](https://github.com/mermaid-js/mermaid/commit/ec0d9c389aa6018043187654044c1e0b5aa4f600) Thanks [@knsv](https://github.com/knsv)! - Adding support for animation of flowchart edges
- [#6373](https://github.com/mermaid-js/mermaid/pull/6373) [`05bdf0e`](https://github.com/mermaid-js/mermaid/commit/05bdf0e20e2629fe77513218fbd4e28e65f75882) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Upgrade Requirement and ER diagram to use the common renderer flow
- Added support for directions
- Added support for hand drawn look
- [#6371](https://github.com/mermaid-js/mermaid/pull/6371) [`4d25cab`](https://github.com/mermaid-js/mermaid/commit/4d25caba8e65df078966a283e7e0ae1200bef595) Thanks [@knsv](https://github.com/knsv)! - The arrowhead color should match the color of the edge. Creates a unique clone of the arrow marker with the appropriate color.
### Patch Changes
- [#6064](https://github.com/mermaid-js/mermaid/pull/6064) [`2a91849`](https://github.com/mermaid-js/mermaid/commit/2a91849a38641e97ed6b20cb60aa4506d1b63177) Thanks [@NicolasNewman](https://github.com/NicolasNewman)! - fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments
- [#6198](https://github.com/mermaid-js/mermaid/pull/6198) [`963efa6`](https://github.com/mermaid-js/mermaid/commit/963efa64c794466dcd0f06bad6de6ba554d05a54) Thanks [@ferozmht](https://github.com/ferozmht)! - Fixes for consistent edge id creation & handling edge cases for animate edge feature
- [#6196](https://github.com/mermaid-js/mermaid/pull/6196) [`127bac1`](https://github.com/mermaid-js/mermaid/commit/127bac1147034d8a8588cc8f7870abe92ebc945e) Thanks [@knsv](https://github.com/knsv)! - Fix for issue #6195 - allowing @ signs inside node labels
- [#6212](https://github.com/mermaid-js/mermaid/pull/6212) [`90bbf90`](https://github.com/mermaid-js/mermaid/commit/90bbf90a83bf5da53fc8030cf1370bc8238fa4aa) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram
- [#6218](https://github.com/mermaid-js/mermaid/pull/6218) [`232e60c`](https://github.com/mermaid-js/mermaid/commit/232e60c8cbaea804e6d98aa90f90d1ce76730e17) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: revert state db to resolve getData returning empty nodes and edges
- [#6250](https://github.com/mermaid-js/mermaid/pull/6250) [`9cad3c7`](https://github.com/mermaid-js/mermaid/commit/9cad3c7aea3bbbc61495b23225ccff76d312783f) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - `mermaidAPI.getDiagramFromText()` now returns a new db instance on each call for state diagrams
- [#6293](https://github.com/mermaid-js/mermaid/pull/6293) [`cfd84e5`](https://github.com/mermaid-js/mermaid/commit/cfd84e54d502f4d36a35b50478121558cfbef2c4) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - Added versioning to StateDB and updated tests and diagrams to use it.
- [#6161](https://github.com/mermaid-js/mermaid/pull/6161) [`6cc31b7`](https://github.com/mermaid-js/mermaid/commit/6cc31b74530baa6d0f527346ab1395b0896bb3c2) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart
- [#6272](https://github.com/mermaid-js/mermaid/pull/6272) [`ffa7804`](https://github.com/mermaid-js/mermaid/commit/ffa7804af0701b3d044d6794e36bd9132d6c7e8d) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages.
- [#6205](https://github.com/mermaid-js/mermaid/pull/6205) [`32a68d4`](https://github.com/mermaid-js/mermaid/commit/32a68d489ed83a5b79f516d6b2fb3a7505c5eb24) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config.
- [#6295](https://github.com/mermaid-js/mermaid/pull/6295) [`da6361f`](https://github.com/mermaid-js/mermaid/commit/da6361f6527918b4b6a9c07cc9558cf2e2c709d2) Thanks [@omkarht](https://github.com/omkarht)! - fix: `getDirection` and `setDirection` in `stateDb` refactored to return and set actual direction
- [#6185](https://github.com/mermaid-js/mermaid/pull/6185) [`3e32332`](https://github.com/mermaid-js/mermaid/commit/3e32332814c659e7ed1bb73d4a26ed4e61b77d59) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - `mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram
## 11.4.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.4.1",
"version": "11.5.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module",
"module": "./dist/mermaid.core.mjs",

View File

@@ -807,6 +807,8 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
*
*/
entityPadding?: number;
nodeSpacing?: number;
rankSpacing?: number;
/**
* Stroke color of box edges and lines.
*/

View File

@@ -1,103 +0,0 @@
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
let entities = new Map();
let relationships = [];
const Cardinality = {
ZERO_OR_ONE: 'ZERO_OR_ONE',
ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT',
};
const Identification = {
NON_IDENTIFYING: 'NON_IDENTIFYING',
IDENTIFYING: 'IDENTIFYING',
};
/**
* Add entity
* @param {string} name - The name of the entity
* @param {string | undefined} alias - The alias of the entity
*/
const addEntity = function (name, alias = undefined) {
if (!entities.has(name)) {
entities.set(name, { attributes: [], alias });
log.info('Added new entity :', name);
} else if (!entities.get(name).alias && alias) {
entities.get(name).alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return entities.get(name);
};
const getEntities = () => entities;
const addAttributes = function (entityName, attribs) {
let entity = addEntity(entityName); // May do nothing (if entity has already been added)
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
let i;
for (i = attribs.length - 1; i >= 0; i--) {
entity.attributes.push(attribs[i]);
log.debug('Added attribute ', attribs[i].attributeName);
}
};
/**
* Add a relationship
*
* @param entA The first entity in the relationship
* @param rolA The role played by the first entity in relation to the second
* @param entB The second entity in the relationship
* @param rSpec The details of the relationship between the two entities
*/
const addRelationship = function (entA, rolA, entB, rSpec) {
let rel = {
entityA: entA,
roleA: rolA,
entityB: entB,
relSpec: rSpec,
};
relationships.push(rel);
log.debug('Added new relationship :', rel);
};
const getRelationships = () => relationships;
const clear = function () {
entities = new Map();
relationships = [];
commonClear();
};
export default {
Cardinality,
Identification,
getConfig: () => getConfig().er,
addEntity,
addAttributes,
getEntities,
addRelationship,
getRelationships,
clear,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
setDiagramTitle,
getDiagramTitle,
};

View File

@@ -0,0 +1,251 @@
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { Edge, Node } from '../../rendering-util/types.js';
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { getEdgeId } from '../../utils.js';
import type { DiagramDB } from '../../diagram-api/types.js';
export class ErDB implements DiagramDB {
private entities = new Map<string, EntityNode>();
private relationships: Relationship[] = [];
private classes = new Map<string, EntityClass>();
private direction = 'TB';
private Cardinality = {
ZERO_OR_ONE: 'ZERO_OR_ONE',
ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT',
};
private Identification = {
NON_IDENTIFYING: 'NON_IDENTIFYING',
IDENTIFYING: 'IDENTIFYING',
};
constructor() {
this.clear();
this.addEntity = this.addEntity.bind(this);
this.addAttributes = this.addAttributes.bind(this);
this.addRelationship = this.addRelationship.bind(this);
this.setDirection = this.setDirection.bind(this);
this.addCssStyles = this.addCssStyles.bind(this);
this.addClass = this.addClass.bind(this);
this.setClass = this.setClass.bind(this);
this.setAccTitle = this.setAccTitle.bind(this);
this.setAccDescription = this.setAccDescription.bind(this);
}
/**
* Add entity
* @param name - The name of the entity
* @param alias - The alias of the entity
*/
public addEntity(name: string, alias = ''): EntityNode {
if (!this.entities.has(name)) {
this.entities.set(name, {
id: `entity-${name}-${this.entities.size}`,
label: name,
attributes: [],
alias,
shape: 'erBox',
look: getConfig().look ?? 'default',
cssClasses: 'default',
cssStyles: [],
});
log.info('Added new entity :', name);
} else if (!this.entities.get(name)?.alias && alias) {
this.entities.get(name)!.alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return this.entities.get(name)!;
}
public getEntity(name: string) {
return this.entities.get(name);
}
public getEntities() {
return this.entities;
}
public getClasses() {
return this.classes;
}
public addAttributes(entityName: string, attribs: Attribute[]) {
const entity = this.addEntity(entityName); // May do nothing (if entity has already been added)
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
let i;
for (i = attribs.length - 1; i >= 0; i--) {
if (!attribs[i].keys) {
attribs[i].keys = [];
}
if (!attribs[i].comment) {
attribs[i].comment = '';
}
entity.attributes.push(attribs[i]);
log.debug('Added attribute ', attribs[i].name);
}
}
/**
* Add a relationship
*
* @param entA - The first entity in the relationship
* @param rolA - The role played by the first entity in relation to the second
* @param entB - The second entity in the relationship
* @param rSpec - The details of the relationship between the two entities
*/
public addRelationship(entA: string, rolA: string, entB: string, rSpec: RelSpec) {
const entityA = this.entities.get(entA);
const entityB = this.entities.get(entB);
if (!entityA || !entityB) {
return;
}
const rel = {
entityA: entityA.id,
roleA: rolA,
entityB: entityB.id,
relSpec: rSpec,
};
this.relationships.push(rel);
log.debug('Added new relationship :', rel);
}
public getRelationships() {
return this.relationships;
}
public getDirection() {
return this.direction;
}
public setDirection(dir: string) {
this.direction = dir;
}
private getCompiledStyles(classDefs: string[]) {
let compiledStyles: string[] = [];
for (const customClass of classDefs) {
const cssClass = this.classes.get(customClass);
if (cssClass?.styles) {
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
}
if (cssClass?.textStyles) {
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim());
}
}
return compiledStyles;
}
public addCssStyles(ids: string[], styles: string[]) {
for (const id of ids) {
const entity = this.entities.get(id);
if (!styles || !entity) {
return;
}
for (const style of styles) {
entity.cssStyles!.push(style);
}
}
}
public addClass(ids: string[], style: string[]) {
ids.forEach((id) => {
let classNode = this.classes.get(id);
if (classNode === undefined) {
classNode = { id, styles: [], textStyles: [] };
this.classes.set(id, classNode);
}
if (style) {
style.forEach(function (s) {
if (/color/.exec(s)) {
const newStyle = s.replace('fill', 'bgFill');
classNode.textStyles.push(newStyle);
}
classNode.styles.push(s);
});
}
});
}
public setClass(ids: string[], classNames: string[]) {
for (const id of ids) {
const entity = this.entities.get(id);
if (entity) {
for (const className of classNames) {
entity.cssClasses += ' ' + className;
}
}
}
}
public clear() {
this.entities = new Map();
this.classes = new Map();
this.relationships = [];
commonClear();
}
public getData() {
const nodes: Node[] = [];
const edges: Edge[] = [];
const config = getConfig();
for (const entityKey of this.entities.keys()) {
const entityNode = this.entities.get(entityKey);
if (entityNode) {
entityNode.cssCompiledStyles = this.getCompiledStyles(entityNode.cssClasses!.split(' '));
nodes.push(entityNode as unknown as Node);
}
}
let count = 0;
for (const relationship of this.relationships) {
const edge: Edge = {
id: getEdgeId(relationship.entityA, relationship.entityB, {
prefix: 'id',
counter: count++,
}),
type: 'normal',
curve: 'basis',
start: relationship.entityA,
end: relationship.entityB,
label: relationship.roleA,
labelpos: 'c',
thickness: 'normal',
classes: 'relationshipLine',
arrowTypeStart: relationship.relSpec.cardB.toLowerCase(),
arrowTypeEnd: relationship.relSpec.cardA.toLowerCase(),
pattern: relationship.relSpec.relType == 'IDENTIFYING' ? 'solid' : 'dashed',
look: config.look,
};
edges.push(edge);
}
return { nodes, edges, other: {}, config, direction: 'TB' };
}
public setAccTitle = setAccTitle;
public getAccTitle = getAccTitle;
public setAccDescription = setAccDescription;
public getAccDescription = getAccDescription;
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getConfig = () => getConfig().er;
}

View File

@@ -1,12 +1,14 @@
// @ts-ignore: TODO: Fix ts errors
import erParser from './parser/erDiagram.jison';
import erDb from './erDb.js';
import erRenderer from './erRenderer.js';
import { ErDB } from './erDb.js';
import * as renderer from './erRenderer-unified.js';
import erStyles from './styles.js';
export const diagram = {
parser: erParser,
db: erDb,
renderer: erRenderer,
get db() {
return new ErDB();
},
renderer,
styles: erStyles,
};

View File

@@ -0,0 +1,66 @@
import { getConfig } from '../../diagram-api/diagramAPI.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';
import { select } from 'd3';
export const draw = async function (text: string, id: string, _version: string, diag: any) {
log.info('REF0:');
log.info('Drawing er diagram (unified)', id);
const { securityLevel, er: conf, layout } = getConfig();
// 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);
// Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing?
data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140;
data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80;
data4Layout.direction = diag.db.getDirection();
data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
data4Layout.diagramId = id;
await render(data4Layout, svg);
// Elk layout algorithm displays markers above nodes, so move edges to top so they are "painted" over by the nodes.
if (data4Layout.layoutAlgorithm === 'elk') {
svg.select('.edges').lower();
}
// Sets the background nodes to the same position as their original counterparts.
// Background nodes are created when the look is handDrawn so the ER diagram markers do not show underneath.
const backgroundNodes = svg.selectAll('[id*="-background"]');
// eslint-disable-next-line unicorn/prefer-spread
if (Array.from(backgroundNodes).length > 0) {
backgroundNodes.each(function (this: SVGElement) {
const backgroundNode = select(this);
const backgroundId = backgroundNode.attr('id');
const nonBackgroundId = backgroundId.replace('-background', '');
const nonBackgroundNode = svg.select(`#${CSS.escape(nonBackgroundId)}`);
if (!nonBackgroundNode.empty()) {
const transform = nonBackgroundNode.attr('transform');
backgroundNode.attr('transform', transform);
}
});
}
const padding = 8;
utils.insertTitle(
svg,
'erDiagramTitleText',
conf?.titleTopMargin ?? 25,
diag.db.getDiagramTitle()
);
setupViewPortForSVG(svg, padding, 'erDiagram', conf?.useMaxWidth ?? true);
};

View File

@@ -0,0 +1,37 @@
export interface EntityNode {
id: string;
label: string;
attributes: Attribute[];
alias: string;
shape: string;
look?: string;
cssClasses?: string;
cssStyles?: string[];
cssCompiledStyles?: string[];
}
export interface Attribute {
type: string;
name: string;
keys: ('PK' | 'FK' | 'UK')[];
comment: string;
}
export interface Relationship {
entityA: string;
roleA: string;
entityB: string;
relSpec: RelSpec;
}
export interface RelSpec {
cardA: string;
cardB: string;
relType: string;
}
export interface EntityClass {
id: string;
styles: string[];
textStyles: string[];
}

View File

@@ -5,6 +5,7 @@
%x acc_title
%x acc_descr
%x acc_descr_multiline
%x style
%%
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@@ -14,6 +15,10 @@ accDescr\s*":"\s* { this.begin("ac
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
[\n]+ return 'NEWLINE';
\s+ /* skip whitespace */
[\s]+ return 'SPACE';
@@ -21,11 +26,15 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
\"[^"]*\" return 'WORD';
"erDiagram" return 'ER_DIAGRAM';
"{" { this.begin("block"); return 'BLOCK_START'; }
<block>"," return 'COMMA';
\# return 'BRKT';
"#" return 'BRKT';
"," return 'COMMA';
":::" return 'STYLE_SEPARATOR';
":" return 'COLON';
<block>\s+ /* skip whitespace in block */
<block>\b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY'
<block>(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD';
<block>[\*A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
<block>([^\s]*)[~].*[~]([^\s]*) return 'ATTRIBUTE_WORD';
<block>([\*A-Za-z_\u00C0-\uFFFF][A-Za-z0-9\-\_\[\]\(\)\u00C0-\uFFFF\*]*) return 'ATTRIBUTE_WORD';
<block>\"[^"]*\" return 'COMMENT';
<block>[\n]+ /* nothing */
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
@@ -33,6 +42,14 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
"[" return 'SQS';
"]" return 'SQE';
"style" { this.begin("style"); return 'STYLE'; }
<style>[\n]+ { this.popState(); return 'NEWLINE'; }
<style>\s+ /* skip whitespace in block */
<style>":" return 'COLON';
<style>"," return 'COMMA';
<style>"#" return 'BRKT';
"classDef" { this.begin("style"); return 'CLASSDEF'; }
"class" return 'CLASS';
"one or zero" return 'ZERO_OR_ONE';
"one or more" return 'ONE_OR_MORE';
"one or many" return 'ONE_OR_MORE';
@@ -61,7 +78,10 @@ o\{ return 'ZERO_OR_MORE';
"optionally to" return 'NON_IDENTIFYING';
\.\- return 'NON_IDENTIFYING';
\-\. return 'NON_IDENTIFYING';
[A-Za-z_][A-Za-z0-9\-_]* return 'ALPHANUM';
<style>([^\x00-\x7F]|\w|\-|\*)+ return 'STYLE_TEXT';
<style>';' return 'SEMI';
([^\x00-\x7F]|\w|\-|\*)+ return 'UNICODE_TEXT';
[0-9] return 'NUM';
. return yytext[0];
<<EOF>> return 'EOF';
@@ -88,35 +108,126 @@ line
statement
: entityName relSpec entityName ':' role
: entityName relSpec entityName COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $5, $3, $2);
}
| entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList COLON role
{
yy.addEntity($1);
yy.addEntity($5);
yy.addRelationship($1, $9, $5, $4);
yy.setClass([$1], $3);
yy.setClass([$5], $7);
}
| entityName STYLE_SEPARATOR idList relSpec entityName COLON role
{
yy.addEntity($1);
yy.addEntity($5);
yy.addRelationship($1, $7, $5, $4);
yy.setClass([$1], $3);
}
| entityName relSpec entityName STYLE_SEPARATOR idList COLON role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $7, $3, $2);
yy.setClass([$3], $5);
}
| entityName BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1);
yy.addAttributes($1, $3);
}
| entityName STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1);
yy.addAttributes($1, $5);
yy.setClass([$1], $3);
}
| entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); }
| entityName STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1); yy.setClass([$1], $3); }
| entityName { yy.addEntity($1); }
| entityName STYLE_SEPARATOR idList { yy.addEntity($1); yy.setClass([$1], $3); }
| entityName SQS entityName SQE BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1, $3);
yy.addAttributes($1, $6);
}
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1, $3);
yy.addAttributes($1, $8);
yy.setClass([$1], $6);
}
| entityName SQS entityName SQE BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); }
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); }
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
| entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); }
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
| direction
| classDefStatement
| classStatement
| styleStatement
;
direction
: direction_tb
{ yy.setDirection('TB');}
| direction_bt
{ yy.setDirection('BT');}
| direction_rl
{ yy.setDirection('RL');}
| direction_lr
{ yy.setDirection('LR');}
;
classDefStatement
: CLASSDEF idList stylesOpt separator {$$ = $CLASSDEF;yy.addClass($idList,$stylesOpt);}
;
idList
: UNICODE_TEXT { $$ = [$UNICODE_TEXT]; }
| STYLE_TEXT { $$ = [$STYLE_TEXT]; }
| idList COMMA UNICODE_TEXT = { $$ = $idList.concat([$UNICODE_TEXT]); }
| idList COMMA STYLE_TEXT = { $$ = $idList.concat([$STYLE_TEXT]); }
;
classStatement
: CLASS idList idList {$$ = $CLASS;yy.setClass($2, $3);}
;
styleStatement
: STYLE idList stylesOpt separator {;$$ = $STYLE;yy.addCssStyles($2,$stylesOpt);}
;
stylesOpt
: style { $$ = [$style] }
| stylesOpt COMMA style {$stylesOpt.push($style);$$ = $stylesOpt;}
;
style
: styleComponent
| style styleComponent { $$ = $style + $styleComponent; }
;
separator
: SEMI
| NEWLINE
| EOF
;
styleComponent: STYLE_TEXT | NUM | COLON | BRKT;
entityName
: 'ALPHANUM' { $$ = $1; }
| 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
: 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
| 'UNICODE_TEXT' { $$ = $1; }
;
attributes
@@ -125,10 +236,10 @@ attributes
;
attribute
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
| attributeType attributeName attributeKeyTypeList { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3 }; }
| attributeType attributeName attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; }
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3, attributeComment: $4 }; }
: attributeType attributeName { $$ = { type: $1, name: $2 }; }
| attributeType attributeName attributeKeyTypeList { $$ = { type: $1, name: $2, keys: $3 }; }
| attributeType attributeName attributeComment { $$ = { type: $1, name: $2, comment: $3 }; }
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { type: $1, name: $2, keys: $3, comment: $4 }; }
;
@@ -142,7 +253,7 @@ attributeName
attributeKeyTypeList
: attributeKeyType { $$ = [$1]; }
| attributeKeyTypeList COMMA attributeKeyType { $1.push($3); $$ = $1; }
| attributeKeyTypeList ',' attributeKeyType { $1.push($3); $$ = $1; }
;
attributeKeyType
@@ -177,7 +288,7 @@ relType
role
: 'WORD' { $$ = $1.replace(/"/g, ''); }
| 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
| 'ALPHANUM' { $$ = $1; }
| 'UNICODE_TEXT' { $$ = $1; }
;
%%

View File

@@ -1,5 +1,5 @@
import { setConfig } from '../../../config.js';
import erDb from '../erDb.js';
import { ErDB } from '../erDb.js';
import erDiagram from './erDiagram.jison'; // jison file
setConfig({
@@ -7,6 +7,7 @@ setConfig({
});
describe('when parsing ER diagram it...', function () {
const erDb = new ErDB();
beforeEach(function () {
erDiagram.parser.yy = erDb;
erDiagram.parser.yy.clear();
@@ -143,32 +144,32 @@ describe('when parsing ER diagram it...', function () {
expect(entities.get(entity).alias).toBe(alias);
});
it('can have an alias even if the relationship is defined before class', function () {
it('can have an alias even if the relationship is defined before buzz', function () {
const firstEntity = 'foo';
const secondEntity = 'bar';
const alias = 'batman';
erDiagram.parser.parse(
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nclass ${firstEntity}["${alias}"]\n`
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nbuzz ${firstEntity}["${alias}"]\n`
);
const entities = erDb.getEntities();
expect(entities.has(firstEntity)).toBe(true);
expect(entities.has(secondEntity)).toBe(true);
expect(entities.get(firstEntity).alias).toBe(alias);
expect(entities.get(secondEntity).alias).toBeUndefined();
expect(entities.get(secondEntity).alias).toBe('');
});
it('can have an alias even if the relationship is defined after class', function () {
it('can have an alias even if the relationship is defined after buzz', function () {
const firstEntity = 'foo';
const secondEntity = 'bar';
const alias = 'batman';
erDiagram.parser.parse(
`erDiagram\nclass ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
`erDiagram\nbuzz ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
);
const entities = erDb.getEntities();
expect(entities.has(firstEntity)).toBe(true);
expect(entities.has(secondEntity)).toBe(true);
expect(entities.get(firstEntity).alias).toBe(alias);
expect(entities.get(secondEntity).alias).toBeUndefined();
expect(entities.get(secondEntity).alias).toBe('');
});
it('can start with an underscore', function () {
@@ -193,9 +194,9 @@ describe('when parsing ER diagram it...', function () {
expect(entities.size).toBe(1);
expect(entities.get(entity).attributes.length).toBe(3);
expect(entities.get(entity).attributes[0].attributeName).toBe('myBookTitle');
expect(entities.get(entity).attributes[1].attributeName).toBe('MYBOOKSUBTITLE_1');
expect(entities.get(entity).attributes[2].attributeName).toBe('author-ref[name](1)');
expect(entities.get(entity).attributes[0].name).toBe('myBookTitle');
expect(entities.get(entity).attributes[1].name).toBe('MYBOOKSUBTITLE_1');
expect(entities.get(entity).attributes[2].name).toBe('author-ref[name](1)');
});
it('should allow asterisk at the start of attribute name', function () {
@@ -258,7 +259,7 @@ describe('when parsing ER diagram it...', function () {
const entities = erDb.getEntities();
expect(entities.size).toBe(1);
expect(entities.get(entity).attributes.length).toBe(1);
expect(entities.get(entity).attributes[0].attributeComment).toBe('comment');
expect(entities.get(entity).attributes[0].comment).toBe('comment');
});
it('should allow an entity with a single attribute to be defined with a key and a comment', function () {
@@ -297,14 +298,14 @@ describe('when parsing ER diagram it...', function () {
`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n${attribute4}\n${attribute5}\n}`
);
const entities = erDb.getEntities();
expect(entities.get(entity).attributes[0].attributeKeyTypeList).toEqual(['PK', 'FK']);
expect(entities.get(entity).attributes[0].attributeComment).toBe('comment1');
expect(entities.get(entity).attributes[1].attributeKeyTypeList).toEqual(['PK', 'UK', 'FK']);
expect(entities.get(entity).attributes[2].attributeKeyTypeList).toEqual(['PK', 'UK']);
expect(entities.get(entity).attributes[2].attributeComment).toBe('comment3');
expect(entities.get(entity).attributes[3].attributeKeyTypeList).toBeUndefined();
expect(entities.get(entity).attributes[4].attributeKeyTypeList).toBeUndefined();
expect(entities.get(entity).attributes[4].attributeComment).toBe('comment5');
expect(entities.get(entity).attributes[0].keys).toEqual(['PK', 'FK']);
expect(entities.get(entity).attributes[0].comment).toBe('comment1');
expect(entities.get(entity).attributes[1].keys).toEqual(['PK', 'UK', 'FK']);
expect(entities.get(entity).attributes[2].keys).toEqual(['PK', 'UK']);
expect(entities.get(entity).attributes[2].comment).toBe('comment3');
expect(entities.get(entity).attributes[3].keys).toEqual([]);
expect(entities.get(entity).attributes[4].keys).toEqual([]);
expect(entities.get(entity).attributes[4].comment).toBe('comment5');
});
it('should allow an entity with attribute that has a generic type', function () {
@@ -341,8 +342,8 @@ describe('when parsing ER diagram it...', function () {
const entities = erDb.getEntities();
expect(entities.size).toBe(1);
expect(entities.get(entity).attributes.length).toBe(2);
expect(entities.get(entity).attributes[0].attributeType).toBe('character(10)');
expect(entities.get(entity).attributes[1].attributeType).toBe('varchar(5)');
expect(entities.get(entity).attributes[0].type).toBe('character(10)');
expect(entities.get(entity).attributes[1].type).toBe('varchar(5)');
});
it('should allow an entity with multiple attributes to be defined', function () {
@@ -764,6 +765,203 @@ describe('when parsing ER diagram it...', function () {
}).toThrowError();
});
it('should be possible to apply a style to an entity', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(`erDiagram
${entityName}
style ${entityName} color:red
`);
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red']);
});
it('should be possible to apply multiple styles to an entity at the same time', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram
${entityName}
style ${entityName} color:red,stroke:blue,fill:#f9f
`
);
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'stroke:blue', 'fill:#f9f']);
});
it('should be possible to apply multiple separately defined styles', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram
${entityName}
style ${entityName} color:red
style ${entityName} fill:#f9f
`
);
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'fill:#f9f']);
});
it('should be possible to assign a class to an entity', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(`erDiagram\n${entityName}\nclass ${entityName} myClass`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign multiple classes to an entity at the same time', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram\n${entityName}\nclass ${entityName} firstClass, secondClass, thirdClass`
);
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass thirdClass');
});
it('should be possible to assign multiple separately defined classes to an entity', function () {
const entityName = 'CUSTOMER';
erDiagram.parser.parse(
`erDiagram\n${entityName}\nclass ${entityName} firstClass\nclass ${entityName} secondClass`
);
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass');
});
it('should be possible to configure the default class and have it apply to each entity', function () {
const firstEntity = 'ENTITY1';
const secondEntity = 'ENTITY2';
erDiagram.parser.parse(
`erDiagram
${firstEntity}
${secondEntity}
classDef default fill:#f9f
`
);
const expectedOutput = new Map([
[
'default',
{
id: 'default',
styles: ['fill:#f9f'],
textStyles: [],
},
],
]);
expect(erDb.getEntity(firstEntity).cssClasses).toBe('default');
expect(erDb.getEntity(secondEntity).cssClasses).toBe('default');
expect(erDb.getClasses()).toEqual(expectedOutput);
});
it('should be possible to define a class with styles', function () {
const className = 'myClass';
const styles = 'fill:#f9f, stroke: red, color: pink';
erDiagram.parser.parse(
`erDiagram
classDef ${className} ${styles}
`
);
const expectedOutput = new Map([
[
className,
{
id: className,
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
textStyles: ['color:pink'],
},
],
]);
expect(erDb.getClasses()).toEqual(expectedOutput);
});
it('should be possible to define multiple class with styles at the same time', function () {
const firstClass = 'firstClass';
const secondClass = 'secondClass';
const styles = 'fill:#f9f, stroke: red, color: pink';
erDiagram.parser.parse(
`erDiagram
classDef ${firstClass},${secondClass} ${styles}
`
);
const expectedOutput = new Map([
[
firstClass,
{
id: firstClass,
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
textStyles: ['color:pink'],
},
],
[
secondClass,
{
id: secondClass,
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
textStyles: ['color:pink'],
},
],
]);
expect(erDb.getClasses()).toEqual(expectedOutput);
});
it('should be possible to assign a class using the shorthand syntax just by itself', function () {
const entityName = 'CUSTOMER';
const className = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign a class using the shorthand syntax with empty block', function () {
const entityName = 'CUSTOMER';
const className = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign a class using the shorthand syntax with block of attributes', function () {
const entityName = 'CUSTOMER';
const className = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {\nstring name\n}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign multiple classes using the shorthand syntax', function () {
const entityName = 'CUSTOMER';
const firstClass = 'firstClass';
const secondClass = 'secondClass';
erDiagram.parser.parse(`erDiagram\n${entityName}:::${firstClass},${secondClass}`);
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass');
});
it('should be possible to assign classes using the shorthand syntax after defining an alias', function () {
const entityName = 'c';
const entityAlias = 'CUSTOMER';
const myClass = 'myClass';
erDiagram.parser.parse(`erDiagram\n${entityName}[${entityAlias}]:::${myClass}`);
expect(erDb.getEntity(entityName).alias).toBe(entityAlias);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
});
it('should be possible to assign classes using the shorthand syntax while defining a relationship', function () {
const entityName = 'CUSTOMER';
const otherEntity = 'PERSON';
const myClass = 'myClass';
erDiagram.parser.parse(
`erDiagram\n${entityName}:::${myClass} ||--o{ ${otherEntity}:::${myClass} : allows`
);
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
expect(erDb.getEntity(otherEntity).cssClasses).toBe('default myClass');
});
describe('relationship labels', function () {
it('should allow an empty quoted label', function () {
erDiagram.parser.parse('erDiagram\nCUSTOMER ||--|{ ORDER : ""');

View File

@@ -1,49 +0,0 @@
const getStyles = (options) =>
`
.entityBox {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
}
.attributeBoxOdd {
fill: ${options.attributeBackgroundColorOdd};
stroke: ${options.nodeBorder};
}
.attributeBoxEven {
fill: ${options.attributeBackgroundColorEven};
stroke: ${options.nodeBorder};
}
.relationshipLabelBox {
fill: ${options.tertiaryColor};
opacity: 0.7;
background-color: ${options.tertiaryColor};
rect {
opacity: 0.5;
}
}
.relationshipLine {
stroke: ${options.lineColor};
}
.entityTitleText {
text-anchor: middle;
font-size: 18px;
fill: ${options.textColor};
}
#MD_PARENT_START {
fill: #f5f5f5 !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
#MD_PARENT_END {
fill: #f5f5f5 !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
`;
export default getStyles;

View File

@@ -0,0 +1,73 @@
import * as khroma from 'khroma';
import type { FlowChartStyleOptions } from '../flowchart/styles.js';
const fade = (color: string, opacity: number) => {
// @ts-ignore TODO: incorrect types from khroma
const channel = khroma.channel;
const r = channel(color, 'r');
const g = channel(color, 'g');
const b = channel(color, 'b');
// @ts-ignore incorrect types from khroma
return khroma.rgba(r, g, b, opacity);
};
const getStyles = (options: FlowChartStyleOptions) =>
`
.entityBox {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
}
.relationshipLabelBox {
fill: ${options.tertiaryColor};
opacity: 0.7;
background-color: ${options.tertiaryColor};
rect {
opacity: 0.5;
}
}
.labelBkg {
background-color: ${fade(options.tertiaryColor, 0.5)};
}
.edgeLabel .label {
fill: ${options.nodeBorder};
font-size: 14px;
}
.label {
font-family: ${options.fontFamily};
color: ${options.nodeTextColor || options.textColor};
}
.edge-pattern-dashed {
stroke-dasharray: 8,8;
}
.node rect,
.node circle,
.node ellipse,
.node polygon
{
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
.relationshipLine {
stroke: ${options.lineColor};
stroke-width: 1;
fill: none;
}
.marker {
fill: none !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
`;
export default getStyles;

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { type Ref, ref, onMounted } from 'vue';
interface Taglines {
label: string;
@@ -93,13 +93,22 @@ const allTaglines: { [key: string]: { design: number; taglines: Taglines[] } } =
},
};
const { design, taglines } =
Object.values(allTaglines)[Math.floor(Math.random() * Object.values(allTaglines).length)];
// Initialize with default values
const design: Ref<number> = ref(1);
const taglines: Ref<Taglines[]> = ref([]);
const index: Ref<number> = ref(0);
let index = ref(Math.floor(Math.random() * taglines.length));
onMounted(() => {
// Select a random variant on client side
const variant =
Object.values(allTaglines)[Math.floor(Math.random() * Object.values(allTaglines).length)];
design.value = variant.design;
taglines.value = variant.taglines;
index.value = Math.floor(Math.random() * taglines.value.length);
// Set up the interval for cycling through taglines
setInterval(() => {
index.value = (index.value + 1) % taglines.length;
index.value = (index.value + 1) % taglines.value.length;
}, 5_000);
});
</script>
@@ -109,16 +118,17 @@ onMounted(() => {
:class="[design === 1 ? 'bg-gradient-to-r from-[#bd34fe] to-[#ff3670] ' : 'bg-[#E0095F]']"
class="mb-4 w-full top-bar flex p-2"
>
<p class="w-full tracking-wide fade-text">
<p class="w-full tracking-wide fade-text text-sm">
<transition name="fade" mode="out-in">
<a
v-if="taglines.length > 0 && taglines[index]"
:key="index"
:href="taglines[index].url"
target="_blank"
class="unstyled flex justify-center items-center gap-4 text-white tracking-wide plausible-event-name=bannerClick"
>
<span class="font-semibold">{{ taglines[index].label }}</span>
<button class="bg-[#1E1A2E] rounded-lg p-1.5 px-4 font-semibold tracking-wide">
<button class="bg-[#1E1A2E] shrink-0 rounded-lg p-1.5 px-4 font-semibold tracking-wide">
Try now
</button>
</a>

View File

@@ -65,6 +65,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Notion](https://notion.so) ✅
- [Observable](https://observablehq.com/@observablehq/mermaid) ✅
- [Obsidian](https://help.obsidian.md/Editing+and+formatting/Advanced+formatting+syntax#Diagram) ✅
- [Outline](https://docs.getoutline.com/s/guide/doc/diagrams-KQiKoT4wzK) ✅
- [Redmine](https://redmine.org)
- [Mermaid Macro](https://www.redmine.org/plugins/redmine_mermaid_macro)
- [Markdown for mermaid plugin](https://github.com/jamieh-mongolian/markdown-for-mermaid-plugin)

View File

@@ -56,7 +56,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to
Where:
- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v10.5.0+), and may also contain digits and hyphens.
- `first-entity` is the name of an entity. Names support any unicode characters and can include spaces if surrounded by double quotes (e.g. "name with space").
- `relationship` describes the way that both entities inter-relate. See below.
- `second-entity` is the name of the other entity.
- `relationship-label` describes the relationship from the perspective of the first entity.
@@ -71,6 +71,24 @@ This statement can be read as _a property contains one or more rooms, and a room
Only the `first-entity` part of a statement is mandatory. This makes it possible to show an entity with no relationships, which can be useful during iterative construction of diagrams. If any other parts of a statement are specified, then all parts are mandatory.
#### Unicode text
Entity names, relationships, and attributes all support unicode text.
```mermaid-example
erDiagram
"This ❤ Unicode"
```
#### Markdown formatting
Markdown formatting and text is also supported.
```mermaid-example
erDiagram
"This **is** _Markdown_"
```
### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components:
@@ -109,6 +127,11 @@ Cardinality is a property that describes how many elements of another entity can
Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line:
| Value | Alias for |
| :---: | :---------------: |
| -- | _identifying_ |
| .. | _non-identifying_ |
**Aliases**
| Value | Alias for |
@@ -116,10 +139,16 @@ Relationships may be classified as either _identifying_ or _non-identifying_ and
| to | _identifying_ |
| optionally to | _non-identifying_ |
```mermaid
```mermaid-example
erDiagram
CAR ||--o{ NAMED-DRIVER : allows
PERSON ||--o{ NAMED-DRIVER : is
PERSON }o..o{ NAMED-DRIVER : is
```
```mermaid-example
erDiagram
CAR 1 to zero or more NAMED-DRIVER : allows
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
```
### Attributes
@@ -144,9 +173,9 @@ erDiagram
The `type` values must begin with an alphabetic character and may contain digits, hyphens, underscores, parentheses and square brackets. The `name` values follow a similar format to `type`, but may start with an asterisk as another option to indicate an attribute is a primary key. Other than that, there are no restrictions, and there is no implicit set of valid data types.
### Entity Name Aliases (v10.5.0+)
### Entity Name Aliases
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name.
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. Alias names follow all of the same rules as entity names.
```mermaid-example
erDiagram
@@ -162,7 +191,7 @@ erDiagram
#### Attribute Keys and Comments
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key (markdown formatting and unicode is not supported for keys). To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
```mermaid-example
erDiagram
@@ -188,35 +217,211 @@ erDiagram
MANUFACTURER only one to zero or more CAR : makes
```
### Other Things
### Direction
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
- (v11.1.0+) If you want a multi-line label on a relationship, use `<br />` between the two lines (`"first line<br />second line"`)
The direction statement declares the direction of the diagram.
## Styling
This declares that the diagram is oriented from top to bottom (`TB`). This can be reversed to be oriented from bottom to top (`BT`).
### Config options
```mermaid-example
erDiagram
direction TB
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
For simple color customization:
This declares that the diagram is oriented from left to right (`LR`). This can be reversed to be oriented from right to left (`RL`).
| Name | Used as |
| :------- | :------------------------------------------------------------------- |
| `fill` | Background color of an entity or attribute |
| `stroke` | Border color of an entity or attribute, line color of a relationship |
```mermaid-example
erDiagram
direction LR
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string name
string custNumber
string sector
}
ORDER ||--|{ LINE-ITEM : contains
ORDER {
int orderNumber
string deliveryAddress
}
LINE-ITEM {
string productCode
int quantity
float pricePerUnit
}
```
### Classes used
Possible diagram orientations are:
The following CSS class selectors are available for richer styling:
- TB - Top to bottom
- BT - Bottom to top
- RL - Right to left
- LR - Left to right
| Selector | Description |
| :------------------------- | :---------------------------------------------------- |
| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows |
| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows |
| `.er.entityBox` | The box representing an entity |
| `.er.entityLabel` | The label for an entity |
| `.er.relationshipLabel` | The label for a relationship |
| `.er.relationshipLabelBox` | The box surrounding a relationship label |
| `.er.relationshipLine` | The line representing a relationship between entities |
### Styling a node
It is possible to apply specific styles such as a thicker border or a different background color to a node.
```mermaid-example
erDiagram
id1||--||id2 : label
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
```
It is also possible to attach styles to a list of nodes in one statement:
```
style nodeId1,nodeId2 styleList
```
#### 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.
A class definition looks like the example below:
```
classDef className fill:#f9f,stroke:#333,stroke-width:4px
```
It is also possible to define multiple classes in one statement:
```
classDef firstClassName,secondClassName font-size:12pt
```
Attachment of a class to a node is done as per below:
```
class nodeId1 className
```
It is also possible to attach a class to a list of nodes in one statement:
```
class nodeId1,nodeId2 className
```
Multiple classes can be attached at the same time as well:
```
class nodeId1,nodeId2 className1,className2
```
A shorter form of adding a class is to attach the classname to the node using the `:::`operator as per below:
```mermaid-example
erDiagram
direction TB
CAR:::someclass {
string registrationNumber
string make
string model
}
PERSON:::someclass {
string firstName
string lastName
int age
}
HOUSE:::someclass
classDef someclass fill:#f96
```
This form can be used when declaring relationships between entities:
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
Similar to the class statement, the shorthand syntax can also apply multiple classes at once:
```
nodeId:::className1,className2
```
### Default class
If a class is named default it will be assigned to all classes without specific class definitions.
```
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
```
> **Note:** Custom styles from style or other class statements take priority and will overwrite the default styles. (e.g. The `default` class gives nodes a background color of pink but the `blue` class will give that node a background color of blue if applied.)
```mermaid-example
erDiagram
CAR {
string registrationNumber
string make
string model
}
PERSON {
string firstName
string lastName
int age
}
PERSON:::foo ||--|| CAR : owns
PERSON o{--|| HOUSE:::bar : has
classDef default fill:#f9f,stroke-width:4px
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef foobar stroke:#00f
```
## Configuration
### Renderer
The layout of the diagram is done with the renderer. The default renderer is dagre.
You can opt to use an alternate renderer named elk by editing the configuration. The elk renderer is better for larger and/or more complex diagrams.
```
---
config:
layout: elk
---
```
```note
Note that the site needs to use mermaid version 9.4+ for this to work and have this featured enabled in the lazy-loading configuration.
```
<!--- cspell:locale en,en-gb --->

View File

@@ -346,6 +346,7 @@ export const render = async (data4Layout, svg) => {
edge1.label = '';
edge1.arrowTypeEnd = 'none';
edge1.id = nodeId + '-cyclic-special-1';
edgeMid.arrowTypeStart = 'none';
edgeMid.arrowTypeEnd = 'none';
edgeMid.id = nodeId + '-cyclic-special-mid';
edge2.label = '';
@@ -354,6 +355,7 @@ export const render = async (data4Layout, svg) => {
edge2.toCluster = nodeId;
}
edge2.id = nodeId + '-cyclic-special-2';
edge2.arrowTypeStart = 'none';
graph.setEdge(nodeId, specialId1, edge1, nodeId + '-cyclic-special-0');
graph.setEdge(specialId1, specialId2, edgeMid, nodeId + '-cyclic-special-1');
graph.setEdge(specialId2, nodeId, edge2, nodeId + '-cyc<lic-special-2');

View File

@@ -15,28 +15,33 @@ export const addEdgeMarkers = (
edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>,
url: string,
id: string,
diagramType: string
diagramType: string,
strokeColor?: string
) => {
if (edge.arrowTypeStart) {
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType);
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType, strokeColor);
}
if (edge.arrowTypeEnd) {
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType);
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType, strokeColor);
}
};
const arrowTypesMap = {
arrow_cross: 'cross',
arrow_point: 'point',
arrow_barb: 'barb',
arrow_circle: 'circle',
aggregation: 'aggregation',
extension: 'extension',
composition: 'composition',
dependency: 'dependency',
lollipop: 'lollipop',
requirement_arrow: 'requirement_arrow',
requirement_contains: 'requirement_contains',
arrow_cross: { type: 'cross', fill: false },
arrow_point: { type: 'point', fill: true },
arrow_barb: { type: 'barb', fill: true },
arrow_circle: { type: 'circle', fill: false },
aggregation: { type: 'aggregation', fill: false },
extension: { type: 'extension', fill: false },
composition: { type: 'composition', fill: true },
dependency: { type: 'dependency', fill: true },
lollipop: { type: 'lollipop', fill: false },
only_one: { type: 'onlyOne', fill: false },
zero_or_one: { type: 'zeroOrOne', fill: false },
one_or_more: { type: 'oneOrMore', fill: false },
zero_or_more: { type: 'zeroOrMore', fill: false },
requirement_arrow: { type: 'requirement_arrow', fill: false },
requirement_contains: { type: 'requirement_contains', fill: false },
} as const;
const addEdgeMarker = (
@@ -45,15 +50,55 @@ const addEdgeMarker = (
arrowType: string,
url: string,
id: string,
diagramType: string
diagramType: string,
strokeColor?: string
) => {
const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
const arrowTypeInfo = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
if (!endMarkerType) {
if (!arrowTypeInfo) {
log.warn(`Unknown arrow type: ${arrowType}`);
return; // unknown arrow type, ignore
}
const endMarkerType = arrowTypeInfo.type;
const suffix = position === 'start' ? 'Start' : 'End';
svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`);
const originalMarkerId = `${id}_${diagramType}-${endMarkerType}${suffix}`;
// If stroke color is specified and non-empty, create or use a colored variant of the marker
if (strokeColor && strokeColor.trim() !== '') {
// Create a sanitized color value for use in IDs
const colorId = strokeColor.replace(/[^\dA-Za-z]/g, '_');
const coloredMarkerId = `${originalMarkerId}_${colorId}`;
// Check if the colored marker already exists
if (!document.getElementById(coloredMarkerId)) {
// Get the original marker
const originalMarker = document.getElementById(originalMarkerId);
if (originalMarker) {
// Clone the marker and create colored version
const coloredMarker = originalMarker.cloneNode(true) as Element;
coloredMarker.id = coloredMarkerId;
// Apply colors to the paths inside the marker
const paths = coloredMarker.querySelectorAll('path, circle, line');
paths.forEach((path) => {
path.setAttribute('stroke', strokeColor);
// Apply fill only to markers that should be filled
if (arrowTypeInfo.fill) {
path.setAttribute('fill', strokeColor);
}
});
// Add the new colored marker to the defs section
originalMarker.parentNode?.appendChild(coloredMarker);
}
}
// Use the colored marker
svgPath.attr(`marker-${position}`, `url(${url}#${coloredMarkerId})`);
} else {
// Always use the original marker for unstyled edges
svgPath.attr(`marker-${position}`, `url(${url}#${originalMarkerId})`);
}
};

View File

@@ -5,7 +5,8 @@ import { createText } from '../createText.js';
import utils from '../../utils.js';
import { getLineFunctionsWithOffset } from '../../utils/lineWithOffset.js';
import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';
import { curveBasis, line, select } from 'd3';
import { curveBasis, curveLinear, curveCardinal, line, select } from 'd3';
import rough from 'roughjs';
import createLabel from './createLabel.js';
import { addEdgeMarkers } from './edgeMarker.ts';
@@ -472,8 +473,19 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
let lineData = points.filter((p) => !Number.isNaN(p.y));
lineData = fixCorners(lineData);
let curve = curveBasis;
if (edge.curve) {
curve = edge.curve;
curve = curveLinear;
switch (edge.curve) {
case 'linear':
curve = curveLinear;
break;
case 'basis':
curve = curveBasis;
break;
case 'cardinal':
curve = curveCardinal;
break;
default:
curve = curveBasis;
}
const { x, y } = getLineFunctionsWithOffset(edge);
@@ -509,6 +521,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
let svgPath;
let linePath = lineFunction(lineData);
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
if (edge.look === 'handDrawn') {
const rc = rough.svg(elem);
@@ -539,18 +552,18 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
if (edge.animation) {
animationClass = ' edge-animation-' + edge.animation;
}
const pathStyle = stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles;
svgPath = elem
.append('path')
.attr('d', linePath)
.attr('id', edge.id)
.attr(
'class',
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? animationClass : '')
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
)
.attr('style', stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles);
.attr('style', pathStyle);
strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1];
}
// DEBUG code, DO NOT REMOVE
@@ -587,7 +600,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
log.info('arrowTypeStart', edge.arrowTypeStart);
log.info('arrowTypeEnd', edge.arrowTypeEnd);
addEdgeMarkers(svgPath, edge, url, id, diagramType);
addEdgeMarkers(svgPath, edge, url, id, diagramType, strokeColor);
let paths = {};
if (pointsHasChanged) {

View File

@@ -277,6 +277,129 @@ const barb = (elem, type, id) => {
.append('path')
.attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z');
};
// erDiagram specific markers
const only_one = (elem, type, id) => {
elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-onlyOneStart')
.attr('class', 'marker onlyOne ' + type)
.attr('refX', 0)
.attr('refY', 9)
.attr('markerWidth', 18)
.attr('markerHeight', 18)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M9,0 L9,18 M15,0 L15,18');
elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-onlyOneEnd')
.attr('class', 'marker onlyOne ' + type)
.attr('refX', 18)
.attr('refY', 9)
.attr('markerWidth', 18)
.attr('markerHeight', 18)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M3,0 L3,18 M9,0 L9,18');
};
const zero_or_one = (elem, type, id) => {
const startMarker = elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-zeroOrOneStart')
.attr('class', 'marker zeroOrOne ' + type)
.attr('refX', 0)
.attr('refY', 9)
.attr('markerWidth', 30)
.attr('markerHeight', 18)
.attr('orient', 'auto');
startMarker
.append('circle')
.attr('fill', 'white') // Fill white for now?
.attr('cx', 21)
.attr('cy', 9)
.attr('r', 6);
startMarker.append('path').attr('d', 'M9,0 L9,18');
const endMarker = elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-zeroOrOneEnd')
.attr('class', 'marker zeroOrOne ' + type)
.attr('refX', 30)
.attr('refY', 9)
.attr('markerWidth', 30)
.attr('markerHeight', 18)
.attr('orient', 'auto');
endMarker
.append('circle')
.attr('fill', 'white') // Fill white for now?
.attr('cx', 9)
.attr('cy', 9)
.attr('r', 6);
endMarker.append('path').attr('d', 'M21,0 L21,18');
};
const one_or_more = (elem, type, id) => {
elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-oneOrMoreStart')
.attr('class', 'marker oneOrMore ' + type)
.attr('refX', 18)
.attr('refY', 18)
.attr('markerWidth', 45)
.attr('markerHeight', 36)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27');
elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-oneOrMoreEnd')
.attr('class', 'marker oneOrMore ' + type)
.attr('refX', 27)
.attr('refY', 18)
.attr('markerWidth', 45)
.attr('markerHeight', 36)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18');
};
const zero_or_more = (elem, type, id) => {
const startMarker = elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-zeroOrMoreStart')
.attr('class', 'marker zeroOrMore ' + type)
.attr('refX', 18)
.attr('refY', 18)
.attr('markerWidth', 57)
.attr('markerHeight', 36)
.attr('orient', 'auto');
startMarker.append('circle').attr('fill', 'white').attr('cx', 48).attr('cy', 18).attr('r', 6);
startMarker.append('path').attr('d', 'M0,18 Q18,0 36,18 Q18,36 0,18');
const endMarker = elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-zeroOrMoreEnd')
.attr('class', 'marker zeroOrMore ' + type)
.attr('refX', 39)
.attr('refY', 18)
.attr('markerWidth', 57)
.attr('markerHeight', 36)
.attr('orient', 'auto');
endMarker.append('circle').attr('fill', 'white').attr('cx', 9).attr('cy', 18).attr('r', 6);
endMarker.append('path').attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
};
const requirement_arrow = (elem, type, id) => {
elem
.append('defs')
@@ -326,6 +449,10 @@ const markers = {
circle,
cross,
barb,
only_one,
zero_or_one,
one_or_more,
zero_or_more,
requirement_arrow,
requirement_contains,
};

View File

@@ -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 { erBox } from './shapes/erBox.js';
import { classBox } from './shapes/classBox.js';
import { requirementBox } from './shapes/requirementBox.js';
import { kanbanItem } from './shapes/kanbanItem.js';
@@ -478,6 +479,9 @@ const generateShapeMap = () => {
// class diagram
classBox,
// er diagram
erBox,
// Requirement diagram
requirementBox,
} as const;

View File

@@ -0,0 +1,359 @@
import { updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { drawRect } from './drawRect.js';
import { getConfig } from '../../../config.js';
import type { EntityNode } from '../../../diagrams/er/erTypes.js';
import { createText } from '../../createText.js';
import { evaluate, parseGenericTypes } from '../../../diagrams/common/common.js';
import { select } from 'd3';
import { calculateTextWidth } from '../../../utils.js';
import type { MermaidConfig } from '../../../config.type.js';
import type { D3Selection } from '../../../types.js';
export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
// Treat node as entityNode for certain entityNode checks
const entityNode = node as unknown as EntityNode;
if (entityNode.alias) {
node.label = entityNode.alias;
}
// Background shapes are drawn to fill in the background color and cover up the ER diagram edge markers.
// Draw background shape once.
if (node.look === 'handDrawn') {
const { themeVariables } = getConfig();
const { background } = themeVariables;
const backgroundNode = {
...node,
id: node.id + '-background',
look: 'default',
cssStyles: ['stroke: none', `fill: ${background}`],
};
await erBox(parent, backgroundNode);
}
const config = getConfig();
node.useHtmlLabels = config.htmlLabels;
let PADDING = config.er?.diagramPadding ?? 10;
let TEXT_PADDING = config.er?.entityPadding ?? 6;
const { cssStyles } = node;
const { labelStyles } = styles2String(node);
// Draw rect if no attributes are found
if (entityNode.attributes.length === 0 && node.label) {
const options = {
rx: 0,
ry: 0,
labelPaddingX: PADDING,
labelPaddingY: PADDING * 1.5,
classes: '',
};
// Set minimum width
if (
calculateTextWidth(node.label, config) + options.labelPaddingX * 2 <
config.er!.minEntityWidth!
) {
node.width = config.er!.minEntityWidth;
}
const shapeSvg = await drawRect(parent, node, options);
// drawRect doesn't center non-htmlLabels correctly as of now, so translate label
if (!evaluate(config.htmlLabels)) {
const textElement = shapeSvg.select('text');
const bbox = (textElement.node() as SVGTextElement)?.getBBox();
textElement.attr('transform', `translate(${-bbox.width / 2}, 0)`);
}
return shapeSvg;
}
if (!config.htmlLabels) {
PADDING *= 1.25;
TEXT_PADDING *= 1.25;
}
let cssClasses = getNodeClasses(node);
if (!cssClasses) {
cssClasses = 'node default';
}
const shapeSvg = parent
// @ts-ignore Ignore .insert on SVGAElement
.insert('g')
.attr('class', cssClasses)
.attr('id', node.domId || node.id);
const nameBBox = await addText(shapeSvg, node.label ?? '', config, 0, 0, ['name'], labelStyles);
nameBBox.height += TEXT_PADDING;
let yOffset = 0;
const yOffsets = [];
let maxTypeWidth = 0;
let maxNameWidth = 0;
let maxKeysWidth = 0;
let maxCommentWidth = 0;
let keysPresent = true;
let commentPresent = true;
for (const attribute of entityNode.attributes) {
const typeBBox = await addText(
shapeSvg,
attribute.type,
config,
0,
yOffset,
['attribute-type'],
labelStyles
);
maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width + PADDING);
const nameBBox = await addText(
shapeSvg,
attribute.name,
config,
0,
yOffset,
['attribute-name'],
labelStyles
);
maxNameWidth = Math.max(maxNameWidth, nameBBox.width + PADDING);
const keysBBox = await addText(
shapeSvg,
attribute.keys.join(),
config,
0,
yOffset,
['attribute-keys'],
labelStyles
);
maxKeysWidth = Math.max(maxKeysWidth, keysBBox.width + PADDING);
const commentBBox = await addText(
shapeSvg,
attribute.comment,
config,
0,
yOffset,
['attribute-comment'],
labelStyles
);
maxCommentWidth = Math.max(maxCommentWidth, commentBBox.width + PADDING);
yOffset +=
Math.max(typeBBox.height, nameBBox.height, keysBBox.height, commentBBox.height) +
TEXT_PADDING;
yOffsets.push(yOffset);
}
yOffsets.pop();
let totalWidthSections = 4;
if (maxKeysWidth <= PADDING) {
keysPresent = false;
maxKeysWidth = 0;
totalWidthSections--;
}
if (maxCommentWidth <= PADDING) {
commentPresent = false;
maxCommentWidth = 0;
totalWidthSections--;
}
const shapeBBox = shapeSvg.node()!.getBBox();
// Add extra padding to attribute components to accommodate for difference in width
if (
nameBBox.width + PADDING * 2 - (maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth) >
0
) {
const difference =
nameBBox.width + PADDING * 2 - (maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth);
maxTypeWidth += difference / totalWidthSections;
maxNameWidth += difference / totalWidthSections;
if (maxKeysWidth > 0) {
maxKeysWidth += difference / totalWidthSections;
}
if (maxCommentWidth > 0) {
maxCommentWidth += difference / totalWidthSections;
}
}
const maxWidth = maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth;
// @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 = Math.max(shapeBBox.width + PADDING * 2, node?.width || 0, maxWidth);
const h = Math.max(shapeBBox.height + (yOffsets[0] || yOffset) + TEXT_PADDING, node?.height || 0);
const x = -w / 2;
const y = -h / 2;
// Translate attribute text labels
shapeSvg.selectAll('g:not(:first-child)').each((_: any, i: number, nodes: any) => {
const text = select<any, unknown>(nodes[i]);
const transform = text.attr('transform');
let translateX = 0;
let translateY = 0;
if (transform) {
const regex = RegExp(/translate\(([^,]+),([^)]+)\)/);
const translate = regex.exec(transform);
if (translate) {
translateX = parseFloat(translate[1]);
translateY = parseFloat(translate[2]);
if (text.attr('class').includes('attribute-name')) {
translateX += maxTypeWidth;
} else if (text.attr('class').includes('attribute-keys')) {
translateX += maxTypeWidth + maxNameWidth;
} else if (text.attr('class').includes('attribute-comment')) {
translateX += maxTypeWidth + maxNameWidth + maxKeysWidth;
}
}
}
text.attr(
'transform',
`translate(${x + PADDING / 2 + translateX}, ${translateY + y + nameBBox.height + TEXT_PADDING / 2})`
);
});
// Center the name
shapeSvg
.select('.name')
.attr('transform', 'translate(' + -nameBBox.width / 2 + ', ' + (y + TEXT_PADDING / 2) + ')');
// Draw shape
const roughRect = rc.rectangle(x, y, w, h, options);
const rect = shapeSvg.insert(() => roughRect, ':first-child').attr('style', cssStyles!.join(''));
const { themeVariables } = getConfig();
const { rowEven, rowOdd, nodeBorder } = themeVariables;
yOffsets.push(0);
// Draw row rects
for (const [i, yOffset] of yOffsets.entries()) {
if (i === 0 && yOffsets.length > 1) {
continue;
// Skip first row
}
const isEven = i % 2 === 0 && yOffset !== 0;
const roughRect = rc.rectangle(x, nameBBox.height + y + yOffset, w, nameBBox.height, {
...options,
fill: isEven ? rowEven : rowOdd,
stroke: nodeBorder,
});
shapeSvg
.insert(() => roughRect, 'g.label')
.attr('style', cssStyles!.join(''))
.attr('class', `row-rect-${i % 2 === 0 ? 'even' : 'odd'}`);
}
// Draw divider lines
// Name line
let roughLine = rc.line(x, nameBBox.height + y, w + x, nameBBox.height + y, options);
shapeSvg.insert(() => roughLine).attr('class', 'divider');
// First line
roughLine = rc.line(maxTypeWidth + x, nameBBox.height + y, maxTypeWidth + x, h + y, options);
shapeSvg.insert(() => roughLine).attr('class', 'divider');
// Second line
if (keysPresent) {
roughLine = rc.line(
maxTypeWidth + maxNameWidth + x,
nameBBox.height + y,
maxTypeWidth + maxNameWidth + x,
h + y,
options
);
shapeSvg.insert(() => roughLine).attr('class', 'divider');
}
// Third line
if (commentPresent) {
roughLine = rc.line(
maxTypeWidth + maxNameWidth + maxKeysWidth + x,
nameBBox.height + y,
maxTypeWidth + maxNameWidth + maxKeysWidth + x,
h + y,
options
);
shapeSvg.insert(() => roughLine).attr('class', 'divider');
}
// Attribute divider lines
for (const yOffset of yOffsets) {
roughLine = rc.line(
x,
nameBBox.height + y + yOffset,
w + x,
nameBBox.height + y + yOffset,
options
);
shapeSvg.insert(() => roughLine).attr('class', 'divider');
}
updateNodeBounds(node, rect);
node.intersect = function (point) {
return intersect.rect(node, point);
};
return shapeSvg;
}
// Helper function to add label text g with translate position and style
async function addText<T extends SVGGraphicsElement>(
shapeSvg: D3Selection<T>,
labelText: string,
config: MermaidConfig,
translateX = 0,
translateY = 0,
classes: string[] = [],
style = ''
) {
const label = shapeSvg
.insert('g')
.attr('class', `label ${classes.join(' ')}`)
.attr('transform', `translate(${translateX}, ${translateY})`)
.attr('style', style);
// Return types need to be parsed
if (labelText !== parseGenericTypes(labelText)) {
labelText = parseGenericTypes(labelText);
// Work around
labelText = labelText.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
const text = label.node()!.appendChild(
await createText(
label,
labelText,
{
width: calculateTextWidth(labelText, config) + 100,
style,
useHtmlLabels: config.htmlLabels,
},
config
)
);
// Undo work around now that text passed through correctly
if (labelText.includes('&lt;') || labelText.includes('&gt;')) {
let child = text.children[0];
child.textContent = child.textContent.replaceAll('&lt;', '<').replaceAll('&gt;', '>');
while (child.childNodes[0]) {
child = child.childNodes[0];
// Replace its text content
child.textContent = child.textContent.replaceAll('&lt;', '<').replaceAll('&gt;', '>');
}
}
let bbox = text.getBBox();
if (evaluate(config.htmlLabels)) {
const div = text.children[0];
div.style.textAlign = 'start';
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
return bbox;
}

View File

@@ -1300,6 +1300,14 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: integer
default: 15
minimum: 0
nodeSpacing:
type: integer
default: 140
minimum: 0
rankSpacing:
type: integer
default: 80
minimum: 0
stroke:
description: Stroke color of box edges and lines.
type: string

View File

@@ -110,6 +110,16 @@ class Theme {
this.personBorder = this.personBorder || this.primaryBorderColor;
this.personBkg = this.personBkg || this.mainBkg;
/* ER diagram */
if (this.darkMode) {
this.rowOdd = this.rowOdd || darken(this.mainBkg, 5) || '#ffffff';
this.rowEven = this.rowEven || darken(this.mainBkg, 10);
} else {
this.rowOdd = this.rowOdd || lighten(this.mainBkg, 75) || '#ffffff';
this.rowEven = this.rowEven || lighten(this.mainBkg, 5);
}
/* state colors */
this.transitionColor = this.transitionColor || this.lineColor;
this.transitionLabelColor = this.transitionLabelColor || this.textColor;

View File

@@ -91,6 +91,10 @@ class Theme {
this.archGroupBorderColor = this.primaryBorderColor;
this.archGroupBorderWidth = '2px';
/* Entity Relationship variables */
this.rowOdd = this.rowOdd || lighten(this.mainBkg, 5) || '#ffffff';
this.rowEven = this.rowEven || darken(this.mainBkg, 10);
/* state colors */
this.labelColor = 'calculated';

View File

@@ -119,6 +119,10 @@ class Theme {
this.archGroupBorderColor = this.primaryBorderColor;
this.archGroupBorderWidth = '2px';
/* Entity Relationship variables */
this.rowOdd = 'calculated';
this.rowEven = 'calculated';
/* state colors */
this.labelColor = 'black';
this.errorBkgColor = '#552222';
@@ -205,6 +209,9 @@ class Theme {
this.archEdgeColor = this.lineColor;
this.archEdgeArrowColor = this.lineColor;
/* Entity Relationship variables */
this.rowOdd = this.rowOdd || lighten(this.primaryColor, 75) || '#ffffff';
this.rowEven = this.rowEven || lighten(this.primaryColor, 1);
/* state colors */
this.transitionColor = this.transitionColor || this.lineColor;
this.transitionLabelColor = this.transitionLabelColor || this.textColor;
@@ -373,6 +380,13 @@ class Theme {
/* -------------------------------------------------- */
}
calculate(overrides) {
// for all keys in this object, if it is 'calculated' then set it to undefined
Object.keys(this).forEach((k) => {
if (this[k] === 'calculated') {
this[k] = undefined;
}
});
if (typeof overrides !== 'object') {
// Calculate colors form base colors
this.updateColors();

View File

@@ -173,6 +173,10 @@ class Theme {
this.archEdgeColor = this.lineColor;
this.archEdgeArrowColor = this.lineColor;
/* ER diagram */
this.rowOdd = this.rowOdd || lighten(this.mainBkg, 75) || '#ffffff';
this.rowEven = this.rowEven || lighten(this.mainBkg, 20);
/* state colors */
this.transitionColor = this.transitionColor || this.lineColor;
this.transitionLabelColor = this.transitionLabelColor || this.textColor;

View File

@@ -105,6 +105,10 @@ class Theme {
this.archGroupBorderColor = this.primaryBorderColor;
this.archGroupBorderWidth = '2px';
/* ER diagram */
this.rowOdd = this.rowOdd || lighten(this.mainBkg, 75) || '#ffffff';
this.rowEven = this.rowEven || '#f4f4f4';
/* state colors */
this.labelColor = 'black';

29
pnpm-lock.yaml generated
View File

@@ -20366,6 +20366,35 @@ snapshots:
- supports-color
- vue
unocss@0.59.4(postcss@8.4.41)(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0)):
dependencies:
'@unocss/astro': 0.59.4(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))
'@unocss/cli': 0.59.4(rollup@4.21.1)
'@unocss/core': 0.59.4
'@unocss/extractor-arbitrary-variants': 0.59.4
'@unocss/postcss': 0.59.4(postcss@8.4.41)
'@unocss/preset-attributify': 0.59.4
'@unocss/preset-icons': 0.59.4
'@unocss/preset-mini': 0.59.4
'@unocss/preset-tagify': 0.59.4
'@unocss/preset-typography': 0.59.4
'@unocss/preset-uno': 0.59.4
'@unocss/preset-web-fonts': 0.59.4
'@unocss/preset-wind': 0.59.4
'@unocss/reset': 0.59.4
'@unocss/transformer-attributify-jsx': 0.59.4
'@unocss/transformer-attributify-jsx-babel': 0.59.4
'@unocss/transformer-compile-class': 0.59.4
'@unocss/transformer-directives': 0.59.4
'@unocss/transformer-variant-group': 0.59.4
'@unocss/vite': 0.59.4(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))
optionalDependencies:
vite: 5.4.2(@types/node@22.5.1)(terser@5.33.0)
transitivePeerDependencies:
- postcss
- rollup
- supports-color
unpipe@1.0.0: {}
unplugin-utils@0.2.4: