Merge branch 'release/8.5.0'
This commit is contained in:
commit
5996920249
|
@ -18,4 +18,5 @@ dist/classTest.html
|
|||
dist/sequenceTest.html
|
||||
|
||||
.vscode/
|
||||
cypress/platform/current.html
|
||||
cypress/platform/current.html
|
||||
cypress/platform/experimental.html
|
41
README.md
41
README.md
|
@ -5,13 +5,46 @@
|
|||
<!-- </Remove this in the future> -->
|
||||
|
||||
# mermaid [![Build Status](https://travis-ci.org/mermaid-js/mermaid.svg?branch=master)](https://travis-ci.org/mermaid-js/mermaid) [![NPM](https://img.shields.io/npm/v/mermaid)](https://www.npmjs.com/package/mermaid) [![Coverage Status](https://coveralls.io/repos/github/mermaid-js/mermaid/badge.svg?branch=master)](https://coveralls.io/github/mermaid-js/mermaid?branch=master) [![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/Mermaid/mermaid)
|
||||
|
||||
<!-- <Main description> -->
|
||||
__Generate diagrams, charts, graphs or flows from markdown-like text via javascript.__
|
||||
See our [documentation](http://mermaid-js.github.io/mermaid/) and start simplifying yours. Play in our [live editor](https://mermaidjs.github.io/mermaid-live-editor/) or jump straight to the [installation and usage](http://mermaid-js.github.io/mermaid/#/usage).
|
||||
__mermaid is a Javascript based diagramming and charting tool. It generates diagrams flowcharts and more, using markdown-inspired text for ease and speed.__
|
||||
|
||||
Check out the list of [Integrations and Usages of Mermaid](https://github.com/mermaid-js/mermaid/blob/develop/docs/integrations.md)
|
||||
|
||||
For more information and help in getting started, please view our [documentation](http://mermaid-js.github.io/mermaid/) and start simplifying yours. Alternatively, you can also play with our [live editor](https://mermaidjs.github.io/mermaid-live-editor/).
|
||||
<!-- </Main description> -->
|
||||
|
||||
:trophy: **Mermaid was nominated and won the [JS Open Source Awards (2019)](https://osawards.com/javascript/#nominees) in the category "The most exciting use of technology"!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.**
|
||||
|
||||
## New diagrams in 8.4
|
||||
|
||||
With version 8.4 class diagrams have got some new features, bug fixes and documentation. Another new feature in 8.4 is the new diagram type, state diagrams.
|
||||
|
||||
![Image show the two new diagram types](./docs/img/new-diagrams.png)
|
||||
|
||||
## Special note regarding version 8.2
|
||||
|
||||
In version 8.2 a security improvement was introduced. A securityLevel configuration was introduced which sets the level of trust to be used on the parsed diagrams.
|
||||
|
||||
- **true**: (default) tags in text are encoded, click functionality is disabled
|
||||
- false: tags in text are allowed, click functionality is enabled
|
||||
|
||||
Closed issues:
|
||||
|
||||
⚠️ **Note** : This changes the default behaviour of mermaid so that after upgrade to 8.2, if the securityLevel is not configured, tags in flowcharts are encoded as tags and clicking is prohibited.
|
||||
|
||||
If your application is taking resposibility for the diagram source security you can set the securityLevel accordingly. By doing this clicks and tags are again allowed.
|
||||
|
||||
```javascript
|
||||
mermaidAPI.initialize({
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
```
|
||||
|
||||
For more information and help in getting started, please view our [documentation](http://mermaid-js.github.io/mermaid/) and start simplifying yours. Play with our [live editor](https://mermaidjs.github.io/mermaid-live-editor/) or jump straight to the [installation and usage](http://mermaid-js.github.io/mermaid/#/usage).
|
||||
<!-- </Main description> -->
|
||||
|
||||
:trophy: _"The most exciting use of technology"_ - [JS Open Source Awards (2019)](https://osawards.com/javascript/#nominees)
|
||||
|
||||
__The following are some examples of the diagrams, charts and graphs that can be made using mermaid and the Markdown-inspired text specific to it. Click here jump into the [text syntax](https://mermaid-js.github.io/mermaid/#/n00b-syntaxReference).__
|
||||
<table>
|
||||
<!-- <Flowchart> -->
|
||||
<tr><td colspan=2 align="center">
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/* eslint-env jest */
|
||||
import { imgSnapshotTest } from '../../helpers/util';
|
||||
|
||||
describe('Entity Relationship Diagram', () => {
|
||||
it('should render a simple ER diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render an ER diagram with a recursive relationship', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||..o{ CUSTOMER : refers
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render an ER diagram with multiple relationships between the same two entities', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--|{ ADDRESS : "invoiced at"
|
||||
CUSTOMER ||--|{ ADDRESS : "receives goods at"
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render a cyclical ER diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
erDiagram
|
||||
A ||--|{ B : likes
|
||||
B ||--|{ C : likes
|
||||
C ||--|{ A : likes
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('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"
|
||||
`,
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render multiple ER diagrams', () => {
|
||||
imgSnapshotTest(
|
||||
[
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`,
|
||||
`
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
`
|
||||
],
|
||||
{logLevel : 1}
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
});
|
|
@ -524,7 +524,8 @@ describe('Flowchart', () => {
|
|||
{ flowchart: { htmlLabels: false } }
|
||||
);
|
||||
});
|
||||
it('25: Handle link click events (link, anchor, mailto, other protocol, script)', () => {
|
||||
|
||||
it('25: Handle link click events (link, anchor, mailto, other protocol, script)', () => {
|
||||
imgSnapshotTest(
|
||||
`graph TB
|
||||
TITLE["Link Click Events<br>(click the nodes below)"]
|
||||
|
@ -583,6 +584,39 @@ it('25: Handle link click events (link, anchor, mailto, other protocol, script)'
|
|||
{ flowchart: { htmlLabels: false } }
|
||||
);
|
||||
});
|
||||
|
||||
it('28: Apply default class to all nodes which do not have another class assigned (htmlLabels enabled)', () => {
|
||||
imgSnapshotTest(
|
||||
`graph TD
|
||||
A[myClass1] --> B[default] & C[default]
|
||||
B[default] & C[default] --> D[myClass2]
|
||||
classDef default stroke-width:2px,fill:none,stroke:silver
|
||||
classDef node color:red
|
||||
classDef myClass1 color:#0000ff
|
||||
classDef myClass2 stroke:#0000ff,fill:#ccccff
|
||||
class A myClass1
|
||||
class D myClass2
|
||||
`,
|
||||
{ flowchart: { htmlLabels: true } }
|
||||
);
|
||||
});
|
||||
|
||||
it('29: Apply default class to all nodes which do not have another class assigned (htmlLabels disabled)', () => {
|
||||
imgSnapshotTest(
|
||||
`graph TD
|
||||
A[myClass1] --> B[default] & C[default]
|
||||
B[default] & C[default] --> D[myClass2]
|
||||
classDef default stroke-width:2px,fill:none,stroke:silver
|
||||
classDef node color:red
|
||||
classDef myClass1 color:#0000ff
|
||||
classDef myClass2 stroke:#0000ff,fill:#ccccff
|
||||
class A myClass1
|
||||
class D myClass2
|
||||
`,
|
||||
{ flowchart: { htmlLabels: false } }
|
||||
);
|
||||
});
|
||||
|
||||
it('30: Possibility to style text color of nodes and subgraphs as well as apply classes to subgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`graph LR
|
||||
|
|
|
@ -20,16 +20,8 @@
|
|||
<h1>info below</h1>
|
||||
<div style="display: flex;width: 100%; height: 100%">
|
||||
<div class="mermaid" style="width: 100%; height: 100%">
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
|
||||
flowchart LR
|
||||
A --> B
|
||||
</div>
|
||||
</div>
|
||||
<script src="./mermaid.js"></script>
|
||||
|
|
|
@ -384,6 +384,17 @@ graph TB
|
|||
click B "index.html#link-clicked" "link test"
|
||||
click D testClick "click test"
|
||||
</div>
|
||||
<div class="mermaid">
|
||||
graph TD
|
||||
A[myClass1] --> B[default] & C[default]
|
||||
B[default] & C[default] --> D[myClass2]
|
||||
classDef default stroke-width:2px,fill:none,stroke:silver
|
||||
classDef node color:red
|
||||
classDef myClass1 color:#0000ff
|
||||
classDef myClass2 stroke:#0000ff,fill:#ccccff
|
||||
class A myClass1
|
||||
class D myClass2
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
|
|
133
docs/README.md
133
docs/README.md
|
@ -1,38 +1,9 @@
|
|||
# Mermaid
|
||||
|
||||
[![Build Status](https://travis-ci.org/mermaid-js/mermaid.svg?branch=master)](https://travis-ci.org/mermaid-js/mermaid)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/knsv/mermaid/badge.svg?branch=master)](https://coveralls.io/github/knsv/mermaid?branch=master)
|
||||
[![Join the chat at https://gitter.im/knsv/mermaid](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/knsv/mermaid?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
# mermaid
|
||||
|
||||
## New diagrams in 8.4
|
||||
|
||||
With version 8.4 class diagrams has got some new features, bug fixes and documentation. Another new feature in 8.4 is the new diagram
|
||||
type, state diagrams.
|
||||
|
||||
![Image show the two new diagram types](./img/new-diagrams.png)
|
||||
|
||||
|
||||
## Special note regarding version 8.2
|
||||
|
||||
In version 8.2 a security improvement was introduced. A securityLevel configuration was introduced which sets the level of trust to be used on the parsed diagrams.
|
||||
|
||||
* **true**: (default) tags in text are encoded, click functionality is disabled
|
||||
* false: tags in text are allowed, click functionality is enabled
|
||||
|
||||
Closed issues:
|
||||
|
||||
⚠️ **Note** : This changes the default behaviour of mermaid so that after upgrade to 8.2, if the securityLevel is not configured, tags in flowcharts are encoded as tags and clicking is prohibited.
|
||||
|
||||
If your application is taking resposibility for the diagram source security you can set the securityLevel accordingly. By doing this clicks and tags are again allowed.
|
||||
|
||||
```javascript
|
||||
mermaidAPI.initialize({
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
```
|
||||
|
||||
**🖖 Keep a steady pulse: mermaid needs more Collaborators [#866](https://github.com/knsv/mermaid/issues/866)**
|
||||
|
||||
![banner](./img/header.png)
|
||||
|
||||
Generation of diagrams and flowcharts from text in a similar manner as markdown.
|
||||
|
@ -41,8 +12,39 @@ Ever wanted to simplify documentation and avoid heavy tools like Visio when expl
|
|||
|
||||
This is why mermaid was born, a simple markdown-like script language for generating charts from text via javascript.
|
||||
|
||||
Check out the list of [Integrations and Usages of Mermaid](./integrations.md)
|
||||
|
||||
**Mermaid was nominated and won the JS Open Source Awards (2019) in the category "The most exciting use of technology"!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.**
|
||||
|
||||
## New diagrams in 8.4
|
||||
|
||||
With version 8.4 class diagrams have got some new features, bug fixes and documentation. Another new feature in 8.4 is the new diagram type, state diagrams.
|
||||
|
||||
![Image show the two new diagram types](./img/new-diagrams.png)
|
||||
|
||||
## Special note regarding version 8.2
|
||||
|
||||
In version 8.2 a security improvement was introduced. A securityLevel configuration was introduced which sets the level of trust to be used on the parsed diagrams.
|
||||
|
||||
- **true**: (default) tags in text are encoded, click functionality is disabled
|
||||
- false: tags in text are allowed, click functionality is enabled
|
||||
|
||||
Closed issues:
|
||||
|
||||
⚠️ **Note** : This changes the default behaviour of mermaid so that after upgrade to 8.2, if the securityLevel is not configured, tags in flowcharts are encoded as tags and clicking is prohibited.
|
||||
|
||||
If your application is taking resposibility for the diagram source security you can set the securityLevel accordingly. By doing this clicks and tags are again allowed.
|
||||
|
||||
```javascript
|
||||
mermaidAPI.initialize({
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
```
|
||||
|
||||
**🖖 Keep a steady pulse: mermaid needs more Collaborators [#866](https://github.com/knsv/mermaid/issues/866)**
|
||||
|
||||
## Diagrams
|
||||
|
||||
### Flowchart
|
||||
|
||||
```
|
||||
|
@ -52,8 +54,8 @@ graph TD;
|
|||
B-->D;
|
||||
C-->D;
|
||||
```
|
||||
![Flowchart](./img/flow.png)
|
||||
|
||||
![Flowchart](./img/flow.png)
|
||||
|
||||
### Sequence diagram
|
||||
|
||||
|
@ -70,8 +72,8 @@ sequenceDiagram
|
|||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
```
|
||||
![Sequence diagram](./img/sequence.png)
|
||||
|
||||
![Sequence diagram](./img/sequence.png)
|
||||
|
||||
### Gantt diagram
|
||||
|
||||
|
@ -87,8 +89,8 @@ Active task :active, des2, 2014-01-09, 3d
|
|||
Future task : des3, after des2, 5d
|
||||
Future task2 : des4, after des3, 5d
|
||||
```
|
||||
![Gantt diagram](./img/gantt.png)
|
||||
|
||||
![Gantt diagram](./img/gantt.png)
|
||||
|
||||
### Class diagram - :exclamation: experimental
|
||||
|
||||
|
@ -108,8 +110,8 @@ Class01 : int chimp
|
|||
Class01 : int gorilla
|
||||
Class08 <--> C2: Cool label
|
||||
```
|
||||
![Class diagram](./img/class.png)
|
||||
|
||||
![Class diagram](./img/class.png)
|
||||
|
||||
### Git graph - :exclamation: experimental
|
||||
|
||||
|
@ -135,7 +137,6 @@ merge newbranch
|
|||
|
||||
![Git graph](./img/git.png)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### CDN
|
||||
|
@ -154,12 +155,10 @@ Example: https://unpkg.com/mermaid@7.1.0/dist/
|
|||
yarn add mermaid
|
||||
```
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
https://mermaidjs.github.io
|
||||
|
||||
|
||||
## Sibling projects
|
||||
|
||||
- [mermaid CLI](https://github.com/mermaidjs/mermaid.cli)
|
||||
|
@ -167,8 +166,7 @@ https://mermaidjs.github.io
|
|||
- [mermaid webpack demo](https://github.com/mermaidjs/mermaid-webpack-demo)
|
||||
- [mermaid Parcel demo](https://github.com/mermaidjs/mermaid-parcel-demo)
|
||||
|
||||
|
||||
# Request for assistance
|
||||
## Request for assistance
|
||||
|
||||
Things are piling up and I have a hard time keeping up. To remedy this
|
||||
it would be great if we could form a core team of developers to cooperate
|
||||
|
@ -178,61 +176,62 @@ As part of this team you would get write access to the repository and would
|
|||
represent the project when answering questions and issues.
|
||||
|
||||
Together we could continue the work with things like:
|
||||
* adding more types of diagrams like mindmaps, ert diagrams, etc.
|
||||
* improving existing diagrams
|
||||
|
||||
- Adding more types of diagrams like mindmaps, ert diagrams, etc.
|
||||
- Improving existing diagrams
|
||||
|
||||
Don't hesitate to contact me if you want to get involved.
|
||||
|
||||
## For contributors
|
||||
|
||||
# For contributors
|
||||
### Setup
|
||||
|
||||
## Setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
yarn install
|
||||
### Build
|
||||
|
||||
```
|
||||
yarn build:watch
|
||||
```
|
||||
|
||||
## Build
|
||||
### Lint
|
||||
|
||||
yarn build:watch
|
||||
|
||||
|
||||
## Lint
|
||||
|
||||
yarn lint
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
We use [eslint](https://eslint.org/).
|
||||
We recommend you installing [editor plugins](https://eslint.org/docs/user-guide/integrations) so you can get real time lint result.
|
||||
|
||||
### Test
|
||||
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
Manual test in browser: open `dist/index.html`
|
||||
|
||||
## Test
|
||||
|
||||
yarn test
|
||||
|
||||
Manual test in browser:
|
||||
|
||||
open dist/index.html
|
||||
|
||||
|
||||
## Release
|
||||
### Release
|
||||
|
||||
For those who have the permission to do so:
|
||||
|
||||
Update version number in `package.json`.
|
||||
|
||||
npm publish
|
||||
```
|
||||
npm publish
|
||||
```
|
||||
|
||||
Command above generates files into the `dist` folder and publishes them to npmjs.org.
|
||||
|
||||
|
||||
# Credits
|
||||
## Credits
|
||||
|
||||
Many thanks to the [d3](http://d3js.org/) and [dagre-d3](https://github.com/cpettitt/dagre-d3) projects for providing the graphical layout and drawing libraries!
|
||||
|
||||
Thanks also to the [js-sequence-diagram](http://bramp.github.io/js-sequence-diagrams) project for usage of the grammar for the sequence diagrams. Thanks to Jessica Peter for inspiration and starting point for gantt rendering.
|
||||
|
||||
*Mermaid was created by Knut Sveidqvist for easier documentation.*
|
||||
_Mermaid was created by Knut Sveidqvist for easier documentation._
|
||||
|
||||
*[Tyler Long](https://github.com/tylerlong) has became a collaborator since April 2017.*
|
||||
_[Tyler Long](https://github.com/tylerlong) has became a collaborator since April 2017._
|
||||
|
||||
Here is the full list of the projects [contributors](https://github.com/knsv/mermaid/graphs/contributors).
|
||||
|
|
|
@ -3,9 +3,31 @@
|
|||
|
||||
## Updating the documentation
|
||||
|
||||
We write documention with GitBook.
|
||||
Please continue writing documentation at [mermaid-js/mermaid/docs](https://github.com/mermaid-js/mermaid/tree/develop/docs).
|
||||
|
||||
Please continue with the [mermaid-gitbook](https://github.com/mermaidjs/mermaid-gitbook) project.
|
||||
We publish documentation using GitHub Pages.
|
||||
|
||||
|
||||
### Questions and/or suggestions ?
|
||||
After logging in at [GitHub.com](https://www.github.com), open or append to an issue [using the GitHub issue tracker of the mermaid-js repository](https://github.com/mermaid-js/mermaid/issues?q=is%3Aissue+is%3Aopen+label%3A%22Area%3A+Documentation%22).
|
||||
|
||||
### How to contribute a suggestion
|
||||
Markdown is used to format the text, for more information about Markdown [see the GitHub Markdown help page](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax).
|
||||
|
||||
If you want to use an editor on your own computer, you may follow these steps:
|
||||
* Find the Markdown file (.md) to edit in the [mermaid-js/mermaid/docs](https://github.com/mermaid-js/mermaid/tree/develop/docs) directory on the develop branch.
|
||||
* Create a fork of the develop branch.
|
||||
* Make changes or add new documentation.
|
||||
* Commit changes to your fork and push it to GitHub.
|
||||
* Create a pull request of your fork.
|
||||
|
||||
If you don't have such editor on your computer, you may follow these steps:
|
||||
* Login at [GitHub.com](https://www.github.com).
|
||||
* Navigate to [mermaid-js/mermaid/docs](https://github.com/mermaid-js/mermaid/tree/develop/docs).
|
||||
* To edit a file, click the pencil icon at the top-right of the file contents panel.
|
||||
* Describe what you changed in the "Propose file change" section, located at the bottom of the page.
|
||||
* Submit your changes by clicking the button "Propose file change" at the bottom (by automatic creation of a fork and a new branch).
|
||||
* Create a pull request of your newly forked branch, by clicking the green "Create pull request" button.
|
||||
|
||||
|
||||
## How to add a new diagram type
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# Entity Relationship Diagrams
|
||||
|
||||
> An entity–relationship model (or ER model) describes interrelated things of interest in a specific domain of knowledge. A basic ER model is composed of entity types (which classify the things of interest) and specifies relationships that can exist between entities (instances of those entity types). Wikipedia.
|
||||
|
||||
Note that practitioners of ER modelling almost always refer to entity types simply as entities. For example the CUSTOMER entity type would be referred to simply as the CUSTOMER entity. This is so common it would be inadvisable to do anything else, but technically an entity is an abstract *instance* of an entity type, and this is what an ER diagram shows - abstract instances, and the relationships between them. This is why entities are always named using singular nouns.
|
||||
|
||||
Mermaid can render ER diagrams
|
||||
```
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
|
||||
```
|
||||
```mermaid
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
CUSTOMER }|..|{ : DELIVERY-ADDRESS : uses
|
||||
```
|
||||
|
||||
Entity names are often capitalised, although there is no accepted standard on this, and it is not required in Mermaid.
|
||||
|
||||
Relationships between entities are represented by lines with end markers representing cardinality. Mermaid uses the most popular crow's foot notation. The crow's foot intuitively conveys the possibility of many instances of the entity that it connects to.
|
||||
|
||||
## Status
|
||||
|
||||
ER diagrams are a new feature in Mermaid and are **experimental**. There are likely to be a few bugs and constraints, and enhancements will be made in due course.
|
||||
|
||||
## Syntax
|
||||
|
||||
### Entities and Relationships
|
||||
|
||||
Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to label the relationship. Each statement consists of the following parts, all of which are mandatory:
|
||||
```
|
||||
<first-entity> <relationship> <second-entity> : <relationship-label>
|
||||
```
|
||||
Where:
|
||||
|
||||
- `first-entity` is the name of an entity. Names must begin with an alphabetic character and may also contain digits and hyphens
|
||||
- `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.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
PROPERTY ||--|{ ROOM : contains
|
||||
```
|
||||
|
||||
This statement can be read as *a property contains one or more rooms, and a room is part of one and only one property*. You can see that the label here is from the first entity's perspective: a property contains a room, but a room does not contain a property. When considered from the perspective of the second entity, the equivalent label is usually very easy to infer. (Some ER diagrams label relationships from both perspectives, but this is not supported here, and is usually superfluous).
|
||||
|
||||
### Relationship Syntax
|
||||
|
||||
The `relationship` part of each statement can be broken down into three sub-components:
|
||||
|
||||
- the cardinality of the first entity with respect to the second,
|
||||
- whether the relationship confers identity on a 'child' entity
|
||||
- the cardinality of the second entity with respect to the first
|
||||
|
||||
Cardinality is a property that describes how many elements of another entity can be related to the entity in question. In the above example a `PROPERTY` can have one or more `ROOM` instances associated to it, whereas a `ROOM` can only be associated with one `PROPERTY`. In each cardinality marker there are two characters. The outermost character represents a maximum value, and the innermost character represents a minimum value. The table below summarises possible cardinalities.
|
||||
|
||||
| Value (left) | Value (right) | Meaning |
|
||||
|:------------:|:-------------:|--------------------------------------------------------|
|
||||
| `|o` | `o|` | Zero or one |
|
||||
| `||` | `||` | Exactly one |
|
||||
| `}o` | `o{` | Zero or more (no upper limit) |
|
||||
| `}|` | `|{` | One or more (no upper limit) |
|
||||
|
||||
### Identification
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
```
|
||||
### Other Things
|
||||
|
||||
- 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
|
|
@ -425,10 +425,10 @@ Examples of tooltip usage below:
|
|||
|
||||
```
|
||||
<script>
|
||||
var callback = function(){
|
||||
alert('A callback was triggered');
|
||||
}
|
||||
<script>
|
||||
var callback = function(){
|
||||
alert('A callback was triggered');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
```
|
||||
|
@ -448,28 +448,30 @@ graph LR;
|
|||
```
|
||||
> **Success** The tooltip functionality and the ability to link to urls are available from version 0.5.2.
|
||||
|
||||
?> Due to limitations with how Docsify handles JavaScript callback functions, an alternate working demo for the above code can be viewed at [this jsfiddle](https://jsfiddle.net/s37cjoau/3/).
|
||||
|
||||
Beginners tip, a full example using interactive links in a html context:
|
||||
```
|
||||
<body>
|
||||
<div class="mermaid">
|
||||
graph LR;
|
||||
A-->B;
|
||||
click A callback "Tooltip"
|
||||
click B "http://www.github.com" "This is a link"
|
||||
A-->B;
|
||||
click A callback "Tooltip"
|
||||
click B "http://www.github.com" "This is a link"
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var callback = function(){
|
||||
var callback = function(){
|
||||
alert('A callback was triggered');
|
||||
}
|
||||
var config = {
|
||||
startOnLoad:true,
|
||||
flowchart:{
|
||||
useMaxWidth:true,
|
||||
htmlLabels:true,
|
||||
curve:'cardinal',
|
||||
},
|
||||
securityLevel:'loose',
|
||||
startOnLoad:true,
|
||||
flowchart:{
|
||||
useMaxWidth:true,
|
||||
htmlLabels:true,
|
||||
curve:'cardinal',
|
||||
},
|
||||
securityLevel:'loose',
|
||||
};
|
||||
|
||||
mermaid.initialize(config);
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
# Gantt diagrams
|
||||
|
||||
> A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule. Gantt charts illustrate the start and finish dates of the terminal elements and summary elements of a project.
|
||||
|
||||
Mermaid can render Gantt diagrams.
|
||||
> A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule and the amount of time it would take for any one project to finish. Gantt charts illustrate number of days between the start and finish dates of the terminal elements and summary elements of a project.
|
||||
|
||||
## A note to users
|
||||
Gannt Charts will record each scheduled task as one continuous bar that extends from the left to the right. The x axis represents time and the y records the different tasks and the order in which they are to be completed.
|
||||
|
||||
It is important to remember that when a date, day or collection of dates specific to a task are "excluded", the Gannt Chart will accomodate those changes by extending an equal number of day, towards the right, not by creating a gap inside the task.
|
||||
As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-excluded-days-within.png)
|
||||
|
||||
However, if the excluded date/s is between two tasks that are set to start consecutively, the excluded dates will be skipped graphically and left blank, and the following task will begin after the end of the excluded date/s.
|
||||
As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-long-weekend-look.png)
|
||||
|
||||
A Gantt chart is useful for tracking the amount of time it would take before a project is finished, but it can also be used to graphically represent "non-working days, with a few tweaks.
|
||||
|
||||
Mermaid can render Gantt diagrams as SVG, PNG or a MarkDown link that can be pasted into docs.
|
||||
|
||||
```
|
||||
gantt
|
||||
|
@ -30,9 +41,12 @@ gantt
|
|||
|
||||
```
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Adding GANTT diagram functionality to mermaid
|
||||
|
||||
|
||||
dateFormat :YYYY-MM-DD
|
||||
title :Adding GANTT diagram functionality to mermaid
|
||||
excludes :excludes the named dates/days from being included in a charted task..
|
||||
(Accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
section A section
|
||||
Completed task :done, des1, 2014-01-06,2014-01-08
|
||||
Active task :active, des2, 2014-01-09, 3d
|
||||
|
@ -118,7 +132,7 @@ Tbd
|
|||
|
||||
### Date format
|
||||
|
||||
The default date format is YYYY-MM-DD. You can define your ``dateFormat``. For example:
|
||||
The default date format is YYYY-MM-DD. You can define your ``dateFormat``. For example: 2020-3-7
|
||||
|
||||
```
|
||||
dateFormat YYYY MM DD
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
|
@ -7,7 +7,7 @@
|
|||
<meta name="description" content="Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
||||
<script src="//cdn.jsdelivr.net/npm/mermaid@8.4.8/dist/mermaid.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/mermaid@8.5.0/dist/mermaid.min.js"></script>
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
# Integrations
|
||||
|
||||
The following is a list of different integrations and plugins where mermaid is being used
|
||||
|
||||
## Productivity
|
||||
|
||||
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) (**Native support**)
|
||||
- [GitHub](https://github.com)
|
||||
- [Github action: Compile mermaid to image](https://github.com/neenjaw/compile-mermaid-markdown-action)
|
||||
- [svg-generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator)
|
||||
- [GitBook](http://gitbook.com)
|
||||
- [Mermaid Plugin](https://github.com/JozoVilcek/gitbook-plugin-mermaid)
|
||||
- [Markdown with Mermaid CLI](https://github.com/miao1007/gitbook-plugin-mermaid-cli)
|
||||
- [Mermaid plugin for GitBook](https://github.com/wwformat/gitbook-plugin-mermaid-pdf)
|
||||
- [Atlassian Products](https://www.atlassian.com)
|
||||
- [Mermaid Plugin for Confluence](https://marketplace.atlassian.com/apps/1214124/mermaid-plugin-for-confluence?hosting=server&tab=overview)
|
||||
- [CloudScript.io Addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon?hosting=cloud&tab=overview)
|
||||
- [Auto convert diagrams in Jira](https://github.com/coddingtonbear/jirafs-mermaid)
|
||||
- [Redmine](https://redmine.org)
|
||||
- [Mermaid Macro](https://www.redmine.org/plugins/redmine_mermaid_macro)
|
||||
- [redmine-mermaid](https://github.com/styz/redmine_mermaid)
|
||||
- [markdown-for-mermaid-plugin](https://github.com/jamieh-mongolian/markdown-for-mermaid-plugin)
|
||||
|
||||
## CRM/ERP/Similar
|
||||
|
||||
- [coreBOS](http://blog.corebos.org/blog/december2019)
|
||||
|
||||
## Blogs
|
||||
|
||||
- [Wordpress](https://wordpress.org)
|
||||
- [WordPress Markdown Editor](https://wordpress.org/plugins/wp-githuber-md)
|
||||
- [WP-ReliableMD](https://wordpress.org/plugins/wp-reliablemd/)
|
||||
- [Hexo](https://hexo.io)
|
||||
- [hexo-filter-mermid-diagrams](https://github.com/webappdevelp/hexo-filter-mermaid-diagrams)
|
||||
- [hexo-tag-mermaid](https://github.com/JameChou/hexo-tag-mermaid)
|
||||
- [hexo-mermaid-diagrams](https://github.com/mslxl/hexo-mermaid-diagrams)
|
||||
|
||||
## CMS
|
||||
|
||||
- [VuePress](https://vuepress.vuejs.org/)
|
||||
- [Plugin for Mermaid.js](https://github.com/eFrane/vuepress-plugin-mermaidjs)
|
||||
- [vuepress-plugin-mermaidjs-cli](https://github.com/gwleclerc/vuepress-plugin-mermaidjs-cli)
|
||||
- [Grav CMS](https://getgrav.org/)
|
||||
- [Mermaid Diagrams](https://github.com/DanielFlaum/grav-plugin-mermaid-diagrams)
|
||||
- [Gitlab Markdown Adapter](https://github.com/Goutte/grav-plugin-gitlab-markdown-adapter)
|
||||
|
||||
## Communication
|
||||
|
||||
- [Discourse](https://discourse.org)
|
||||
- [Mermaid Plugin](https://github.com/pnewell/discourse-mermaid), [And](https://github.com/unfoldingWord-dev/discourse-mermaid)
|
||||
- [Mattermost](https://mattermost.com/)
|
||||
- [Mermaid Plugin](https://github.com/SpikeTings/Mermaid)
|
||||
- [phpBB](https://phpbb.com)
|
||||
- [phpbb-ext-mermaid](https://github.com/AlfredoRamos/phpbb-ext-mermaid)
|
||||
- [NodeBB](https://nodebb.org)
|
||||
- [Mermaid Plugin](https://www.npmjs.com/package/nodebb-plugin-mermaid)
|
||||
|
||||
## Wikis
|
||||
|
||||
- [Media Wiki](https://www.mediawiki.org/wiki/Extension:Mermaid)
|
||||
- [Semantic Media Wiki](https://semantic-mediawiki.org)
|
||||
- [Mermaid Plugin](https://github.com/SemanticMediaWiki/Mermaid)
|
||||
- [FosWiki](https://foswiki.org)
|
||||
- [Mermaid Plugin](https://foswiki.org/Extensions/MermaidPlugin)
|
||||
- [DokuWiki](https://dokuwiki.org)
|
||||
- [Flowcharts](https://www.dokuwiki.org/plugin:flowcharts?s[]=mermaid)
|
||||
- [TiddlyWiki](https://tiddlywiki.com/)
|
||||
- [mermaid-tw5-plugin](https://github.com/michaeljmcd/mermaid-tw5-plugin)
|
||||
|
||||
## Editor Plugins
|
||||
|
||||
- [Vs Code](https://code.visualstudio.com/)
|
||||
- [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
|
||||
- [Mermaid Preview](https://marketplace.visualstudio.com/items?itemName=vstirbu.vscode-mermaid-preview)
|
||||
- [Mermaid Markdown Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=bpruitt-goddard.mermaid-markdown-syntax-highlighting)
|
||||
- [Mermaid Editor](https://marketplace.visualstudio.com/items?itemName=tomoyukim.vscode-mermaid-editor)
|
||||
- [Mermaid Export](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.mermaid-export)
|
||||
- [Markdown PDF](https://marketplace.visualstudio.com/items?itemName=yzane.markdown-pdf)
|
||||
- [Preview](https://marketplace.visualstudio.com/items?itemName=searKing.preview-vscode)
|
||||
- [Preview Sequence Diagrams](https://marketplace.visualstudio.com/items?itemName=arichika.previewseqdiag-vscode)
|
||||
- [Markdown-It](https://github.com/markdown-it/markdown-it)
|
||||
- [Textual UML Parser](https://github.com/manastalukdar/markdown-it-textual-uml)
|
||||
- [Mermaid Plugin](https://github.com/tylingsoft/markdown-it-mermaid)
|
||||
- [md-it-mermaid](https://github.com/iamcco/md-it-mermaid)
|
||||
- [markdown-it-mermaid-fence-new](https://github.com/Revomatico/markdown-it-mermaid-fence-new)
|
||||
- [markdown-it-mermaid-less](https://github.com/searKing/markdown-it-mermaid-less)
|
||||
- [Atom](https://atom.io)
|
||||
- [Markdown Preview Enhanced](https://atom.io/packages/markdown-preview-enhanced)
|
||||
- [Atom Mermaid](https://atom.io/packages/atom-mermaid)
|
||||
- [Language Mermaid Syntax Highlighter](https://atom.io/packages/language-mermaid)
|
||||
- [Sublime Text 3](https://sublimetext.com)
|
||||
- [Mermaid Package](https://packagecontrol.io/packages/Mermaid)
|
||||
- [Astah](http://astah.net)
|
||||
- [Export to Mermaid](https://github.com/Avens666/Astah_Jude_UML_export_to_Markdown-mermaid-Plantuml-)
|
||||
- [Light Table](http://lighttable.com/)
|
||||
- [Mermaid Plugin](https://github.com/cldwalker/Mermaid)
|
||||
- [Draw.io](http://draw.io) - [Plugin](https://github.com/nopeslide/drawio_mermaid_plugin)
|
||||
- [Inkdrop](http://inkdrop.app) - [Plugin](https://github.com/inkdropapp/inkdrop-mermaid)
|
||||
- [Vim](https://vim.org)
|
||||
- [Vim Diagram Syntax](https://github.com/zhaozg/vim-diagram)
|
||||
- [Brackets](http://brackets.io/)
|
||||
- [Mermaid Preview](https://s3.amazonaws.com/extend.brackets/alanhohn.mermaid-preview/alanhohn.mermaid-preview-1.0.2.zip)
|
||||
- [Iodide](https://github.com/iodide-project/iodide)
|
||||
- [iodide-mermaid-plugin](https://github.com/iodide-project/iodide-mermaid-plugin)
|
||||
|
||||
## Document Generation
|
||||
|
||||
- [Sphinx](https://www.sphinx-doc.org/en/master/)
|
||||
- [sphinxcontrib-mermaid](https://github.com/mgaitan/sphinxcontrib-mermaid)
|
||||
- [remark.js](https://remark.js.org/)
|
||||
- [remark-mermaid](https://github.com/temando/remark-mermaid)
|
||||
- [jSDoc](https://jsdoc.app/)
|
||||
- [jsdoc-mermaid](https://github.com/Jellyvision/jsdoc-mermaid)
|
||||
- [MkDocs](https://mkdocs.org)
|
||||
- [MarkdownMermaid](https://github.com/pugong/mkdocs-mermaid-plugin)
|
||||
- [Type Doc](https://typedoc.org/)
|
||||
- [typedoc-plugin-mermaid](https://www.npmjs.com/package/typedoc-plugin-mermaid)
|
||||
|
||||
## Chrome Extensions
|
||||
|
||||
- [Mermaid Diagrams](https://chrome.google.com/webstore/detail/mermaid-diagrams/phfcghedmopjadpojhmmaffjmfiakfil)
|
||||
- [GitHub + Mermaid](https://chrome.google.com/webstore/detail/github-%2B-mermaid/goiiopgdnkogdbjmncgedmgpoajilohe)
|
||||
- [Mermaid Markdown](https://chrome.google.com/webstore/detail/mermaid-markdown/mboeoikjijmjcjgpccghbcoegikliijg)
|
||||
- [Monkeys](https://chrome.google.com/webstore/detail/monkeys-mermaid-for-githu/cplfdpoajbclbgphaphphcldamfkjlgi)
|
||||
- [Asciidoctor Live Preview](https://chrome.google.com/webstore/detail/asciidoctorjs-live-previe/iaalpfgpbocpdfblpnhhgllgbdbchmia) __works in the new IE as well__
|
||||
- [Chrome Diagrammer](https://chrome.google.com/webstore/detail/chrome-diagrammer/bkpbgjmkomfoakfklcjeoegkklgjnnpk)
|
||||
- [Diagram Tab](https://github.com/khafast/diagramtab)
|
||||
|
||||
## Other
|
||||
|
||||
- [Jekyll](https://jekyllrb.com/)
|
||||
- [jekyll-mermaid](https://rubygems.org/gems/jekyll-mermaid)
|
||||
- [jekyll-mermaid-diagrams](https://github.com/fuzhibo/jekyll-mermaid-diagrams)
|
||||
- [Reveal.js](https://github.com/hakimel/reveal.js)
|
||||
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
|
||||
- [Bisheng](https://www.npmjs.com/package/bisheng)
|
||||
- [bisheng-plugin-mermaid](https://github.com/yct21/bisheng-plugin-mermaid)
|
||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
|
@ -174,6 +174,14 @@ margin around notes.
|
|||
Space between messages.
|
||||
**Default value 35**.
|
||||
|
||||
### messageAlign
|
||||
|
||||
Multiline message alignment. Possible values are:
|
||||
|
||||
- left
|
||||
- center **default**
|
||||
- right
|
||||
|
||||
### mirrorActors
|
||||
|
||||
mirror actors under diagram.
|
||||
|
@ -255,6 +263,43 @@ The number of alternating section styles.
|
|||
Datetime format of the axis. This might need adjustment to match your locale and preferences
|
||||
**Default value '%Y-%m-%d'**.
|
||||
|
||||
## er
|
||||
|
||||
The object containing configurations specific for entity relationship diagrams
|
||||
|
||||
### diagramPadding
|
||||
|
||||
The amount of padding around the diagram as a whole so that embedded diagrams have margins, expressed in pixels
|
||||
|
||||
### layoutDirection
|
||||
|
||||
Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL',
|
||||
where T = top, B = bottom, L = left, and R = right.
|
||||
|
||||
### minEntityWidth
|
||||
|
||||
The mimimum width of an entity box, expressed in pixels
|
||||
|
||||
### minEntityHeight
|
||||
|
||||
The minimum height of an entity box, expressed in pixels
|
||||
|
||||
### entityPadding
|
||||
|
||||
The minimum internal padding between the text in an entity box and the enclosing box borders, expressed in pixels
|
||||
|
||||
### stroke
|
||||
|
||||
Stroke color of box edges and lines
|
||||
|
||||
### fill
|
||||
|
||||
Fill color of entity boxes
|
||||
|
||||
### fontSize
|
||||
|
||||
Font size
|
||||
|
||||
## render
|
||||
|
||||
Function that renders an svg with a graph from a chart definition. Usage example below.
|
||||
|
@ -310,6 +355,7 @@ mermaidAPI.initialize({
|
|||
boxTextMargin:5,
|
||||
noteMargin:10,
|
||||
messageMargin:35,
|
||||
messageAlign:'center',
|
||||
mirrorActors:true,
|
||||
bottomMarginAdj:1,
|
||||
useMaxWidth:true,
|
||||
|
|
|
@ -1,42 +1,46 @@
|
|||
# A more basic getting started
|
||||
# A basic mermaid User-Guide for Beginners
|
||||
|
||||
Writing mermaid code is simple.
|
||||
Creating diagrams and charts, using mermaid code is simple.
|
||||
|
||||
But how is the code turned into a diagram in a web page? To do this we need a mermaid renderer.
|
||||
But how is the code turned into a diagram in a web page? This is done with the use of a mermaid renderer.
|
||||
|
||||
Thankfully the mermaid renderer is very accessible, in essence it is a javascript.
|
||||
Thankfully the mermaid renderer is very accessible, in essence it is a piece of javascript that can be called.
|
||||
|
||||
The requirement is on the part of the web browser. Modern web browsers, such as Firefox, Chrome and Safari, can render mermaid. But Internet Explorer cannot. The web browser also needs access to the online mermaid renderer which it downloads from https://cdn.jsdelivr.net/npm/mermaid
|
||||
Most widely used web browsers, such as Firefox, Chrome and Safari, can render mermaid, Internet Explorer however cannot. The web browser also needs access to the online mermaid renderer which it downloads from https://cdn.jsdelivr.net/npm/mermaid
|
||||
|
||||
For an easy introduction, here follows three practical examples using:
|
||||
1. an online mermaid editor
|
||||
2. a mermaid plugin
|
||||
3. a generic web server of your choosing
|
||||
# For beginners, there are three relatively easy ways you can use mermaid:
|
||||
1. Using the mermaid [live editor](https://mermaid-js.github.io/mermaid-live-editor/)
|
||||
2. Using a mermaid plugin, such as that for Confluence or [Atom](https://atom.io/packages/atom-mermaid).
|
||||
3. Calling mermaid renderer with HTML, deployed in a friendly browser.
|
||||
|
||||
Following either of these examples, you can get started with converting your own mermaid code into web diagrams.
|
||||
# Following either of these examples, you can get started with creating your own diagrams using mermaid code.
|
||||
|
||||
## the mermaid live editor
|
||||
## 1. The mermaid live editor:
|
||||
|
||||
The quickest way to get started with mermaid is to visit [The mermaid live editor](https://mermaidjs.github.io/mermaid-live-editor).
|
||||
A great way to get started with mermaid is to visit [The mermaid live editor](https://mermaidjs.github.io/mermaid-live-editor).
|
||||
|
||||
In the `Code` section one can write or edit raw mermaid code, and instantly `Preview` the rendered result.
|
||||
In the `Code` section one can write or edit raw mermaid code, and instantly `Preview` the rendered result on the panel beside it.
|
||||
|
||||
This is a great way to get started.
|
||||
![Flowchart](./img/n00b-liveEditor.png)
|
||||
|
||||
It is also the easiest way to develop diagrams, the code of which can be pasted straight into documentation.
|
||||
|
||||
![Flowchart](./img/n00b-liveEditor.png)
|
||||
You can also copy the code from the code section and paste it into either a mermaid plugin or in inside an html file, which will be taught in numbers 2 and 3.
|
||||
|
||||
It is also an easier way to develop diagrams. You can also click "Copy Markdown" to copy the markdown code for the diagram, that can then be pasted directly into your documentation.
|
||||
|
||||
![Flowchart](./img/liveEditorOptions.png)
|
||||
|
||||
The `Mermaid configuration` is for controlling mermaid behaviour. An easy introduction to mermaid configuration is found in the [Advanced usage](n00b-advanced.md) section. A complete configuration reference cataloguing default values is found on the [mermaidAPI](mermaidAPI.md) page.
|
||||
|
||||
|
||||
## mermaid using plugins
|
||||
## 2. Using mermaid plugins:
|
||||
|
||||
Thanks to the growing popularity of mermaid, many plugins already exist which incorporate a mermaid renderer.
|
||||
Thanks to the growing popularity of mermaid, many plugins already exist which incorporate a mermaid renderer. An extensive list can be found [here](integrations.md).
|
||||
|
||||
One example is the [Atlassian Confluence mermaid plugin](https://marketplace.atlassian.com/apps/1214124/mermaid-plugin-for-confluence?hosting=server&tab=overview)
|
||||
One example in the list is the [Atlassian Confluence mermaid plugin](https://marketplace.atlassian.com/apps/1214124/mermaid-plugin-for-confluence?hosting=server&tab=overview)
|
||||
|
||||
When the mermaid plugin is installed on a Confluence server, one can insert a mermaid object into any Confluence page.
|
||||
# Here is a step by step process for using the mermaid-Confluence plugin:
|
||||
|
||||
---
|
||||
|
||||
|
@ -64,24 +68,32 @@ When the mermaid plugin is installed on a Confluence server, one can insert a me
|
|||
|
||||
---
|
||||
|
||||
## mermaid using any web server (or just a browser)
|
||||
## 3. mermaid using any web server (or just a browser):
|
||||
|
||||
This example can be used with any common web server. Apache, IIS, nginx, node express [...], you pick your favourite.
|
||||
This method can be used with any common web server. Apache, IIS, nginx, node express [...], you pick your favourite.
|
||||
|
||||
We do not need to install anything on the server, apart from a program (like Notepad++) that can generate an html file, which is then deployed by a web browser (such as Firefox, Chrome, Safari, but not Internet Explorer).
|
||||
|
||||
So if you want to really simplify things when testing this out, don't use a web server at all but just create the file locally and drag it into your browser window. It is the browser which does all the work of rendering mermaid!
|
||||
|
||||
# Here are instructions for creating an html file with mermaid code:
|
||||
# Note that all this is written in the html `<body>` section of the web page.
|
||||
|
||||
When writing the html file, we give the web browser three instructions inside the html code:
|
||||
|
||||
a. A reference for fetching the online mermaid renderer, which is written in Javascript.
|
||||
|
||||
b. The mermaid code for the diagram we want to create.
|
||||
|
||||
c. The `mermaid.initialize()` command to start the rendering process.
|
||||
|
||||
We do not need to install anything on the server, apart from a normal file of html to be reached by a web browser (such as Firefox, Chrome, Safari, but not Internet Explorer). So if you want to really simplify things when testing this out, don't use a web server at all but just create the file locally and drag it into your browser window. It is the browser which does all the work of rendering mermaid!
|
||||
|
||||
Through the html file, we give the web browser three instructions inside the html code it retrieves:
|
||||
1. a reference for fetching the online mermaid renderer, the renderer is just a javascript.
|
||||
2. the mermaid code we want to diagram.
|
||||
3. the `mermaid.initialize()` command to start the rendering process
|
||||
|
||||
All this is done in the html `<body>` section of the web page.
|
||||
|
||||
This is what needs to go into the html file:
|
||||
|
||||
|
||||
|
||||
1. The reference to the mermaid renderer is done in a `<script src>` tag like so:
|
||||
# a. The reference to the mermaid renderer has to be contained in a `<script src>` tag like so:
|
||||
|
||||
```
|
||||
<body>
|
||||
|
@ -89,7 +101,7 @@ This is what needs to go into the html file:
|
|||
</body>
|
||||
```
|
||||
|
||||
2. The embedded mermaid code is similarly placed in a `<div>` tag:
|
||||
# b. The embedded mermaid code is similarly placed inside a `<div>` tag:
|
||||
|
||||
```
|
||||
<body>
|
||||
|
@ -102,8 +114,9 @@ This is what needs to go into the html file:
|
|||
</div>
|
||||
</body>
|
||||
```
|
||||
(take note that every mermaid chart/graph/diagram, has to have separate `<div>` tags.)
|
||||
|
||||
3. When initializing mermaid using `mermaid.initialize()`, mermaid takes all the `<div class="mermaid">` tags it can find in the html body and starts to render them one by one. This is done like so:
|
||||
# c. When initializing mermaid using `mermaid.initialize()`, mermaid takes all the `<div class="mermaid">` tags it can find in the html body and starts to render them one by one. This is done like so:
|
||||
|
||||
```
|
||||
<body>
|
||||
|
@ -111,8 +124,8 @@ This is what needs to go into the html file:
|
|||
</body>
|
||||
```
|
||||
|
||||
*Finally*
|
||||
4. Putting the three steps together is as simple as:
|
||||
# *Finally*
|
||||
# If the three steps mentioned are followed you will end up with something like this:
|
||||
```
|
||||
<html>
|
||||
<body>
|
||||
|
@ -137,7 +150,7 @@ This is what needs to go into the html file:
|
|||
</body>
|
||||
</html>
|
||||
```
|
||||
Save this to a html file and fetch it with a browser from the web server (or just drag it into your web browser window) and voila!
|
||||
# Save this to a html file and fetch it with a browser from the web server (or just drag it into your web browser window) and voila!
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Overview for n00bs
|
||||
|
||||
As a sysadmin I frequently have to document things, including drawing stuff.
|
||||
mermaid is a tool that aims to make diagrams and flowcharts for documentation, easier.
|
||||
|
||||
Using mermaid, I can type this as a comment in a script:
|
||||
with mermaid, diagrams can be created through comments like this in a script:
|
||||
|
||||
```
|
||||
graph TD
|
||||
|
@ -11,17 +11,19 @@ B --> C[Server01]
|
|||
B --> D[Server02]
|
||||
```
|
||||
|
||||
And end up with this in the documentation:
|
||||
And they are rendered into this and made part of the documentation:
|
||||
|
||||
![Flowchart](./img/n00b-firstFlow.png)
|
||||
|
||||
Most of the stuff I need to visualize can be scripted in a similar way, with a varitety of different symbols and chart types available. Since the diagram source is text based, it can be part of production scripts (and other pieces of code). So less time needs be spent on documenting as a separate task.
|
||||
Most of the similar visuals that you might need to create can be scripted in a similar way, with a varitety of different symbols and chart types available.
|
||||
Since the diagram source is text based, it can be part of production scripts (and other pieces of code). So less time needs be spent on documenting as a separate task.
|
||||
|
||||
Comparing with Visio and similar applications, mermaid is a really fast way to create good visualizations. This is especially apparent when editing a complex visualisation, this could take me hours in a desktop application but takes minutes (or even less if generation has been scripted) with mermaid.
|
||||
|
||||
With mermaid I can spend a fraction of the time I normally would spend, and instead automate the diagram generation and end up saving even more time. I love it!
|
||||
Comparing with Visio and similar applications, mermaid is a really fast way to create good visualizations. This is especially apparent when editing a complex visualisations, a process that usually takes hours in a desktop application, but only takes minutes (or even less if generation has been scripted) with mermaid.
|
||||
|
||||
mermaid can potentially cut down the amount of time and effort spent on the process of creating diagrams, to a fraction of what you usually put in.
|
||||
|
||||
However, a lot of the mermaid documentation is geared to professional frontend developers, presuming a skill set which I simply do not have.
|
||||
|
||||
I needed a really basic instruction. And here it is.
|
||||
If you need some basic instructions and introductions, here are a few good places to start:
|
||||
|
||||
For information on how to use mermaid, click [here](https://mermaid-js.github.io/mermaid/#/n00b-gettingStarted), or you can try out the mermaid [live editor](https://mermaid-js.github.io/mermaid-live-editor/), alternatively, you could also view the [integrations and uses](https://github.com/mermaid-js/mermaid/blob/develop/docs/integrations.md) for mermaid.
|
||||
|
|
|
@ -77,7 +77,6 @@ Type | Description
|
|||
-x | Solid line with a cross at the end (async)
|
||||
--x | Dotted line with a cross at the end (async)
|
||||
|
||||
|
||||
## Activations
|
||||
|
||||
It is possible to activate and deactivate an actor. (de)activation can be dedicated declarations:
|
||||
|
@ -127,7 +126,6 @@ sequenceDiagram
|
|||
John-->>-Alice: I feel great!
|
||||
```
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
It is possible to add notes to a sequence diagram. This is done by the notation
|
||||
|
@ -159,7 +157,6 @@ sequenceDiagram
|
|||
Note over Alice,John: A typical interaction
|
||||
```
|
||||
|
||||
|
||||
## Loops
|
||||
|
||||
It is possible to express loops in a sequence diagram. This is done by the notation
|
||||
|
@ -187,7 +184,6 @@ sequenceDiagram
|
|||
end
|
||||
```
|
||||
|
||||
|
||||
## Alt
|
||||
|
||||
It is possible to express alternative paths in a sequence diagram. This is done by the notation
|
||||
|
@ -235,7 +231,53 @@ sequenceDiagram
|
|||
end
|
||||
```
|
||||
|
||||
## Parallel
|
||||
|
||||
It is possible to show actions that are happening in parallel.
|
||||
|
||||
This is done by the notation
|
||||
|
||||
```
|
||||
par [Action 1]
|
||||
... statements ...
|
||||
and [Action 2]
|
||||
... statements ...
|
||||
and [Action N]
|
||||
... statements ...
|
||||
end
|
||||
```
|
||||
|
||||
See the example below:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
par Alice to Bob
|
||||
Alice->>Bob: Hello guys!
|
||||
and Alice to John
|
||||
Alice->>John: Hello guys!
|
||||
end
|
||||
Bob-->>Alice: Hi Alice!
|
||||
John-->>Alice: Hi Alice!
|
||||
```
|
||||
|
||||
It is also possible to nest parallel blocks.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
par Alice to Bob
|
||||
Alice->>Bob: Go help John
|
||||
and Alice to John
|
||||
Alice->>John: I want this done today
|
||||
par John to Charlie
|
||||
John->>Charlie: Can we do this today?
|
||||
and John to Diana
|
||||
John->>Diana: Can you help us today?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Background Highlighting
|
||||
|
||||
It is possible to highlight flows by providing colored background rects. This is done by the notation
|
||||
|
||||
The colors are defined using rgb and rgba syntax.
|
||||
|
@ -340,10 +382,8 @@ loopLine | Defines styles for the lines in the loop box.
|
|||
note | Styles for the note box.
|
||||
noteText | Styles for the text on in the note boxes.
|
||||
|
||||
|
||||
### Sample stylesheet
|
||||
|
||||
|
||||
```css
|
||||
body {
|
||||
background: white;
|
||||
|
@ -426,7 +466,6 @@ text.actor {
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Is it possible to adjust the margins for rendering the sequence diagram.
|
||||
|
@ -452,4 +491,3 @@ Param | Description | Default value
|
|||
--- | --- | ---
|
||||
mirrorActor | Turns on/off the rendering of actors below the diagram as well as above it | false
|
||||
bottomMarginAdj | Adjusts how far down the graph ended. Wide borders styles with css could generate unwanted clipping which is why this config param exists. | 1
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mermaid",
|
||||
"version": "8.4.8",
|
||||
"version": "8.5.0",
|
||||
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||
"main": "dist/mermaid.core.js",
|
||||
"keywords": [
|
||||
|
@ -13,7 +13,7 @@
|
|||
"git graph"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --progress --colors",
|
||||
"build": "webpack --progress --colors -p",
|
||||
"postbuild": "documentation build src/mermaidAPI.js --shallow -f md --markdown-toc false -o docs/mermaidAPI.md",
|
||||
"build:watch": "yarn build --watch",
|
||||
"minify": "minify ./dist/mermaid.js > ./dist/mermaid.min.js",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as d3 from 'd3';
|
||||
import { logger } from '../../logger';
|
||||
import { getConfig } from '../../config';
|
||||
import common from '../common/common';
|
||||
import utils from '../../utils';
|
||||
|
||||
const MERMAID_DOM_ID_PREFIX = 'classid-';
|
||||
|
@ -175,7 +176,7 @@ export const setLink = function(ids, linkStr, tooltip) {
|
|||
classes[id].link = utils.formatUrl(linkStr, config);
|
||||
|
||||
if (tooltip) {
|
||||
classes[id].tooltip = utils.sanitize(tooltip, config);
|
||||
classes[id].tooltip = common.sanitizeText(tooltip, config);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -207,7 +208,7 @@ const setClickFunc = function(domId, functionName, tooltip) {
|
|||
}
|
||||
if (typeof classes[id] !== 'undefined') {
|
||||
if (tooltip) {
|
||||
classes[id].tooltip = utils.sanitize(tooltip, config);
|
||||
classes[id].tooltip = common.sanitizeText(tooltip, config);
|
||||
}
|
||||
|
||||
funs.push(function() {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
export const getRows = s => {
|
||||
if (!s) return 1;
|
||||
let str = breakToPlaceholder(s);
|
||||
str = str.replace(/\\n/g, '#br#');
|
||||
return str.split('#br#');
|
||||
};
|
||||
|
||||
export const sanitizeText = (text, config) => {
|
||||
let txt = text;
|
||||
let htmlLabels = true;
|
||||
if (
|
||||
config.flowchart &&
|
||||
(config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false')
|
||||
)
|
||||
htmlLabels = false;
|
||||
|
||||
if (config.securityLevel !== 'loose' && htmlLabels) {
|
||||
// eslint-disable-line
|
||||
txt = breakToPlaceholder(txt);
|
||||
txt = txt.replace(/</g, '<').replace(/>/g, '>');
|
||||
txt = txt.replace(/=/g, '=');
|
||||
txt = placeholderToBreak(txt);
|
||||
}
|
||||
|
||||
return txt;
|
||||
};
|
||||
|
||||
const breakToPlaceholder = s => {
|
||||
return s.replace(/<br\s*\/?>/gi, '#br#');
|
||||
};
|
||||
|
||||
const placeholderToBreak = s => {
|
||||
return s.replace(/#br#/g, '<br/>');
|
||||
};
|
||||
|
||||
export default {
|
||||
getRows,
|
||||
sanitizeText
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
import { logger } from '../../logger';
|
||||
|
||||
let entities = {};
|
||||
let relationships = [];
|
||||
let title = '';
|
||||
|
||||
const Cardinality = {
|
||||
ZERO_OR_ONE: 'ZERO_OR_ONE',
|
||||
ZERO_OR_MORE: 'ZERO_OR_MORE',
|
||||
ONE_OR_MORE: 'ONE_OR_MORE',
|
||||
ONLY_ONE: 'ONLY_ONE'
|
||||
};
|
||||
|
||||
const Identification = {
|
||||
NON_IDENTIFYING: 'NON_IDENTIFYING',
|
||||
IDENTIFYING: 'IDENTIFYING'
|
||||
};
|
||||
|
||||
const addEntity = function(name) {
|
||||
if (typeof entities[name] === 'undefined') {
|
||||
entities[name] = name;
|
||||
logger.debug('Added new entity :', name);
|
||||
}
|
||||
};
|
||||
|
||||
const getEntities = () => entities;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
logger.debug('Added new relationship :', rel);
|
||||
};
|
||||
|
||||
const getRelationships = () => relationships;
|
||||
|
||||
// Keep this - TODO: revisit...allow the diagram to have a title
|
||||
const setTitle = function(txt) {
|
||||
title = txt;
|
||||
};
|
||||
|
||||
const getTitle = function() {
|
||||
return title;
|
||||
};
|
||||
|
||||
const clear = function() {
|
||||
entities = {};
|
||||
relationships = [];
|
||||
title = '';
|
||||
};
|
||||
|
||||
export default {
|
||||
Cardinality,
|
||||
Identification,
|
||||
addEntity,
|
||||
getEntities,
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
clear,
|
||||
setTitle,
|
||||
getTitle
|
||||
};
|
|
@ -0,0 +1,168 @@
|
|||
const ERMarkers = {
|
||||
ONLY_ONE_START: 'ONLY_ONE_START',
|
||||
ONLY_ONE_END: 'ONLY_ONE_END',
|
||||
ZERO_OR_ONE_START: 'ZERO_OR_ONE_START',
|
||||
ZERO_OR_ONE_END: 'ZERO_OR_ONE_END',
|
||||
ONE_OR_MORE_START: 'ONE_OR_MORE_START',
|
||||
ONE_OR_MORE_END: 'ONE_OR_MORE_END',
|
||||
ZERO_OR_MORE_START: 'ZERO_OR_MORE_START',
|
||||
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END'
|
||||
};
|
||||
|
||||
/**
|
||||
* Put the markers into the svg DOM for later use with edge paths
|
||||
*/
|
||||
const insertMarkers = function(elem, conf) {
|
||||
let marker;
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONLY_ONE_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 18)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M9,0 L9,18 M15,0 L15,18');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONLY_ONE_END)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 18)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M3,0 L3,18 M9,0 L9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_ONE_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 30)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 21)
|
||||
.attr('cy', 9)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M9,0 L9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_ONE_END)
|
||||
.attr('refX', 30)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 30)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 9)
|
||||
.attr('cy', 9)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,0 L21,18');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONE_OR_MORE_START)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 45)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.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', ERMarkers.ONE_OR_MORE_END)
|
||||
.attr('refX', 27)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 45)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_MORE_START)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 57)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 48)
|
||||
.attr('cy', 18)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M0,18 Q18,0 36,18 Q18,36 0,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_MORE_END)
|
||||
.attr('refX', 39)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 57)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 9)
|
||||
.attr('cy', 18)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export default {
|
||||
ERMarkers,
|
||||
insertMarkers
|
||||
};
|
|
@ -0,0 +1,346 @@
|
|||
import graphlib from 'graphlib';
|
||||
import * as d3 from 'd3';
|
||||
import erDb from './erDb';
|
||||
import erParser from './parser/erDiagram';
|
||||
import dagre from 'dagre';
|
||||
import { getConfig } from '../../config';
|
||||
import { logger } from '../../logger';
|
||||
import erMarkers from './erMarkers';
|
||||
|
||||
const conf = {};
|
||||
|
||||
/**
|
||||
* Allows the top-level API module to inject config specific to this renderer,
|
||||
* storing it in the local conf object. Note that generic config still needs to be
|
||||
* retrieved using getConfig() imported from the config module
|
||||
*/
|
||||
export const setConf = function(cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
conf[keys[i]] = cnf[keys[i]];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Use D3 to construct the svg elements for the entities
|
||||
* @param svgNode the svg node that contains the diagram
|
||||
* @param entities The entities to be drawn
|
||||
* @param graph The graph that contains the vertex and edge definitions post-layout
|
||||
* @return The first entity that was inserted
|
||||
*/
|
||||
const drawEntities = function(svgNode, entities, graph) {
|
||||
const keys = Object.keys(entities);
|
||||
let firstOne;
|
||||
|
||||
keys.forEach(function(id) {
|
||||
// Create a group for each entity
|
||||
const groupNode = svgNode.append('g').attr('id', id);
|
||||
|
||||
firstOne = firstOne === undefined ? id : firstOne;
|
||||
|
||||
// Label the entity - this is done first so that we can get the bounding box
|
||||
// which then determines the size of the rectangle
|
||||
const textId = 'entity-' + id;
|
||||
const textNode = groupNode
|
||||
.append('text')
|
||||
.attr('id', textId)
|
||||
.attr('x', 0)
|
||||
.attr('y', (conf.fontSize + 2 * conf.entityPadding) / 2)
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize)
|
||||
.text(id);
|
||||
|
||||
// Calculate the width and height of the entity
|
||||
const textBBox = textNode.node().getBBox();
|
||||
const entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2);
|
||||
const entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2);
|
||||
|
||||
// Make sure the text gets centred relative to the entity box
|
||||
textNode.attr('transform', 'translate(' + entityWidth / 2 + ',' + entityHeight / 2 + ')');
|
||||
|
||||
// Draw the rectangle - insert it before the text so that the text is not obscured
|
||||
const rectNode = groupNode
|
||||
.insert('rect', '#' + textId)
|
||||
.attr('fill', conf.fill)
|
||||
.attr('fill-opacity', '100%')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', entityWidth)
|
||||
.attr('height', entityHeight);
|
||||
|
||||
const rectBBox = rectNode.node().getBBox();
|
||||
|
||||
// Add the entity to the graph
|
||||
graph.setNode(id, {
|
||||
width: rectBBox.width,
|
||||
height: rectBBox.height,
|
||||
shape: 'rect',
|
||||
id: id
|
||||
});
|
||||
});
|
||||
return firstOne;
|
||||
}; // drawEntities
|
||||
|
||||
const adjustEntities = function(svgNode, graph) {
|
||||
graph.nodes().forEach(function(v) {
|
||||
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
|
||||
svgNode
|
||||
.select('#' + v)
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(graph.node(v).x - graph.node(v).width / 2) +
|
||||
',' +
|
||||
(graph.node(v).y - graph.node(v).height / 2) +
|
||||
' )'
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const getEdgeName = function(rel) {
|
||||
return (rel.entityA + rel.roleA + rel.entityB).replace(/\s/g, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Add each relationship to the graph
|
||||
* @param relationships the relationships to be added
|
||||
* @param g the graph
|
||||
* @return {Array} The array of relationships
|
||||
*/
|
||||
const addRelationships = function(relationships, g) {
|
||||
relationships.forEach(function(r) {
|
||||
g.setEdge(r.entityA, r.entityB, { relationship: r }, getEdgeName(r));
|
||||
});
|
||||
return relationships;
|
||||
}; // addRelationships
|
||||
|
||||
let relCnt = 0;
|
||||
/**
|
||||
* Draw a relationship using edge information from the graph
|
||||
* @param svg the svg node
|
||||
* @param rel the relationship to draw in the svg
|
||||
* @param g the graph containing the edge information
|
||||
* @param insert the insertion point in the svg DOM (because relationships have markers that need to sit 'behind' opaque entity boxes)
|
||||
*/
|
||||
const drawRelationshipFromLayout = function(svg, rel, g, insert) {
|
||||
relCnt++;
|
||||
|
||||
// Find the edge relating to this relationship
|
||||
const edge = g.edge(rel.entityA, rel.entityB, getEdgeName(rel));
|
||||
|
||||
// Get a function that will generate the line path
|
||||
const lineFunction = d3
|
||||
.line()
|
||||
.x(function(d) {
|
||||
return d.x;
|
||||
})
|
||||
.y(function(d) {
|
||||
return d.y;
|
||||
})
|
||||
.curve(d3.curveBasis);
|
||||
|
||||
// Insert the line at the right place
|
||||
const svgPath = svg
|
||||
.insert('path', '#' + insert)
|
||||
.attr('d', lineFunction(edge.points))
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none');
|
||||
|
||||
// ...and with dashes if necessary
|
||||
if (rel.relSpec.relType === erDb.Identification.NON_IDENTIFYING) {
|
||||
svgPath.attr('stroke-dasharray', '8,8');
|
||||
}
|
||||
|
||||
// TODO: Understand this better
|
||||
let url = '';
|
||||
if (conf.arrowMarkerAbsolute) {
|
||||
url =
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
window.location.search;
|
||||
url = url.replace(/\(/g, '\\(');
|
||||
url = url.replace(/\)/g, '\\)');
|
||||
}
|
||||
|
||||
// Decide which start and end markers it needs. It may be possible to be more concise here
|
||||
// by reversing a start marker to make an end marker...but this will do for now
|
||||
|
||||
// Note that the 'A' entity's marker is at the end of the relationship and the 'B' entity's marker is at the start
|
||||
switch (rel.relSpec.cardA) {
|
||||
case erDb.Cardinality.ZERO_OR_ONE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONLY_ONE:
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
|
||||
break;
|
||||
}
|
||||
|
||||
switch (rel.relSpec.cardB) {
|
||||
case erDb.Cardinality.ZERO_OR_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'
|
||||
);
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'
|
||||
);
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'
|
||||
);
|
||||
break;
|
||||
case erDb.Cardinality.ONLY_ONE:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
|
||||
break;
|
||||
}
|
||||
|
||||
// Now label the relationship
|
||||
|
||||
// Find the half-way point
|
||||
const len = svgPath.node().getTotalLength();
|
||||
const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
|
||||
|
||||
// Append a text node containing the label
|
||||
const labelId = 'rel' + relCnt;
|
||||
|
||||
const labelNode = svg
|
||||
.append('text')
|
||||
.attr('id', labelId)
|
||||
.attr('x', labelPoint.x)
|
||||
.attr('y', labelPoint.y)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize)
|
||||
.text(rel.roleA);
|
||||
|
||||
// Figure out how big the opaque 'container' rectangle needs to be
|
||||
const labelBBox = labelNode.node().getBBox();
|
||||
|
||||
// Insert the opaque rectangle before the text label
|
||||
svg
|
||||
.insert('rect', '#' + labelId)
|
||||
.attr('x', labelPoint.x - labelBBox.width / 2)
|
||||
.attr('y', labelPoint.y - labelBBox.height / 2)
|
||||
.attr('width', labelBBox.width)
|
||||
.attr('height', labelBBox.height)
|
||||
.attr('fill', 'white')
|
||||
.attr('fill-opacity', '85%');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw en E-R diagram in the tag with id: id based on the text definition of the diagram
|
||||
* @param text the text of the diagram
|
||||
* @param id the unique id of the DOM node that contains the diagram
|
||||
*/
|
||||
export const draw = function(text, id) {
|
||||
logger.info('Drawing ER diagram');
|
||||
erDb.clear();
|
||||
const parser = erParser.parser;
|
||||
parser.yy = erDb;
|
||||
|
||||
// Parse the text to populate erDb
|
||||
try {
|
||||
parser.parse(text);
|
||||
} catch (err) {
|
||||
logger.debug('Parsing failed');
|
||||
}
|
||||
|
||||
// Get a reference to the svg node that contains the text
|
||||
const svg = d3.select(`[id='${id}']`);
|
||||
|
||||
// Add cardinality marker definitions to the svg
|
||||
erMarkers.insertMarkers(svg, conf);
|
||||
|
||||
// Now we have to construct the diagram in a specific way:
|
||||
// ---
|
||||
// 1. Create all the entities in the svg node at 0,0, but with the correct dimensions (allowing for text content)
|
||||
// 2. Make sure they are all added to the graph
|
||||
// 3. Add all the edges (relationships) to the graph aswell
|
||||
// 4. Let dagre do its magic to layout the graph. This assigns:
|
||||
// - the centre co-ordinates for each node, bearing in mind the dimensions and edge relationships
|
||||
// - the path co-ordinates for each edge
|
||||
// But it has no impact on the svg child nodes - the diagram remains with every entity rooted at 0,0
|
||||
// 5. Now assign a transform to each entity in the svg node so that it gets drawn in the correct place, as determined by
|
||||
// its centre point, which is obtained from the graph, and it's width and height
|
||||
// 6. And finally, create all the edges in the svg node using information from the graph
|
||||
// ---
|
||||
|
||||
// Create the graph
|
||||
let g;
|
||||
|
||||
// TODO: Explore directed vs undirected graphs, and how the layout is affected
|
||||
// An E-R diagram could be said to be undirected, but there is merit in setting
|
||||
// the direction from parent to child in a one-to-many as this influences graphlib to
|
||||
// put the parent above the child (does it?), which is intuitive. Most relationships
|
||||
// in ER diagrams are one-to-many.
|
||||
g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
directed: true,
|
||||
compound: false
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: conf.layoutDirection,
|
||||
marginx: 20,
|
||||
marginy: 20,
|
||||
nodesep: 100,
|
||||
edgesep: 100,
|
||||
ranksep: 100
|
||||
})
|
||||
.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
// Draw the entities (at 0,0), returning the first svg node that got
|
||||
// inserted - this represents the insertion point for relationship paths
|
||||
const firstEntity = drawEntities(svg, erDb.getEntities(), g);
|
||||
|
||||
// TODO: externalise the addition of entities to the graph - it's a bit 'buried' in the above
|
||||
|
||||
// Add all the relationships to the graph
|
||||
const relationships = addRelationships(erDb.getRelationships(), g);
|
||||
|
||||
dagre.layout(g); // Node and edge positions will be updated
|
||||
|
||||
// Adjust the positions of the entities so that they adhere to the layout
|
||||
adjustEntities(svg, g);
|
||||
|
||||
// Draw the relationships
|
||||
relationships.forEach(function(rel) {
|
||||
drawRelationshipFromLayout(svg, rel, g, firstEntity);
|
||||
});
|
||||
|
||||
const padding = conf.diagramPadding;
|
||||
|
||||
const svgBounds = svg.node().getBBox();
|
||||
const width = svgBounds.width + padding * 2;
|
||||
const height = svgBounds.height + padding * 2;
|
||||
|
||||
svg.attr('height', height);
|
||||
svg.attr('width', '100%');
|
||||
svg.attr('style', `max-width: ${width}px;`);
|
||||
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
|
||||
}; // draw
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
draw
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
%lex
|
||||
|
||||
%x string
|
||||
%options case-insensitive
|
||||
|
||||
%%
|
||||
\s+ /* skip whitespace */
|
||||
[\s]+ return 'SPACE';
|
||||
["] { this.begin("string");}
|
||||
<string>["] { this.popState(); }
|
||||
<string>[^"]* { return 'STR'; }
|
||||
"erDiagram" return 'ER_DIAGRAM';
|
||||
\|o return 'ZERO_OR_ONE';
|
||||
\}o return 'ZERO_OR_MORE';
|
||||
\}\| return 'ONE_OR_MORE';
|
||||
\|\| return 'ONLY_ONE';
|
||||
o\| return 'ZERO_OR_ONE';
|
||||
o\{ return 'ZERO_OR_MORE';
|
||||
\|\{ return 'ONE_OR_MORE';
|
||||
\.\. return 'NON_IDENTIFYING';
|
||||
\-\- return 'IDENTIFYING';
|
||||
\.\- return 'NON_IDENTIFYING';
|
||||
\-\. return 'NON_IDENTIFYING';
|
||||
[A-Za-z][A-Za-z0-9\-]* return 'ALPHANUM';
|
||||
. return yytext[0];
|
||||
<<EOF>> return 'EOF';
|
||||
|
||||
/lex
|
||||
|
||||
%start start
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
|
||||
;
|
||||
|
||||
document
|
||||
: /* empty */
|
||||
| document statement
|
||||
;
|
||||
|
||||
statement
|
||||
: entityName relSpec entityName ':' role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
/*console.log($1 + $2 + $3 + ':' + $5);*/
|
||||
};
|
||||
|
||||
entityName
|
||||
: 'ALPHANUM' { $$ = $1; /*console.log('Entity: ' + $1);*/ }
|
||||
;
|
||||
|
||||
relSpec
|
||||
: cardinality relType cardinality
|
||||
{
|
||||
$$ = { cardA: $3, relType: $2, cardB: $1 };
|
||||
/*console.log('relSpec: ' + $3 + $2 + $1);*/
|
||||
}
|
||||
;
|
||||
|
||||
cardinality
|
||||
: 'ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE; }
|
||||
| 'ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE; }
|
||||
| 'ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE; }
|
||||
| 'ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE; }
|
||||
;
|
||||
|
||||
relType
|
||||
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
|
||||
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
|
||||
;
|
||||
|
||||
role
|
||||
: 'STR' { $$ = $1; }
|
||||
| 'ALPHANUM' { $$ = $1; }
|
||||
;
|
||||
%%
|
|
@ -0,0 +1,255 @@
|
|||
import erDb from '../erDb';
|
||||
import erDiagram from './erDiagram';
|
||||
import { setConfig } from '../../../config';
|
||||
import logger from '../../../logger';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict'
|
||||
});
|
||||
|
||||
describe('when parsing ER diagram it...', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
erDiagram.parser.yy = erDb;
|
||||
erDiagram.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should associate two entities correctly', function() {
|
||||
erDiagram.parser.parse('erDiagram\nCAR ||--o{ DRIVER : "insured for"');
|
||||
const entities = erDb.getEntities();
|
||||
const relationships = erDb.getRelationships();
|
||||
const carEntity = entities.CAR;
|
||||
const driverEntity = entities.DRIVER;
|
||||
|
||||
expect(carEntity).toBe('CAR');
|
||||
expect(driverEntity).toBe('DRIVER');
|
||||
expect(relationships.length).toBe(1);
|
||||
expect(relationships[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(relationships[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(relationships[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
|
||||
});
|
||||
|
||||
it('should not create duplicate entities', function() {
|
||||
const line1 = 'CAR ||--o{ DRIVER : "insured for"';
|
||||
const line2 = 'DRIVER ||--|| LICENSE : has';
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`);
|
||||
const entities = erDb.getEntities();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(3);
|
||||
});
|
||||
|
||||
it('should create the role specified', function() {
|
||||
const teacherRole = 'is teacher of';
|
||||
const line1 = `TEACHER }o--o{ STUDENT : "${teacherRole}"`;
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}`);
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(rels[0].roleA).toBe(`${teacherRole}`);
|
||||
});
|
||||
|
||||
it('should allow recursive relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nNODE ||--o{ NODE : "leads to"');
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow more than one relationship between the same two entities', function() {
|
||||
const line1 = 'CAR ||--o{ PERSON : "insured for"';
|
||||
const line2 = 'CAR }o--|| PERSON : "owned by"';
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`);
|
||||
const entities = erDb.getEntities();
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(2);
|
||||
expect(rels.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should limit the number of relationships between the same two entities', function() {
|
||||
/* TODO */
|
||||
});
|
||||
|
||||
it ('should not allow multiple relationships between the same two entities unless the roles are different', function() {
|
||||
/* TODO */
|
||||
});
|
||||
|
||||
|
||||
it('should handle only-one-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||--|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||..o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o..o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o--|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|--|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o--|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o..o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|..o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o..|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||..|| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ||--o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA |o..o| B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o--o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|..|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }o--|{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA }|..o{ B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
|
||||
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should represent identifying relationships properly', function() {
|
||||
erDiagram.parser.parse('erDiagram\nHOUSE ||--|{ ROOM : contains');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
|
||||
});
|
||||
|
||||
it('should represent non-identifying relationships properly', function() {
|
||||
erDiagram.parser.parse('erDiagram\n PERSON ||..o{ POSSESSION : owns');
|
||||
const rels = erDb.getRelationships();
|
||||
expect(rels[0].relSpec.relType).toBe(erDb.Identification.NON_IDENTIFYING);
|
||||
});
|
||||
|
||||
it('should not accept a syntax error', function() {
|
||||
const doc = 'erDiagram\nA xxx B : has';
|
||||
expect(() => {
|
||||
erDiagram.parser.parse(doc);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,261 @@
|
|||
import dagreD3 from 'dagre-d3';
|
||||
|
||||
function question(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const h = bbox.height;
|
||||
const s = (w + h) * 0.9;
|
||||
const points = [
|
||||
{ x: s / 2, y: 0 },
|
||||
{ x: s, y: -s / 2 },
|
||||
{ x: s / 2, y: -s },
|
||||
{ x: 0, y: -s / 2 }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, s, s, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function hexagon(parent, bbox, node) {
|
||||
const f = 4;
|
||||
const h = bbox.height;
|
||||
const m = h / f;
|
||||
const w = bbox.width + 2 * m;
|
||||
const points = [
|
||||
{ x: m, y: 0 },
|
||||
{ x: w - m, y: 0 },
|
||||
{ x: w, y: -h / 2 },
|
||||
{ x: w - m, y: -h },
|
||||
{ x: m, y: -h },
|
||||
{ x: 0, y: -h / 2 }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function rect_left_inv_arrow(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const h = bbox.height;
|
||||
const points = [
|
||||
{ x: -h / 2, y: 0 },
|
||||
{ x: w, y: 0 },
|
||||
{ x: w, y: -h },
|
||||
{ x: -h / 2, y: -h },
|
||||
{ x: 0, y: -h / 2 }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function lean_right(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const h = bbox.height;
|
||||
const points = [
|
||||
{ x: (-2 * h) / 6, y: 0 },
|
||||
{ x: w - h / 6, y: 0 },
|
||||
{ x: w + (2 * h) / 6, y: -h },
|
||||
{ x: h / 6, y: -h }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function lean_left(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const h = bbox.height;
|
||||
const points = [
|
||||
{ x: (2 * h) / 6, y: 0 },
|
||||
{ x: w + h / 6, y: 0 },
|
||||
{ x: w - (2 * h) / 6, y: -h },
|
||||
{ x: -h / 6, y: -h }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function trapezoid(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const h = bbox.height;
|
||||
const points = [
|
||||
{ x: (-2 * h) / 6, y: 0 },
|
||||
{ x: w + (2 * h) / 6, y: 0 },
|
||||
{ x: w - h / 6, y: -h },
|
||||
{ x: h / 6, y: -h }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function inv_trapezoid(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const h = bbox.height;
|
||||
const points = [
|
||||
{ x: h / 6, y: 0 },
|
||||
{ x: w - h / 6, y: 0 },
|
||||
{ x: w + (2 * h) / 6, y: -h },
|
||||
{ x: (-2 * h) / 6, y: -h }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function rect_right_inv_arrow(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const h = bbox.height;
|
||||
const points = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: w + h / 2, y: 0 },
|
||||
{ x: w, y: -h / 2 },
|
||||
{ x: w + h / 2, y: -h },
|
||||
{ x: 0, y: -h }
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(parent, w, h, points);
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.polygon(node, points, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function stadium(parent, bbox, node) {
|
||||
const h = bbox.height;
|
||||
const w = bbox.width + h / 4;
|
||||
|
||||
const shapeSvg = parent
|
||||
.insert('rect', ':first-child')
|
||||
.attr('rx', h / 2)
|
||||
.attr('ry', h / 2)
|
||||
.attr('x', -w / 2)
|
||||
.attr('y', -h / 2)
|
||||
.attr('width', w)
|
||||
.attr('height', h);
|
||||
|
||||
node.intersect = function(point) {
|
||||
return dagreD3.intersect.rect(node, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
function cylinder(parent, bbox, node) {
|
||||
const w = bbox.width;
|
||||
const rx = w / 2;
|
||||
const ry = rx / (2.5 + w / 50);
|
||||
const h = bbox.height + ry;
|
||||
|
||||
const shape =
|
||||
'M 0,' +
|
||||
ry +
|
||||
' a ' +
|
||||
rx +
|
||||
',' +
|
||||
ry +
|
||||
' 0,0,0 ' +
|
||||
w +
|
||||
' 0 a ' +
|
||||
rx +
|
||||
',' +
|
||||
ry +
|
||||
' 0,0,0 ' +
|
||||
-w +
|
||||
' 0 l 0,' +
|
||||
h +
|
||||
' a ' +
|
||||
rx +
|
||||
',' +
|
||||
ry +
|
||||
' 0,0,0 ' +
|
||||
w +
|
||||
' 0 l 0,' +
|
||||
-h;
|
||||
|
||||
const shapeSvg = parent
|
||||
.attr('label-offset-y', ry)
|
||||
.insert('path', ':first-child')
|
||||
.attr('d', shape)
|
||||
.attr('transform', 'translate(' + -w / 2 + ',' + -(h / 2 + ry) + ')');
|
||||
|
||||
node.intersect = function(point) {
|
||||
const pos = dagreD3.intersect.rect(node, point);
|
||||
const x = pos.x - node.x;
|
||||
|
||||
if (
|
||||
rx != 0 &&
|
||||
(Math.abs(x) < node.width / 2 ||
|
||||
(Math.abs(x) == node.width / 2 && Math.abs(pos.y - node.y) > node.height / 2 - ry))
|
||||
) {
|
||||
// ellipsis equation: x*x / a*a + y*y / b*b = 1
|
||||
// solve for y to get adjustion value for pos.y
|
||||
let y = ry * ry * (1 - (x * x) / (rx * rx));
|
||||
if (y != 0) y = Math.sqrt(y);
|
||||
y = ry - y;
|
||||
if (point.y - node.y > 0) y = -y;
|
||||
|
||||
pos.y += y;
|
||||
}
|
||||
|
||||
return pos;
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
export function addToRender(render) {
|
||||
render.shapes().question = question;
|
||||
render.shapes().hexagon = hexagon;
|
||||
render.shapes().stadium = stadium;
|
||||
render.shapes().cylinder = cylinder;
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
render.shapes().rect_left_inv_arrow = rect_left_inv_arrow;
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
render.shapes().lean_right = lean_right;
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
render.shapes().lean_left = lean_left;
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
render.shapes().trapezoid = trapezoid;
|
||||
|
||||
// Add custom shape for box with inverted arrow on left side
|
||||
render.shapes().inv_trapezoid = inv_trapezoid;
|
||||
|
||||
// Add custom shape for box with inverted arrow on right side
|
||||
render.shapes().rect_right_inv_arrow = rect_right_inv_arrow;
|
||||
}
|
||||
|
||||
function insertPolygonShape(parent, w, h, points) {
|
||||
return parent
|
||||
.insert('polygon', ':first-child')
|
||||
.attr(
|
||||
'points',
|
||||
points
|
||||
.map(function(d) {
|
||||
return d.x + ',' + d.y;
|
||||
})
|
||||
.join(' ')
|
||||
)
|
||||
.attr('transform', 'translate(' + -w / 2 + ',' + h / 2 + ')');
|
||||
}
|
||||
|
||||
export default {
|
||||
addToRender
|
||||
};
|
|
@ -0,0 +1,131 @@
|
|||
import { addToRender } from './flowChartShapes';
|
||||
|
||||
describe('flowchart shapes', function() {
|
||||
// rect-based shapes
|
||||
[
|
||||
['stadium', useWidth, useHeight]
|
||||
].forEach(function([shapeType, getW, getH]) {
|
||||
it(`should add a ${shapeType} shape that renders a properly positioned rect element`, function() {
|
||||
const mockRender = MockRender();
|
||||
const mockSvg = MockSvg();
|
||||
addToRender(mockRender);
|
||||
|
||||
[[100, 100], [123, 45], [71, 300]].forEach(function([width, height]) {
|
||||
const shape = mockRender.shapes()[shapeType](mockSvg, { width, height }, {});
|
||||
const w = width + height / 4;
|
||||
const h = height;
|
||||
const dx = -getW(w, h) / 2;
|
||||
const dy = -getH(w, h) / 2;
|
||||
expect(shape.__tag).toEqual('rect');
|
||||
expect(shape.__attrs).toHaveProperty('x', dx);
|
||||
expect(shape.__attrs).toHaveProperty('y', dy);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// path-based shapes
|
||||
[
|
||||
['cylinder', useWidth, useHeight]
|
||||
].forEach(function([shapeType, getW, getH]) {
|
||||
it(`should add a ${shapeType} shape that renders a properly positioned path element`, function() {
|
||||
const mockRender = MockRender();
|
||||
const mockSvg = MockSvg();
|
||||
addToRender(mockRender);
|
||||
|
||||
[[100, 100], [123, 45], [71, 300]].forEach(function([width, height]) {
|
||||
const shape = mockRender.shapes()[shapeType](mockSvg, { width, height }, {});
|
||||
expect(shape.__tag).toEqual('path');
|
||||
expect(shape.__attrs).toHaveProperty('d');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// polygon-based shapes
|
||||
[
|
||||
[
|
||||
'question',
|
||||
4,
|
||||
function(w, h) {
|
||||
return (w + h) * 0.9;
|
||||
},
|
||||
function(w, h) {
|
||||
return (w + h) * 0.9;
|
||||
}
|
||||
],
|
||||
[
|
||||
'hexagon',
|
||||
6,
|
||||
function(w, h) {
|
||||
return w + h / 2;
|
||||
},
|
||||
useHeight
|
||||
],
|
||||
['rect_left_inv_arrow', 5, useWidth, useHeight],
|
||||
['rect_right_inv_arrow', 5, useWidth, useHeight],
|
||||
['lean_right', 4, useWidth, useHeight],
|
||||
['lean_left', 4, useWidth, useHeight],
|
||||
['trapezoid', 4, useWidth, useHeight],
|
||||
['inv_trapezoid', 4, useWidth, useHeight]
|
||||
].forEach(function([shapeType, expectedPointCount, getW, getH]) {
|
||||
it(`should add a ${shapeType} shape that renders a properly translated polygon element`, function() {
|
||||
const mockRender = MockRender();
|
||||
const mockSvg = MockSvg();
|
||||
addToRender(mockRender);
|
||||
|
||||
[[100, 100], [123, 45], [71, 300]].forEach(function([width, height]) {
|
||||
const shape = mockRender.shapes()[shapeType](mockSvg, { width, height }, {});
|
||||
const dx = -getW(width, height) / 2;
|
||||
const dy = getH(width, height) / 2;
|
||||
const points = shape.__attrs.points.split(' ');
|
||||
expect(shape.__tag).toEqual('polygon');
|
||||
expect(shape.__attrs).toHaveProperty('transform', `translate(${dx},${dy})`);
|
||||
expect(points).toHaveLength(expectedPointCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function MockRender() {
|
||||
const shapes = {};
|
||||
return {
|
||||
shapes() {
|
||||
return shapes;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function MockSvg(tag, ...args) {
|
||||
const children = [];
|
||||
const attributes = {};
|
||||
return {
|
||||
get __args() {
|
||||
return args;
|
||||
},
|
||||
get __tag() {
|
||||
return tag;
|
||||
},
|
||||
get __children() {
|
||||
return children;
|
||||
},
|
||||
get __attrs() {
|
||||
return attributes;
|
||||
},
|
||||
insert: function(tag, ...args) {
|
||||
const child = MockSvg(tag, ...args);
|
||||
children.push(child);
|
||||
return child;
|
||||
},
|
||||
attr(name, value) {
|
||||
this.__attrs[name] = value;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useWidth(w, h) {
|
||||
return w;
|
||||
}
|
||||
|
||||
function useHeight(w, h) {
|
||||
return h;
|
||||
}
|
|
@ -0,0 +1,644 @@
|
|||
import * as d3 from 'd3';
|
||||
import { logger } from '../../logger';
|
||||
import utils from '../../utils';
|
||||
import { getConfig } from '../../config';
|
||||
import common from '../common/common';
|
||||
|
||||
// const MERMAID_DOM_ID_PREFIX = 'mermaid-dom-id-';
|
||||
const MERMAID_DOM_ID_PREFIX = '';
|
||||
|
||||
const config = getConfig();
|
||||
let vertices = {};
|
||||
let edges = [];
|
||||
let classes = [];
|
||||
let subGraphs = [];
|
||||
let subGraphLookup = {};
|
||||
let tooltips = {};
|
||||
let subCount = 0;
|
||||
let firstGraphFlag = true;
|
||||
let direction;
|
||||
// Functions to be run after graph rendering
|
||||
let funs = [];
|
||||
|
||||
/**
|
||||
* Function called by parser when a node definition has been found
|
||||
* @param id
|
||||
* @param text
|
||||
* @param type
|
||||
* @param style
|
||||
* @param classes
|
||||
*/
|
||||
export const addVertex = function(_id, text, type, style, classes) {
|
||||
let txt;
|
||||
let id = _id;
|
||||
if (typeof id === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (id.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
|
||||
if (typeof vertices[id] === 'undefined') {
|
||||
vertices[id] = { id: id, styles: [], classes: [] };
|
||||
}
|
||||
if (typeof text !== 'undefined') {
|
||||
txt = common.sanitizeText(text.trim(), config);
|
||||
|
||||
// strip quotes if string starts and ends with a quote
|
||||
if (txt[0] === '"' && txt[txt.length - 1] === '"') {
|
||||
txt = txt.substring(1, txt.length - 1);
|
||||
}
|
||||
|
||||
vertices[id].text = txt;
|
||||
} else {
|
||||
if (typeof vertices[id].text === 'undefined') {
|
||||
vertices[id].text = _id;
|
||||
}
|
||||
}
|
||||
if (typeof type !== 'undefined') {
|
||||
vertices[id].type = type;
|
||||
}
|
||||
if (typeof style !== 'undefined') {
|
||||
if (style !== null) {
|
||||
style.forEach(function(s) {
|
||||
vertices[id].styles.push(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (typeof classes !== 'undefined') {
|
||||
if (classes !== null) {
|
||||
classes.forEach(function(s) {
|
||||
vertices[id].classes.push(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function called by parser when a link/edge definition has been found
|
||||
* @param start
|
||||
* @param end
|
||||
* @param type
|
||||
* @param linktext
|
||||
*/
|
||||
export const addSingleLink = function(_start, _end, type, linktext) {
|
||||
let start = _start;
|
||||
let end = _end;
|
||||
if (start[0].match(/\d/)) start = MERMAID_DOM_ID_PREFIX + start;
|
||||
if (end[0].match(/\d/)) end = MERMAID_DOM_ID_PREFIX + end;
|
||||
logger.info('Got edge...', start, end);
|
||||
|
||||
const edge = { start: start, end: end, type: undefined, text: '' };
|
||||
linktext = type.text;
|
||||
|
||||
if (typeof linktext !== 'undefined') {
|
||||
edge.text = common.sanitizeText(linktext.trim(), config);
|
||||
|
||||
// strip quotes if string starts and exnds with a quote
|
||||
if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') {
|
||||
edge.text = edge.text.substring(1, edge.text.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof type !== 'undefined') {
|
||||
edge.type = type.type;
|
||||
edge.stroke = type.stroke;
|
||||
}
|
||||
edges.push(edge);
|
||||
};
|
||||
export const addLink = function(_start, _end, type, linktext) {
|
||||
let i, j;
|
||||
for (i = 0; i < _start.length; i++) {
|
||||
for (j = 0; j < _end.length; j++) {
|
||||
addSingleLink(_start[i], _end[j], type, linktext);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a link's line interpolation algorithm
|
||||
* @param pos
|
||||
* @param interpolate
|
||||
*/
|
||||
export const updateLinkInterpolate = function(positions, interp) {
|
||||
positions.forEach(function(pos) {
|
||||
if (pos === 'default') {
|
||||
edges.defaultInterpolate = interp;
|
||||
} else {
|
||||
edges[pos].interpolate = interp;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a link with a style
|
||||
* @param pos
|
||||
* @param style
|
||||
*/
|
||||
export const updateLink = function(positions, style) {
|
||||
positions.forEach(function(pos) {
|
||||
if (pos === 'default') {
|
||||
edges.defaultStyle = style;
|
||||
} else {
|
||||
if (utils.isSubstringInArray('fill', style) === -1) {
|
||||
style.push('fill:none');
|
||||
}
|
||||
edges[pos].style = style;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addClass = function(id, style) {
|
||||
if (typeof classes[id] === 'undefined') {
|
||||
classes[id] = { id: id, styles: [], textStyles: [] };
|
||||
}
|
||||
|
||||
if (typeof style !== 'undefined') {
|
||||
if (style !== null) {
|
||||
style.forEach(function(s) {
|
||||
if (s.match('color')) {
|
||||
const newStyle1 = s.replace('fill', 'bgFill');
|
||||
const newStyle2 = newStyle1.replace('color', 'fill');
|
||||
classes[id].textStyles.push(newStyle2);
|
||||
}
|
||||
classes[id].styles.push(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by parser when a graph definition is found, stores the direction of the chart.
|
||||
* @param dir
|
||||
*/
|
||||
export const setDirection = function(dir) {
|
||||
direction = dir;
|
||||
if (direction.match(/.*</)) {
|
||||
direction = 'RL';
|
||||
}
|
||||
if (direction.match(/.*\^/)) {
|
||||
direction = 'BT';
|
||||
}
|
||||
if (direction.match(/.*>/)) {
|
||||
direction = 'LR';
|
||||
}
|
||||
if (direction.match(/.*v/)) {
|
||||
direction = 'TB';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by parser when a special node is found, e.g. a clickable element.
|
||||
* @param ids Comma separated list of ids
|
||||
* @param className Class to add
|
||||
*/
|
||||
export const setClass = function(ids, className) {
|
||||
ids.split(',').forEach(function(_id) {
|
||||
let id = _id;
|
||||
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
if (typeof vertices[id] !== 'undefined') {
|
||||
vertices[id].classes.push(className);
|
||||
}
|
||||
|
||||
if (typeof subGraphLookup[id] !== 'undefined') {
|
||||
subGraphLookup[id].classes.push(className);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setTooltip = function(ids, tooltip) {
|
||||
ids.split(',').forEach(function(id) {
|
||||
if (typeof tooltip !== 'undefined') {
|
||||
tooltips[id] = common.sanitizeText(tooltip, config);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setClickFun = function(_id, functionName) {
|
||||
let id = _id;
|
||||
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
if (config.securityLevel !== 'loose') {
|
||||
return;
|
||||
}
|
||||
if (typeof functionName === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (typeof vertices[id] !== 'undefined') {
|
||||
funs.push(function() {
|
||||
const elem = document.querySelector(`[id="${id}"]`);
|
||||
if (elem !== null) {
|
||||
elem.addEventListener(
|
||||
'click',
|
||||
function() {
|
||||
window[functionName](id);
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by parser when a link is found. Adds the URL to the vertex data.
|
||||
* @param ids Comma separated list of ids
|
||||
* @param linkStr URL to create a link for
|
||||
* @param tooltip Tooltip for the clickable element
|
||||
*/
|
||||
export const setLink = function(ids, linkStr, tooltip) {
|
||||
ids.split(',').forEach(function(_id) {
|
||||
let id = _id;
|
||||
if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
if (typeof vertices[id] !== 'undefined') {
|
||||
vertices[id].link = utils.formatUrl(linkStr, config);
|
||||
}
|
||||
});
|
||||
setTooltip(ids, tooltip);
|
||||
setClass(ids, 'clickable');
|
||||
};
|
||||
export const getTooltip = function(id) {
|
||||
return tooltips[id];
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by parser when a click definition is found. Registers an event handler.
|
||||
* @param ids Comma separated list of ids
|
||||
* @param functionName Function to be called on click
|
||||
* @param tooltip Tooltip for the clickable element
|
||||
*/
|
||||
export const setClickEvent = function(ids, functionName, tooltip) {
|
||||
ids.split(',').forEach(function(id) {
|
||||
setClickFun(id, functionName);
|
||||
});
|
||||
setTooltip(ids, tooltip);
|
||||
setClass(ids, 'clickable');
|
||||
};
|
||||
|
||||
export const bindFunctions = function(element) {
|
||||
funs.forEach(function(fun) {
|
||||
fun(element);
|
||||
});
|
||||
};
|
||||
export const getDirection = function() {
|
||||
return direction.trim();
|
||||
};
|
||||
/**
|
||||
* Retrieval function for fetching the found nodes after parsing has completed.
|
||||
* @returns {{}|*|vertices}
|
||||
*/
|
||||
export const getVertices = function() {
|
||||
return vertices;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieval function for fetching the found links after parsing has completed.
|
||||
* @returns {{}|*|edges}
|
||||
*/
|
||||
export const getEdges = function() {
|
||||
return edges;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieval function for fetching the found class definitions after parsing has completed.
|
||||
* @returns {{}|*|classes}
|
||||
*/
|
||||
export const getClasses = function() {
|
||||
return classes;
|
||||
};
|
||||
|
||||
const setupToolTips = function(element) {
|
||||
let tooltipElem = d3.select('.mermaidTooltip');
|
||||
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
|
||||
tooltipElem = d3
|
||||
.select('body')
|
||||
.append('div')
|
||||
.attr('class', 'mermaidTooltip')
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
const svg = d3.select(element).select('svg');
|
||||
|
||||
const nodes = svg.selectAll('g.node');
|
||||
nodes
|
||||
.on('mouseover', function() {
|
||||
const el = d3.select(this);
|
||||
const title = el.attr('title');
|
||||
// Dont try to draw a tooltip if no data is provided
|
||||
if (title === null) {
|
||||
return;
|
||||
}
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
tooltipElem
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', '.9');
|
||||
tooltipElem
|
||||
.html(el.attr('title'))
|
||||
.style('left', rect.left + (rect.right - rect.left) / 2 + 'px')
|
||||
.style('top', rect.top - 14 + document.body.scrollTop + 'px');
|
||||
el.classed('hover', true);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
tooltipElem
|
||||
.transition()
|
||||
.duration(500)
|
||||
.style('opacity', 0);
|
||||
const el = d3.select(this);
|
||||
el.classed('hover', false);
|
||||
});
|
||||
};
|
||||
funs.push(setupToolTips);
|
||||
|
||||
/**
|
||||
* Clears the internal graph db so that a new graph can be parsed.
|
||||
*/
|
||||
export const clear = function() {
|
||||
vertices = {};
|
||||
classes = {};
|
||||
edges = [];
|
||||
funs = [];
|
||||
funs.push(setupToolTips);
|
||||
subGraphs = [];
|
||||
subGraphLookup = {};
|
||||
subCount = 0;
|
||||
tooltips = [];
|
||||
firstGraphFlag = true;
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export const defaultStyle = function() {
|
||||
return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;';
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the internal graph db so that a new graph can be parsed.
|
||||
*/
|
||||
export const addSubGraph = function(_id, list, _title) {
|
||||
let id = _id.trim();
|
||||
let title = _title;
|
||||
if (_id === _title && _title.match(/\s/)) {
|
||||
id = undefined;
|
||||
}
|
||||
function uniq(a) {
|
||||
const prims = { boolean: {}, number: {}, string: {} };
|
||||
const objs = [];
|
||||
|
||||
return a.filter(function(item) {
|
||||
const type = typeof item;
|
||||
if (item.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
if (type in prims) {
|
||||
return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); // eslint-disable-line
|
||||
} else {
|
||||
return objs.indexOf(item) >= 0 ? false : objs.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let nodeList = [];
|
||||
|
||||
nodeList = uniq(nodeList.concat.apply(nodeList, list));
|
||||
for (let i = 0; i < nodeList.length; i++) {
|
||||
if (nodeList[i][0].match(/\d/)) nodeList[i] = MERMAID_DOM_ID_PREFIX + nodeList[i];
|
||||
}
|
||||
|
||||
id = id || 'subGraph' + subCount;
|
||||
if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
title = title || '';
|
||||
title = common.sanitizeText(title, config);
|
||||
subCount = subCount + 1;
|
||||
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] };
|
||||
subGraphs.push(subGraph);
|
||||
subGraphLookup[id] = subGraph;
|
||||
return id;
|
||||
};
|
||||
|
||||
const getPosForId = function(id) {
|
||||
for (let i = 0; i < subGraphs.length; i++) {
|
||||
if (subGraphs[i].id === id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
let secCount = -1;
|
||||
const posCrossRef = [];
|
||||
const indexNodes2 = function(id, pos) {
|
||||
const nodes = subGraphs[pos].nodes;
|
||||
secCount = secCount + 1;
|
||||
if (secCount > 2000) {
|
||||
return;
|
||||
}
|
||||
posCrossRef[secCount] = pos;
|
||||
// Check if match
|
||||
if (subGraphs[pos].id === id) {
|
||||
return {
|
||||
result: true,
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
let posCount = 1;
|
||||
while (count < nodes.length) {
|
||||
const childPos = getPosForId(nodes[count]);
|
||||
// Ignore regular nodes (pos will be -1)
|
||||
if (childPos >= 0) {
|
||||
const res = indexNodes2(id, childPos);
|
||||
if (res.result) {
|
||||
return {
|
||||
result: true,
|
||||
count: posCount + res.count
|
||||
};
|
||||
} else {
|
||||
posCount = posCount + res.count;
|
||||
}
|
||||
}
|
||||
count = count + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
result: false,
|
||||
count: posCount
|
||||
};
|
||||
};
|
||||
|
||||
export const getDepthFirstPos = function(pos) {
|
||||
return posCrossRef[pos];
|
||||
};
|
||||
export const indexNodes = function() {
|
||||
secCount = -1;
|
||||
if (subGraphs.length > 0) {
|
||||
indexNodes2('none', subGraphs.length - 1, 0);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSubGraphs = function() {
|
||||
return subGraphs;
|
||||
};
|
||||
|
||||
export const firstGraph = () => {
|
||||
if (firstGraphFlag) {
|
||||
firstGraphFlag = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const destructStartLink = _str => {
|
||||
const str = _str.trim();
|
||||
|
||||
switch (str) {
|
||||
case '<--':
|
||||
return { type: 'arrow', stroke: 'normal' };
|
||||
case 'x--':
|
||||
return { type: 'arrow_cross', stroke: 'normal' };
|
||||
case 'o--':
|
||||
return { type: 'arrow_circle', stroke: 'normal' };
|
||||
case '<-.':
|
||||
return { type: 'arrow', stroke: 'dotted' };
|
||||
case 'x-.':
|
||||
return { type: 'arrow_cross', stroke: 'dotted' };
|
||||
case 'o-.':
|
||||
return { type: 'arrow_circle', stroke: 'dotted' };
|
||||
case '<==':
|
||||
return { type: 'arrow', stroke: 'thick' };
|
||||
case 'x==':
|
||||
return { type: 'arrow_cross', stroke: 'thick' };
|
||||
case 'o==':
|
||||
return { type: 'arrow_circle', stroke: 'thick' };
|
||||
case '--':
|
||||
return { type: 'arrow_open', stroke: 'normal' };
|
||||
case '==':
|
||||
return { type: 'arrow_open', stroke: 'thick' };
|
||||
case '-.':
|
||||
return { type: 'arrow_open', stroke: 'dotted' };
|
||||
}
|
||||
};
|
||||
|
||||
const destructEndLink = _str => {
|
||||
const str = _str.trim();
|
||||
|
||||
switch (str) {
|
||||
case '--x':
|
||||
return { type: 'arrow_cross', stroke: 'normal' };
|
||||
case '-->':
|
||||
return { type: 'arrow', stroke: 'normal' };
|
||||
case '<-->':
|
||||
return { type: 'double_arrow_point', stroke: 'normal' };
|
||||
case 'x--x':
|
||||
return { type: 'double_arrow_cross', stroke: 'normal' };
|
||||
case 'o--o':
|
||||
return { type: 'double_arrow_circle', stroke: 'normal' };
|
||||
case 'o.-o':
|
||||
return { type: 'double_arrow_circle', stroke: 'dotted' };
|
||||
case '<==>':
|
||||
return { type: 'double_arrow_point', stroke: 'thick' };
|
||||
case 'o==o':
|
||||
return { type: 'double_arrow_circle', stroke: 'thick' };
|
||||
case 'x==x':
|
||||
return { type: 'double_arrow_cross', stroke: 'thick' };
|
||||
case 'x.-x':
|
||||
return { type: 'double_arrow_cross', stroke: 'dotted' };
|
||||
case 'x-.-x':
|
||||
return { type: 'double_arrow_cross', stroke: 'dotted' };
|
||||
case '<.->':
|
||||
return { type: 'double_arrow_point', stroke: 'dotted' };
|
||||
case '<-.->':
|
||||
return { type: 'double_arrow_point', stroke: 'dotted' };
|
||||
case 'o-.-o':
|
||||
return { type: 'double_arrow_circle', stroke: 'dotted' };
|
||||
case '--o':
|
||||
return { type: 'arrow_circle', stroke: 'normal' };
|
||||
case '---':
|
||||
return { type: 'arrow_open', stroke: 'normal' };
|
||||
case '-.-x':
|
||||
return { type: 'arrow_cross', stroke: 'dotted' };
|
||||
case '-.->':
|
||||
return { type: 'arrow', stroke: 'dotted' };
|
||||
case '-.-o':
|
||||
return { type: 'arrow_circle', stroke: 'dotted' };
|
||||
case '-.-':
|
||||
return { type: 'arrow_open', stroke: 'dotted' };
|
||||
case '.-x':
|
||||
return { type: 'arrow_cross', stroke: 'dotted' };
|
||||
case '.->':
|
||||
return { type: 'arrow', stroke: 'dotted' };
|
||||
case '.-o':
|
||||
return { type: 'arrow_circle', stroke: 'dotted' };
|
||||
case '.-':
|
||||
return { type: 'arrow_open', stroke: 'dotted' };
|
||||
case '==x':
|
||||
return { type: 'arrow_cross', stroke: 'thick' };
|
||||
case '==>':
|
||||
return { type: 'arrow', stroke: 'thick' };
|
||||
case '==o':
|
||||
return { type: 'arrow_circle', stroke: 'thick' };
|
||||
case '===':
|
||||
return { type: 'arrow_open', stroke: 'thick' };
|
||||
}
|
||||
};
|
||||
|
||||
const destructLink = (_str, _startStr) => {
|
||||
const info = destructEndLink(_str);
|
||||
let startInfo;
|
||||
if (_startStr) {
|
||||
startInfo = destructStartLink(_startStr);
|
||||
|
||||
if (startInfo.stroke !== info.stroke) {
|
||||
return { type: 'INVALID', stroke: 'INVALID' };
|
||||
}
|
||||
|
||||
if (startInfo.type === 'arrow_open') {
|
||||
// -- xyz --> - take arrow type form ending
|
||||
startInfo.type = info.type;
|
||||
} else {
|
||||
// x-- xyz --> - not supported
|
||||
if (startInfo.type !== info.type) return { type: 'INVALID', stroke: 'INVALID' };
|
||||
|
||||
startInfo.type = 'double_' + startInfo.type;
|
||||
}
|
||||
|
||||
if (startInfo.type === 'double_arrow') {
|
||||
startInfo.type = 'double_arrow_point';
|
||||
}
|
||||
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
export default {
|
||||
addVertex,
|
||||
addLink,
|
||||
updateLinkInterpolate,
|
||||
updateLink,
|
||||
addClass,
|
||||
setDirection,
|
||||
setClass,
|
||||
getTooltip,
|
||||
setClickEvent,
|
||||
setLink,
|
||||
bindFunctions,
|
||||
getDirection,
|
||||
getVertices,
|
||||
getEdges,
|
||||
getClasses,
|
||||
clear,
|
||||
defaultStyle,
|
||||
addSubGraph,
|
||||
getDepthFirstPos,
|
||||
indexNodes,
|
||||
getSubGraphs,
|
||||
destructLink,
|
||||
lex: {
|
||||
firstGraph
|
||||
}
|
||||
};
|
|
@ -0,0 +1,487 @@
|
|||
import graphlib from 'graphlib';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
import flowDb from '../flowchart/flowDb';
|
||||
import flow from '../flowchart/parser/flow';
|
||||
import { getConfig } from '../../config';
|
||||
|
||||
import dagreD3 from 'dagre-d3';
|
||||
import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js';
|
||||
import { logger } from '../../logger';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils';
|
||||
import flowChartShapes from '../flowchart/flowChartShapes';
|
||||
|
||||
const conf = {};
|
||||
export const setConf = function(cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
conf[keys[i]] = cnf[keys[i]];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the vertices found in the graph definition to the graph to be rendered.
|
||||
* @param vert Object containing the vertices.
|
||||
* @param g The graph that is to be drawn.
|
||||
*/
|
||||
export const addVertices = function(vert, g, svgId) {
|
||||
const svg = d3.select(`[id="${svgId}"]`);
|
||||
const keys = Object.keys(vert);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function(id) {
|
||||
const vertex = vert[id];
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
* @type {string}
|
||||
*/
|
||||
let classStr = 'default';
|
||||
if (vertex.classes.length > 0) {
|
||||
classStr = vertex.classes.join(' ');
|
||||
}
|
||||
|
||||
const styles = getStylesFromArray(vertex.styles);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
let vertexNode;
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
const node = {
|
||||
label: vertexText.replace(
|
||||
/fa[lrsb]?:fa-[\w-]+/g,
|
||||
s => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
)
|
||||
};
|
||||
vertexNode = addHtmlLabel(svg, node).node();
|
||||
vertexNode.parentNode.removeChild(vertexNode);
|
||||
} else {
|
||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||
|
||||
const rows = vertexText.split(/<br\s*\/?>/gi);
|
||||
|
||||
for (let j = 0; j < rows.length; j++) {
|
||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
tspan.setAttribute('dy', '1em');
|
||||
tspan.setAttribute('x', '1');
|
||||
tspan.textContent = rows[j];
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
vertexNode = svgLabel;
|
||||
}
|
||||
|
||||
let radious = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radious = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'diamond':
|
||||
_shape = 'question';
|
||||
break;
|
||||
case 'hexagon':
|
||||
_shape = 'hexagon';
|
||||
break;
|
||||
case 'odd':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'lean_right':
|
||||
_shape = 'lean_right';
|
||||
break;
|
||||
case 'lean_left':
|
||||
_shape = 'lean_left';
|
||||
break;
|
||||
case 'trapezoid':
|
||||
_shape = 'trapezoid';
|
||||
break;
|
||||
case 'inv_trapezoid':
|
||||
_shape = 'inv_trapezoid';
|
||||
break;
|
||||
case 'odd_right':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'circle':
|
||||
_shape = 'circle';
|
||||
break;
|
||||
case 'ellipse':
|
||||
_shape = 'ellipse';
|
||||
break;
|
||||
case 'stadium':
|
||||
_shape = 'stadium';
|
||||
break;
|
||||
case 'cylinder':
|
||||
_shape = 'cylinder';
|
||||
break;
|
||||
case 'group':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
default:
|
||||
_shape = 'rect';
|
||||
}
|
||||
// Add the node
|
||||
g.setNode(vertex.id, {
|
||||
labelType: 'svg',
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
label: vertexNode,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add edges to graph based on parsed graph defninition
|
||||
* @param {Object} edges The edges to add to the graph
|
||||
* @param {Object} g The graph object
|
||||
*/
|
||||
export const addEdges = function(edges, g) {
|
||||
let cnt = 0;
|
||||
|
||||
let defaultStyle;
|
||||
let defaultLabelStyle;
|
||||
|
||||
if (typeof edges.defaultStyle !== 'undefined') {
|
||||
const defaultStyles = getStylesFromArray(edges.defaultStyle);
|
||||
defaultStyle = defaultStyles.style;
|
||||
defaultLabelStyle = defaultStyles.labelStyle;
|
||||
}
|
||||
|
||||
edges.forEach(function(edge) {
|
||||
cnt++;
|
||||
const edgeData = {};
|
||||
|
||||
// Set link type for rendering
|
||||
if (edge.type === 'arrow_open') {
|
||||
edgeData.arrowhead = 'none';
|
||||
} else {
|
||||
edgeData.arrowhead = 'normal';
|
||||
}
|
||||
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
if (typeof edge.style !== 'undefined') {
|
||||
const styles = getStylesFromArray(edge.style);
|
||||
style = styles.style;
|
||||
labelStyle = styles.labelStyle;
|
||||
} else {
|
||||
switch (edge.stroke) {
|
||||
case 'normal':
|
||||
style = 'fill:none';
|
||||
if (typeof defaultStyle !== 'undefined') {
|
||||
style = defaultStyle;
|
||||
}
|
||||
if (typeof defaultLabelStyle !== 'undefined') {
|
||||
labelStyle = defaultLabelStyle;
|
||||
}
|
||||
break;
|
||||
case 'dotted':
|
||||
style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||
break;
|
||||
case 'thick':
|
||||
style = ' stroke-width: 3.5px;fill:none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
edgeData.style = style;
|
||||
edgeData.labelStyle = labelStyle;
|
||||
|
||||
if (typeof edge.interpolate !== 'undefined') {
|
||||
edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear);
|
||||
} else if (typeof edges.defaultInterpolate !== 'undefined') {
|
||||
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear);
|
||||
} else {
|
||||
edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear);
|
||||
}
|
||||
|
||||
if (typeof edge.text === 'undefined') {
|
||||
if (typeof edge.style !== 'undefined') {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
}
|
||||
} else {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
edgeData.labelType = 'html';
|
||||
edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>';
|
||||
} else {
|
||||
edgeData.labelType = 'text';
|
||||
edgeData.label = edge.text.replace(/<br\s*\/?>/gi, '\n');
|
||||
|
||||
if (typeof edge.style === 'undefined') {
|
||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
|
||||
}
|
||||
|
||||
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||
}
|
||||
}
|
||||
// Add the edge to the graph
|
||||
g.setEdge(edge.start, edge.end, edgeData, cnt);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
* @returns {object} classDef styles
|
||||
*/
|
||||
export const getClasses = function(text) {
|
||||
logger.info('Extracting classes');
|
||||
flowDb.clear();
|
||||
const parser = flow.parser;
|
||||
parser.yy = flowDb;
|
||||
|
||||
// Parse the graph definition
|
||||
parser.parse(text);
|
||||
return flowDb.getClasses();
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
* @param text
|
||||
* @param id
|
||||
*/
|
||||
export const draw = function(text, id) {
|
||||
logger.info('Drawing flowchart');
|
||||
flowDb.clear();
|
||||
const parser = flow.parser;
|
||||
parser.yy = flowDb;
|
||||
|
||||
// Parse the graph definition
|
||||
try {
|
||||
parser.parse(text);
|
||||
} catch (err) {
|
||||
logger.debug('Parsing failed');
|
||||
}
|
||||
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = flowDb.getDirection();
|
||||
if (typeof dir === 'undefined') {
|
||||
dir = 'TD';
|
||||
}
|
||||
|
||||
const conf = getConfig().flowchart;
|
||||
const nodeSpacing = conf.nodeSpacing || 50;
|
||||
const rankSpacing = conf.rankSpacing || 50;
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: dir,
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8
|
||||
})
|
||||
.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
let subG;
|
||||
const subGraphs = flowDb.getSubGraphs();
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
|
||||
}
|
||||
|
||||
// Fetch the verices/nodes and edges/links from the parsed graph definition
|
||||
const vert = flowDb.getVertices();
|
||||
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
let i = 0;
|
||||
for (i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
|
||||
d3.selectAll('cluster').append('text');
|
||||
|
||||
for (let j = 0; j < subG.nodes.length; j++) {
|
||||
g.setParent(subG.nodes[j], subG.id);
|
||||
}
|
||||
}
|
||||
addVertices(vert, g, id);
|
||||
addEdges(edges, g);
|
||||
|
||||
// Create the renderer
|
||||
const Render = dagreD3.render;
|
||||
const render = new Render();
|
||||
|
||||
// Add custom shapes
|
||||
flowChartShapes.addToRender(render);
|
||||
|
||||
// Add our custom arrow - an empty arrowhead
|
||||
render.arrows().none = function normal(parent, id, edge, type) {
|
||||
const marker = parent
|
||||
.append('marker')
|
||||
.attr('id', id)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 8)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto');
|
||||
|
||||
const path = marker.append('path').attr('d', 'M 0 0 L 0 0 L 0 0 z');
|
||||
dagreD3.util.applyStyle(path, edge[type + 'Style']);
|
||||
};
|
||||
|
||||
// Override normal arrowhead defined in d3. Remove style & add class to allow css styling.
|
||||
render.arrows().normal = function normal(parent, id) {
|
||||
const marker = parent
|
||||
.append('marker')
|
||||
.attr('id', id)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 8)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto');
|
||||
|
||||
marker
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
|
||||
.attr('class', 'arrowheadPath')
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
const svg = d3.select(`[id="${id}"]`);
|
||||
|
||||
// Run the renderer. This is what draws the final graph.
|
||||
const element = d3.select('#' + id + ' g');
|
||||
render(element, g);
|
||||
|
||||
element.selectAll('g.node').attr('title', function() {
|
||||
return flowDb.getTooltip(this.id);
|
||||
});
|
||||
|
||||
const padding = 8;
|
||||
const svgBounds = svg.node().getBBox();
|
||||
const width = svgBounds.width + padding * 2;
|
||||
const height = svgBounds.height + padding * 2;
|
||||
logger.debug(
|
||||
`new ViewBox 0 0 ${width} ${height}`,
|
||||
`translate(${padding - g._label.marginx}, ${padding - g._label.marginy})`
|
||||
);
|
||||
|
||||
if (conf.useMaxWidth) {
|
||||
svg.attr('width', '100%');
|
||||
svg.attr('style', `max-width: ${width}px;`);
|
||||
} else {
|
||||
svg.attr('height', height);
|
||||
svg.attr('width', width);
|
||||
}
|
||||
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
svg
|
||||
.select('g')
|
||||
.attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`);
|
||||
|
||||
// Index nodes
|
||||
flowDb.indexNodes('subGraph' + i);
|
||||
|
||||
// reposition labels
|
||||
for (i = 0; i < subGraphs.length; i++) {
|
||||
subG = subGraphs[i];
|
||||
|
||||
if (subG.title !== 'undefined') {
|
||||
const clusterRects = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"] rect');
|
||||
const clusterEl = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"]');
|
||||
|
||||
const xPos = clusterRects[0].x.baseVal.value;
|
||||
const yPos = clusterRects[0].y.baseVal.value;
|
||||
const width = clusterRects[0].width.baseVal.value;
|
||||
const cluster = d3.select(clusterEl[0]);
|
||||
const te = cluster.select('.label');
|
||||
te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`);
|
||||
te.attr('id', id + 'Text');
|
||||
|
||||
for (let j = 0; j < subG.classes.length; j++) {
|
||||
clusterEl[0].classList.add(subG.classes[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add label rects for non html labels
|
||||
if (!conf.htmlLabels) {
|
||||
const labels = document.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (let k = 0; k < labels.length; k++) {
|
||||
const label = labels[k];
|
||||
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
rect.setAttribute('style', 'fill:#e8e8e8;');
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// If node has a link, wrap it in an anchor SVG object.
|
||||
const keys = Object.keys(vert);
|
||||
keys.forEach(function(key) {
|
||||
const vertex = vert[key];
|
||||
|
||||
if (vertex.link) {
|
||||
const node = d3.select('#' + id + ' [id="' + key + '"]');
|
||||
if (node) {
|
||||
const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' '));
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
|
||||
|
||||
const linkNode = node.insert(function() {
|
||||
return link;
|
||||
}, ':first-child');
|
||||
|
||||
const shape = node.select('.label-container');
|
||||
if (shape) {
|
||||
linkNode.append(function() {
|
||||
return shape.node();
|
||||
});
|
||||
}
|
||||
|
||||
const label = node.select('.label');
|
||||
if (label) {
|
||||
linkNode.append(function() {
|
||||
return label.node();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
addVertices,
|
||||
addEdges,
|
||||
getClasses,
|
||||
draw
|
||||
};
|
|
@ -2,6 +2,7 @@ import * as d3 from 'd3';
|
|||
import { logger } from '../../logger';
|
||||
import utils from '../../utils';
|
||||
import { getConfig } from '../../config';
|
||||
import common from '../common/common';
|
||||
|
||||
// const MERMAID_DOM_ID_PREFIX = 'mermaid-dom-id-';
|
||||
const MERMAID_DOM_ID_PREFIX = '';
|
||||
|
@ -43,7 +44,7 @@ export const addVertex = function(_id, text, type, style, classes) {
|
|||
vertices[id] = { id: id, styles: [], classes: [] };
|
||||
}
|
||||
if (typeof text !== 'undefined') {
|
||||
txt = utils.sanitize(text.trim(), config);
|
||||
txt = common.sanitizeText(text.trim(), config);
|
||||
|
||||
// strip quotes if string starts and ends with a quote
|
||||
if (txt[0] === '"' && txt[txt.length - 1] === '"') {
|
||||
|
@ -93,7 +94,7 @@ export const addSingleLink = function(_start, _end, type, linktext) {
|
|||
linktext = type.text;
|
||||
|
||||
if (typeof linktext !== 'undefined') {
|
||||
edge.text = utils.sanitize(linktext.trim(), config);
|
||||
edge.text = common.sanitizeText(linktext.trim(), config);
|
||||
|
||||
// strip quotes if string starts and exnds with a quote
|
||||
if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') {
|
||||
|
@ -210,7 +211,7 @@ export const setClass = function(ids, className) {
|
|||
const setTooltip = function(ids, tooltip) {
|
||||
ids.split(',').forEach(function(id) {
|
||||
if (typeof tooltip !== 'undefined') {
|
||||
tooltips[id] = utils.sanitize(tooltip, config);
|
||||
tooltips[id] = common.sanitizeText(tooltip, config);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -410,7 +411,7 @@ export const addSubGraph = function(_id, list, _title) {
|
|||
id = id || 'subGraph' + subCount;
|
||||
if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
title = title || '';
|
||||
title = utils.sanitize(title, config);
|
||||
title = common.sanitizeText(title, config);
|
||||
subCount = subCount + 1;
|
||||
const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] };
|
||||
subGraphs.push(subGraph);
|
||||
|
|
|
@ -36,7 +36,7 @@ export const addVertices = function(vert, g, svgId) {
|
|||
* Variable for storing the classes for the vertex
|
||||
* @type {string}
|
||||
*/
|
||||
let classStr = '';
|
||||
let classStr = 'default';
|
||||
if (vertex.classes.length > 0) {
|
||||
classStr = vertex.classes.join(' ');
|
||||
}
|
||||
|
|
|
@ -127,6 +127,40 @@ describe('the flowchart renderer', function() {
|
|||
expect(addedNodes[0][1]).toHaveProperty('labelStyle', expectedLabelStyle);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should add default class to all nodes which do not have another class assigned`, function() {
|
||||
const addedNodes = [];
|
||||
const mockG = {
|
||||
setNode: function(id, object) {
|
||||
addedNodes.push([id, object]);
|
||||
}
|
||||
};
|
||||
addVertices(
|
||||
{
|
||||
v1: {
|
||||
type: 'rect',
|
||||
id: 'defaultNode',
|
||||
classes: [],
|
||||
styles: [],
|
||||
text: 'my vertex text'
|
||||
},
|
||||
v2: {
|
||||
type: 'rect',
|
||||
id: 'myNode',
|
||||
classes: ['myClass'],
|
||||
styles: [],
|
||||
text: 'my vertex text'
|
||||
}
|
||||
},
|
||||
mockG,
|
||||
'svg-id'
|
||||
);
|
||||
expect(addedNodes).toHaveLength(2);
|
||||
expect(addedNodes[0][0]).toEqual('defaultNode');
|
||||
expect(addedNodes[0][1]).toHaveProperty('class', 'default');
|
||||
expect(addedNodes[1][0]).toEqual('myNode');
|
||||
expect(addedNodes[1][1]).toHaveProperty('class', 'myClass');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adding edges to a graph', function() {
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
"classDef" return 'CLASSDEF';
|
||||
"class" return 'CLASS';
|
||||
"click" return 'CLICK';
|
||||
"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"flowchart" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"subgraph" return 'subgraph';
|
||||
"end"\b\s* return 'end';
|
||||
<dir>\s*"LR" { this.popState(); return 'DIR'; }
|
||||
|
|
|
@ -24,6 +24,8 @@ const conf = {
|
|||
noteMargin: 10,
|
||||
// Space between messages
|
||||
messageMargin: 35,
|
||||
// Multiline message alignment
|
||||
messageAlign: 'center',
|
||||
// mirror actors under diagram
|
||||
mirrorActors: false,
|
||||
// Depending on css styling this might need adjustment
|
||||
|
@ -230,24 +232,38 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
|
|||
const g = elem.append('g');
|
||||
const txtCenter = startx + (stopx - startx) / 2;
|
||||
|
||||
let textElem;
|
||||
let textElems = [];
|
||||
let counterBreaklines = 0;
|
||||
let breaklineOffset = 17;
|
||||
const breaklines = msg.message.split(/<br\s*\/?>/gi);
|
||||
for (const breakline of breaklines) {
|
||||
textElem = g
|
||||
.append('text') // text label for the x axis
|
||||
.attr('x', txtCenter)
|
||||
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('class', 'messageText')
|
||||
.text(breakline.trim());
|
||||
textElems.push(
|
||||
g
|
||||
.append('text') // text label for the x axis
|
||||
.attr('x', txtCenter)
|
||||
.attr('y', verticalPos - 7 + counterBreaklines * breaklineOffset)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('class', 'messageText')
|
||||
.text(breakline.trim())
|
||||
);
|
||||
counterBreaklines++;
|
||||
}
|
||||
const offsetLineCounter = counterBreaklines - 1;
|
||||
const totalOffset = offsetLineCounter * breaklineOffset;
|
||||
|
||||
let textWidth = (textElem._groups || textElem)[0][0].getBBox().width;
|
||||
let textWidths = textElems.map(function(textElem) {
|
||||
return (textElem._groups || textElem)[0][0].getBBox().width;
|
||||
});
|
||||
let textWidth = Math.max(...textWidths);
|
||||
for (const textElem of textElems) {
|
||||
if (conf.messageAlign === 'left') {
|
||||
textElem.attr('x', txtCenter - textWidth / 2).style('text-anchor', 'start');
|
||||
} else if (conf.messageAlign === 'right') {
|
||||
textElem.attr('x', txtCenter + textWidth / 2).style('text-anchor', 'end');
|
||||
}
|
||||
}
|
||||
|
||||
bounds.bumpVerticalPos(totalOffset);
|
||||
|
||||
let line;
|
||||
if (startx === stopx) {
|
||||
|
@ -295,9 +311,9 @@ const drawMessage = function(elem, startx, stopx, verticalPos, msg, sequenceInde
|
|||
} else {
|
||||
line = g.append('line');
|
||||
line.attr('x1', startx);
|
||||
line.attr('y1', verticalPos);
|
||||
line.attr('y1', verticalPos + totalOffset);
|
||||
line.attr('x2', stopx);
|
||||
line.attr('y2', verticalPos);
|
||||
line.attr('y2', verticalPos + totalOffset);
|
||||
bounds.insert(
|
||||
startx,
|
||||
bounds.getVerticalPos() - 10 + totalOffset,
|
||||
|
|
|
@ -2,7 +2,9 @@ import * as d3 from 'd3';
|
|||
import idCache from './id-cache.js';
|
||||
import stateDb from './stateDb';
|
||||
import utils from '../../utils';
|
||||
import common from '../common/common';
|
||||
import { getConfig } from '../../config';
|
||||
import { logger } from '../../logger';
|
||||
|
||||
// let conf;
|
||||
|
||||
|
@ -391,12 +393,6 @@ export const drawState = function(elem, stateDef) {
|
|||
return stateInfo;
|
||||
};
|
||||
|
||||
const getRows = s => {
|
||||
let str = s.replace(/<br\s*\/?>/gi, '#br#');
|
||||
str = str.replace(/\\n/g, '#br#');
|
||||
return str.split('#br#');
|
||||
};
|
||||
|
||||
let edgeCount = 0;
|
||||
export const drawEdge = function(elem, path, relation) {
|
||||
const getRelationType = function(type) {
|
||||
|
@ -455,12 +451,15 @@ export const drawEdge = function(elem, path, relation) {
|
|||
|
||||
const { x, y } = utils.calcLabelPosition(path.points);
|
||||
|
||||
const rows = getRows(relation.title);
|
||||
const rows = common.getRows(relation.title);
|
||||
|
||||
// console.warn(rows);
|
||||
|
||||
let titleHeight = 0;
|
||||
const titleRows = [];
|
||||
let maxWidth = 0;
|
||||
let minX = 0;
|
||||
|
||||
for (let i = 0; i <= rows.length; i++) {
|
||||
const title = label
|
||||
.append('text')
|
||||
|
@ -469,27 +468,39 @@ export const drawEdge = function(elem, path, relation) {
|
|||
.attr('x', x)
|
||||
.attr('y', y + titleHeight);
|
||||
|
||||
const boundstmp = title.node().getBBox();
|
||||
maxWidth = Math.max(maxWidth, boundstmp.width);
|
||||
minX = Math.min(minX, boundstmp.x);
|
||||
|
||||
logger.info(boundstmp.x, x, y + titleHeight);
|
||||
|
||||
if (titleHeight === 0) {
|
||||
const titleBox = title.node().getBBox();
|
||||
titleHeight = titleBox.height;
|
||||
logger.info('Title height', titleHeight, y);
|
||||
}
|
||||
titleRows.push(title);
|
||||
}
|
||||
|
||||
let boxHeight = titleHeight * rows.length;
|
||||
if (rows.length > 1) {
|
||||
const heightAdj = rows.length * titleHeight * 0.25;
|
||||
const heightAdj = (rows.length - 1) * titleHeight * 0.5;
|
||||
|
||||
titleRows.forEach((title, i) => title.attr('y', y + i * titleHeight - heightAdj));
|
||||
boxHeight = titleHeight * rows.length;
|
||||
}
|
||||
|
||||
const bounds = label.node().getBBox();
|
||||
|
||||
label
|
||||
.insert('rect', ':first-child')
|
||||
.attr('class', 'box')
|
||||
.attr('x', bounds.x - getConfig().state.padding / 2)
|
||||
.attr('y', bounds.y - getConfig().state.padding / 2)
|
||||
.attr('width', bounds.width + getConfig().state.padding)
|
||||
.attr('height', bounds.height + getConfig().state.padding);
|
||||
.attr('x', x - maxWidth / 2 - getConfig().state.padding / 2)
|
||||
.attr('y', y - boxHeight / 2 - getConfig().state.padding / 2 - 3.5)
|
||||
.attr('width', maxWidth + getConfig().state.padding)
|
||||
.attr('height', boxHeight + getConfig().state.padding);
|
||||
|
||||
logger.info(bounds);
|
||||
|
||||
//label.attr('transform', '0 -' + (bounds.y / 2));
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import dagre from 'dagre';
|
|||
import graphlib from 'graphlib';
|
||||
import { logger } from '../../logger';
|
||||
import stateDb from './stateDb';
|
||||
import common from '../common/common';
|
||||
import { parser } from './parser/stateDiagram';
|
||||
// import idCache from './id-cache';
|
||||
import { drawState, addTitleAndBox, drawEdge } from './shapes';
|
||||
|
@ -99,14 +100,6 @@ const getLabelWidth = text => {
|
|||
return text ? text.length * conf.fontSizeFactor : 1;
|
||||
};
|
||||
|
||||
/* TODO: REMOVE DUPLICATION, SEE SHAPES */
|
||||
const getRows = s => {
|
||||
if (!s) return 1;
|
||||
let str = s.replace(/<br\s*\/?>/gi, '#br#');
|
||||
str = str.replace(/\\n/g, '#br#');
|
||||
return str.split('#br#');
|
||||
};
|
||||
|
||||
const renderDoc = (doc, diagram, parentId, altBkg) => {
|
||||
// // Layout graph, Create a new directed graph
|
||||
const graph = new graphlib.Graph({
|
||||
|
@ -239,7 +232,7 @@ const renderDoc = (doc, diagram, parentId, altBkg) => {
|
|||
{
|
||||
relation: relation,
|
||||
width: getLabelWidth(relation.title),
|
||||
height: conf.labelHeight * getRows(relation.title).length,
|
||||
height: conf.labelHeight * common.getRows(relation.title).length,
|
||||
labelpos: 'c'
|
||||
},
|
||||
'id' + cnt
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import dagre from 'dagre';
|
||||
|
||||
// Create a new directed graph
|
||||
var g = new dagre.graphlib.Graph({ compound: true });
|
||||
|
||||
// Set an object for the graph label
|
||||
g.setGraph({});
|
||||
|
||||
// Default to assigning a new object as a label for each new edge.
|
||||
g.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
// Add nodes to the graph. The first argument is the node id. The second is
|
||||
// metadata about the node. In this case we're going to add labels to each of
|
||||
// our nodes.
|
||||
g.setNode('root', { label: 'Cluster' });
|
||||
g.setNode('kspacey', { label: 'Kevin Spacey', width: 144, height: 100, x: 200 });
|
||||
// g.setParent('kspacey', 'root');
|
||||
g.setNode('swilliams', { label: 'Saul Williams', width: 160, height: 100 });
|
||||
// g.setNode('bpitt', { label: 'Brad Pitt', width: 108, height: 100 });
|
||||
// g.setNode('hford', { label: 'Harrison Ford', width: 168, height: 100 });
|
||||
// g.setNode('lwilson', { label: 'Luke Wilson', width: 144, height: 100 });
|
||||
// g.setNode('kbacon', { label: 'Kevin Bacon', width: 121, height: 100 });
|
||||
|
||||
// Add edges to the graph.
|
||||
g.setEdge('kspacey', 'swilliams');
|
||||
g.setEdge('swilliams');
|
||||
// g.setEdge('swilliams', 'kbacon');
|
||||
// g.setEdge('bpitt', 'kbacon');
|
||||
// g.setEdge('hford', 'lwilson');
|
||||
// g.setEdge('lwilson', 'kbacon');
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
g.nodes().forEach(function(v) {
|
||||
console.log('Node ' + v + ': ' + JSON.stringify(g.node(v)));
|
||||
});
|
||||
g.edges().forEach(function(e) {
|
||||
console.log('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e)));
|
||||
});
|
|
@ -6,7 +6,6 @@ import he from 'he';
|
|||
|
||||
import mermaidAPI from './mermaidAPI';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* ## init
|
||||
* Function that goes through the document to find the chart definitions in there and render them.
|
||||
|
|
|
@ -17,6 +17,7 @@ import { setConfig, getConfig } from './config';
|
|||
import { logger, setLogLevel } from './logger';
|
||||
import utils from './utils';
|
||||
import flowRenderer from './diagrams/flowchart/flowRenderer';
|
||||
import flowRendererV2 from './diagrams/flowchart-v2/flowRenderer';
|
||||
import flowParser from './diagrams/flowchart/parser/flow';
|
||||
import flowDb from './diagrams/flowchart/flowDb';
|
||||
import sequenceRenderer from './diagrams/sequence/sequenceRenderer';
|
||||
|
@ -40,6 +41,9 @@ import infoDb from './diagrams/info/infoDb';
|
|||
import pieRenderer from './diagrams/pie/pieRenderer';
|
||||
import pieParser from './diagrams/pie/parser/pie';
|
||||
import pieDb from './diagrams/pie/pieDb';
|
||||
import erDb from './diagrams/er/erDb';
|
||||
import erParser from './diagrams/er/parser/erDiagram';
|
||||
import erRenderer from './diagrams/er/erRenderer';
|
||||
|
||||
const themes = {};
|
||||
for (const themeName of ['default', 'forest', 'dark', 'neutral']) {
|
||||
|
@ -225,6 +229,14 @@ const config = {
|
|||
*/
|
||||
messageMargin: 35,
|
||||
|
||||
/**
|
||||
* Multiline message alignment. Possible values are:
|
||||
* * left
|
||||
* * center **default**
|
||||
* * right
|
||||
*/
|
||||
messageAlign: 'center',
|
||||
|
||||
/**
|
||||
* mirror actors under diagram.
|
||||
* **Default value true**.
|
||||
|
@ -342,6 +354,52 @@ const config = {
|
|||
edgeLengthFactor: '20',
|
||||
compositTitleSize: 35,
|
||||
radius: 5
|
||||
},
|
||||
|
||||
/**
|
||||
* The object containing configurations specific for entity relationship diagrams
|
||||
*/
|
||||
er: {
|
||||
/**
|
||||
* The amount of padding around the diagram as a whole so that embedded diagrams have margins, expressed in pixels
|
||||
*/
|
||||
diagramPadding: 20,
|
||||
|
||||
/**
|
||||
* Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL',
|
||||
* where T = top, B = bottom, L = left, and R = right.
|
||||
*/
|
||||
layoutDirection: 'TB',
|
||||
|
||||
/**
|
||||
* The mimimum width of an entity box, expressed in pixels
|
||||
*/
|
||||
minEntityWidth: 100,
|
||||
|
||||
/**
|
||||
* The minimum height of an entity box, expressed in pixels
|
||||
*/
|
||||
minEntityHeight: 75,
|
||||
|
||||
/**
|
||||
* The minimum internal padding between the text in an entity box and the enclosing box borders, expressed in pixels
|
||||
*/
|
||||
entityPadding: 15,
|
||||
|
||||
/**
|
||||
* Stroke color of box edges and lines
|
||||
*/
|
||||
stroke: 'gray',
|
||||
|
||||
/**
|
||||
* Fill color of entity boxes
|
||||
*/
|
||||
fill: 'honeydew',
|
||||
|
||||
/**
|
||||
* Font size
|
||||
*/
|
||||
fontSize: '12px'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -363,6 +421,11 @@ function parse(text) {
|
|||
parser = flowParser;
|
||||
parser.parser.yy = flowDb;
|
||||
break;
|
||||
case 'flowchart-v2':
|
||||
flowDb.clear();
|
||||
parser = flowRendererV2;
|
||||
parser.parser.yy = flowDb;
|
||||
break;
|
||||
case 'sequence':
|
||||
parser = sequenceParser;
|
||||
parser.parser.yy = sequenceDb;
|
||||
|
@ -389,6 +452,11 @@ function parse(text) {
|
|||
parser = pieParser;
|
||||
parser.parser.yy = pieDb;
|
||||
break;
|
||||
case 'er':
|
||||
logger.debug('er');
|
||||
parser = erParser;
|
||||
parser.parser.yy = erDb;
|
||||
break;
|
||||
}
|
||||
|
||||
parser.parser.yy.parseError = (str, hash) => {
|
||||
|
@ -568,6 +636,11 @@ const render = function(id, _txt, cb, container) {
|
|||
flowRenderer.setConf(config.flowchart);
|
||||
flowRenderer.draw(txt, id, false);
|
||||
break;
|
||||
case 'flowchart-v2':
|
||||
config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||
flowRendererV2.setConf(config.flowchart);
|
||||
flowRendererV2.draw(txt, id, false);
|
||||
break;
|
||||
case 'sequence':
|
||||
config.sequence.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||
if (config.sequenceDiagram) {
|
||||
|
@ -606,6 +679,10 @@ const render = function(id, _txt, cb, container) {
|
|||
pieRenderer.setConf(config.class);
|
||||
pieRenderer.draw(txt, id, pkg.version);
|
||||
break;
|
||||
case 'er':
|
||||
erRenderer.setConf(config.er);
|
||||
erRenderer.draw(txt, id, pkg.version);
|
||||
break;
|
||||
}
|
||||
|
||||
d3.select(`[id="${id}"]`)
|
||||
|
@ -738,6 +815,7 @@ export default mermaidAPI;
|
|||
* boxTextMargin:5,
|
||||
* noteMargin:10,
|
||||
* messageMargin:35,
|
||||
* messageAlign:'center',
|
||||
* mirrorActors:true,
|
||||
* bottomMarginAdj:1,
|
||||
* useMaxWidth:true,
|
||||
|
|
27
src/utils.js
27
src/utils.js
|
@ -41,6 +41,9 @@ export const detectType = function(text) {
|
|||
if (text.match(/^\s*gitGraph/)) {
|
||||
return 'git';
|
||||
}
|
||||
if (text.match(/^\s*flowchart/)) {
|
||||
return 'flowchart-v2';
|
||||
}
|
||||
|
||||
if (text.match(/^\s*info/)) {
|
||||
return 'info';
|
||||
|
@ -49,6 +52,10 @@ export const detectType = function(text) {
|
|||
return 'pie';
|
||||
}
|
||||
|
||||
if (text.match(/^\s*erDiagram/)) {
|
||||
return 'er';
|
||||
}
|
||||
|
||||
return 'flowchart';
|
||||
};
|
||||
|
||||
|
@ -74,25 +81,6 @@ export const interpolateToCurve = (interpolate, defaultCurve) => {
|
|||
return d3[curveName] || defaultCurve;
|
||||
};
|
||||
|
||||
export const sanitize = (text, config) => {
|
||||
let txt = text;
|
||||
let htmlLabels = true;
|
||||
if (
|
||||
config.flowchart &&
|
||||
(config.flowchart.htmlLabels === false || config.flowchart.htmlLabels === 'false')
|
||||
)
|
||||
htmlLabels = false;
|
||||
|
||||
if (config.securityLevel !== 'loose' && htmlLabels) { // eslint-disable-line
|
||||
txt = txt.replace(/<br\s*\/?>/gi, '#br#');
|
||||
txt = txt.replace(/</g, '<').replace(/>/g, '>');
|
||||
txt = txt.replace(/=/g, '=');
|
||||
txt = txt.replace(/#br#/g, '<br/>');
|
||||
}
|
||||
|
||||
return txt;
|
||||
};
|
||||
|
||||
export const formatUrl = (linkStr, config) => {
|
||||
let url = linkStr.trim();
|
||||
|
||||
|
@ -225,7 +213,6 @@ export default {
|
|||
interpolateToCurve,
|
||||
calcLabelPosition,
|
||||
calcCardinalityPosition,
|
||||
sanitize,
|
||||
formatUrl,
|
||||
getStylesFromArray
|
||||
};
|
||||
|
|
|
@ -1681,9 +1681,9 @@ acorn-walk@^6.0.1:
|
|||
integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
|
||||
|
||||
acorn@^5.2.1, acorn@^5.5.3:
|
||||
version "5.7.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
|
||||
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
|
||||
version "5.7.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
|
||||
integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
|
||||
|
||||
acorn@^6.0.1, acorn@^6.2.1:
|
||||
version "6.4.0"
|
||||
|
|
Loading…
Reference in New Issue