mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-29 21:26:51 +02:00
Compare commits
16 Commits
6623-add-i
...
4743-timel
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ea8260aa51 | ||
![]() |
6d9fad01a9 | ||
![]() |
075e1b5e1f | ||
![]() |
3c9bd7be29 | ||
![]() |
93467a6fce | ||
![]() |
e438e035bc | ||
![]() |
2bc5b6d2fa | ||
![]() |
dabc220ed2 | ||
![]() |
d2c5cbd408 | ||
![]() |
20467bcbe6 | ||
![]() |
dd213fe86c | ||
![]() |
800f23fc01 | ||
![]() |
67aa1a4dc1 | ||
![]() |
c1bcdcfbad | ||
![]() |
f528e2daa4 | ||
![]() |
a867842f32 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': minor
|
|
||||||
---
|
|
||||||
|
|
||||||
feat: Add custom images and icons support for sequence diagram actors
|
|
5
.changeset/ninety-adults-wink.md
Normal file
5
.changeset/ninety-adults-wink.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': major
|
||||||
|
---
|
||||||
|
|
||||||
|
Currently, HTML tags such as <em>, <strong>, <sup>, <a>, <ul>, and <li> are supported in Flowchart and Class diagram labels but not in Timeline diagrams. This change introduces support for basic HTML formatting in Timeline labels, enabling richer text formatting and better usability for multi-line content like descriptions, footnotes, and styled annotations
|
@@ -1,118 +0,0 @@
|
|||||||
import { imgSnapshotTest } from '../../helpers/util';
|
|
||||||
|
|
||||||
const looks = ['classic'] as const;
|
|
||||||
|
|
||||||
looks.forEach((look) => {
|
|
||||||
describe(`SequenceDiagram icon participants in ${look} look`, () => {
|
|
||||||
it(`single participant with icon`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
Note over Bob: Icon participant`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two participants, one icon and one normal`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two icon participants`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice@{ type: "icon", icon: "fa:user" }
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
Alice-->>Bob: Hi`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:true content`, () => {
|
|
||||||
// html/markdown in messages/notes (participants themselves don't support label/form/w/h)
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
|
|
||||||
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:false content`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
|
|
||||||
imgSnapshotTest(diagram, {
|
|
||||||
look,
|
|
||||||
htmlLabels: false,
|
|
||||||
flowchart: { htmlLabels: false },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with styles applied to participant`, () => {
|
|
||||||
// style by participant id
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Styled participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with classDef and class application`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Classed participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Colored emoji icon tests (analogous to the flowchart colored icon tests), no direction line.
|
|
||||||
describe('SequenceDiagram colored icon participant', () => {
|
|
||||||
it('colored emoji icon without styles', () => {
|
|
||||||
const icon = 'fluent-emoji:tropical-fish';
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
|
||||||
Note over Bob: colored emoji icon
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('colored emoji icon with styles', () => {
|
|
||||||
const icon = 'fluent-emoji:tropical-fish';
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mixed scenario: multiple interactions, still no direction line.
|
|
||||||
describe('SequenceDiagram icon participant with multiple interactions', () => {
|
|
||||||
const icon = 'fa:bell-slash';
|
|
||||||
it('icon participant interacts with two normal participants', () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "icon", icon: "${icon}" }
|
|
||||||
participant Alice
|
|
||||||
participant Carol
|
|
||||||
Bob->>Alice: Ping
|
|
||||||
Alice-->>Bob: Pong
|
|
||||||
Bob->>Carol: Notify
|
|
||||||
Note right of Bob: Icon side note`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,94 +0,0 @@
|
|||||||
import { imgSnapshotTest } from '../../helpers/util';
|
|
||||||
|
|
||||||
const looks = ['classic'] as const;
|
|
||||||
|
|
||||||
looks.forEach((look) => {
|
|
||||||
describe(`SequenceDiagram image participants in ${look} look`, () => {
|
|
||||||
it(`single participant with image`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
Note over Bob: Image participant`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two participants, one image and one normal`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`two image participants`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
Alice-->>Bob: Hi`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:true content`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>
|
|
||||||
Note over Bob,Alice: Mixed <em>HTML</em> and **markdown**`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with markdown htmlLabels:false content`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: This is **bold** </br>and <strong>strong</strong>`;
|
|
||||||
imgSnapshotTest(diagram, {
|
|
||||||
look,
|
|
||||||
htmlLabels: false,
|
|
||||||
flowchart: { htmlLabels: false },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with styles applied to participant`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Styled participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`with classDef and class application`, () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Classed participant
|
|
||||||
`;
|
|
||||||
imgSnapshotTest(diagram, { look });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mixed scenario: multiple interactions, still no direction line.
|
|
||||||
describe('SequenceDiagram image participant with multiple interactions', () => {
|
|
||||||
const imageUrl = 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg';
|
|
||||||
it('image participant interacts with two normal participants', () => {
|
|
||||||
const diagram = `
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ type: "image", "image": "${imageUrl}" }
|
|
||||||
participant Alice
|
|
||||||
participant Carol
|
|
||||||
Bob->>Alice: Ping
|
|
||||||
Alice-->>Bob: Pong
|
|
||||||
Bob->>Carol: Notify
|
|
||||||
Note right of Bob: Image side note`;
|
|
||||||
imgSnapshotTest(diagram);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -225,4 +225,24 @@ timeline
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('13: should render markdown htmlLabels', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`---
|
||||||
|
config:
|
||||||
|
theme: forest
|
||||||
|
---
|
||||||
|
|
||||||
|
timeline
|
||||||
|
title Timeline of Industrial Revolution
|
||||||
|
section 17th-20th century
|
||||||
|
Industry 1.0 : Machinery, Water power, Steam <br>power
|
||||||
|
Industry 2.0 : Electricity, <strong>Internal combustion engine </strong>, Mass production
|
||||||
|
Industry 3.0 : Electronics, Computers, Automation
|
||||||
|
section 21st century
|
||||||
|
Industry 4.0 : Internet, Robotics, Internet of Things
|
||||||
|
Industry 5.0 : Artificial intelligence, Big data, 3D printing
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: ParseOptions
|
# Interface: ParseOptions
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L72)
|
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:72](https://github.com/mermaid-js/mer
|
|||||||
|
|
||||||
> `optional` **suppressErrors**: `boolean`
|
> `optional` **suppressErrors**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L77)
|
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
||||||
|
|
||||||
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
||||||
The `parseError` function will not be called.
|
The `parseError` function will not be called.
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: ParseResult
|
# Interface: ParseResult
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80)
|
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mer
|
|||||||
|
|
||||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
||||||
|
|
||||||
The config passed as YAML frontmatter or directives
|
The config passed as YAML frontmatter or directives
|
||||||
|
|
||||||
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
|
|||||||
|
|
||||||
> **diagramType**: `string`
|
> **diagramType**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||||
|
|
||||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
# Interface: RenderResult
|
# Interface: RenderResult
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
|
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mer
|
|||||||
|
|
||||||
> `optional` **bindFunctions**: (`element`) => `void`
|
> `optional` **bindFunctions**: (`element`) => `void`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
|
Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
|
||||||
|
|
||||||
Bind function to be called after the svg has been inserted into the DOM.
|
Bind function to be called after the svg has been inserted into the DOM.
|
||||||
This is necessary for adding event listeners to the elements in the svg.
|
This is necessary for adding event listeners to the elements in the svg.
|
||||||
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
|||||||
|
|
||||||
> **diagramType**: `string`
|
> **diagramType**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
|
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||||
|
|
||||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||||
|
|
||||||
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
|
|||||||
|
|
||||||
> **svg**: `string`
|
> **svg**: `string`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
|
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||||
|
|
||||||
The svg code for the rendered graph.
|
The svg code for the rendered graph.
|
||||||
|
@@ -983,11 +983,23 @@ flowchart TD
|
|||||||
- `b`
|
- `b`
|
||||||
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
|
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
|
||||||
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
|
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
|
||||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
|
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
|
||||||
- `on`
|
- `on`
|
||||||
- `off`
|
- `off`
|
||||||
|
|
||||||
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
|
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
flowchart TD
|
||||||
|
%% My image with a constrained aspect ratio
|
||||||
|
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
%% My image with a constrained aspect ratio
|
||||||
|
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
|
||||||
|
```
|
||||||
|
|
||||||
## Links between nodes
|
## Links between nodes
|
||||||
|
|
||||||
|
@@ -194,46 +194,6 @@ sequenceDiagram
|
|||||||
Bob->>Alice: Queue response
|
Bob->>Alice: Queue response
|
||||||
```
|
```
|
||||||
|
|
||||||
### Icon
|
|
||||||
|
|
||||||
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Icon participant
|
|
||||||
Bob->>Alice: Response to icon
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Icon participant
|
|
||||||
Bob->>Alice: Response to icon
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image
|
|
||||||
|
|
||||||
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Image participant
|
|
||||||
Bob->>Alice: Response to image
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Image participant
|
|
||||||
Bob->>Alice: Response to image
|
|
||||||
```
|
|
||||||
|
|
||||||
### Aliases
|
### Aliases
|
||||||
|
|
||||||
The actor can have a convenient identifier and a descriptive label.
|
The actor can have a convenient identifier and a descriptive label.
|
||||||
|
@@ -13,7 +13,8 @@ import {
|
|||||||
setAccTitle,
|
setAccTitle,
|
||||||
setDiagramTitle,
|
setDiagramTitle,
|
||||||
} from '../common/commonDb.js';
|
} from '../common/commonDb.js';
|
||||||
import type { Actor, AddMessageParams, Box, Message, Note, ParticipantMetaData } from './types.js';
|
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
|
||||||
|
import type { ParticipantMetaData } from '../../types.js';
|
||||||
|
|
||||||
interface SequenceState {
|
interface SequenceState {
|
||||||
prevActor?: string;
|
prevActor?: string;
|
||||||
@@ -85,8 +86,6 @@ export const PARTICIPANT_TYPE = {
|
|||||||
ENTITY: 'entity',
|
ENTITY: 'entity',
|
||||||
PARTICIPANT: 'participant',
|
PARTICIPANT: 'participant',
|
||||||
QUEUE: 'queue',
|
QUEUE: 'queue',
|
||||||
ICON: 'icon',
|
|
||||||
IMAGE: 'image',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class SequenceDB implements DiagramDB {
|
export class SequenceDB implements DiagramDB {
|
||||||
@@ -187,7 +186,6 @@ export class SequenceDB implements DiagramDB {
|
|||||||
actorCnt: null,
|
actorCnt: null,
|
||||||
rectData: null,
|
rectData: null,
|
||||||
type: type ?? 'participant',
|
type: type ?? 'participant',
|
||||||
doc: doc,
|
|
||||||
});
|
});
|
||||||
if (this.state.records.prevActor) {
|
if (this.state.records.prevActor) {
|
||||||
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);
|
const prevActorInRecords = this.state.records.actors.get(this.state.records.prevActor);
|
||||||
|
@@ -2326,73 +2326,4 @@ Bob->>Alice:Got it!
|
|||||||
expect(actors.get('E').description).toBe('E');
|
expect(actors.get('E').description).toBe('E');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('image and icon participant parsing', () => {
|
|
||||||
it('should parse image participant with image URL', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
Bob->>Bob: test
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('image');
|
|
||||||
expect(actors.get('Bob').doc.image).toBe(
|
|
||||||
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse icon participant with icon name', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type": "icon", "icon": "fa:bell" }
|
|
||||||
Alice->>Alice: test
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Alice').type).toBe('icon');
|
|
||||||
expect(actors.get('Alice').doc.icon).toBe('fa:bell');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse two image participants', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg" }
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
Alice-->>Bob: Hi
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('image');
|
|
||||||
expect(actors.get('Bob').doc.image).toBe(
|
|
||||||
'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg'
|
|
||||||
);
|
|
||||||
expect(actors.get('Alice').type).toBe('image');
|
|
||||||
expect(actors.get('Alice').doc.image).toBe(
|
|
||||||
'https://cdn.pixabay.com/photo/2016/11/29/09/32/animal-1867121_1280.jpg'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse image participant with normal participant', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('image');
|
|
||||||
expect(actors.get('Alice').type).toBe('participant');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse icon participant with normal participant', async () => {
|
|
||||||
const diagram = await Diagram.fromText(`
|
|
||||||
sequenceDiagram
|
|
||||||
participant Bob@{ "type": "icon", "icon": "fa:bell" }
|
|
||||||
participant Alice
|
|
||||||
Bob->>Alice: Hello
|
|
||||||
`);
|
|
||||||
const actors = diagram.db.getActors();
|
|
||||||
expect(actors.get('Bob').type).toBe('icon');
|
|
||||||
expect(actors.get('Alice').type).toBe('participant');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -752,8 +752,6 @@ function adjustCreatedDestroyedData(
|
|||||||
PARTICIPANT_TYPE.CONTROL,
|
PARTICIPANT_TYPE.CONTROL,
|
||||||
PARTICIPANT_TYPE.ENTITY,
|
PARTICIPANT_TYPE.ENTITY,
|
||||||
PARTICIPANT_TYPE.DATABASE,
|
PARTICIPANT_TYPE.DATABASE,
|
||||||
PARTICIPANT_TYPE.ICON,
|
|
||||||
PARTICIPANT_TYPE.IMAGE,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// if it is a create message
|
// if it is a create message
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||||
import * as configApi from '../../config.js';
|
import * as configApi from '../../config.js';
|
||||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
|
||||||
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
|
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
|
||||||
import common, {
|
import common, {
|
||||||
calculateMathMLDimensions,
|
calculateMathMLDimensions,
|
||||||
@@ -323,89 +322,6 @@ export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawActorTypeIcon = async function (elem, actor, conf, isFooter) {
|
|
||||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
|
||||||
const center = actor.x + actor.width / 2;
|
|
||||||
const centerY = actorY + actor.height / 2;
|
|
||||||
|
|
||||||
const line = elem.append('g').lower();
|
|
||||||
if (!isFooter) {
|
|
||||||
actorCnt++;
|
|
||||||
line
|
|
||||||
.append('line')
|
|
||||||
.attr('id', 'actor' + actorCnt)
|
|
||||||
.attr('x1', center)
|
|
||||||
.attr('y1', centerY + 25)
|
|
||||||
.attr('x2', center)
|
|
||||||
.attr('y2', 2000)
|
|
||||||
.attr('class', 'actor-line')
|
|
||||||
.attr('stroke-width', '0.5px')
|
|
||||||
.attr('stroke', '#999')
|
|
||||||
.attr('name', actor.name);
|
|
||||||
actor.actorCnt = actorCnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actElem = elem.append('g');
|
|
||||||
let cssClass = 'actor-icon';
|
|
||||||
if (isFooter) {
|
|
||||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
|
||||||
} else {
|
|
||||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
actElem.attr('class', cssClass);
|
|
||||||
actElem.attr('name', actor.name);
|
|
||||||
|
|
||||||
// Define the size of the square and icon
|
|
||||||
const iconSize = actor.width / 5;
|
|
||||||
const squareX = center - iconSize / 2;
|
|
||||||
const squareY = !isFooter ? actorY + 10 : actorY;
|
|
||||||
|
|
||||||
// Draw a square rectangle for the actor icon background
|
|
||||||
|
|
||||||
actElem
|
|
||||||
.append('rect')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', iconSize)
|
|
||||||
.attr('height', iconSize)
|
|
||||||
.attr('rx', 3) // rounded corners, optional
|
|
||||||
.attr('ry', 3)
|
|
||||||
.attr('fill', 'none'); // light gray background or customize as needed
|
|
||||||
|
|
||||||
// Render icon SVG inside the rectangle
|
|
||||||
const iconGroup = actElem.append('g').attr('transform', `translate(${squareX}, ${squareY})`);
|
|
||||||
|
|
||||||
iconGroup
|
|
||||||
.append('svg')
|
|
||||||
.attr('width', iconSize)
|
|
||||||
.attr('height', iconSize)
|
|
||||||
.html(
|
|
||||||
`<g>${await getIconSVG(actor.doc.icon, {
|
|
||||||
height: iconSize,
|
|
||||||
width: iconSize,
|
|
||||||
fallbackPrefix: '',
|
|
||||||
})}</g>`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add text label below icon
|
|
||||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
|
||||||
actor.description,
|
|
||||||
actElem,
|
|
||||||
actor.x,
|
|
||||||
actorY + (!isFooter ? 40 : 30), // positioning below the square icon
|
|
||||||
actor.width,
|
|
||||||
20,
|
|
||||||
{ class: 'actor-icon-text' },
|
|
||||||
conf
|
|
||||||
);
|
|
||||||
|
|
||||||
const bounds = actElem.node().getBBox();
|
|
||||||
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
|
|
||||||
|
|
||||||
return actor.height;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws an actor in the diagram with the attached line
|
* Draws an actor in the diagram with the attached line
|
||||||
*
|
*
|
||||||
@@ -498,174 +414,6 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
|
|||||||
|
|
||||||
return height;
|
return height;
|
||||||
};
|
};
|
||||||
const drawActorTypeImage = async function (elem, actor, conf, isFooter) {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = actor.doc.image ?? '';
|
|
||||||
await img.decode();
|
|
||||||
|
|
||||||
const imageNaturalWidth = Number(img.naturalWidth.toString().replace('px', ''));
|
|
||||||
const imageNaturalHeight = Number(img.naturalHeight.toString().replace('px', ''));
|
|
||||||
|
|
||||||
actor.doc.imageAspectRatio = imageNaturalWidth / imageNaturalHeight;
|
|
||||||
|
|
||||||
// Calculate image dimensions with proper sizing logic
|
|
||||||
let imageWidth, imageHeight;
|
|
||||||
|
|
||||||
// Check if custom dimensions are provided and valid
|
|
||||||
const hasValidCustomDimensions =
|
|
||||||
actor.doc.height && actor.doc.height > 10 && actor.doc.width && actor.doc.width > 10;
|
|
||||||
|
|
||||||
if (hasValidCustomDimensions) {
|
|
||||||
if (actor.doc.constraint === 'on') {
|
|
||||||
// Maintain aspect ratio with constraint
|
|
||||||
const customAspectRatio = imageNaturalWidth / imageNaturalHeight;
|
|
||||||
|
|
||||||
if (customAspectRatio > actor.doc.imageAspectRatio) {
|
|
||||||
// Width is the limiting factor
|
|
||||||
imageHeight = actor.doc.height;
|
|
||||||
imageWidth = actor.doc.height * actor.doc.imageAspectRatio;
|
|
||||||
} else {
|
|
||||||
// Height is the limiting factor
|
|
||||||
imageWidth = actor.doc.width;
|
|
||||||
imageHeight = actor.doc.width / actor.doc.imageAspectRatio;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use custom dimensions without maintaining aspect ratio
|
|
||||||
imageWidth = actor.doc.width;
|
|
||||||
imageHeight = actor.doc.height;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use default sizing based on actor width
|
|
||||||
const defaultImageSize = actor.width / 3.5;
|
|
||||||
|
|
||||||
// Ensure minimum and maximum sizes
|
|
||||||
const minSize = 30;
|
|
||||||
const maxSize = actor.width * 0.8;
|
|
||||||
|
|
||||||
if (actor.doc.imageAspectRatio >= 1) {
|
|
||||||
// Landscape or square image
|
|
||||||
imageWidth = Math.min(Math.max(defaultImageSize, minSize), maxSize);
|
|
||||||
imageHeight = imageWidth / actor.doc.imageAspectRatio;
|
|
||||||
} else {
|
|
||||||
// Portrait image
|
|
||||||
imageHeight = Math.min(Math.max(defaultImageSize, minSize), maxSize);
|
|
||||||
imageWidth = imageHeight * actor.doc.imageAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the image doesn't exceed actor bounds
|
|
||||||
if (imageWidth > actor.width * 0.9) {
|
|
||||||
imageWidth = actor.width * 0.9;
|
|
||||||
imageHeight = imageWidth / actor.doc.imageAspectRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actorY = isFooter ? actor.stopy : actor.starty;
|
|
||||||
const center = actor.x + actor.width / 2;
|
|
||||||
const centerY = actorY + imageHeight;
|
|
||||||
|
|
||||||
// Calculate positioning
|
|
||||||
const squareX = center - imageWidth / 2;
|
|
||||||
let squareY;
|
|
||||||
|
|
||||||
if (isFooter) {
|
|
||||||
squareY = actorY + (imageHeight - imageHeight * 1);
|
|
||||||
} else {
|
|
||||||
squareY = actorY + 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate text position based on image position and size
|
|
||||||
const textY = !isFooter ? squareY + imageHeight : actorY + imageHeight; // Place text below image for header
|
|
||||||
|
|
||||||
// Draw actor line for non-footer elements
|
|
||||||
const x = center;
|
|
||||||
const y = centerY + (isFooter ? 0 : imageHeight / 2); // Adjust line start based on image height
|
|
||||||
const line = elem.append('g').lower();
|
|
||||||
if (!isFooter) {
|
|
||||||
actorCnt++;
|
|
||||||
line
|
|
||||||
.append('line')
|
|
||||||
.attr('id', 'actor' + actorCnt)
|
|
||||||
.attr('x1', x)
|
|
||||||
.attr('y1', y) // Adjust line start based on image height
|
|
||||||
.attr('x2', center)
|
|
||||||
.attr('y2', 2000)
|
|
||||||
.attr('class', 'actor-line')
|
|
||||||
.attr('stroke-width', '0.5px')
|
|
||||||
.attr('stroke', '#999')
|
|
||||||
.attr('name', actor.name);
|
|
||||||
actor.actorCnt = actorCnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actElem = elem.append('g');
|
|
||||||
let cssClass = 'actor-image';
|
|
||||||
if (isFooter) {
|
|
||||||
cssClass += ` ${BOTTOM_ACTOR_CLASS}`;
|
|
||||||
} else {
|
|
||||||
cssClass += ` ${TOP_ACTOR_CLASS}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
actElem.attr('class', cssClass);
|
|
||||||
actElem.attr('name', actor.name);
|
|
||||||
|
|
||||||
// Draw background rectangle for the actor image
|
|
||||||
actElem
|
|
||||||
.append('rect')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', imageWidth)
|
|
||||||
.attr('height', imageHeight)
|
|
||||||
.attr('rx', 3)
|
|
||||||
.attr('ry', 3)
|
|
||||||
.attr('fill', 'white')
|
|
||||||
.attr('stroke', '#ddd')
|
|
||||||
.attr('stroke-width', '1px');
|
|
||||||
|
|
||||||
// Create clipping path for the image
|
|
||||||
const clipId = `clip-actor-${actorCnt || 'footer'}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
actElem
|
|
||||||
.append('defs')
|
|
||||||
.append('clipPath')
|
|
||||||
.attr('id', clipId)
|
|
||||||
.append('rect')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', imageWidth)
|
|
||||||
.attr('height', imageHeight)
|
|
||||||
.attr('rx', 3)
|
|
||||||
.attr('ry', 3);
|
|
||||||
|
|
||||||
// Render image inside the rectangle
|
|
||||||
const imageGroup = actElem.append('g');
|
|
||||||
|
|
||||||
if (actor.doc.image) {
|
|
||||||
imageGroup
|
|
||||||
.append('image')
|
|
||||||
.attr('x', squareX)
|
|
||||||
.attr('y', squareY)
|
|
||||||
.attr('width', imageWidth)
|
|
||||||
.attr('height', imageHeight)
|
|
||||||
.attr('href', img.src)
|
|
||||||
.attr('preserveAspectRatio', actor.doc.constraint === 'on' ? 'xMidYMid meet' : 'none');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add text label
|
|
||||||
_drawTextCandidateFunc(conf, hasKatex(actor.description))(
|
|
||||||
actor.description,
|
|
||||||
actElem,
|
|
||||||
actor.x,
|
|
||||||
textY,
|
|
||||||
actor.width,
|
|
||||||
20,
|
|
||||||
{ class: 'actor-image-text' },
|
|
||||||
conf
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate final bounds and update actor height
|
|
||||||
const bounds = actElem.node().getBBox();
|
|
||||||
actor.height = bounds.height + (conf.sequence?.labelBoxHeight ?? 0);
|
|
||||||
|
|
||||||
return actor.height;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws an actor in the diagram with the attached line
|
* Draws an actor in the diagram with the attached line
|
||||||
@@ -1374,10 +1122,6 @@ export const drawActor = async function (elem, actor, conf, isFooter) {
|
|||||||
return await drawActorTypeCollections(elem, actor, conf, isFooter);
|
return await drawActorTypeCollections(elem, actor, conf, isFooter);
|
||||||
case 'queue':
|
case 'queue':
|
||||||
return await drawActorTypeQueue(elem, actor, conf, isFooter);
|
return await drawActorTypeQueue(elem, actor, conf, isFooter);
|
||||||
case 'icon':
|
|
||||||
return await drawActorTypeIcon(elem, actor, conf, isFooter);
|
|
||||||
case 'image':
|
|
||||||
return await drawActorTypeImage(elem, actor, conf, isFooter);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -17,9 +17,6 @@ export interface Actor {
|
|||||||
actorCnt: number | null;
|
actorCnt: number | null;
|
||||||
rectData: unknown;
|
rectData: unknown;
|
||||||
type: string;
|
type: string;
|
||||||
doc?: ParticipantMetaData; // For documentation
|
|
||||||
iconName?: string; // For icon type
|
|
||||||
imgSrc?: string; // For img type
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
@@ -93,20 +90,3 @@ export interface Note {
|
|||||||
message: string;
|
message: string;
|
||||||
wrap: boolean;
|
wrap: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParticipantMetaData {
|
|
||||||
type?:
|
|
||||||
| 'actor'
|
|
||||||
| 'participant'
|
|
||||||
| 'boundary'
|
|
||||||
| 'control'
|
|
||||||
| 'entity'
|
|
||||||
| 'database'
|
|
||||||
| 'collections'
|
|
||||||
| 'queue'
|
|
||||||
| 'icon'
|
|
||||||
| 'img';
|
|
||||||
icon?: string;
|
|
||||||
img?: string;
|
|
||||||
form?: string;
|
|
||||||
}
|
|
||||||
|
@@ -1,6 +1,91 @@
|
|||||||
import { arc as d3arc, select } from 'd3';
|
import { arc as d3arc, select } from 'd3';
|
||||||
|
import { createText } from '../../rendering-util/createText.js';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
const MAX_SECTIONS = 12;
|
const MAX_SECTIONS = 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process HTML content in node descriptions
|
||||||
|
* @param {object} textElem - The SVG element to append text to
|
||||||
|
* @param {object} node - The node object containing description and dimensions
|
||||||
|
* @param {object} conf - Configuration object
|
||||||
|
* @param {boolean} isVirtual - Whether this is for virtual height calculation
|
||||||
|
*/
|
||||||
|
const processHtmlContent = async function (textElem, node, conf, isVirtual = false) {
|
||||||
|
// Create temporary text to get initial dimensions
|
||||||
|
const sanitizedHtml = DOMPurify.sanitize(node.descr, { ALLOWED_TAGS: [] });
|
||||||
|
const tempText = textElem
|
||||||
|
.append('text')
|
||||||
|
.text(sanitizedHtml)
|
||||||
|
.attr('dy', '1em')
|
||||||
|
.attr('alignment-baseline', 'middle')
|
||||||
|
.attr('dominant-baseline', 'middle')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.call(wrap, node.width);
|
||||||
|
|
||||||
|
if (!isVirtual) {
|
||||||
|
tempText.attr('visibility', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bbox = tempText.node().getBBox();
|
||||||
|
tempText.remove();
|
||||||
|
|
||||||
|
// Create the actual HTML content
|
||||||
|
const textObj = await createText(
|
||||||
|
textElem,
|
||||||
|
node.descr,
|
||||||
|
{
|
||||||
|
useHtmlLabels: true,
|
||||||
|
width: node.width,
|
||||||
|
classes: 'timeline-node-label',
|
||||||
|
isNode: true,
|
||||||
|
},
|
||||||
|
conf
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isVirtual) {
|
||||||
|
select(textObj).attr('transform', 'translate(0, 0)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the foreign object
|
||||||
|
const foreignObject = textElem.select('foreignObject');
|
||||||
|
if (foreignObject.node()) {
|
||||||
|
foreignObject.attr('width', `${10 * node.width}px`).attr('height', `${10 * node.width}px`);
|
||||||
|
|
||||||
|
const div = foreignObject.select('div');
|
||||||
|
if (div.node()) {
|
||||||
|
div
|
||||||
|
.style('display', 'table-cell')
|
||||||
|
.style('white-space', 'nowrap')
|
||||||
|
.style('line-height', '1.5')
|
||||||
|
.style('max-width', node.width + 'px')
|
||||||
|
.style('text-align', 'center');
|
||||||
|
|
||||||
|
let divBBox = div.node().getBoundingClientRect();
|
||||||
|
|
||||||
|
if (divBBox.width === node.width) {
|
||||||
|
div
|
||||||
|
.style('display', 'table')
|
||||||
|
.style('white-space', 'break-spaces')
|
||||||
|
.style('width', node.width + 'px');
|
||||||
|
|
||||||
|
divBBox = div.node().getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignObject.attr('width', node.width).attr('height', divBBox.height);
|
||||||
|
|
||||||
|
if (!isVirtual) {
|
||||||
|
foreignObject.attr('x', -node.width / 2).attr('y', 3);
|
||||||
|
|
||||||
|
div.style('width', node.width + 'px');
|
||||||
|
}
|
||||||
|
bbox.height = divBBox.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bbox;
|
||||||
|
};
|
||||||
|
|
||||||
export const drawRect = function (elem, rectData) {
|
export const drawRect = function (elem, rectData) {
|
||||||
const rectElem = elem.append('rect');
|
const rectElem = elem.append('rect');
|
||||||
rectElem.attr('x', rectData.x);
|
rectElem.attr('x', rectData.x);
|
||||||
@@ -409,6 +494,9 @@ const _drawTextCandidateFunc = (function () {
|
|||||||
.style('display', 'table-cell')
|
.style('display', 'table-cell')
|
||||||
.style('text-align', 'center')
|
.style('text-align', 'center')
|
||||||
.style('vertical-align', 'middle')
|
.style('vertical-align', 'middle')
|
||||||
|
.style('word-wrap', 'break-word')
|
||||||
|
.style('overflow-wrap', 'break-word')
|
||||||
|
.style('white-space', 'normal')
|
||||||
.text(content);
|
.text(content);
|
||||||
|
|
||||||
byTspan(content, body, x, y, width, height, textAttrs, conf);
|
byTspan(content, body, x, y, width, height, textAttrs, conf);
|
||||||
@@ -493,7 +581,7 @@ function wrap(text, width) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const drawNode = function (elem, node, fullSection, conf) {
|
export const drawNode = async function (elem, node, fullSection, conf) {
|
||||||
const section = (fullSection % MAX_SECTIONS) - 1;
|
const section = (fullSection % MAX_SECTIONS) - 1;
|
||||||
const nodeElem = elem.append('g');
|
const nodeElem = elem.append('g');
|
||||||
node.section = section;
|
node.section = section;
|
||||||
@@ -506,19 +594,28 @@ export const drawNode = function (elem, node, fullSection, conf) {
|
|||||||
// Create the wrapped text element
|
// Create the wrapped text element
|
||||||
const textElem = nodeElem.append('g');
|
const textElem = nodeElem.append('g');
|
||||||
|
|
||||||
const txt = textElem
|
const hasHtml = /<[a-z][\S\s]*>/i.test(node.descr);
|
||||||
.append('text')
|
|
||||||
.text(node.descr)
|
if (hasHtml) {
|
||||||
.attr('dy', '1em')
|
const bbox = await processHtmlContent(textElem, node, conf, false);
|
||||||
.attr('alignment-baseline', 'middle')
|
node.height = bbox.height + node.padding;
|
||||||
.attr('dominant-baseline', 'middle')
|
node.height = Math.max(node.height, node.maxHeight);
|
||||||
.attr('text-anchor', 'middle')
|
node.width = node.width + 2 * node.padding;
|
||||||
.call(wrap, node.width);
|
} else {
|
||||||
const bbox = txt.node().getBBox();
|
const txt = textElem
|
||||||
const fontSize = conf.fontSize?.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
|
.append('text')
|
||||||
node.height = bbox.height + fontSize * 1.1 * 0.5 + node.padding;
|
.text(node.descr)
|
||||||
node.height = Math.max(node.height, node.maxHeight);
|
.attr('dy', '1em')
|
||||||
node.width = node.width + 2 * node.padding;
|
.attr('alignment-baseline', 'middle')
|
||||||
|
.attr('dominant-baseline', 'middle')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.call(wrap, node.width);
|
||||||
|
const bbox = txt.node().getBBox();
|
||||||
|
const fontSize = conf.fontSize?.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
|
||||||
|
node.height = bbox.height + fontSize * 1.1 * 0.5 + node.padding;
|
||||||
|
node.height = Math.max(node.height, node.maxHeight);
|
||||||
|
node.width = node.width + 2 * node.padding;
|
||||||
|
}
|
||||||
|
|
||||||
textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')');
|
textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')');
|
||||||
|
|
||||||
@@ -528,17 +625,25 @@ export const drawNode = function (elem, node, fullSection, conf) {
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getVirtualNodeHeight = function (elem, node, conf) {
|
export const getVirtualNodeHeight = async function (elem, node, conf) {
|
||||||
const textElem = elem.append('g');
|
const textElem = elem.append('g');
|
||||||
const txt = textElem
|
|
||||||
.append('text')
|
const hasHtml = /<[a-z][\S\s]*>/i.test(node.descr);
|
||||||
.text(node.descr)
|
|
||||||
.attr('dy', '1em')
|
let bbox;
|
||||||
.attr('alignment-baseline', 'middle')
|
if (hasHtml) {
|
||||||
.attr('dominant-baseline', 'middle')
|
bbox = await processHtmlContent(textElem, node, conf, true);
|
||||||
.attr('text-anchor', 'middle')
|
} else {
|
||||||
.call(wrap, node.width);
|
const txt = textElem
|
||||||
const bbox = txt.node().getBBox();
|
.append('text')
|
||||||
|
.text(node.descr)
|
||||||
|
.attr('dy', '1em')
|
||||||
|
.attr('alignment-baseline', 'middle')
|
||||||
|
.attr('dominant-baseline', 'middle')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.call(wrap, node.width);
|
||||||
|
bbox = txt.node().getBBox();
|
||||||
|
}
|
||||||
const fontSize = conf.fontSize?.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
|
const fontSize = conf.fontSize?.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
|
||||||
textElem.remove();
|
textElem.remove();
|
||||||
return bbox.height + fontSize * 1.1 * 0.5 + node.padding;
|
return bbox.height + fontSize * 1.1 * 0.5 + node.padding;
|
||||||
|
@@ -25,7 +25,7 @@ interface TimelineTask {
|
|||||||
score: number;
|
score: number;
|
||||||
events: string[];
|
events: string[];
|
||||||
}
|
}
|
||||||
export const draw = function (text: string, id: string, version: string, diagObj: Diagram) {
|
export const draw = async function (text: string, id: string, version: string, diagObj: Diagram) {
|
||||||
//1. Fetch the configuration
|
//1. Fetch the configuration
|
||||||
const conf = getConfig();
|
const conf = getConfig();
|
||||||
const LEFT_MARGIN = conf.timeline?.leftMargin ?? 50;
|
const LEFT_MARGIN = conf.timeline?.leftMargin ?? 50;
|
||||||
@@ -76,7 +76,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
let hasSections = true;
|
let hasSections = true;
|
||||||
|
|
||||||
//Calculate the max height of the sections
|
//Calculate the max height of the sections
|
||||||
sections.forEach(function (section: string) {
|
for (const section of sections) {
|
||||||
const sectionNode: Block<string, number> = {
|
const sectionNode: Block<string, number> = {
|
||||||
number: sectionNumber,
|
number: sectionNumber,
|
||||||
descr: section,
|
descr: section,
|
||||||
@@ -85,10 +85,10 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
padding: 20,
|
padding: 20,
|
||||||
maxHeight: maxSectionHeight,
|
maxHeight: maxSectionHeight,
|
||||||
};
|
};
|
||||||
const sectionHeight = svgDraw.getVirtualNodeHeight(svg, sectionNode, conf);
|
const sectionHeight = await svgDraw.getVirtualNodeHeight(svg, sectionNode, conf);
|
||||||
log.debug('sectionHeight before draw', sectionHeight);
|
log.debug('sectionHeight before draw', sectionHeight);
|
||||||
maxSectionHeight = Math.max(maxSectionHeight, sectionHeight + 20);
|
maxSectionHeight = Math.max(maxSectionHeight, sectionHeight + 20);
|
||||||
});
|
}
|
||||||
|
|
||||||
//tasks length and maxEventCount
|
//tasks length and maxEventCount
|
||||||
let maxEventCount = 0;
|
let maxEventCount = 0;
|
||||||
@@ -106,7 +106,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
padding: 20,
|
padding: 20,
|
||||||
maxHeight: maxTaskHeight,
|
maxHeight: maxTaskHeight,
|
||||||
};
|
};
|
||||||
const taskHeight = svgDraw.getVirtualNodeHeight(svg, taskNode, conf);
|
const taskHeight = await svgDraw.getVirtualNodeHeight(svg, taskNode, conf);
|
||||||
log.debug('taskHeight before draw', taskHeight);
|
log.debug('taskHeight before draw', taskHeight);
|
||||||
maxTaskHeight = Math.max(maxTaskHeight, taskHeight + 20);
|
maxTaskHeight = Math.max(maxTaskHeight, taskHeight + 20);
|
||||||
|
|
||||||
@@ -123,9 +123,8 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
padding: 20,
|
padding: 20,
|
||||||
maxHeight: 50,
|
maxHeight: 50,
|
||||||
};
|
};
|
||||||
maxEventLineLengthTemp += svgDraw.getVirtualNodeHeight(svg, eventNode, conf);
|
maxEventLineLengthTemp += await svgDraw.getVirtualNodeHeight(svg, eventNode, conf);
|
||||||
}
|
}
|
||||||
// Add spacing between events (10px per event except the last one)
|
|
||||||
if (task.events.length > 0) {
|
if (task.events.length > 0) {
|
||||||
maxEventLineLengthTemp += (task.events.length - 1) * 10;
|
maxEventLineLengthTemp += (task.events.length - 1) * 10;
|
||||||
}
|
}
|
||||||
@@ -136,7 +135,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
log.debug('maxTaskHeight before draw', maxTaskHeight);
|
log.debug('maxTaskHeight before draw', maxTaskHeight);
|
||||||
|
|
||||||
if (sections && sections.length > 0) {
|
if (sections && sections.length > 0) {
|
||||||
sections.forEach((section) => {
|
for (const section of sections) {
|
||||||
//filter task where tasks.section == section
|
//filter task where tasks.section == section
|
||||||
const tasksForSection = tasks.filter((task) => task.section === section);
|
const tasksForSection = tasks.filter((task) => task.section === section);
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
};
|
};
|
||||||
log.debug('sectionNode', sectionNode);
|
log.debug('sectionNode', sectionNode);
|
||||||
const sectionNodeWrapper = svg.append('g');
|
const sectionNodeWrapper = svg.append('g');
|
||||||
const node = svgDraw.drawNode(sectionNodeWrapper, sectionNode, sectionNumber, conf);
|
const node = await svgDraw.drawNode(sectionNodeWrapper, sectionNode, sectionNumber, conf);
|
||||||
log.debug('sectionNode output', node);
|
log.debug('sectionNode output', node);
|
||||||
|
|
||||||
sectionNodeWrapper.attr('transform', `translate(${masterX}, ${sectionBeginY})`);
|
sectionNodeWrapper.attr('transform', `translate(${masterX}, ${sectionBeginY})`);
|
||||||
@@ -159,7 +158,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
|
|
||||||
//draw tasks for this section
|
//draw tasks for this section
|
||||||
if (tasksForSection.length > 0) {
|
if (tasksForSection.length > 0) {
|
||||||
drawTasks(
|
await drawTasks(
|
||||||
svg,
|
svg,
|
||||||
tasksForSection,
|
tasksForSection,
|
||||||
sectionNumber,
|
sectionNumber,
|
||||||
@@ -178,11 +177,11 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
|
|
||||||
masterY = sectionBeginY;
|
masterY = sectionBeginY;
|
||||||
sectionNumber++;
|
sectionNumber++;
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
//draw tasks
|
//draw tasks
|
||||||
hasSections = false;
|
hasSections = false;
|
||||||
drawTasks(
|
await drawTasks(
|
||||||
svg,
|
svg,
|
||||||
tasks,
|
tasks,
|
||||||
sectionNumber,
|
sectionNumber,
|
||||||
@@ -236,7 +235,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
|
|||||||
// addSVGAccessibilityFields(diagObj.db, diagram, id);
|
// addSVGAccessibilityFields(diagObj.db, diagram, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawTasks = function (
|
export const drawTasks = async function (
|
||||||
diagram: Selection<SVGElement, unknown, null, undefined>,
|
diagram: Selection<SVGElement, unknown, null, undefined>,
|
||||||
tasks: TimelineTask[],
|
tasks: TimelineTask[],
|
||||||
sectionColor: number,
|
sectionColor: number,
|
||||||
@@ -265,7 +264,7 @@ export const drawTasks = function (
|
|||||||
// create task wrapper
|
// create task wrapper
|
||||||
|
|
||||||
const taskWrapper = diagram.append('g').attr('class', 'taskWrapper');
|
const taskWrapper = diagram.append('g').attr('class', 'taskWrapper');
|
||||||
const node = svgDraw.drawNode(taskWrapper, taskNode, sectionColor, conf);
|
const node = await svgDraw.drawNode(taskWrapper, taskNode, sectionColor, conf);
|
||||||
const taskHeight = node.height;
|
const taskHeight = node.height;
|
||||||
//log task height
|
//log task height
|
||||||
log.debug('taskHeight after draw', taskHeight);
|
log.debug('taskHeight after draw', taskHeight);
|
||||||
@@ -282,7 +281,7 @@ export const drawTasks = function (
|
|||||||
//add margin to task
|
//add margin to task
|
||||||
masterY += 100;
|
masterY += 100;
|
||||||
lineLength =
|
lineLength =
|
||||||
lineLength + drawEvents(diagram, task.events, sectionColor, masterX, masterY, conf);
|
lineLength + (await drawEvents(diagram, task.events, sectionColor, masterX, masterY, conf));
|
||||||
masterY -= 100;
|
masterY -= 100;
|
||||||
|
|
||||||
lineWrapper
|
lineWrapper
|
||||||
@@ -307,7 +306,7 @@ export const drawTasks = function (
|
|||||||
masterY = masterY - 10;
|
masterY = masterY - 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawEvents = function (
|
export const drawEvents = async function (
|
||||||
diagram: Selection<SVGElement, unknown, null, undefined>,
|
diagram: Selection<SVGElement, unknown, null, undefined>,
|
||||||
events: string[],
|
events: string[],
|
||||||
sectionColor: number,
|
sectionColor: number,
|
||||||
@@ -334,7 +333,7 @@ export const drawEvents = function (
|
|||||||
log.debug('eventNode', eventNode);
|
log.debug('eventNode', eventNode);
|
||||||
// create event wrapper
|
// create event wrapper
|
||||||
const eventWrapper = diagram.append('g').attr('class', 'eventWrapper');
|
const eventWrapper = diagram.append('g').attr('class', 'eventWrapper');
|
||||||
const node = svgDraw.drawNode(eventWrapper, eventNode, sectionColor, conf);
|
const node = await svgDraw.drawNode(eventWrapper, eventNode, sectionColor, conf);
|
||||||
const eventHeight = node.height;
|
const eventHeight = node.height;
|
||||||
maxEventHeight = maxEventHeight + eventHeight;
|
maxEventHeight = maxEventHeight + eventHeight;
|
||||||
eventWrapper.attr('transform', `translate(${masterX}, ${masterY})`);
|
eventWrapper.attr('transform', `translate(${masterX}, ${masterY})`);
|
||||||
|
@@ -590,11 +590,17 @@ flowchart TD
|
|||||||
- `b`
|
- `b`
|
||||||
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
|
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
|
||||||
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
|
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
|
||||||
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
|
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the width (`w`) accordingly to the height (`h`). If not defined, this will default to `off` Possible values are:
|
||||||
- `on`
|
- `on`
|
||||||
- `off`
|
- `off`
|
||||||
|
|
||||||
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
|
If you want to resize an image, but keep the same aspect ratio, set `h`, and set `constraint: on` to constrain the aspect ratio. E.g.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
%% My image with a constrained aspect ratio
|
||||||
|
A@{ img: "https://mermaid.js.org/favicon.svg", label: "My example image label", pos: "t", h: 60, constraint: "on" }
|
||||||
|
```
|
||||||
|
|
||||||
## Links between nodes
|
## Links between nodes
|
||||||
|
|
||||||
|
@@ -118,30 +118,6 @@ sequenceDiagram
|
|||||||
Bob->>Alice: Queue response
|
Bob->>Alice: Queue response
|
||||||
```
|
```
|
||||||
|
|
||||||
### Icon
|
|
||||||
|
|
||||||
If you want to use a custom icon for a participant, use the JSON configuration syntax as shown below. The `icon` value can be a FontAwesome icon name, emoji, or other supported icon identifier.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "icon", "icon": "fa:bell" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Icon participant
|
|
||||||
Bob->>Alice: Response to icon
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image
|
|
||||||
|
|
||||||
If you want to use a custom image for a participant, use the JSON configuration syntax as shown below. The `image` value should be a valid image URL.
|
|
||||||
|
|
||||||
```mermaid-example
|
|
||||||
sequenceDiagram
|
|
||||||
participant Alice@{ "type" : "image", "image": "https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg" }
|
|
||||||
participant Bob
|
|
||||||
Alice->>Bob: Image participant
|
|
||||||
Bob->>Alice: Response to image
|
|
||||||
```
|
|
||||||
|
|
||||||
### Aliases
|
### Aliases
|
||||||
|
|
||||||
The actor can have a convenient identifier and a descriptive label.
|
The actor can have a convenient identifier and a descriptive label.
|
||||||
|
@@ -13,6 +13,18 @@ export interface NodeMetaData {
|
|||||||
ticket?: string;
|
ticket?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParticipantMetaData {
|
||||||
|
type?:
|
||||||
|
| 'actor'
|
||||||
|
| 'participant'
|
||||||
|
| 'boundary'
|
||||||
|
| 'control'
|
||||||
|
| 'entity'
|
||||||
|
| 'database'
|
||||||
|
| 'collections'
|
||||||
|
| 'queue';
|
||||||
|
}
|
||||||
|
|
||||||
export interface EdgeMetaData {
|
export interface EdgeMetaData {
|
||||||
animation?: 'fast' | 'slow';
|
animation?: 'fast' | 'slow';
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
|
Reference in New Issue
Block a user