Merge from develop

This commit is contained in:
Ashish Jain
2024-08-28 14:29:03 +02:00
65 changed files with 7719 additions and 501 deletions

View File

@@ -25,6 +25,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'sankey',
'block',
'packet',
'architecture',
] as const;
/**

View File

@@ -0,0 +1,6 @@
---
'mermaid': patch
---
Fix for self loops in cluster
Supporting legacy defaultRenderer directive

View File

@@ -0,0 +1,9 @@
---
'mermaid': minor
'@mermaid-js/docs': patch
'@mermaid-js/parser': minor
---
New Diagram: Architecture
Adds architecture diagrams which allows users to show relations between services.

View File

@@ -0,0 +1,6 @@
---
'@mermaid-js/layout-elk': patch
---
fix: Updates to the default elk configuration
feat: exposing cycleBreakingStrategy to the configuration so that it can be modified suing the configuration.

View File

@@ -55,6 +55,7 @@ GENERICTYPE
getBoundarys
grammr
graphtype
halign
iife
interp
introdcued
@@ -66,6 +67,7 @@ Kaufmann
keyify
LABELPOS
LABELTYPE
layoutstop
lcov
LEFTOF
Lexa

View File

@@ -24,6 +24,7 @@ Doctave
DokuWiki
dompurify
elkjs
fcose
fontawesome
Foswiki
Gitea

View File

@@ -35,7 +35,7 @@ Try Live Editor previews of future releases: <a href="https://develop.git.mermai
[![NPM Downloads](https://img.shields.io/npm/dm/mermaid)](https://www.npmjs.com/package/mermaid)
[![Join our Discord!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=discord&label=discord)](https://discord.gg/AgrbSrBer3)
[![Twitter Follow](https://img.shields.io/badge/Social-mermaidjs__-blue?style=social&logo=X)](https://twitter.com/mermaidjs_)
[![Covered by Argos Visual Testing](https://argos-ci.com/badge.svg)](https://argos-ci.com)
[![Covered by Argos Visual Testing](https://argos-ci.com/badge.svg)](https://argos-ci.com?utm_source=mermaid&utm_campaign=oss)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/mermaid-js/mermaid/badge)](https://securityscorecards.dev/viewer/?uri=github.com/mermaid-js/mermaid)
<img src="./img/header.png" alt="" />
@@ -83,6 +83,10 @@ You can also use Mermaid within [GitHub](https://github.blog/2022-02-14-include-
For a more detailed introduction to Mermaid and some of its more basic uses, look to the [Beginner's Guide](https://mermaid.js.org/intro/getting-started.html), [Usage](https://mermaid.js.org/config/usage.html) and [Tutorials](https://mermaid.js.org/ecosystem/tutorials.html).
Our PR Visual Regression Testing is powered by [Argos](https://argos-ci.com/?utm_source=mermaid&utm_campaign=oss) with their generous Open Source plan. It makes the process of reviewing PRs with visual changes a breeze.
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=mermaid&utm_campaign=oss)
In our release process we rely heavily on visual regression tests using [applitools](https://applitools.com/). Applitools is a great service which has been easy to use and integrate with our tests.
<a href="https://applitools.com/">

View File

@@ -0,0 +1,174 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
describe('architecture diagram', () => {
it('should render a simple architecture diagram with groups', () => {
imgSnapshotTest(
`architecture
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
service gateway(internet)[Gateway]
db L--R server
disk1 T--B server
disk2 T--B db
server T--B gateway
`
);
});
it('should render an architecture diagram with groups within groups', () => {
imgSnapshotTest(
`architecture
group api[API]
group public[Public API] in api
group private[Private API] in api
service serv1(server)[Server] in public
service serv2(server)[Server] in private
service db(database)[Database] in private
service gateway(internet)[Gateway] in api
serv1 B--T serv2
serv2 L--R db
serv1 L--R gateway
`
);
});
it('should render an architecture diagram with the fallback icon', () => {
imgSnapshotTest(
`architecture
service unknown(iconnamedoesntexist)[Unknown Icon]
`
);
});
it('should render an architecture diagram with split directioning', () => {
imgSnapshotTest(
`architecture
service db(database)[Database]
service s3(disk)[Storage]
service serv1(server)[Server 1]
service serv2(server)[Server 2]
service disk(disk)[Disk]
db L--R s3
serv1 L--T s3
serv2 L--B s3
serv1 T--B disk
`
);
});
it('should render an architecture diagram with directional arrows', () => {
imgSnapshotTest(
`architecture
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC (L--R) servL
servC (R--L) servR
servC (T--B) servT
servC (B--T) servB
servL (T--L) servT
servL (B--L) servB
servR (T--R) servT
servR (B--R) servB
`
);
});
it('should render an architecture diagram with group edges', () => {
imgSnapshotTest(
`architecture
group left_group(cloud)[Left]
group right_group(cloud)[Right]
group top_group(cloud)[Top]
group bottom_group(cloud)[Bottom]
group center_group(cloud)[Center]
service left_disk(disk)[Disk] in left_group
service right_disk(disk)[Disk] in right_group
service top_disk(disk)[Disk] in top_group
service bottom_disk(disk)[Disk] in bottom_group
service center_disk(disk)[Disk] in center_group
left_disk{group} (R--L) center_disk{group}
right_disk{group} (L--R) center_disk{group}
top_disk{group} (B--T) center_disk{group}
bottom_disk{group} (T--B) center_disk{group}
`
);
});
it('should render an architecture diagram with edge labels', () => {
imgSnapshotTest(
`architecture
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC L-[Label]-R servL
servC R-[Label]-L servR
servC T-[Label]-B servT
servC B-[Label]-T servB
servL T-[Label]-L servT
servL B-[Label]-L servB
servR T-[Label]-R servT
servR B-[Label]-R servB
`
);
});
it('should render an architecture diagram with simple junction edges', () => {
imgSnapshotTest(
`architecture
service left_disk(disk)[Disk]
service top_disk(disk)[Disk]
service bottom_disk(disk)[Disk]
service top_gateway(internet)[Gateway]
service bottom_gateway(internet)[Gateway]
junction juncC
junction juncR
left_disk R--L juncC
top_disk B--T juncC
bottom_disk T--B juncC
juncC R--L juncR
top_gateway B--T juncR
bottom_gateway T--B juncR
`
);
});
it('should render an architecture diagram with complex junction edges', () => {
imgSnapshotTest(
`architecture
group left
group right
service left_disk(disk)[Disk] in left
service top_disk(disk)[Disk] in left
service bottom_disk(disk)[Disk] in left
service top_gateway(internet)[Gateway] in right
service bottom_gateway(internet)[Gateway] in right
junction juncC in left
junction juncR in right
left_disk R--L juncC
top_disk B--T juncC
bottom_disk T--B juncC
top_gateway (B--T juncR
bottom_gateway (T--B juncR
juncC{group} R--L) juncR{group}
`
);
});
});

309
demos/architecture.html Normal file
View File

@@ -0,0 +1,309 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Architecture Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
<style>
div.mermaid {
/* font-family: 'trebuchet ms', verdana, arial; */
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<h1>Architecture diagram demo</h1>
<h2>Simple diagram with groups</h2>
<pre class="mermaid">
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
service gateway(internet)[Gateway]
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
server:T -- B:gateway
</pre>
<hr />
<h2>Groups within groups</h2>
<pre class="mermaid">
architecture-beta
group api[API]
group public[Public API] in api
group private[Private API] in api
service serv1(server)[Server] in public
service serv2(server)[Server] in private
service db(database)[Database] in private
service gateway(internet)[Gateway] in api
serv1:B -- T:serv2
serv2:L -- R:db
serv1:L -- R:gateway
</pre>
<hr />
<h2>Default icon (?) from unknown icon name</h2>
<pre class="mermaid">
architecture-beta
service unknown(iconnamedoesntexist)[Unknown Icon]
</pre>
<hr />
<h2>Split Direction</h2>
<pre class="mermaid">
architecture-beta
service db(database)[Database]
service s3(disk)[Storage]
service serv1(server)[Server 1]
service serv2(server)[Server 2]
service disk(disk)[Disk]
db:L -- R:s3
serv1:L -- T:s3
serv2:L -- B:s3
serv1:T -- B:disk
</pre>
<hr />
<h2>Arrow Tests</h2>
<pre class="mermaid">
architecture-beta
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC:L <--> R:servL
servC:R <--> L:servR
servC:T <--> B:servT
servC:B <--> T:servB
servL:T <--> L:servT
servL:B <--> L:servB
servR:T <--> R:servT
servR:B <--> R:servB
</pre>
<pre class="mermaid">
architecture-beta
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC:L <--> R:servL
servC:R <--> L:servR
servC:T <--> B:servT
servC:B <--> T:servB
servT:L <--> T:servL
servB:L <--> B:servL
servT:R <--> T:servR
servB:R <--> B:servR
</pre>
<hr />
<h2>Group Edges</h2>
<pre class="mermaid">
architecture-beta
group left_group(cloud)[Left]
group right_group(cloud)[Right]
group top_group(cloud)[Top]
group bottom_group(cloud)[Bottom]
group center_group(cloud)[Center]
service left_disk(disk)[Disk] in left_group
service right_disk(disk)[Disk] in right_group
service top_disk(disk)[Disk] in top_group
service bottom_disk(disk)[Disk] in bottom_group
service center_disk(disk)[Disk] in center_group
left_disk{group}:R <--> L:center_disk{group}
right_disk{group}:L <--> R:center_disk{group}
top_disk{group}:B <--> T:center_disk{group}
bottom_disk{group}:T <--> B:center_disk{group}
</pre
>
<hr />
<h2>Edge Label Test</h2>
<pre class="mermaid">
architecture-beta
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC:L -[Label]- R:servL
servC:R -[Label]- L:servR
servC:T -[Label]- B:servT
servC:B -[Label]- T:servB
servL:T -[Label]- L:servT
servL:B -[Label]- L:servB
servR:T -[Label]- R:servT
servR:B -[Label]- R:servB
</pre>
<pre class="mermaid">
architecture-beta
service servC(server)[Server 1]
service servL(server)[Server 2]
service servR(server)[Server 3]
service servT(server)[Server 4]
service servB(server)[Server 5]
servC:L -[Label that is Long]- R:servL
servC:R -[Label that is Long]- L:servR
servC:T -[Label that is Long]- B:servT
servC:B -[Label that is Long]- T:servB
servL:T -[Label that is Long]- L:servT
servL:B -[Label that is Long]- L:servB
servR:T -[Label that is Long]- R:servT
servR:B -[Label that is Long]- R:servB
</pre>
<hr />
<h2>Junction Demo</h2>
<pre class="mermaid">
architecture-beta
service left_disk(disk)[Disk]
service top_disk(disk)[Disk]
service bottom_disk(disk)[Disk]
service top_gateway(internet)[Gateway]
service bottom_gateway(internet)[Gateway]
junction juncC
junction juncR
left_disk:R -- L:juncC
top_disk:B -- T:juncC
bottom_disk:T -- B:juncC
juncC:R -- L:juncR
top_gateway:B -- T:juncR
bottom_gateway:T -- B:juncR
</pre>
<hr />
<h2>Junction Demo Groups</h2>
<pre class="mermaid">
architecture-beta
group left
group right
service left_disk(disk)[Disk] in left
service top_disk(disk)[Disk] in left
service bottom_disk(disk)[Disk] in left
service top_gateway(internet)[Gateway] in right
service bottom_gateway(internet)[Gateway] in right
junction juncC in left
junction juncR in right
left_disk:R -- L:juncC
top_disk:B -- T:juncC
bottom_disk:T -- B:juncC
top_gateway:B <-- T:juncR
bottom_gateway:T <-- B:juncR
juncC{group}:R --> L:juncR{group}
</pre>
<hr />
<h2>AWS Icon Demo</h2>
<pre class="mermaid">
architecture-beta
service s3(aws:s3)[Cloud Store]
service ec2(aws:ec2)[Server]
service wave(aws:wavelength)[Wave]
service droplet(do:droplet)[Droplet]
service repo(gh:github)[Repository]
</pre
>
<script type="module">
import mermaid from './mermaid.esm.mjs';
const ALLOWED_TAGS = [
'a',
'b',
'blockquote',
'br',
'dd',
'div',
'dl',
'dt',
'em',
'foreignObject',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'h7',
'h8',
'hr',
'i',
'li',
'ul',
'ol',
'p',
'pre',
'span',
'strike',
'strong',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
];
mermaid.parseError = function (err, hash) {
// console.error('Mermaid error: ', err);
};
mermaid.initialize({
theme: 'base',
startOnLoad: true,
logLevel: 0,
flowchart: {
useMaxWidth: false,
htmlLabels: true,
},
gantt: {
useMaxWidth: false,
},
architecture: {
iconSize: 80,
},
useMaxWidth: false,
iconLibraries: ['aws:common', 'aws:full', 'github', 'digital-ocean'],
});
function callback() {
alert('It worked');
}
mermaid.parseError = function (err, hash) {
console.error('In parse error:');
console.error(err);
};
</script>
</body>
</html>

View File

@@ -88,6 +88,9 @@
<li>
<h2><a href="./block.html">Layered Blocks</a></h2>
</li>
<li>
<h2><a href="./architecture.html">Architecture</a></h2>
</li>
</ul>
</body>
</html>

67
docs/config/icons.md Normal file
View File

@@ -0,0 +1,67 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/icons.md](../../packages/mermaid/src/docs/config/icons.md).
# SVG Icons (v???+)
SVG Icons can be used with supported diagrams. Alongside the icon packs included with Mermaid, 3rd party libraries can be included in the configuration to cover additional use-cases.
## Supported Diagrams
| Diagram | Usage |
| ------------ | --------------------------------- |
| Architecture | Icon names are surrounded by `()` |
## Included Icon Packs
| Icon Pack | Prefix |
| ------------- | ------ |
| default | N/A |
| Amazon AWS | `aws:` |
| Digital Ocean | `do:` |
| GitHub | `gh:` |
Note that in order to use non-generic icons that are provided with Mermaid, the packs must be explicitly loaded when on initialization initialized.
```js
import sampleIconPack from 'sample-icon-pack';
mermaid.initialize({
iconLibraries: ['aws:common', 'aws:full', 'github', 'digital-ocean'],
});
```
## Using Custom Icon Packs
Custom icon packs can be used by including them in the `iconLibraries` array on mermaid initialization.
```js
import sampleIconPack from 'sample-icon-pack';
mermaid.initialize({
iconLibraries: [sampleIconPack, 'aws:full', ...],
});
```
## Creating Custom Icon Packs
```js
import { createIcon } from 'mermaid';
import type { IconLibrary, IconResolver } from 'mermaid';
// type IconLibrary = Record<string, IconResolver>;
// createIcon: (icon: string, originalSize: number) => IconResolver
const myIconLibrary: IconLibrary = {
defaultCloudExample: createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<path d="..." style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
</g>`,
80
)
};
export default myIconLibrary
```

View File

@@ -28,7 +28,7 @@ page.
#### Defined in
[packages/mermaid/src/mermaid.ts:435](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L435)
[packages/mermaid/src/mermaid.ts:441](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L441)
---
@@ -59,7 +59,7 @@ A graph definition key
#### Defined in
[packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
[packages/mermaid/src/mermaid.ts:443](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L443)
---
@@ -89,7 +89,7 @@ Use [initialize](mermaid.Mermaid.md#initialize) and [run](mermaid.Mermaid.md#run
#### Defined in
[packages/mermaid/src/mermaid.ts:430](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L430)
[packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
---
@@ -116,7 +116,7 @@ This function should be called before the run function.
#### Defined in
[packages/mermaid/src/mermaid.ts:434](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L434)
[packages/mermaid/src/mermaid.ts:440](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L440)
---
@@ -130,7 +130,7 @@ Use [parse](mermaid.Mermaid.md#parse) and [render](mermaid.Mermaid.md#render) in
#### Defined in
[packages/mermaid/src/mermaid.ts:424](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L424)
[packages/mermaid/src/mermaid.ts:430](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L430)
---
@@ -180,7 +180,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not
#### Defined in
[packages/mermaid/src/mermaid.ts:425](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L425)
[packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431)
---
@@ -190,7 +190,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not
#### Defined in
[packages/mermaid/src/mermaid.ts:419](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L419)
[packages/mermaid/src/mermaid.ts:425](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L425)
---
@@ -218,7 +218,7 @@ Used to register external diagram types.
#### Defined in
[packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L433)
[packages/mermaid/src/mermaid.ts:439](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L439)
---
@@ -242,7 +242,7 @@ Used to register external diagram types.
#### Defined in
[packages/mermaid/src/mermaid.ts:432](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L432)
[packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438)
---
@@ -268,7 +268,7 @@ Used to register external diagram types.
#### Defined in
[packages/mermaid/src/mermaid.ts:426](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L426)
[packages/mermaid/src/mermaid.ts:432](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L432)
---
@@ -316,7 +316,7 @@ Renders the mermaid diagrams
#### Defined in
[packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431)
[packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
---
@@ -351,7 +351,7 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me
#### Defined in
[packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
[packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
---
@@ -361,4 +361,4 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me
#### Defined in
[packages/mermaid/src/mermaid.ts:418](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L418)
[packages/mermaid/src/mermaid.ts:424](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L424)

View File

@@ -20,6 +20,16 @@
---
### architecture
`Optional` **architecture**: `ArchitectureDiagramConfig`
#### Defined in
[packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
---
### arrowMarkerAbsolute
`Optional` **arrowMarkerAbsolute**: `boolean`
@@ -39,7 +49,7 @@ This matters if you are using base tag settings.
#### Defined in
[packages/mermaid/src/config.type.ts:199](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L199)
[packages/mermaid/src/config.type.ts:209](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L209)
---
@@ -49,7 +59,7 @@ This matters if you are using base tag settings.
#### Defined in
[packages/mermaid/src/config.type.ts:196](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L196)
[packages/mermaid/src/config.type.ts:206](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L206)
---
@@ -59,7 +69,7 @@ This matters if you are using base tag settings.
#### Defined in
[packages/mermaid/src/config.type.ts:187](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L187)
[packages/mermaid/src/config.type.ts:196](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L196)
---
@@ -83,7 +93,7 @@ You can set this attribute to base the seed on a static string.
#### Defined in
[packages/mermaid/src/config.type.ts:181](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L181)
[packages/mermaid/src/config.type.ts:190](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L190)
---
@@ -101,7 +111,7 @@ should not change unless content is changed.
#### Defined in
[packages/mermaid/src/config.type.ts:174](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L174)
[packages/mermaid/src/config.type.ts:183](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L183)
---
@@ -111,7 +121,7 @@ should not change unless content is changed.
#### Defined in
[packages/mermaid/src/config.type.ts:200](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L200)
[packages/mermaid/src/config.type.ts:210](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L210)
---
@@ -139,7 +149,7 @@ should not change unless content is changed.
#### Defined in
[packages/mermaid/src/config.type.ts:189](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L189)
[packages/mermaid/src/config.type.ts:198](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L198)
---
@@ -149,7 +159,7 @@ should not change unless content is changed.
#### Defined in
[packages/mermaid/src/config.type.ts:182](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L182)
[packages/mermaid/src/config.type.ts:191](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L191)
---
@@ -173,7 +183,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
#### Defined in
[packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
[packages/mermaid/src/config.type.ts:212](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L212)
---
@@ -187,7 +197,7 @@ If set to true, ignores legacyMathML.
#### Defined in
[packages/mermaid/src/config.type.ts:163](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L163)
[packages/mermaid/src/config.type.ts:172](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L172)
---
@@ -197,7 +207,7 @@ If set to true, ignores legacyMathML.
#### Defined in
[packages/mermaid/src/config.type.ts:184](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L184)
[packages/mermaid/src/config.type.ts:193](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L193)
---
@@ -207,7 +217,7 @@ If set to true, ignores legacyMathML.
#### Defined in
[packages/mermaid/src/config.type.ts:195](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L195)
[packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205)
---
@@ -233,13 +243,26 @@ Defines the seed to be used when using handDrawn look. This is important for the
---
### iconLibraries
`Optional` **iconLibraries**: ([`IconLibrary`](../modules/mermaid.md#iconlibrary) | `"aws:common"` | `"aws:full"` | `"github"` | `"digital-ocean"`)\[]
This option specifies an object contianing a mappig of SVG icon names to a resolver that returns the svg code.
For supported diagrams (i.e., Architecture), their syntax allows refering to key names in this object to display the corresponding SVG icon in the rendered diagram.
#### Defined in
[packages/mermaid/src/config.type.ts:162](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L162)
---
### journey
`Optional` **journey**: `JourneyDiagramConfig`
#### Defined in
[packages/mermaid/src/config.type.ts:185](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L185)
[packages/mermaid/src/config.type.ts:194](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L194)
---
@@ -300,7 +323,7 @@ Defines which main look to use for the diagram.
#### Defined in
[packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
[packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
---
@@ -334,7 +357,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:194](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L194)
[packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204)
---
@@ -344,7 +367,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:198](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L198)
[packages/mermaid/src/config.type.ts:208](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L208)
---
@@ -354,7 +377,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:190](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L190)
[packages/mermaid/src/config.type.ts:199](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L199)
---
@@ -364,7 +387,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:191](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L191)
[packages/mermaid/src/config.type.ts:200](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L200)
---
@@ -374,7 +397,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:193](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L193)
[packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
---
@@ -384,7 +407,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:197](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L197)
[packages/mermaid/src/config.type.ts:207](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L207)
---
@@ -420,7 +443,7 @@ Level of trust for parsed diagram
#### Defined in
[packages/mermaid/src/config.type.ts:183](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L183)
[packages/mermaid/src/config.type.ts:192](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L192)
---
@@ -442,7 +465,7 @@ Dictates whether mermaid starts on Page load
#### Defined in
[packages/mermaid/src/config.type.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L188)
[packages/mermaid/src/config.type.ts:197](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L197)
---
@@ -455,7 +478,7 @@ This is useful when you want to control how to handle syntax errors in your appl
#### Defined in
[packages/mermaid/src/config.type.ts:209](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L209)
[packages/mermaid/src/config.type.ts:219](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L219)
---
@@ -498,7 +521,7 @@ You may also use `themeCSS` to override this value.
#### Defined in
[packages/mermaid/src/config.type.ts:186](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L186)
[packages/mermaid/src/config.type.ts:195](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L195)
---
@@ -508,7 +531,7 @@ You may also use `themeCSS` to override this value.
#### Defined in
[packages/mermaid/src/config.type.ts:201](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L201)
[packages/mermaid/src/config.type.ts:211](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L211)
---
@@ -518,4 +541,4 @@ You may also use `themeCSS` to override this value.
#### Defined in
[packages/mermaid/src/config.type.ts:192](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L192)
[packages/mermaid/src/config.type.ts:201](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L201)

View File

@@ -18,7 +18,7 @@ The nodes to render. If this is set, `querySelector` will be ignored.
#### Defined in
[packages/mermaid/src/mermaid.ts:48](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L48)
[packages/mermaid/src/mermaid.ts:54](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L54)
---
@@ -44,7 +44,7 @@ A callback to call after each diagram is rendered.
#### Defined in
[packages/mermaid/src/mermaid.ts:52](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L52)
[packages/mermaid/src/mermaid.ts:58](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L58)
---
@@ -56,7 +56,7 @@ The query selector to use when finding elements to render. Default: `".mermaid"`
#### Defined in
[packages/mermaid/src/mermaid.ts:44](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L44)
[packages/mermaid/src/mermaid.ts:50](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L50)
---
@@ -68,4 +68,4 @@ If `true`, errors will be logged to the console, but not thrown. Default: `false
#### Defined in
[packages/mermaid/src/mermaid.ts:56](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L56)
[packages/mermaid/src/mermaid.ts:62](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L62)

View File

@@ -26,6 +26,41 @@
## Type Aliases
### IconLibrary
Ƭ **IconLibrary**: `Record`<`string`, [`IconResolver`](mermaid.md#iconresolver)>
#### Defined in
[packages/mermaid/src/rendering-util/svgRegister.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/svgRegister.ts#L7)
---
### IconResolver
Ƭ **IconResolver**: (`parent`: `Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>, `width?`: `number`) => `Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
#### Type declaration
▸ (`parent`, `width?`): `Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
##### Parameters
| Name | Type |
| :------- | :-------------------------------------------------------------------- |
| `parent` | `Selection`<`SVGGElement`, `unknown`, `Element` \| `null`, `unknown`> |
| `width?` | `number` |
##### Returns
`Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
#### Defined in
[packages/mermaid/src/rendering-util/svgRegister.ts:3](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/svgRegister.ts#L3)
---
### InternalHelpers
Ƭ **InternalHelpers**: typeof `internalHelpers`
@@ -87,4 +122,29 @@
#### Defined in
[packages/mermaid/src/mermaid.ts:440](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L440)
[packages/mermaid/src/mermaid.ts:446](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L446)
## Functions
### createIcon
**createIcon**(`icon`, `originalSize`): [`IconResolver`](mermaid.md#iconresolver)
Converts an SVG Icon passed as a string into a properly formatted IconResolver
#### Parameters
| Name | Type | Description |
| :------------- | :------- | :-------------------------------------------------------------------------- |
| `icon` | `string` | html code for the svg icon as a string (the SVG tag should not be included) |
| `originalSize` | `number` | the original size of the SVG Icon in pixels |
#### Returns
[`IconResolver`](mermaid.md#iconresolver)
IconResolver
#### Defined in
[packages/mermaid/src/rendering-util/svgRegister.ts:15](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/svgRegister.ts#L15)

View File

@@ -55,6 +55,10 @@ For a more detailed introduction to Mermaid and some of its more basic uses, loo
**Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project 🙏**
Our PR Visual Regression Testing is powered by [Argos](https://argos-ci.com/?utm_source=mermaid&utm_campaign=oss) with their generous Open Source plan. It makes the process of reviewing PRs with visual changes a breeze.
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=mermaid&utm_campaign=oss)
In our release process we rely heavily on visual regression tests using [applitools](https://applitools.com/). Applitools is a great service which has been easy to use and integrate with our tests.
<a href="https://applitools.com/">

194
docs/syntax/architecture.md Normal file
View File

@@ -0,0 +1,194 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/architecture.md](../../packages/mermaid/src/docs/syntax/architecture.md).
# Architecture Diagrams Documentation (v\<MERMAID_RELEASE_VERSION>+)
> In the context of mermaid-js, the architecture diagram is used to show the relationship between services and resources commonly found within the Cloud or CI/CD deployments. In an architecture diagram, services (nodes) are connected by edges. Related services can be placed within groups to better illustrate how they are organized.
## Example
```mermaid-example
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
```
```mermaid
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
```
## Syntax
The building blocks of an architecture are `groups`, `services`, `edges`, and `junctions`.
For supporting components, icons are declared by surrounding the icon name with `()`, while labels are declared by surrounding the text with `[]`.
To begin an architecture diagram, use the keyword `architecture-beta`, followed by your groups, services, edges, and junctions. While each of the 3 building blocks can be declared in any order, care must be taken to ensure the identifier was previously declared by another component.
### Groups
The syntax for declaring a group is:
```
group {group id}({icon name})[{title}] (in {parent id})?
```
Put together:
```
group public_api(cloud)[Public API]
```
creates a group identified as `public_api`, uses the icon `cloud`, and has the label `Public API`.
Additionally, groups can be placed within a group using the optional `in` keyword
```
group private_api(cloud)[Private API] in public_api
```
### Services
The syntax for declaring a service is:
```
service {service id}({icon name})[{title}] (in {parent id})?
```
Put together:
```
service database(db)[Database]
```
creates the service identified as `database`, using the icon `db`, with the label `Database`.
If the service belongs to a group, it can be placed inside it through the optional `in` keyword
```
service database(db)[Database] in private_api
```
### Edges
The syntax for declaring an edge is:
```
{serviceId}{{group}}?:{T|B|L|R} {<}?--{>}? {T|B|L|R}:{serviceId}{{group}}?
```
#### Edge Direction
The side of the service the edge comes out of is specified by adding a colon (`:`) to the side of the service connecting to the arrow and adding `L|R|T|B`
For example:
```
db:R -- L:server
```
creates an edge between the services `db` and `server`, with the edge coming out of the right of `db` and the left of `server`.
```
db:T -- L:server
```
creates a 90 degree edge between the services `db` and `server`, with the edge coming out of the top of `db` and the left of `server`.
#### Arrows
Arrows can be added to each side of an edge by adding `<` before the direction on the left, and/or `>` after the direction on the right.
For example:
```
subnet:R --> L:gateway
```
creates an edge with the arrow going into the `gateway` service
#### Edges out of Groups
To have an edge go from a group to another group or service within another group, the `{group}` modifier can be added after the `serviceId`.
For example:
```
service server[Server] in groupOne
service subnet[Subnet] in groupTwo
server{group}:B --> T:subnet{group}
```
creates an edge going out of `groupOne`, adjacent to `server`, and into `groupTwo`, adjacent to `subnet`.
It's important to note that `groupId`s cannot be used for specifying edges and the `{group}` modifier can only be used for services within a group.
### Junctions
Junctions are a special type of node which acts as a potential 4-way split between edges.
The syntax for declaring a junction is:
```
junction {junction id} (in {parent id})?
```
```mermaid-example
architecture-beta
service left_disk(disk)[Disk]
service top_disk(disk)[Disk]
service bottom_disk(disk)[Disk]
service top_gateway(internet)[Gateway]
service bottom_gateway(internet)[Gateway]
junction junctionCenter
junction junctionRight
left_disk:R -- L:junctionCenter
top_disk:B -- T:junctionCenter
bottom_disk:T -- B:junctionCenter
junctionCenter:R -- L:junctionRight
top_gateway:B -- T:junctionRight
bottom_gateway:T -- B:junctionRight
```
```mermaid
architecture-beta
service left_disk(disk)[Disk]
service top_disk(disk)[Disk]
service bottom_disk(disk)[Disk]
service top_gateway(internet)[Gateway]
service bottom_gateway(internet)[Gateway]
junction junctionCenter
junction junctionRight
left_disk:R -- L:junctionCenter
top_disk:B -- T:junctionCenter
bottom_disk:T -- B:junctionCenter
junctionCenter:R -- L:junctionRight
top_gateway:B -- T:junctionRight
bottom_gateway:T -- B:junctionRight
```
## Configuration

View File

@@ -71,6 +71,7 @@
"@mermaid-js/parser": "workspace:^",
"cytoscape": "^3.29.2",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.10",
@@ -87,6 +88,7 @@
},
"devDependencies": {
"@adobe/jsonschema2md": "^8.0.0",
"@types/cytoscape-fcose": "^2.2.4",
"@types/cytoscape": "^3.21.4",
"@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.4",

View File

@@ -154,6 +154,15 @@ export interface MermaidConfig {
*
*/
legacyMathML?: boolean;
/**
* This option specifies an object contianing a mappig of SVG icon names to a resolver that returns the svg code.
* For supported diagrams (i.e., Architecture), their syntax allows refering to key names in this object to display the corresponding SVG icon in the rendered diagram.
*
*/
iconLibraries?: Array<
| import('./rendering-util/svgRegister.js').IconLibrary
| import('./rendering-util/svg/index.js').IconNamespaceKeys
>;
/**
* This option forces Mermaid to rely on KaTeX's own stylesheet for rendering MathML. Due to differences between OS
* fonts and browser's MathML implementation, this option is recommended if consistent rendering is important.
@@ -191,6 +200,7 @@ export interface MermaidConfig {
quadrantChart?: QuadrantChartConfig;
xyChart?: XYChartConfig;
requirement?: RequirementDiagramConfig;
architecture?: ArchitectureDiagramConfig;
mindmap?: MindmapDiagramConfig;
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
@@ -1001,6 +1011,17 @@ export interface RequirementDiagramConfig extends BaseDiagramConfig {
rect_padding?: number;
line_height?: number;
}
/**
* The object containing configurations specific for architecture diagrams
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "ArchitectureDiagramConfig".
*/
export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
padding?: number;
iconSize?: number;
fontSize?: number;
}
/**
* The object containing configurations specific for mindmap diagrams
*

View File

@@ -22,6 +22,7 @@ import mindmap from '../diagrams/mindmap/detector.js';
import sankey from '../diagrams/sankey/sankeyDetector.js';
import { packet } from '../diagrams/packet/detector.js';
import block from '../diagrams/block/blockDetector.js';
import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
@@ -90,6 +91,7 @@ export const addDiagrams = () => {
sankey,
packet,
xychart,
block
block,
architecture
);
};

View File

@@ -0,0 +1,336 @@
// TODO remove no-console
/* eslint-disable no-console */
import type {
ArchitectureState,
ArchitectureDB,
ArchitectureService,
ArchitectureGroup,
ArchitectureEdge,
ArchitectureDirectionPairMap,
ArchitectureDirectionPair,
ArchitectureSpatialMap,
ArchitectureNode,
ArchitectureJunction,
} from './architectureTypes.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import {
getArchitectureDirectionPair,
isArchitectureDirection,
isArchitectureJunction,
isArchitectureService,
shiftPositionByArchitectureDirectionPair,
} from './architectureTypes.js';
import {
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../common/commonDb.js';
import type { ArchitectureDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
import type { D3Element } from '../../types.js';
import { ImperativeState } from '../../utils/imperativeState.js';
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
DEFAULT_CONFIG.architecture;
const state = new ImperativeState<ArchitectureState>(() => ({
nodes: {},
groups: {},
edges: [],
registeredIds: {},
config: DEFAULT_ARCHITECTURE_CONFIG,
dataStructures: undefined,
elements: {},
}));
const clear = (): void => {
state.reset();
commonClear();
};
const addService = function ({
id,
icon,
in: parent,
title,
iconText,
}: Omit<ArchitectureService, 'edges'>) {
if (state.records.registeredIds[id] !== undefined) {
throw new Error(
`The service id [${id}] is already in use by another ${state.records.registeredIds[id]}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The service [${id}] cannot be placed within itself`);
}
if (state.records.registeredIds[parent] === undefined) {
throw new Error(
`The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`
);
}
if (state.records.registeredIds[parent] === 'node') {
throw new Error(`The service [${id}]'s parent is not a group`);
}
}
state.records.registeredIds[id] = 'node';
state.records.nodes[id] = {
id,
type: 'service',
icon,
iconText,
title,
edges: [],
in: parent,
};
};
const getServices = (): ArchitectureService[] =>
Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);
const addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) {
state.records.registeredIds[id] = 'node';
state.records.nodes[id] = {
id,
type: 'junction',
edges: [],
in: parent,
};
};
const getJunctions = (): ArchitectureJunction[] =>
Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction);
const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes);
const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id];
const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {
if (state.records.registeredIds[id] !== undefined) {
throw new Error(
`The group id [${id}] is already in use by another ${state.records.registeredIds[id]}`
);
}
if (parent !== undefined) {
if (id === parent) {
throw new Error(`The group [${id}] cannot be placed within itself`);
}
if (state.records.registeredIds[parent] === undefined) {
throw new Error(
`The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`
);
}
if (state.records.registeredIds[parent] === 'node') {
throw new Error(`The group [${id}]'s parent is not a group`);
}
}
state.records.registeredIds[id] = 'group';
state.records.groups[id] = {
id,
icon,
title,
in: parent,
};
};
const getGroups = (): ArchitectureGroup[] => {
return Object.values(state.records.groups);
};
const addEdge = function ({
lhsId,
rhsId,
lhsDir,
rhsDir,
lhsInto,
rhsInto,
lhsGroup,
rhsGroup,
title,
}: ArchitectureEdge<string>) {
if (!isArchitectureDirection(lhsDir)) {
throw new Error(
`Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}`
);
}
if (!isArchitectureDirection(rhsDir)) {
throw new Error(
`Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}`
);
}
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) {
throw new Error(
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
}
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) {
throw new Error(
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
);
}
const lhsGroupId = state.records.nodes[lhsId].in;
const rhsGroupId = state.records.nodes[rhsId].in;
if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error(
`The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
);
}
if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {
throw new Error(
`The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`
);
}
const edge = {
lhsId,
lhsDir,
lhsInto,
lhsGroup,
rhsId,
rhsDir,
rhsInto,
rhsGroup,
title,
};
state.records.edges.push(edge);
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) {
state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);
}
};
const getEdges = (): ArchitectureEdge[] => state.records.edges;
/**
* Returns the current diagram's adjacency list & spatial map.
* If they have not been created, run the algorithms to generate them.
* @returns
*/
const getDataStructures = () => {
if (state.records.dataStructures === undefined) {
// Create an adjacency list of the diagram to perform BFS on
// Outer reduce applied on all services
// Inner reduce applied on the edges for a service
const adjList = Object.entries(state.records.nodes).reduce<
Record<string, ArchitectureDirectionPairMap>
>((prevOuter, [id, service]) => {
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
if (edge.lhsId === id) {
// source is LHS
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
if (pair) {
prevInner[pair] = edge.rhsId;
}
} else {
// source is RHS
const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
if (pair) {
prevInner[pair] = edge.lhsId;
}
}
return prevInner;
}, {});
return prevOuter;
}, {});
// Configuration for the initial pass of BFS
const firstId = Object.keys(adjList)[0];
const visited = { [firstId]: 1 };
const notVisited = Object.keys(adjList).reduce(
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
{} as Record<string, number>
);
// Perform BFS on the adjacency list
const BFS = (startingId: string): ArchitectureSpatialMap => {
const spatialMap = { [startingId]: [0, 0] };
const queue = [startingId];
while (queue.length > 0) {
const id = queue.shift();
if (id) {
visited[id] = 1;
delete notVisited[id];
const adj = adjList[id];
const [posX, posY] = spatialMap[id];
Object.entries(adj).forEach(([dir, rhsId]) => {
if (!visited[rhsId]) {
spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(
[posX, posY],
dir as ArchitectureDirectionPair
);
queue.push(rhsId);
}
});
}
}
return spatialMap;
};
const spatialMaps = [BFS(firstId)];
// If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found
while (Object.keys(notVisited).length > 0) {
spatialMaps.push(BFS(Object.keys(notVisited)[0]));
}
state.records.dataStructures = {
adjList,
spatialMaps,
};
console.log(state.records.dataStructures);
}
return state.records.dataStructures;
};
const setElementForId = (id: string, element: D3Element) => {
state.records.elements[id] = element;
};
const getElementById = (id: string) => state.records.elements[id];
export const db: ArchitectureDB = {
clear,
setDiagramTitle,
getDiagramTitle,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
addService,
getServices,
addJunction,
getJunctions,
getNodes,
getNode,
addGroup,
getGroups,
addEdge,
getEdges,
setElementForId,
getElementById,
getDataStructures,
};
/**
* Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
* @param field - the config field to access
* @returns
*/
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
field: T
): Required<ArchitectureDiagramConfig>[T] {
const arch = getConfig().architecture;
if (arch?.[field]) {
return arch[field] as Required<ArchitectureDiagramConfig>[T];
}
return DEFAULT_ARCHITECTURE_CONFIG[field];
}

View File

@@ -0,0 +1,24 @@
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'architecture';
const detector: DiagramDetector = (txt) => {
return /^\s*architecture/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./architectureDiagram.js');
return { id, diagram };
};
const architecture: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default architecture;

View File

@@ -0,0 +1,12 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
import { parser } from './architectureParser.js';
import { db } from './architectureDb.js';
import styles from './architectureStyles.js';
import { renderer } from './architectureRenderer.js';
export const diagram: DiagramDefinition = {
parser,
db,
renderer,
styles,
};

View File

@@ -0,0 +1,24 @@
import type { Architecture } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js';
import { populateCommonDb } from '../common/populateCommonDb.js';
import type { ArchitectureDB } from './architectureTypes.js';
import { db } from './architectureDb.js';
const populateDb = (ast: Architecture, db: ArchitectureDB) => {
populateCommonDb(ast, db);
ast.groups.map(db.addGroup);
ast.services.map((service) => db.addService({ ...service, type: 'service' }));
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));
// @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type?
ast.edges.map(db.addEdge);
};
export const parser: ParserDefinition = {
parse: async (input: string): Promise<void> => {
const ast: Architecture = await parse('architecture', input);
log.debug(ast);
populateDb(ast, db);
},
};

View File

@@ -0,0 +1,472 @@
// TODO remove no-console
/* eslint-disable no-console */
import type { Position } from 'cytoscape';
import cytoscape from 'cytoscape';
import type { Diagram } from '../../Diagram.js';
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
import fcose from 'cytoscape-fcose';
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import type {
ArchitectureDataStructures,
ArchitectureSpatialMap,
EdgeSingularData,
EdgeSingular,
ArchitectureJunction,
NodeSingularData,
} from './architectureTypes.js';
import {
type ArchitectureDB,
type ArchitectureDirection,
type ArchitectureGroup,
type ArchitectureEdge,
type ArchitectureService,
ArchitectureDirectionName,
getOppositeArchitectureDirection,
isArchitectureDirectionXY,
isArchitectureDirectionY,
nodeData,
edgeData,
} from './architectureTypes.js';
import { select } from 'd3';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import { drawEdges, drawGroups, drawJunctions, drawServices } from './svgDraw.js';
import { getConfigField } from './architectureDb.js';
cytoscape.use(fcose);
function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
services.forEach((service) => {
cy.add({
group: 'nodes',
data: {
type: 'service',
id: service.id,
icon: service.icon,
label: service.title,
parent: service.in,
width: getConfigField('iconSize'),
height: getConfigField('iconSize'),
} as NodeSingularData,
classes: 'node-service',
});
});
}
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
junctions.forEach((junction) => {
cy.add({
group: 'nodes',
data: {
type: 'junction',
id: junction.id,
parent: junction.in,
width: getConfigField('iconSize'),
height: getConfigField('iconSize'),
} as NodeSingularData,
classes: 'node-junction',
});
});
}
function positionNodes(db: ArchitectureDB, cy: cytoscape.Core) {
cy.nodes().map((node) => {
const data = nodeData(node);
if (data.type === 'group') {
return;
}
data.x = node.position().x;
data.y = node.position().y;
const nodeElem = db.getElementById(data.id);
nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')');
});
}
function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {
groups.forEach((group) => {
cy.add({
group: 'nodes',
data: {
type: 'group',
id: group.id,
icon: group.icon,
label: group.title,
parent: group.in,
} as NodeSingularData,
classes: 'node-group',
});
});
}
function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) {
edges.forEach((parsedEdge) => {
const { lhsId, rhsId, lhsInto, lhsGroup, rhsInto, lhsDir, rhsDir, rhsGroup, title } =
parsedEdge;
const edgeType = isArchitectureDirectionXY(parsedEdge.lhsDir, parsedEdge.rhsDir)
? 'segments'
: 'straight';
const edge: EdgeSingularData = {
id: `${lhsId}-${rhsId}`,
label: title,
source: lhsId,
sourceDir: lhsDir,
sourceArrow: lhsInto,
sourceGroup: lhsGroup,
sourceEndpoint:
lhsDir === 'L'
? '0 50%'
: lhsDir === 'R'
? '100% 50%'
: lhsDir === 'T'
? '50% 0'
: '50% 100%',
target: rhsId,
targetDir: rhsDir,
targetArrow: rhsInto,
targetGroup: rhsGroup,
targetEndpoint:
rhsDir === 'L'
? '0 50%'
: rhsDir === 'R'
? '100% 50%'
: rhsDir === 'T'
? '50% 0'
: '50% 100%',
};
cy.add({
group: 'edges',
data: edge,
classes: edgeType,
});
});
}
function getAlignments(spatialMaps: ArchitectureSpatialMap[]): fcose.FcoseAlignmentConstraint {
const alignments = spatialMaps.map((spatialMap) => {
const horizontalAlignments: Record<number, string[]> = {};
const verticalAlignments: Record<number, string[]> = {};
// Group service ids in an object with their x and y coordinate as the key
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
if (!horizontalAlignments[y]) {
horizontalAlignments[y] = [];
}
if (!verticalAlignments[x]) {
verticalAlignments[x] = [];
}
horizontalAlignments[y].push(id);
verticalAlignments[x].push(id);
});
// Merge the values of each object into a list if the inner list has at least 2 elements
return {
horiz: Object.values(horizontalAlignments).filter((arr) => arr.length > 1),
vert: Object.values(verticalAlignments).filter((arr) => arr.length > 1),
};
});
// Merge the alignment lists for each spatial map into one 2d array per axis
const [horizontal, vertical] = alignments.reduce(
([prevHoriz, prevVert], { horiz, vert }) => {
return [
[...prevHoriz, ...horiz],
[...prevVert, ...vert],
];
},
[[] as string[][], [] as string[][]]
);
return {
horizontal,
vertical,
};
}
function getRelativeConstraints(
spatialMaps: ArchitectureSpatialMap[]
): fcose.FcoseRelativePlacementConstraint[] {
const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
const strToPos = (pos: string) => pos.split(',').map((p) => parseInt(p));
spatialMaps.forEach((spatialMap) => {
const invSpatialMap = Object.fromEntries(
Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id])
);
// perform BFS
const queue = [posToStr([0, 0])];
const visited: Record<string, number> = {};
const directions: Record<ArchitectureDirection, number[]> = {
L: [-1, 0],
R: [1, 0],
T: [0, 1],
B: [0, -1],
};
while (queue.length > 0) {
const curr = queue.shift();
if (curr) {
visited[curr] = 1;
const currId = invSpatialMap[curr];
if (currId) {
const currPos = strToPos(curr);
Object.entries(directions).forEach(([dir, shift]) => {
const newPos = posToStr([currPos[0] + shift[0], currPos[1] + shift[1]]);
const newId = invSpatialMap[newPos];
// If there is an adjacent service to the current one and it has not yet been visited
if (newId && !visited[newPos]) {
queue.push(newPos);
// @ts-ignore cannot determine if left/right or top/bottom are paired together
relativeConstraints.push({
[ArchitectureDirectionName[dir as ArchitectureDirection]]: newId,
[ArchitectureDirectionName[
getOppositeArchitectureDirection(dir as ArchitectureDirection)
]]: currId,
gap: 1.5 * getConfigField('iconSize'),
});
}
});
}
}
}
});
return relativeConstraints;
}
function layoutArchitecture(
services: ArchitectureService[],
junctions: ArchitectureJunction[],
groups: ArchitectureGroup[],
edges: ArchitectureEdge[],
{ spatialMaps }: ArchitectureDataStructures
): Promise<cytoscape.Core> {
return new Promise((resolve) => {
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'edge',
style: {
'curve-style': 'straight',
label: 'data(label)',
'source-endpoint': 'data(sourceEndpoint)',
'target-endpoint': 'data(targetEndpoint)',
},
},
{
selector: 'edge.segments',
style: {
'curve-style': 'segments',
'segment-weights': '0',
'segment-distances': [0.5],
// @ts-ignore Incorrect library types
'edge-distances': 'endpoints',
'source-endpoint': 'data(sourceEndpoint)',
'target-endpoint': 'data(targetEndpoint)',
},
},
{
selector: 'node',
style: {
// @ts-ignore Incorrect library types
'compound-sizing-wrt-labels': 'include',
},
},
{
selector: 'node[label]',
style: {
'text-valign': 'bottom',
'text-halign': 'center',
'font-size': `${getConfigField('fontSize')}px`,
},
},
{
selector: '.node-service',
style: {
label: 'data(label)',
width: 'data(width)',
height: 'data(height)',
},
},
{
selector: '.node-junction',
style: {
width: 'data(width)',
height: 'data(height)',
},
},
{
selector: '.node-group',
style: {
// @ts-ignore Incorrect library types
padding: `${getConfigField('padding')}px`,
},
},
],
});
// Remove element after layout
renderEl.remove();
addGroups(groups, cy);
addServices(services, cy);
addJunctions(junctions, cy);
addEdges(edges, cy);
// Use the spatial map to create alignment arrays for fcose
const alignmentConstraint = getAlignments(spatialMaps);
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
console.log(`Horizontal Alignments:`);
console.log(alignmentConstraint.horizontal);
console.log(`Vertical Alignments:`);
console.log(alignmentConstraint.vertical);
console.log(`Relative Alignments:`);
console.log(relativePlacementConstraint);
const layout = cy.layout({
name: 'fcose',
quality: 'proof',
styleEnabled: false,
animate: false,
nodeDimensionsIncludeLabels: false,
// Adjust the edge parameters if it passes through the border of a group
// Hacky fix for: https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/issues/67
idealEdgeLength(edge: EdgeSingular) {
const [nodeA, nodeB] = edge.connectedNodes();
const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB);
const elasticity =
parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');
return elasticity;
},
edgeElasticity(edge: EdgeSingular) {
const [nodeA, nodeB] = edge.connectedNodes();
const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB);
const elasticity = parentA === parentB ? 0.45 : 0.001;
return elasticity;
},
alignmentConstraint,
relativePlacementConstraint,
} as FcoseLayoutOptions);
// Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend
layout.one('layoutstop', () => {
function getSegmentWeights(
source: Position,
target: Position,
pointX: number,
pointY: number
) {
let W, D;
const { x: sX, y: sY } = source;
const { x: tX, y: tY } = target;
D =
(pointY - sY + ((sX - pointX) * (sY - tY)) / (sX - tX)) /
Math.sqrt(1 + Math.pow((sY - tY) / (sX - tX), 2));
W = Math.sqrt(Math.pow(pointY - sY, 2) + Math.pow(pointX - sX, 2) - Math.pow(D, 2));
const distAB = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2));
W = W / distAB;
//check whether the point (pointX, pointY) is on right or left of the line src to tgt. for instance : a point C(X, Y) and line (AB). d=(xB-xA)(yC-yA)-(yB-yA)(xC-xA). if d>0, then C is on left of the line. if d<0, it is on right. if d=0, it is on the line.
let delta1 = (tX - sX) * (pointY - sY) - (tY - sY) * (pointX - sX);
switch (true) {
case delta1 >= 0:
delta1 = 1;
break;
case delta1 < 0:
delta1 = -1;
break;
}
//check whether the point (pointX, pointY) is "behind" the line src to tgt
let delta2 = (tX - sX) * (pointX - sX) + (tY - sY) * (pointY - sY);
switch (true) {
case delta2 >= 0:
delta2 = 1;
break;
case delta2 < 0:
delta2 = -1;
break;
}
D = Math.abs(D) * delta1; //ensure that sign of D is same as sign of delta1. Hence we need to take absolute value of D and multiply by delta1
W = W * delta2;
return {
distances: D,
weights: W,
};
}
cy.startBatch();
for (const edge of Object.values(cy.edges())) {
if (edge.data?.()) {
const { x: sX, y: sY } = edge.source().position();
const { x: tX, y: tY } = edge.target().position();
if (sX !== tX && sY !== tY) {
const sEP = edge.sourceEndpoint();
const tEP = edge.targetEndpoint();
const { sourceDir } = edgeData(edge);
const [pointX, pointY] = isArchitectureDirectionY(sourceDir)
? [sEP.x, tEP.y]
: [tEP.x, sEP.y];
const { weights, distances } = getSegmentWeights(sEP, tEP, pointX, pointY);
edge.style('segment-distances', distances);
edge.style('segment-weights', weights);
}
}
}
cy.endBatch();
layout.run();
});
layout.run();
cy.ready((e) => {
log.info('Ready', e);
resolve(cy);
});
});
}
export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) => {
const db = diagObj.db as ArchitectureDB;
const services = db.getServices();
const junctions = db.getJunctions();
const groups = db.getGroups();
const edges = db.getEdges();
const ds = db.getDataStructures();
console.log('Services: ', services);
console.log('Edges: ', edges);
console.log('Groups: ', groups);
const svg: SVG = selectSvgElement(id);
const edgesElem = svg.append('g');
edgesElem.attr('class', 'architecture-edges');
const servicesElem = svg.append('g');
servicesElem.attr('class', 'architecture-services');
const groupElem = svg.append('g');
groupElem.attr('class', 'architecture-groups');
await drawServices(db, servicesElem, services);
drawJunctions(db, servicesElem, junctions);
const cy = await layoutArchitecture(services, junctions, groups, edges, ds);
await drawEdges(edgesElem, cy);
await drawGroups(groupElem, cy);
positionNodes(db, cy);
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));
console.log('==============================================================');
};
export const renderer = { draw };

View File

@@ -0,0 +1,38 @@
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
import type { ArchitectureStyleOptions } from './architectureTypes.js';
const getStyles: DiagramStylesProvider = (options: ArchitectureStyleOptions) =>
`
.edge {
stroke-width: ${options.archEdgeWidth};
stroke: ${options.archEdgeColor};
fill: none;
}
.arrow {
fill: ${options.archEdgeArrowColor};
}
.node-bkg {
fill: none;
stroke: ${options.archGroupBorderColor};
stroke-width: ${options.archGroupBorderWidth};
stroke-dasharray: 8;
}
.node-icon-text {
display: flex;
align-items: center;
}
.node-icon-text > div {
color: #fff;
margin: 1px;
height: fit-content;
text-align: center;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
}
`;
export default getStyles;

View File

@@ -0,0 +1,351 @@
import type { DiagramDB } from '../../diagram-api/types.js';
import type { ArchitectureDiagramConfig } from '../../config.type.js';
import type { D3Element } from '../../types.js';
import type cytoscape from 'cytoscape';
/*=======================================*\
| Architecture Diagram Types |
\*=======================================*/
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
/**
* Contains LL, RR, TT, BB which are impossible connections
*/
export type InvalidArchitectureDirectionPair = `${ArchitectureDirection}${ArchitectureDirection}`;
export type ArchitectureDirectionPair = Exclude<
InvalidArchitectureDirectionPair,
'LL' | 'RR' | 'TT' | 'BB'
>;
export type ArchitectureDirectionPairXY = Exclude<
InvalidArchitectureDirectionPair,
'LL' | 'RR' | 'TT' | 'BB' | 'LR' | 'RL' | 'TB' | 'BT'
>;
export const ArchitectureDirectionName = {
L: 'left',
R: 'right',
T: 'top',
B: 'bottom',
} as const;
export const ArchitectureDirectionArrow = {
L: (scale: number) => `${scale},${scale / 2} 0,${scale} 0,0`,
R: (scale: number) => `0,${scale / 2} ${scale},0 ${scale},${scale}`,
T: (scale: number) => `0,0 ${scale},0 ${scale / 2},${scale}`,
B: (scale: number) => `${scale / 2},0 ${scale},${scale} 0,${scale}`,
} as const;
export const ArchitectureDirectionArrowShift = {
L: (orig: number, arrowSize: number) => orig - arrowSize + 2,
R: (orig: number, _arrowSize: number) => orig - 2,
T: (orig: number, arrowSize: number) => orig - arrowSize + 2,
B: (orig: number, _arrowSize: number) => orig - 2,
} as const;
export const getOppositeArchitectureDirection = function (
x: ArchitectureDirection
): ArchitectureDirection {
if (isArchitectureDirectionX(x)) {
return x === 'L' ? 'R' : 'L';
} else {
return x === 'T' ? 'B' : 'T';
}
};
export const isArchitectureDirection = function (x: unknown): x is ArchitectureDirection {
const temp = x as ArchitectureDirection;
return temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B';
};
export const isArchitectureDirectionX = function (
x: ArchitectureDirection
): x is ArchitectureDirectionX {
const temp = x as ArchitectureDirectionX;
return temp === 'L' || temp === 'R';
};
export const isArchitectureDirectionY = function (
x: ArchitectureDirection
): x is ArchitectureDirectionY {
const temp = x as ArchitectureDirectionY;
return temp === 'T' || temp === 'B';
};
export const isArchitectureDirectionXY = function (
a: ArchitectureDirection,
b: ArchitectureDirection
) {
const aX_bY = isArchitectureDirectionX(a) && isArchitectureDirectionY(b);
const aY_bX = isArchitectureDirectionY(a) && isArchitectureDirectionX(b);
return aX_bY || aY_bX;
};
export const isArchitecturePairXY = function (
pair: ArchitectureDirectionPair
): pair is ArchitectureDirectionPairXY {
const lhs = pair[0] as ArchitectureDirection;
const rhs = pair[1] as ArchitectureDirection;
const aX_bY = isArchitectureDirectionX(lhs) && isArchitectureDirectionY(rhs);
const aY_bX = isArchitectureDirectionY(lhs) && isArchitectureDirectionX(rhs);
return aX_bY || aY_bX;
};
/**
* Verifies that the architecture direction pair does not contain an invalid match (LL, RR, TT, BB)
* @param x - architecture direction pair which could potentially be invalid
* @returns true if the pair is not LL, RR, TT, or BB
*/
export const isValidArchitectureDirectionPair = function (
x: InvalidArchitectureDirectionPair
): x is ArchitectureDirectionPair {
return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB';
};
export type ArchitectureDirectionPairMap = {
[key in ArchitectureDirectionPair]?: string;
};
/**
* Creates a pair of the directions of each side of an edge. This function should be used instead of manually creating it to ensure that the source is always the first character.
*
* Note: Undefined is returned when sourceDir and targetDir are the same. In theory this should never happen since the diagram parser throws an error if a user defines it as such.
* @param sourceDir - source direction
* @param targetDir - target direction
* @returns
*/
export const getArchitectureDirectionPair = function (
sourceDir: ArchitectureDirection,
targetDir: ArchitectureDirection
): ArchitectureDirectionPair | undefined {
const pair: `${ArchitectureDirection}${ArchitectureDirection}` = `${sourceDir}${targetDir}`;
return isValidArchitectureDirectionPair(pair) ? pair : undefined;
};
/**
* Given an x,y position for an arrow and the direction of the edge it belongs to, return a factor for slightly shifting the edge
* @param param0 - [x, y] coordinate pair
* @param pair - architecture direction pair
* @returns a new [x, y] coordinate pair
*/
export const shiftPositionByArchitectureDirectionPair = function (
[x, y]: number[],
pair: ArchitectureDirectionPair
): number[] {
const lhs = pair[0] as ArchitectureDirection;
const rhs = pair[1] as ArchitectureDirection;
if (isArchitectureDirectionX(lhs)) {
if (isArchitectureDirectionY(rhs)) {
return [x + (lhs === 'L' ? -1 : 1), y + (rhs === 'T' ? 1 : -1)];
} else {
return [x + (lhs === 'L' ? -1 : 1), y];
}
} else {
if (isArchitectureDirectionX(rhs)) {
return [x + (rhs === 'L' ? 1 : -1), y + (lhs === 'T' ? 1 : -1)];
} else {
return [x, y + (lhs === 'T' ? 1 : -1)];
}
}
};
/**
* Given the directional pair of an XY edge, get the scale factors necessary to shift the coordinates inwards towards the edge
* @param pair - XY pair of an edge
* @returns - number[] containing [+/- 1, +/- 1]
*/
export const getArchitectureDirectionXYFactors = function (
pair: ArchitectureDirectionPairXY
): number[] {
if (pair === 'LT' || pair === 'TL') {
return [1, 1];
} else if (pair === 'BL' || pair === 'LB') {
return [1, -1];
} else if (pair === 'BR' || pair === 'RB') {
return [-1, -1];
} else {
return [-1, 1];
}
};
export interface ArchitectureStyleOptions {
archEdgeColor: string;
archEdgeArrowColor: string;
archEdgeWidth: string;
archGroupBorderColor: string;
archGroupBorderWidth: string;
}
export interface ArchitectureService {
id: string;
type: 'service';
edges: ArchitectureEdge[];
icon?: string;
iconText?: string;
title?: string;
in?: string;
width?: number;
height?: number;
}
export interface ArchitectureJunction {
id: string;
type: 'junction';
edges: ArchitectureEdge[];
in?: string;
width?: number;
height?: number;
}
export type ArchitectureNode = ArchitectureService | ArchitectureJunction;
export const isArchitectureService = function (x: ArchitectureNode): x is ArchitectureService {
const temp = x as ArchitectureService;
return temp.type === 'service';
};
export const isArchitectureJunction = function (x: ArchitectureNode): x is ArchitectureJunction {
const temp = x as ArchitectureJunction;
return temp.type === 'junction';
};
export interface ArchitectureGroup {
id: string;
icon?: string;
title?: string;
in?: string;
}
export interface ArchitectureEdge<DT = ArchitectureDirection> {
lhsId: string;
lhsDir: DT;
lhsInto?: boolean;
lhsGroup?: boolean;
rhsId: string;
rhsDir: DT;
rhsInto?: boolean;
rhsGroup?: boolean;
title?: string;
}
export interface ArchitectureDB extends DiagramDB {
clear: () => void;
addService: (service: Omit<ArchitectureService, 'edges'>) => void;
getServices: () => ArchitectureService[];
addJunction: (service: Omit<ArchitectureJunction, 'edges'>) => void;
getJunctions: () => ArchitectureJunction[];
getNodes: () => ArchitectureNode[];
getNode: (id: string) => ArchitectureNode | null;
addGroup: (group: ArchitectureGroup) => void;
getGroups: () => ArchitectureGroup[];
addEdge: (edge: ArchitectureEdge) => void;
getEdges: () => ArchitectureEdge[];
setElementForId: (id: string, element: D3Element) => void;
getElementById: (id: string) => D3Element;
getDataStructures: () => ArchitectureDataStructures;
}
export type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;
export type ArchitectureSpatialMap = Record<string, number[]>;
export interface ArchitectureDataStructures {
adjList: ArchitectureAdjacencyList;
spatialMaps: ArchitectureSpatialMap[];
}
export interface ArchitectureState extends Record<string, unknown> {
nodes: Record<string, ArchitectureNode>;
groups: Record<string, ArchitectureGroup>;
edges: ArchitectureEdge[];
registeredIds: Record<string, 'node' | 'group'>;
dataStructures?: ArchitectureDataStructures;
elements: Record<string, D3Element>;
config: ArchitectureDiagramConfig;
}
/*=======================================*\
| Cytoscape Override Types |
\*=======================================*/
export interface EdgeSingularData {
id: string;
label?: string;
source: string;
sourceDir: ArchitectureDirection;
sourceArrow?: boolean;
sourceGroup?: boolean;
target: string;
targetDir: ArchitectureDirection;
targetArrow?: boolean;
targetGroup?: boolean;
[key: string]: any;
}
export const edgeData = (edge: cytoscape.EdgeSingular) => {
return edge.data() as EdgeSingularData;
};
export interface EdgeSingular extends cytoscape.EdgeSingular {
_private: {
bodyBounds: unknown;
rscratch: {
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
};
};
data(): EdgeSingularData;
data<T extends keyof EdgeSingularData>(key: T): EdgeSingularData[T];
}
export type NodeSingularData =
| {
type: 'service';
id: string;
icon?: string;
label?: string;
parent?: string;
width: number;
height: number;
[key: string]: any;
}
| {
type: 'junction';
id: string;
parent?: string;
width: number;
height: number;
[key: string]: any;
}
| {
type: 'group';
id: string;
icon?: string;
label?: string;
parent?: string;
[key: string]: any;
};
export const nodeData = (node: cytoscape.NodeSingular) => {
return node.data() as NodeSingularData;
};
export interface NodeSingular extends cytoscape.NodeSingular {
_private: {
bodyBounds: {
h: number;
w: number;
x1: number;
x2: number;
y1: number;
y2: number;
};
children: cytoscape.NodeSingular[];
};
data(): NodeSingularData;
data<T extends keyof NodeSingularData>(key: T): NodeSingularData[T];
}

View File

@@ -0,0 +1,366 @@
// TODO remove no-console
/* eslint-disable no-console */
import type { D3Element } from '../../types.js';
import { createText } from '../../rendering-util/createText.js';
import {
ArchitectureDirectionArrow,
type ArchitectureDB,
type ArchitectureService,
ArchitectureDirectionArrowShift,
isArchitectureDirectionX,
isArchitectureDirectionY,
edgeData,
nodeData,
isArchitectureDirectionXY,
getArchitectureDirectionPair,
getArchitectureDirectionXYFactors,
isArchitecturePairXY,
type ArchitectureJunction,
} from './architectureTypes.js';
import type cytoscape from 'cytoscape';
import { getIcon } from '../../rendering-util/svgRegister.js';
import { db, getConfigField } from './architectureDb.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) {
const padding = getConfigField('padding');
const iconSize = getConfigField('iconSize');
const halfIconSize = iconSize / 2;
const arrowSize = iconSize / 6;
const halfArrowSize = arrowSize / 2;
await Promise.all(
cy.edges().map(async (edge) => {
const {
source,
sourceDir,
sourceArrow,
sourceGroup,
target,
targetDir,
targetArrow,
targetGroup,
label,
} = edgeData(edge);
let { x: startX, y: startY } = edge[0].sourceEndpoint();
const { x: midX, y: midY } = edge[0].midpoint();
let { x: endX, y: endY } = edge[0].targetEndpoint();
// Adjust the edge distance if it has the {group} modifier
const groupEdgeShift = padding + 4;
// +18 comes from the service label height that extends the padding on the bottom side of each group
if (sourceGroup) {
if (isArchitectureDirectionX(sourceDir)) {
startX += sourceDir === 'L' ? -groupEdgeShift : groupEdgeShift;
} else {
startY += sourceDir === 'T' ? -groupEdgeShift : groupEdgeShift + 18;
}
}
if (targetGroup) {
if (isArchitectureDirectionX(targetDir)) {
endX += targetDir === 'L' ? -groupEdgeShift : groupEdgeShift;
} else {
endY += targetDir === 'T' ? -groupEdgeShift : groupEdgeShift + 18;
}
}
// Adjust the edge distance if it doesn't have the {group} modifier and the endpoint is a junction node
if (!sourceGroup && db.getNode(source)?.type === 'junction') {
if (isArchitectureDirectionX(sourceDir)) {
startX += sourceDir === 'L' ? halfIconSize : -halfIconSize;
} else {
startY += sourceDir === 'T' ? halfIconSize : -halfIconSize;
}
}
if (!targetGroup && db.getNode(target)?.type === 'junction') {
if (isArchitectureDirectionX(targetDir)) {
endX += targetDir === 'L' ? halfIconSize : -halfIconSize;
} else {
endY += targetDir === 'T' ? halfIconSize : -halfIconSize;
}
}
if (edge[0]._private.rscratch) {
// const bounds = edge[0]._private.rscratch;
const g = edgesEl.insert('g');
g.insert('path')
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
.attr('class', 'edge');
if (sourceArrow) {
const xShift = isArchitectureDirectionX(sourceDir)
? ArchitectureDirectionArrowShift[sourceDir](startX, arrowSize)
: startX - halfArrowSize;
const yShift = isArchitectureDirectionY(sourceDir)
? ArchitectureDirectionArrowShift[sourceDir](startY, arrowSize)
: startY - halfArrowSize;
g.insert('polygon')
.attr('points', ArchitectureDirectionArrow[sourceDir](arrowSize))
.attr('transform', `translate(${xShift},${yShift})`)
.attr('class', 'arrow');
}
if (targetArrow) {
const xShift = isArchitectureDirectionX(targetDir)
? ArchitectureDirectionArrowShift[targetDir](endX, arrowSize)
: endX - halfArrowSize;
const yShift = isArchitectureDirectionY(targetDir)
? ArchitectureDirectionArrowShift[targetDir](endY, arrowSize)
: endY - halfArrowSize;
g.insert('polygon')
.attr('points', ArchitectureDirectionArrow[targetDir](arrowSize))
.attr('transform', `translate(${xShift},${yShift})`)
.attr('class', 'arrow');
}
if (label) {
const axis = !isArchitectureDirectionXY(sourceDir, targetDir)
? isArchitectureDirectionX(sourceDir)
? 'X'
: 'Y'
: 'XY';
let width = 0;
if (axis === 'X') {
width = Math.abs(startX - endX);
} else if (axis === 'Y') {
// Reduce width by a factor of 1.5 to avoid overlapping service labels
width = Math.abs(startY - endY) / 1.5;
} else {
width = Math.abs(startX - endX) / 2;
}
const textElem = g.append('g');
await createText(
textElem,
label,
{
useHtmlLabels: false,
width,
classes: 'architecture-service-label',
},
getConfig()
);
textElem
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle');
if (axis === 'X') {
textElem.attr('transform', 'translate(' + midX + ', ' + midY + ')');
} else if (axis === 'Y') {
textElem.attr('transform', 'translate(' + midX + ', ' + midY + ') rotate(-90)');
} else if (axis === 'XY') {
const pair = getArchitectureDirectionPair(sourceDir, targetDir);
if (pair && isArchitecturePairXY(pair)) {
const bboxOrig = textElem.node().getBoundingClientRect();
const [x, y] = getArchitectureDirectionXYFactors(pair);
textElem
.attr('dominant-baseline', 'auto')
.attr('transform', `rotate(${-1 * x * y * 45})`);
// Calculate the new width/height with the rotation applied, and transform to the proper position
const bboxNew = textElem.node().getBoundingClientRect();
textElem.attr(
'transform',
`
translate(${midX}, ${midY - bboxOrig.height / 2})
translate(${(x * bboxNew.width) / 2}, ${(y * bboxNew.height) / 2})
rotate(${-1 * x * y * 45}, 0, ${bboxOrig.height / 2})
`
);
}
}
}
}
})
);
};
export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) {
const padding = getConfigField('padding');
const groupIconSize = padding * 0.75;
const fontSize = getConfigField('fontSize');
const iconSize = getConfigField('iconSize');
const halfIconSize = iconSize / 2;
await Promise.all(
cy.nodes().map(async (node) => {
const data = nodeData(node);
if (data.type === 'group') {
const { h, w, x1, y1 } = node.boundingBox();
console.log(`Draw group (${data.id}): pos=(${x1}, ${y1}), dim=(${w}, ${h})`);
groupsEl
.append('rect')
.attr('x', x1 + halfIconSize)
.attr('y', y1 + halfIconSize)
.attr('width', w)
.attr('height', h)
.attr('class', 'node-bkg');
const groupLabelContainer = groupsEl.append('g');
let shiftedX1 = x1;
let shiftedY1 = y1;
if (data.icon) {
const bkgElem = groupLabelContainer.append('g');
getIcon(data.icon)?.(bkgElem, groupIconSize);
bkgElem.attr(
'transform',
'translate(' +
(shiftedX1 + halfIconSize + 1) +
', ' +
(shiftedY1 + halfIconSize + 1) +
')'
);
shiftedX1 += groupIconSize;
// TODO: test with more values
// - 1 - 2 comes from the Y axis transform of the icon and label
shiftedY1 += fontSize / 2 - 1 - 2;
}
if (data.label) {
const textElem = groupLabelContainer.append('g');
await createText(
textElem,
data.label,
{
useHtmlLabels: false,
width: w,
classes: 'architecture-service-label',
},
getConfig()
);
textElem
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'start')
.attr('text-anchor', 'start');
textElem.attr(
'transform',
'translate(' +
(shiftedX1 + halfIconSize + 4) +
', ' +
(shiftedY1 + halfIconSize + 2) +
')'
);
}
}
})
);
};
export const drawServices = async function (
db: ArchitectureDB,
elem: D3Element,
services: ArchitectureService[]
): Promise<number> {
for (const service of services) {
const serviceElem = elem.append('g');
const iconSize = getConfigField('iconSize');
if (service.title) {
const textElem = serviceElem.append('g');
await createText(
textElem,
service.title,
{
useHtmlLabels: false,
width: iconSize * 1.5,
classes: 'architecture-service-label',
},
getConfig()
);
textElem
.attr('dy', '1em')
.attr('alignment-baseline', 'middle')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle');
textElem.attr('transform', 'translate(' + iconSize / 2 + ', ' + iconSize + ')');
}
let bkgElem = serviceElem.append('g');
if (service.icon) {
// TODO: should a warning be given to end-users saying which icon names are available?
// if (!isIconNameInUse(service.icon)) {
// throw new Error(`Invalid SVG Icon name: "${service.icon}"`);
// }
bkgElem = getIcon(service.icon)?.(bkgElem, iconSize);
} else if (service.iconText) {
bkgElem = getIcon('blank')?.(bkgElem, iconSize);
const textElemContainer = bkgElem.append('g');
const fo = textElemContainer
.append('foreignObject')
.attr('width', iconSize)
.attr('height', iconSize);
const divElem = fo
.append('div')
.attr('class', 'node-icon-text')
.attr('style', `height: ${iconSize}px;`)
.append('div')
.html(service.iconText);
const fontSize =
parseInt(
window
.getComputedStyle(divElem.node(), null)
.getPropertyValue('font-size')
.replace(/\D/g, '')
) ?? 16;
divElem.attr('style', `-webkit-line-clamp: ${Math.floor((iconSize - 2) / fontSize)};`);
} else {
bkgElem
.append('path')
.attr('class', 'node-bkg')
.attr('id', 'node-' + service.id)
.attr(
'd',
`M0 ${iconSize} v${-iconSize} q0,-5 5,-5 h${iconSize} q5,0 5,5 v${iconSize} H0 Z`
);
}
serviceElem.attr('class', 'architecture-service');
const { width, height } = serviceElem._groups[0][0].getBBox();
service.width = width;
service.height = height;
db.setElementForId(service.id, serviceElem);
}
return 0;
};
export const drawJunctions = function (
db: ArchitectureDB,
elem: D3Element,
junctions: ArchitectureJunction[]
) {
junctions.forEach((junction) => {
const junctionElem = elem.append('g');
const iconSize = getConfigField('iconSize');
const bkgElem = junctionElem.append('g');
bkgElem
.append('rect')
.attr('id', 'node-' + junction.id)
.attr('fill-opacity', '0')
.attr('width', iconSize)
.attr('height', iconSize);
junctionElem.attr('class', 'architecture-junction');
const { width, height } = junctionElem._groups[0][0].getBBox();
junctionElem.width = width;
junctionElem.height = height;
db.setElementForId(junction.id, junctionElem);
});
};

View File

@@ -7,13 +7,14 @@ import type {
const id = 'flowchart-v2';
const detector: DiagramDetector = (txt, config) => {
if (
config?.flowchart?.defaultRenderer === 'dagre-d3' ||
config?.flowchart?.defaultRenderer === 'elk'
) {
if (config?.flowchart?.defaultRenderer === 'dagre-d3') {
return false;
}
if (config?.flowchart?.defaultRenderer === 'elk') {
config.layout = 'elk';
}
// If we have configured to use dagre-wrapper then we should return true in this function for graph code thus making it use the new flowchart diagram
if (/^\s*graph/.test(txt) && config?.flowchart?.defaultRenderer === 'dagre-wrapper') {
return true;

View File

@@ -157,6 +157,7 @@ function sidebarSyntax() {
{ text: 'XY Chart 🔥', link: '/syntax/xyChart' },
{ text: 'Block Diagram 🔥', link: '/syntax/block' },
{ text: 'Packet 🔥', link: '/syntax/packet' },
{ text: 'Architecture 🔥', link: '/syntax/architecture' },
{ text: 'Other Examples', link: '/syntax/examples' },
],
},
@@ -176,6 +177,7 @@ function sidebarConfig() {
{ text: 'Directives', link: '/config/directives' },
{ text: 'Theming', link: '/config/theming' },
{ text: 'Math', link: '/config/math' },
{ text: 'Icons', link: '/config/icons' },
{ text: 'Accessibility', link: '/config/accessibility' },
{ text: 'Mermaid CLI', link: '/config/mermaidCLI' },
{ text: 'FAQ', link: '/config/faq' },

View File

@@ -0,0 +1,61 @@
# SVG Icons (v???+)
SVG Icons can be used with supported diagrams. Alongside the icon packs included with Mermaid, 3rd party libraries can be included in the configuration to cover additional use-cases.
## Supported Diagrams
| Diagram | Usage |
| ------------ | --------------------------------- |
| Architecture | Icon names are surrounded by `()` |
## Included Icon Packs
| Icon Pack | Prefix |
| ------------- | ------ |
| default | N/A |
| Amazon AWS | `aws:` |
| Digital Ocean | `do:` |
| GitHub | `gh:` |
Note that in order to use non-generic icons that are provided with Mermaid, the packs must be explicitly loaded when on initialization initialized.
```js
import sampleIconPack from 'sample-icon-pack';
mermaid.initialize({
iconLibraries: ['aws:common', 'aws:full', 'github', 'digital-ocean'],
});
```
## Using Custom Icon Packs
Custom icon packs can be used by including them in the `iconLibraries` array on mermaid initialization.
```js
import sampleIconPack from 'sample-icon-pack';
mermaid.initialize({
iconLibraries: [sampleIconPack, 'aws:full', ...],
});
```
## Creating Custom Icon Packs
```js
import { createIcon } from 'mermaid';
import type { IconLibrary, IconResolver } from 'mermaid';
// type IconLibrary = Record<string, IconResolver>;
// createIcon: (icon: string, originalSize: number) => IconResolver
const myIconLibrary: IconLibrary = {
defaultCloudExample: createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<path d="..." style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
</g>`,
80
)
};
export default myIconLibrary
```

View File

@@ -50,6 +50,10 @@ For a more detailed introduction to Mermaid and some of its more basic uses, loo
**Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project 🙏**
Our PR Visual Regression Testing is powered by [Argos](https://argos-ci.com/?utm_source=mermaid&utm_campaign=oss) with their generous Open Source plan. It makes the process of reviewing PRs with visual changes a breeze.
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=mermaid&utm_campaign=oss)
In our release process we rely heavily on visual regression tests using [applitools](https://applitools.com/). Applitools is a great service which has been easy to use and integrate with our tests.
<a href="https://applitools.com/">

View File

@@ -0,0 +1,156 @@
# Architecture Diagrams Documentation (v<MERMAID_RELEASE_VERSION>+)
> In the context of mermaid-js, the architecture diagram is used to show the relationship between services and resources commonly found within the Cloud or CI/CD deployments. In an architecture diagram, services (nodes) are connected by edges. Related services can be placed within groups to better illustrate how they are organized.
## Example
```mermaid-example
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
```
## Syntax
The building blocks of an architecture are `groups`, `services`, `edges`, and `junctions`.
For supporting components, icons are declared by surrounding the icon name with `()`, while labels are declared by surrounding the text with `[]`.
To begin an architecture diagram, use the keyword `architecture-beta`, followed by your groups, services, edges, and junctions. While each of the 3 building blocks can be declared in any order, care must be taken to ensure the identifier was previously declared by another component.
### Groups
The syntax for declaring a group is:
```
group {group id}({icon name})[{title}] (in {parent id})?
```
Put together:
```
group public_api(cloud)[Public API]
```
creates a group identified as `public_api`, uses the icon `cloud`, and has the label `Public API`.
Additionally, groups can be placed within a group using the optional `in` keyword
```
group private_api(cloud)[Private API] in public_api
```
### Services
The syntax for declaring a service is:
```
service {service id}({icon name})[{title}] (in {parent id})?
```
Put together:
```
service database(db)[Database]
```
creates the service identified as `database`, using the icon `db`, with the label `Database`.
If the service belongs to a group, it can be placed inside it through the optional `in` keyword
```
service database(db)[Database] in private_api
```
### Edges
The syntax for declaring an edge is:
```
{serviceId}{{group}}?:{T|B|L|R} {<}?--{>}? {T|B|L|R}:{serviceId}{{group}}?
```
#### Edge Direction
The side of the service the edge comes out of is specified by adding a colon (`:`) to the side of the service connecting to the arrow and adding `L|R|T|B`
For example:
```
db:R -- L:server
```
creates an edge between the services `db` and `server`, with the edge coming out of the right of `db` and the left of `server`.
```
db:T -- L:server
```
creates a 90 degree edge between the services `db` and `server`, with the edge coming out of the top of `db` and the left of `server`.
#### Arrows
Arrows can be added to each side of an edge by adding `<` before the direction on the left, and/or `>` after the direction on the right.
For example:
```
subnet:R --> L:gateway
```
creates an edge with the arrow going into the `gateway` service
#### Edges out of Groups
To have an edge go from a group to another group or service within another group, the `{group}` modifier can be added after the `serviceId`.
For example:
```
service server[Server] in groupOne
service subnet[Subnet] in groupTwo
server{group}:B --> T:subnet{group}
```
creates an edge going out of `groupOne`, adjacent to `server`, and into `groupTwo`, adjacent to `subnet`.
It's important to note that `groupId`s cannot be used for specifying edges and the `{group}` modifier can only be used for services within a group.
### Junctions
Junctions are a special type of node which acts as a potential 4-way split between edges.
The syntax for declaring a junction is:
```
junction {junction id} (in {parent id})?
```
```mermaid-example
architecture-beta
service left_disk(disk)[Disk]
service top_disk(disk)[Disk]
service bottom_disk(disk)[Disk]
service top_gateway(internet)[Gateway]
service bottom_gateway(internet)[Gateway]
junction junctionCenter
junction junctionRight
left_disk:R -- L:junctionCenter
top_disk:B -- T:junctionCenter
bottom_disk:T -- B:junctionCenter
junctionCenter:R -- L:junctionRight
top_gateway:B -- T:junctionRight
bottom_gateway:T -- B:junctionRight
```
## Configuration

View File

@@ -19,6 +19,8 @@ import type { LayoutData } from './rendering-util/types.js';
import type { ParseOptions, ParseResult, RenderResult } from './types.js';
import type { DetailedError } from './utils.js';
import utils, { isDetailedError } from './utils.js';
import type { IconLibrary, IconResolver } from './rendering-util/svgRegister.js';
import { createIcon } from './rendering-util/svgRegister.js';
export type {
DetailedError,
@@ -35,8 +37,12 @@ export type {
SVG,
SVGGroup,
UnknownDiagramError,
IconLibrary,
IconResolver,
};
export { createIcon };
export interface RunOptions {
/**
* The query selector to use when finding elements to render. Default: `".mermaid"`.

View File

@@ -23,6 +23,9 @@ import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.
import type { DiagramMetadata, DiagramStyleClassDef } from './diagram-api/types.js';
import { preprocessDiagram } from './preprocess.js';
import { decodeEntities } from './utils.js';
import type { IconLibrary } from './rendering-util/svgRegister.js';
import { registerIcons } from './rendering-util/svgRegister.js';
import defaultIconLibrary from './rendering-util/svg/index.js';
import { toBase64 } from './utils/base64.js';
import type { D3Element, ParseOptions, ParseResult, RenderResult } from './types.js';
import assignWithDepth from './assignWithDepth.js';
@@ -477,7 +480,7 @@ const render = async function (
* @param userOptions - Initial Mermaid options
*/
function initialize(userOptions: MermaidConfig = {}) {
const options = assignWithDepth({}, userOptions);
const options: MermaidConfig = assignWithDepth({}, userOptions);
// Handle legacy location of font-family configuration
if (options?.fontFamily && !options.themeVariables?.fontFamily) {
if (!options.themeVariables) {
@@ -489,6 +492,29 @@ function initialize(userOptions: MermaidConfig = {}) {
// Set default options
configApi.saveConfigFromInitialize(options);
registerIcons(defaultIconLibrary);
if (options?.iconLibraries) {
// TODO: find a better way to handle this, assumed to be resolved by the time diagrams are being generated
// eslint-disable-next-line @typescript-eslint/no-misused-promises
options.iconLibraries.forEach(async (library) => {
if (typeof library === 'string') {
let lib: IconLibrary = {};
if (library === 'aws:common') {
lib = (await import('./rendering-util/svg/aws/awsCommon.js')).default;
} else if (library === 'aws:full') {
lib = (await import('./rendering-util/svg/aws/awsFull.js')).default;
} else if (library === 'digital-ocean') {
lib = (await import('./rendering-util/svg/digital-ocean/digitalOcean.js')).default;
} else if (library === 'github') {
lib = (await import('./rendering-util/svg/github/github.js')).default;
}
registerIcons(lib);
} else {
registerIcons(library);
}
});
}
if (options?.theme && options.theme in theme) {
// Todo merge with user options
options.themeVariables = theme[options.theme as keyof typeof theme].getThemeVariables(

View File

@@ -111,7 +111,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
if (graph.children(v).length > 0) {
// This is a cluster but not to be rendered recursively
// Render as before
log.info(
log.trace(
'Cluster - the non recursive path XBX',
v,
node.id,
@@ -120,11 +120,11 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
'Graph:',
graph
);
log.info(findNonClusterChild(node.id, graph));
log.trace(findNonClusterChild(node.id, graph));
clusterDb.set(node.id, { id: findNonClusterChild(node.id, graph), node });
// insertCluster(clusters, graph.node(v));
} else {
log.warn('Node - the non recursive path XAX', v, nodes, graph.node(v), dir);
log.trace('Node - the non recursive path XAX', v, nodes, graph.node(v), dir);
await insertNode(nodes, graph.node(v), dir);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
/**
* Designer: Nicolas Newman
*/
import { createIcon } from '../../svgRegister.js';
export default createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
</g>`,
80
);

View File

@@ -0,0 +1,13 @@
/**
* Designer: Nicolas Newman
* @see https://github.com/NicolasNewman/IconLibrary
*/
import { createIcon } from '../../svgRegister.js';
export default createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<path d="m65,47.5c0,2.76-2.24,5-5,5H20c-2.76,0-5-2.24-5-5,0-1.87,1.03-3.51,2.56-4.36-.04-.21-.06-.42-.06-.64,0-2.6,2.48-4.74,5.65-4.97,1.65-4.51,6.34-7.76,11.85-7.76.86,0,1.69.08,2.5.23,2.09-1.57,4.69-2.5,7.5-2.5,6.1,0,11.19,4.38,12.28,10.17,2.14.56,3.72,2.51,3.72,4.83,0,.03,0,.07-.01.1,2.29.46,4.01,2.48,4.01,4.9Z" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
</g>`,
80
);

View File

@@ -0,0 +1,18 @@
/**
* Designer: Nicolas Newman
* @see https://github.com/NicolasNewman/IconLibrary
*/
import { createIcon } from '../../svgRegister.js';
export default createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<path id="b" data-name="4" d="m20,57.86c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<path id="c" data-name="3" d="m20,45.95c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<path id="d" data-name="2" d="m20,34.05c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<ellipse id="e" data-name="1" cx="40" cy="22.14" rx="20" ry="7.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="20" y1="57.86" x2="20" y2="22.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="60" y1="57.86" x2="60" y2="22.14" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
</g>`,
80
);

View File

@@ -0,0 +1,20 @@
/**
* Designer: Nicolas Newman
* @see https://github.com/NicolasNewman/IconLibrary
*/
import { createIcon } from '../../svgRegister.js';
export default createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<rect x="20" y="15" width="40" height="50" rx="1" ry="1" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<ellipse cx="24" cy="19.17" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<ellipse cx="56" cy="19.17" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<ellipse cx="24" cy="60.83" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<ellipse cx="56" cy="60.83" rx=".8" ry=".83" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<ellipse cx="40" cy="33.75" rx="14" ry="14.58" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<ellipse cx="40" cy="33.75" rx="4" ry="4.17" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<path d="m37.51,42.52l-4.83,13.22c-.26.71-1.1,1.02-1.76.64l-4.18-2.42c-.66-.38-.81-1.26-.33-1.84l9.01-10.8c.88-1.05,2.56-.08,2.09,1.2Z" style="fill: #fff; stroke-width: 0px;"/>
</g>`,
80
);

View File

@@ -0,0 +1,19 @@
/**
* Designer: Nicolas Newman
* @see https://github.com/NicolasNewman/IconLibrary
*/
import { createIcon } from '../../svgRegister.js';
export default createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<circle cx="40" cy="40" r="22.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="40" y1="17.5" x2="40" y2="62.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="17.5" y1="40" x2="62.5" y2="40" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<path d="m39.99,17.51c-15.28,11.1-15.28,33.88,0,44.98" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<path d="m40.01,17.51c15.28,11.1,15.28,33.88,0,44.98" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="19.75" y1="30.1" x2="60.25" y2="30.1" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="19.75" y1="49.9" x2="60.25" y2="49.9" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
</g>`,
80
);

View File

@@ -0,0 +1,42 @@
/**
* Designer: Nicolas Newman
* @see https://github.com/NicolasNewman/IconLibrary
*/
import { createIcon } from '../../svgRegister.js';
export default createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<rect x="17.5" y="17.5" width="45" height="45" rx="2" ry="2" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="17.5" y1="32.5" x2="62.5" y2="32.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<line x1="17.5" y1="47.5" x2="62.5" y2="47.5" style="fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;"/>
<g>
<path d="m56.25,25c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: #fff; stroke-width: 0px;"/>
<path d="m56.25,25c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: none; stroke: #fff; stroke-miterlimit: 10;"/>
</g>
<g>
<path d="m56.25,40c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: #fff; stroke-width: 0px;"/>
<path d="m56.25,40c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: none; stroke: #fff; stroke-miterlimit: 10;"/>
</g>
<g>
<path d="m56.25,55c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: #fff; stroke-width: 0px;"/>
<path d="m56.25,55c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z" style="fill: none; stroke: #fff; stroke-miterlimit: 10;"/>
</g>
<g>
<circle cx="32.5" cy="25" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
<circle cx="27.5" cy="25" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
<circle cx="22.5" cy="25" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
</g>
<g>
<circle cx="32.5" cy="40" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
<circle cx="27.5" cy="40" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
<circle cx="22.5" cy="40" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
</g>
<g>
<circle cx="32.5" cy="55" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
<circle cx="27.5" cy="55" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
<circle cx="22.5" cy="55" r=".75" style="fill: #fff; stroke: #fff; stroke-miterlimit: 10;"/>
</g>
</g>`,
80
);

View File

@@ -0,0 +1,13 @@
/**
* Designer: Nicolas Newman
* @see https://github.com/NicolasNewman/IconLibrary
*/
import { createIcon } from '../../svgRegister.js';
export default createIcon(
`<g>
<rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/>
<text transform="translate(21.16 64.67)" style="fill: #fff; font-family: ArialMT, Arial; font-size: 67.75px;"><tspan x="0" y="0">?</tspan></text>
</g>`,
80
);

View File

@@ -0,0 +1,321 @@
import { createIcon } from '../../svgRegister.js';
// cSpell:disable
const digitalOceanIcons = {
'do:api': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}.cls-4{font-size:16px;fill:#fff;font-family:ProximaNova-Extrabld,
Proxima
Nova;}.cls-5{letter-spacing:0em;}.cls-6{letter-spacing:-0.01em;}.cls-7{letter-spacing:0em;}.cls-8{letter-spacing:0em;}.cls-9{letter-spacing:0em;}.cls-10{letter-spacing:0em;}.cls-11{letter-spacing:0em;}.cls-12{letter-spacing:0em;}.cls-13{letter-spacing:0em;}.cls-14{letter-spacing:-0.01em;}.cls-15{letter-spacing:-0.02em;}.cls-16{letter-spacing:0em;}.cls-17{letter-spacing:0em;}.cls-18{letter-spacing:0em;}.cls-19{letter-spacing:0em;}.cls-20{letter-spacing:-0.08em;}.cls-21{letter-spacing:0em;}.cls-22{letter-spacing:0em;}.cls-23{letter-spacing:0em;}.cls-24{letter-spacing:0em;}.cls-25{letter-spacing:0em;}.cls-26{letter-spacing:0em;}.cls-27{letter-spacing:0em;}.cls-28{letter-spacing:-0.01em;}.cls-29{letter-spacing:0em;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3"
d="M32.8,22.44V19.65l-3.09-1a9.47,9.47,0,0,0-.89-2.1l1.51-2.9-2-2-2.88,1.45h0a9.45,9.45,0,0,0-2-.88h0l-1-3.07H19.59l-1,3.11a9.39,9.39,0,0,0-2.09.89h0l-2.89-1.5-2,2,1.5,2.89h0a8.91,8.91,0,0,0-.88,2L9.2,19.65v2.83l3.1,1h0a9.07,9.07,0,0,0,.82,2h0l-1.46,2.87,2,2,2.9-1.51a9,9,0,0,0,2,.89l1,3.11H22.4l1-3.08h0a8.65,8.65,0,0,0,2-.82h0l2.89,1.51,2-2.06L28.8,25.49a9.48,9.48,0,0,0,.88-2.07Zm-11.8,2A3.45,3.45,0,1,1,24.44,21v0A3.44,3.44,0,0,1,21,24.42Z" />
</g>
</g>`,
41.61
),
'do:cli': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<rect class="cls-3" x="9.89" y="14.16" width="22.22" height="13.68" />
<polyline class="cls-3" points="14.34 24.5 17.84 21 14.34 17.5" />
<line class="cls-3" x1="20.13" y1="23.89" x2="27.53" y2="23.89" />
</g>
</g>`,
41.61
),
'do:dns': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<circle class="cls-3" cx="21" cy="21" r="11.87" />
<line class="cls-3" x1="21" y1="9.13" x2="21" y2="32.87" />
<line class="cls-3" x1="9.81" y1="17.04" x2="32.19" y2="17.04" />
<line class="cls-3" x1="9.81" y1="24.96" x2="32.19" y2="24.96" />
<path class="cls-3" d="M21,32.87c-3.27,0-5.93-8.6-5.93-11.87S17.73,9.13,21,9.13" />
<path class="cls-3" d="M21.09,9.13C24.36,9.13,27,17.73,27,21s-2.66,11.87-5.93,11.87" />
</g>
</g>`,
41.61
),
'do:droplet': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3"
d="M21,33.06A10.2,10.2,0,0,1,10.83,23C10.83,14.61,21,8.94,21,8.94s10.17,5.87,10.17,14.15A10.14,10.14,0,0,1,21,33.06Z" />
<path class="cls-3"
d="M21,26.28a3.37,3.37,0,0,1-3.39-3.39c0-2.61,3.39-5.09,3.39-5.09s3.39,2.42,3.39,5.09A3.37,3.37,0,0,1,21,26.28Z" />
</g>
</g>`,
41.61
),
'do:kubernetes': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3"
d="M31.15,21.18A10.19,10.19,0,1,1,21,11,10.19,10.19,0,0,1,31.15,21.18Zm-6.51,0A3.64,3.64,0,1,1,21,17.56,3.64,3.64,0,0,1,24.64,21.2ZM21,17.47v0ZM10.1,12.33,18,18.79Zm21.79,0L24,18.79ZM34.72,24.2,24.75,22Zm-27.44,0,10-2.18Zm19.93,9.62-4.53-9.43Zm-12.41,0,4.45-9.43Z" />
</g>
</g>`,
41.61
),
'do:local-ssd': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3"
d="M21,33.06A10.2,10.2,0,0,1,10.83,23C10.83,14.61,21,8.94,21,8.94s10.17,5.87,10.17,14.15A10.14,10.14,0,0,1,21,33.06Z" />
<path class="cls-3"
d="M21,26.28a3.37,3.37,0,0,1-3.39-3.39c0-2.61,3.39-5.09,3.39-5.09s3.39,2.42,3.39,5.09A3.37,3.37,0,0,1,21,26.28Z" />
</g>
</g>`,
41.61
),
'do:mysql': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-4{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3,.cls-5{fill:#fff;}.cls-4{stroke:#fff;stroke-width:1.49px;}.cls-5{font-size:16px;font-family:ProximaNova-Extrabld,
Proxima
Nova;}.cls-6{letter-spacing:0em;}.cls-7{letter-spacing:-0.01em;}.cls-8{letter-spacing:0em;}.cls-9{letter-spacing:0em;}.cls-10{letter-spacing:0em;}.cls-11{letter-spacing:0em;}.cls-12{letter-spacing:0em;}.cls-13{letter-spacing:0em;}.cls-14{letter-spacing:0em;}.cls-15{letter-spacing:-0.01em;}.cls-16{letter-spacing:-0.02em;}.cls-17{letter-spacing:0em;}.cls-18{letter-spacing:0em;}.cls-19{letter-spacing:0em;}.cls-20{letter-spacing:0em;}.cls-21{letter-spacing:-0.08em;}.cls-22{letter-spacing:0em;}.cls-23{letter-spacing:0em;}.cls-24{letter-spacing:0em;}.cls-25{letter-spacing:0em;}.cls-26{letter-spacing:0em;}.cls-27{letter-spacing:0em;}.cls-28{letter-spacing:0em;}.cls-29{letter-spacing:-0.01em;}.cls-30{letter-spacing:0em;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3"
d="M22.15,29V21.86a.63.63,0,0,1,.59-.58.86.86,0,0,1,.84.33c.2.36.43.72.64,1.08.37.62.84,1.36,1.21,2,.37-.6.85-1.34,1.21-2,.2-.36.43-.72.64-1.08a.86.86,0,0,1,.84-.33.65.65,0,0,1,.61.58V29a.69.69,0,0,1-.77.59A.68.68,0,0,1,27.2,29v-4.5c-.28.44-.55.88-.75,1.22l-.35.56a.81.81,0,0,1-.69.35.76.76,0,0,1-.7-.35l-.35-.56c-.2-.34-.48-.78-.73-1.22V29a.71.71,0,0,1-.77.59A.69.69,0,0,1,22.15,29Z" />
<path class="cls-4" d="M9.65,20.87v4.34c0,1.43,4,2.59,8.89,2.59" />
<path class="cls-4" d="M27.41,19V16.57" />
<path class="cls-4" d="M9.65,16.57v4.34c0,1.43,4,2.58,8.89,2.58H19" />
<path class="cls-4"
d="M27.41,12.24c0-1.42-4-2.58-8.87-2.58s-8.89,1.16-8.89,2.58v4.33c0,1.43,4,2.59,8.89,2.59s8.87-1.16,8.87-2.59Z" />
<ellipse class="cls-4" cx="18.54" cy="12.24" rx="8.88" ry="2.58" />
<path class="cls-4"
d="M27.41,12.24c0-1.42-4-2.58-8.87-2.58s-8.89,1.16-8.89,2.58v4.33c0,1.43,4,2.59,8.89,2.59s8.87-1.16,8.87-2.59Z" />
<path class="cls-4"
d="M13.89,14.44c-2.54-.46-4.23-1.27-4.23-2.2,0-1.43,4-2.58,8.88-2.58s8.87,1.15,8.87,2.58-4,2.58-8.87,2.58a27,27,0,0,1-4.65-.38" />
<circle class="cls-4" cx="25.44" cy="25.43" r="6.91" />
</g>
</g>`,
41.61
),
'do:redis-copy': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}.cls-4{fill:#fff;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3" d="M11.59,23.14v4.34c0,1.43,4,2.59,8.89,2.59" />
<path class="cls-3" d="M29.35,23.14V18.85" />
<path class="cls-3" d="M11.59,23.14V18.85" />
<path class="cls-3"
d="M29.35,14.51c0-1.42-4-2.58-8.87-2.58s-8.89,1.16-8.89,2.58v4.34c0,1.43,4,2.59,8.89,2.59s8.87-1.16,8.87-2.59Z" />
<ellipse class="cls-3" cx="20.48" cy="14.51" rx="8.88" ry="2.58" />
<path class="cls-3"
d="M29.35,14.51c0-1.42-4-2.58-8.87-2.58s-8.89,1.16-8.89,2.58v4.34c0,1.43,4,2.59,8.89,2.59s8.87-1.16,8.87-2.59Z" />
<path class="cls-3"
d="M15.83,16.71c-2.54-.45-4.23-1.27-4.23-2.2,0-1.43,4-2.58,8.88-2.58s8.87,1.15,8.87,2.58-4,2.58-8.87,2.58a27,27,0,0,1-4.65-.38" />
<path class="cls-3" d="M11.59,23.14v4.34c0,1.43,4,2.59,8.89,2.59s8.87-1.16,8.87-2.59V23.14" />
<path class="cls-3" d="M29.35,23.14c0,1.43-4,2.58-8.87,2.58a26,26,0,0,1-4.65-.38" />
<path class="cls-3" d="M15.83,25.34c-2.54-.45-4.23-1.27-4.23-2.2" />
<path class="cls-3" d="M29.35,27.49c0,1.43-4,2.58-8.87,2.58a27,27,0,0,1-4.65-.38" />
<path class="cls-3" d="M15.83,29.69c-2.54-.45-4.23-1.27-4.23-2.2" />
<path class="cls-4" d="M30.41,26.2v0Z" />
</g>
</g>`,
41.61
),
'do:redis': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}.cls-4{fill:#fff;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3" d="M9.65,20.87v4.34c0,1.43,4,2.59,8.89,2.59" />
<path class="cls-3" d="M27.41,19V16.57" />
<path class="cls-3" d="M9.65,16.57v4.34c0,1.43,4,2.58,8.89,2.58H19" />
<path class="cls-3"
d="M27.41,12.24c0-1.42-4-2.58-8.87-2.58s-8.89,1.16-8.89,2.58v4.33c0,1.43,4,2.59,8.89,2.59s8.87-1.16,8.87-2.59Z" />
<ellipse class="cls-3" cx="18.54" cy="12.24" rx="8.88" ry="2.58" />
<path class="cls-3"
d="M27.41,12.24c0-1.42-4-2.58-8.87-2.58s-8.89,1.16-8.89,2.58v4.33c0,1.43,4,2.59,8.89,2.59s8.87-1.16,8.87-2.59Z" />
<path class="cls-3"
d="M13.89,14.44c-2.54-.46-4.23-1.27-4.23-2.2,0-1.43,4-2.58,8.88-2.58s8.87,1.15,8.87,2.58-4,2.58-8.87,2.58a27,27,0,0,1-4.65-.38" />
<circle class="cls-3" cx="25.44" cy="25.43" r="6.91" />
<path class="cls-4" d="M28.47,23.93v0Z" />
<path class="cls-4"
d="M28.47,24a2.68,2.68,0,0,0-2.68-2.65H23.88a.75.75,0,0,0-.57.21.77.77,0,0,0-.23.57v6.73a.77.77,0,0,0,1.53,0V26.62h.53l1.5,2.58a.76.76,0,0,0,.66.37.75.75,0,0,0,.38-.1.76.76,0,0,0,.27-1l-1.17-2a2.78,2.78,0,0,0,.9-.58A2.72,2.72,0,0,0,28.47,24Zm-3.86-1.12h1.14a1.18,1.18,0,0,1,.82.33,1.11,1.11,0,0,1,0,1.58,1.14,1.14,0,0,1-.82.33H24.61Z" />
</g>
</g>`,
41.61
),
'do:monitoring': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3" d="M29.6,14.92v12.7a1.5,1.5,0,0,1-1.51,1.51H14.79" />
<path class="cls-3" d="M12.4,26.74V14.38a1.51,1.51,0,0,1,1.51-1.51H27.47" />
<line class="cls-3" x1="29.57" y1="21" x2="12.36" y2="21" />
<path class="cls-3"
d="M13.5,27l5-9.82a.33.33,0,0,1,.45-.14.32.32,0,0,1,.14.14l3.74,7.61a.33.33,0,0,0,.45.13.29.29,0,0,0,.14-.13l5.09-9.88" />
<circle class="cls-3" cx="12.4" cy="29.13" r="2.39" />
<circle class="cls-3" cx="29.6" cy="12.87" r="2.39" />
</g>
</g>`,
41.61
),
'do:projects': createIcon(
`<g>
<defs>
<style>
.cls-1{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{fill:#fff;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3"
d="M28.45,12.75H25.76A2.79,2.79,0,0,0,23,10.41h0a2.26,2.26,0,0,0,.27-1h0a2.27,2.27,0,0,0-4.53,0,2.39,2.39,0,0,0,.27,1h0a2.79,2.79,0,0,0-2.75,2.34h-2.7A1.68,1.68,0,0,0,12,14.52V31a1.68,1.68,0,0,0,1.6,1.74H28.45A1.68,1.68,0,0,0,30,31V14.5A1.68,1.68,0,0,0,28.45,12.75ZM20,9.37a1,1,0,0,1,2.09,0h0a1,1,0,0,1-1,1H21A1,1,0,0,1,20,9.37Zm-1,2.26h4a1.57,1.57,0,0,1,1.51,1.12h-7A1.57,1.57,0,0,1,19,11.63ZM28.82,31c0,.29-.17.53-.37.53H13.56c-.2,0-.38-.24-.38-.53V14.5c0-.28.18-.53.38-.53H28.45c.2,0,.37.25.37.53Z" />
<rect class="cls-3" x="19.02" y="18.88" width="6.77" height="1.22" />
<rect class="cls-3" x="19.02" y="22.18" width="6.77" height="1.22" />
<rect class="cls-3" x="19.02" y="25.49" width="6.77" height="1.22" />
<rect class="cls-3" x="16.21" y="18.88" width="1.18" height="1.22" />
<rect class="cls-3" x="16.21" y="22.18" width="1.18" height="1.22" />
<rect class="cls-3" x="16.21" y="25.49" width="1.18" height="1.22" />
</g>
</g>`,
41.61
),
'do:spaces-cdn': createIcon(
`<g>
<defs>
<style>
.cls-1{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{fill:#fff;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3"
d="M12.7,13.49a1.22,1.22,0,0,0,.66-.2,1,1,0,0,0,.33-.68,1.22,1.22,0,0,0-.19-.69.88.88,0,0,0-.69-.33h0a1,1,0,0,0-1,.86,1.16,1.16,0,0,0,.21.68A.91.91,0,0,0,12.7,13.49Zm-2.6,3.89a1,1,0,0,0,1-.87,1.26,1.26,0,0,0-.2-.68.85.85,0,0,0-.69-.34h-.06a1,1,0,0,0-.95.87,1.12,1.12,0,0,0,.21.68,1.09,1.09,0,0,0,.67.34ZM10.17,21a1.26,1.26,0,0,0-.2-.68A.9.9,0,0,0,9.28,20H9.22a1,1,0,0,0-.62.21.87.87,0,0,0-.33.69,1.08,1.08,0,0,0,.2.67.84.84,0,0,0,.68.35.87.87,0,0,0,1-.7.61.61,0,0,0,0-.19Zm0,3.61a1.07,1.07,0,0,0-.69.21.88.88,0,0,0-.34.69,1.16,1.16,0,0,0,.21.67.87.87,0,0,0,.68.35,1.15,1.15,0,0,0,.72-.21.84.84,0,0,0,.34-.68.87.87,0,0,0-.69-1Zm2.67,3.9a1,1,0,0,0-1,.86,1.2,1.2,0,0,0,.21.69.89.89,0,0,0,.68.34,1.21,1.21,0,0,0,.66-.19.85.85,0,0,0,.33-.68,1.22,1.22,0,0,0-.19-.69A1.13,1.13,0,0,0,12.84,28.51ZM16.72,31a1,1,0,0,0-.68.18.86.86,0,0,0-.34.68,1.12,1.12,0,0,0,.21.68.83.83,0,0,0,.68.34,1.11,1.11,0,0,0,.68-.2.86.86,0,0,0,.34-.68,1.12,1.12,0,0,0-.2-.69A1,1,0,0,0,16.72,31Z" />
<circle class="cls-3" cx="25.39" cy="26.73" r="0.95" />
<circle class="cls-3" cx="27.92" cy="23.18" r="0.95" />
<circle class="cls-3" cx="27.99" cy="18.88" r="0.95" />
<circle class="cls-3" cx="25.32" cy="15.2" r="0.95" />
<path class="cls-3"
d="M16.66,10.9a1,1,0,0,0,1-.87.94.94,0,0,0-.86-1h-.1a.92.92,0,0,0-.61.2.88.88,0,0,0-.34.69,1.08,1.08,0,0,0,.19.65A.84.84,0,0,0,16.66,10.9ZM21,28.84V27.2a6.27,6.27,0,0,1,0-12.54V13a7.92,7.92,0,0,0,0,15.83Zm.42-20.53V10a11.05,11.05,0,0,1,0,22.1v1.64a12.69,12.69,0,0,0,0-25.38Z" />
</g>
</g>`,
41.61
),
'do:spaces-object-storage': createIcon(
`<g>
<defs>
<style>
.cls-1{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-2{font-size:16px;font-family:ProximaNova-Extrabld,
Proxima
Nova;}.cls-2,.cls-20{fill:#fff;}.cls-3{letter-spacing:0em;}.cls-4{letter-spacing:0em;}.cls-5{letter-spacing:0em;}.cls-6{letter-spacing:0em;}.cls-7{letter-spacing:0em;}.cls-8{letter-spacing:-0.01em;}.cls-9{letter-spacing:0em;}.cls-10{letter-spacing:-0.01em;}.cls-11{letter-spacing:0em;}.cls-12{letter-spacing:0em;}.cls-13{letter-spacing:0.02em;}.cls-14{letter-spacing:-0.01em;}.cls-15{letter-spacing:-0.01em;}.cls-16{letter-spacing:0em;}.cls-17{letter-spacing:0em;}.cls-18{letter-spacing:0em;}.cls-19{fill:#0069ff;}</style>
</defs>
<g>
<circle class="cls-19" cx="21" cy="21" r="20.79" />
<path class="cls-20"
d="M12.7,13.49a1.22,1.22,0,0,0,.66-.2,1,1,0,0,0,.33-.68,1.22,1.22,0,0,0-.19-.69.88.88,0,0,0-.69-.33h0a1,1,0,0,0-1,.86,1.16,1.16,0,0,0,.21.68A.91.91,0,0,0,12.7,13.49Zm-2.6,3.89a1,1,0,0,0,1-.87,1.26,1.26,0,0,0-.2-.68.85.85,0,0,0-.69-.34h-.06a1,1,0,0,0-.95.87,1.12,1.12,0,0,0,.21.68,1.09,1.09,0,0,0,.67.34ZM10.17,21a1.26,1.26,0,0,0-.2-.68A.9.9,0,0,0,9.28,20H9.22a1,1,0,0,0-.62.21.87.87,0,0,0-.33.69,1.08,1.08,0,0,0,.2.67.84.84,0,0,0,.68.35.87.87,0,0,0,1-.7.61.61,0,0,0,0-.19Zm0,3.61a1.07,1.07,0,0,0-.69.21.88.88,0,0,0-.34.69,1.16,1.16,0,0,0,.21.67.87.87,0,0,0,.68.35,1.15,1.15,0,0,0,.72-.21.84.84,0,0,0,.34-.68.87.87,0,0,0-.69-1Zm2.67,3.9a1,1,0,0,0-1,.86,1.2,1.2,0,0,0,.21.69.89.89,0,0,0,.68.34,1.21,1.21,0,0,0,.66-.19.85.85,0,0,0,.33-.68,1.22,1.22,0,0,0-.19-.69A1.13,1.13,0,0,0,12.84,28.51ZM16.72,31a1,1,0,0,0-.68.18.86.86,0,0,0-.34.68,1.12,1.12,0,0,0,.21.68.83.83,0,0,0,.68.34,1.11,1.11,0,0,0,.68-.2.86.86,0,0,0,.34-.68,1.12,1.12,0,0,0-.2-.69A1,1,0,0,0,16.72,31Z" />
<circle class="cls-20" cx="25.39" cy="26.73" r="0.95" />
<circle class="cls-20" cx="27.92" cy="23.18" r="0.95" />
<circle class="cls-20" cx="27.99" cy="18.88" r="0.95" />
<circle class="cls-20" cx="25.32" cy="15.2" r="0.95" />
<path class="cls-20"
d="M16.66,10.9a1,1,0,0,0,1-.87.94.94,0,0,0-.86-1h-.1a.92.92,0,0,0-.61.2.88.88,0,0,0-.34.69,1.08,1.08,0,0,0,.19.65A.84.84,0,0,0,16.66,10.9ZM21,28.84V27.2a6.27,6.27,0,0,1,0-12.54V13a7.92,7.92,0,0,0,0,15.83Zm.42-20.53V10a11.05,11.05,0,0,1,0,22.1v1.64a12.69,12.69,0,0,0,0-25.38Z" />
</g>
</g>`,
41.61
),
'do:teams': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3" d="M33.83,23.9a5.13,5.13,0,0,0-4.94-5.29h-.41a5.1,5.1,0,0,0-5.3,4.9v.39" />
<circle class="cls-3" cx="28.48" cy="15.86" r="2.7" />
<path class="cls-3"
d="M18.82,23.9a5.11,5.11,0,0,0-4.92-5.29h-.42a5.1,5.1,0,0,0-5.3,4.91,2.41,2.41,0,0,0,0,.38" />
<circle class="cls-3" cx="13.48" cy="15.86" r="2.7" />
<path class="cls-3"
d="M26.33,28.79a5.1,5.1,0,0,0-4.92-5.29,2.94,2.94,0,0,0-.42,0,5.12,5.12,0,0,0-5.3,4.94,2.67,2.67,0,0,0,0,.4" />
<circle class="cls-3" cx="20.99" cy="20.8" r="2.7" />
</g>
</g>`,
41.61
),
'do:terraform-provider': createIcon(
`<g>
<defs>
<style>
.cls-1,.cls-3{fill:none;stroke-miterlimit:10;}.cls-1{stroke:#000;}.cls-2{fill:#0069ff;}.cls-3{stroke:#fff;stroke-width:1.49px;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<path class="cls-3" d="M10.89,17.53l4.61,2.64V14.93l-4.61-2.64Z" />
<path class="cls-3" d="M18.63,21.94l4.62,2.65V19.34L18.63,16.7Z" />
<path class="cls-3" d="M26.5,19.34v5.25l4.61-2.65V16.7Z" />
<path class="cls-3" d="M18.63,31l4.62,2.64V28.43l-4.6-2.62Z" />
</g>
</g>`,
41.61
),
'do:volumes-block-storage': createIcon(
`<g>
<defs>
<style>.cls-1,.cls-3,.cls-4,.cls-5,.cls-6,.cls-7{fill:none;}.cls-1{stroke:#000;stroke-miterlimit:10;}.cls-2{fill:#0069ff;}.cls-3,.cls-4,.cls-5,.cls-6,.cls-7{stroke:#fff;stroke-width:1.49px;}.cls-3,.cls-4,.cls-5,.cls-6{stroke-linecap:round;}.cls-4{stroke-dasharray:0
3.11;}.cls-5{stroke-dasharray:0 2.94;}.cls-6{stroke-dasharray:0 2.95;}</style>
</defs>
<g>
<circle class="cls-2" cx="21" cy="21" r="20.79" />
<line class="cls-3" x1="20.98" y1="21.2" x2="20.98" y2="21.2" />
<line class="cls-4" x1="20.98" y1="18.09" x2="20.98" y2="10.32" />
<line class="cls-3" x1="20.98" y1="8.76" x2="20.98" y2="8.76" />
<line class="cls-3" x1="10.92" y1="27.22" x2="10.92" y2="27.22" />
<line class="cls-5" x1="13.42" y1="25.66" x2="19.68" y2="21.78" />
<line class="cls-3" x1="20.93" y1="21" x2="20.93" y2="21" />
<line class="cls-6" x1="23.44" y1="22.54" x2="29.72" y2="26.4" />
<line class="cls-3" x1="30.98" y1="27.17" x2="30.98" y2="27.17" />
<path class="cls-7"
d="M21,21V33.44M21,21.2,11,15m20.11,0L21,21.2m-10.1-6.42V27.17l10,6.22L31,27.17V14.73l-10-6.17Z" />
</g>
</g>`,
41.61
),
};
export default digitalOceanIcons;

View File

@@ -0,0 +1,26 @@
import { createIcon } from '../../svgRegister.js';
const githubIcons = {
'gh:action': createIcon(
`<g>
<path d="M46.62.3c23.51,2.97,39.85,24.77,36.41,48.15-3.04,18.63-19.42,34.09-38.47,34.89-2.09-.07-2.23,1.19-1.34,3.21,3.19,9.13,13.37,14.89,22.82,13.27.55-.09,1.02-.48,1.21-1,.05-.14.1-.3.13-.46.58-2.8,1.67-5.54,3.04-8.07,13.28-23.34,47.81-18.65,53.98,7.57.23,1.5,1.13,2.27,2.52,2.22,4.21.01,8.46,0,12.72,0,2.24.2,2.74-1.22,3.06-3.18.01-.07.03-.14.04-.2,3.26-11.55,14-20.72,26.11-21.53,15.8-1.4,30.54,11.69,31.32,27.5,1.12,23.22-23.72,38.33-43.92,26.85-6.53-3.98-12-11.09-13.56-18.62-.17-.77-.35-1.65-1.02-2.1-.14-.09-.31-.15-.47-.19-2.48-.57-5.2-.08-7.73-.23-2.53.16-5.21-.34-7.66.23-.16.04-.33.09-.47.19-.58.4-.78,1.14-.91,1.82-.3,2.05-1.09,4.03-1.95,5.94-12.63,25.63-49.17,20.91-55.28-6.89-.04-.2-.11-.4-.24-.55-.43-.54-1.08-.72-1.84-.76-7.29.43-14.77-1.31-20.49-6.1,0,0-.01,0-.02-.01-1.06-.63-2.41,0-2.47,1.23-.61,12.63-.33,23.57-.18,36.22s10.41,26.21,22.99,27.03c1.25.15,2.22-.37,2.48-1.7,8.11-33.08,54.11-29.65,57.79,4.14,1.2,17.3-14.66,32.64-31.85,30.93-12.42-1.1-23.31-10.88-25.89-23.1-.21-1.05-.98-1.78-2.06-1.83-19.33-.73-32.03-16.21-32.03-35.45,0-.04,0-3.67,0-3.71-.06-17.49-.02-33.52-.02-50.98.27-2.06-1.33-2.74-3.05-3.06C10.77,76.56-2.44,56.23.38,36.18,3.55,14.18,24.38-2.44,46.62.3ZM41.85,75.27c44.21-2.12,44.37-64.72-.02-66.93-.08,0-.17,0-.25,0-3.69.33-7.43.81-10.96,1.98C-4.29,23.25,4.48,74.43,41.68,75.27c.05,0,.11,0,.17,0ZM75.05,171.2c-.09,13.85,14.54,24.2,27.55,19.6,17.62-6.27,19.43-30.16,2.64-38.57-13.49-6.81-30.67,3.69-30.19,18.97ZM95.82,125.2c16.03.11,25.96-16.9,18.54-30.94-9.48-16.62-34.24-13.51-38.65,5.26-3.3,12.67,7.01,25.63,20.11,25.68ZM171.09,125.19c14.62.2,24.97-15.63,19.18-28.94-6.98-16.77-30.91-17.13-38.28-.53-6.29,13.76,3.91,29.61,19.1,29.47Z" style="fill: #228afe; stroke-width: 0px;"/>
<path d="M167.78,199.97c-12.35-1.36-23-11.04-25.4-23.25-.2-1.05-.9-1.59-1.92-1.59-1.75-.08-3.65.24-5.12-.7-1.86-1.06-2.38-3.51-1.36-5.35,1.26-2.51,4.42-1.98,6.72-2.25,1.71-.19,1.68-2.06,2.04-3.37.67-2.29,1.52-4.67,2.84-6.84,10.46-18.3,36.75-19.95,48.97-2.66,15.1,20.27-1.86,48.47-26.61,46.02l-.16-.03ZM170.33,150.06c-17.86.92-26.76,21.77-14.72,35.12,9.48,9.89,25.18,8.64,32.89-2.69,9.06-14.11-1.07-32.64-17.99-32.43h-.18Z" style="fill: #79b9fe; stroke-width: 0px;"/>
<path d="M33.52,20.07c1.33.07,2.9.44,4.08,1.22,5.34,3.05,10.44,6.45,15.62,9.75,2.96,2.15,6.82,3.53,8.76,6.78,1.35,2.66,1.34,6.21-.33,8.69-3.01,3.94-7.92,5.77-11.91,8.56-3.01,1.8-6.09,3.62-8.95,5.47-2,1.19-4.26,2.93-6.62,2.9-3.05.04-6.36-1.31-7.78-4-1.18-2.06-1.35-4.27-1.24-6.57,0-3.35,0-6.7,0-10.05,0-4.56,0-9.12,0-13.67-.4-4.71,3.69-8.75,8.22-9.07h.15ZM54.58,42.18c-6.35-5-13.46-9.01-20.07-13.33-.28-.17-.83-.49-1.02-.22-.48,8.53-.04,17.15-.16,25.79.04.62.33.8.89.52,6.79-4.13,13.78-8.4,20.36-12.66v-.09Z" style="fill: #228afe; stroke-width: 0px;"/>
<path d="M108.23,100.28c-4.18,4.75-8.97,9.06-13.33,13.56-2.84,3.36-6.28.73-8.56-1.87-2.21-2.49-6.33-4.87-4.58-8.69.72-2.09,4.24-3.27,6.04-1.56,1.12,1,2.06,2.04,3.1,2.97.21.13.46.27.71.24.44-.04.8-.42,1.13-.73,2.63-2.6,5.29-5.23,7.87-7.91,1.01-1.05,2.4-1.92,3.86-2.01,2.98.03,4.95,3.08,3.84,5.82l-.08.17Z" style="fill: #228afe; stroke-width: 0px;"/>
<path d="M165.95,115.32c-3.16-.96-4.95-4-7.45-6.2-.66-.71-1.43-1.39-1.85-2.28-.71-2.11-.09-4.52,2.07-5.54,2.51-1.22,4.14.35,5.81,2.09.76.65,1.59,1.81,2.35,1.5,3.09-2.55,5.82-5.72,8.72-8.56,1.5-1.51,3.64-2.72,5.72-1.63,1.55.67,1.99,1.84,2.36,3.2.29,1.28-.44,2.53-1.3,3.43-4.22,4.25-8.49,8.52-12.73,12.76-.9.97-2.21,1.52-3.53,1.26l-.18-.04Z" style="fill: #228afe; stroke-width: 0px;"/>
<circle cx="87.78" cy="170.74" r="5" style="fill: #79b9fe; stroke-width: 0px;"/>
<circle cx="104.28" cy="170.74" r="5" style="fill: #79b9fe; stroke-width: 0px;"/>
</g>`,
200
),
'gh:github': createIcon(
`<g>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="#24292f" />
</g>`,
98
),
};
export default githubIcons;

View File

@@ -0,0 +1,32 @@
import type { IconLibrary } from '../svgRegister.js';
import database from './default/database.js';
import server from './default/server.js';
import disk from './default/disk.js';
import internet from './default/internet.js';
import cloud from './default/cloud.js';
import unknown from './default/unknown.js';
import blank from './default/blank.js';
/** Creates a resolver to the path to lazy-load included icon packs */
const getIconNamespaces = (basePath: string) => ({
'aws:common': `${basePath}/aws/awsCommon.js`,
'aws:full': `${basePath}/aws/awsFull.js`,
github: `${basePath}/github/github.js`,
'digital-ocean': `${basePath}/digital-ocean/digitalOcean.js`,
});
type IconNamespaceKeys = keyof ReturnType<typeof getIconNamespaces>;
const defaultIconLibrary: IconLibrary = {
database: database,
server: server,
disk: disk,
internet: internet,
cloud: cloud,
unknown: unknown,
blank: blank,
};
export default defaultIconLibrary;
export { getIconNamespaces };
export type { IconNamespaceKeys };

View File

@@ -0,0 +1,60 @@
import type { Selection } from 'd3-selection';
type IconResolver = (
parent: Selection<SVGGElement, unknown, Element | null, unknown>,
width?: number
) => Selection<SVGGElement, unknown, Element | null, unknown>;
type IconLibrary = Record<string, IconResolver>;
/**
* Converts an SVG Icon passed as a string into a properly formatted IconResolver
* @param icon - html code for the svg icon as a string (the SVG tag should not be included)
* @param originalSize - the original size of the SVG Icon in pixels
* @returns IconResolver
*/
const createIcon: (icon: string, originalSize: number) => IconResolver = (icon, originalSize) => {
return (
parent: Selection<SVGGElement, unknown, Element | null, unknown>,
size: number = originalSize
) => {
parent.html(`<g style="transform: scale(${size / originalSize})">${icon}</g>`);
return parent;
};
};
const icons: IconLibrary = {};
const isIconNameInUse = (name: string): boolean => {
return icons[name] !== undefined;
};
const registerIcon = (name: string, resolver: IconResolver) => {
if (!isIconNameInUse(name)) {
icons[name] = resolver;
}
};
const registerIcons = (library: IconLibrary) => {
Object.entries(library).forEach(([name, resolver]) => {
if (!isIconNameInUse(name)) {
icons[name] = resolver;
}
});
};
const getIcon = (name: string): IconResolver | null => {
if (isIconNameInUse(name)) {
return icons[name];
}
return icons.unknown;
};
export {
registerIcon,
registerIcons,
getIcon,
isIconNameInUse,
createIcon,
IconLibrary,
IconResolver,
};

View File

@@ -46,6 +46,7 @@ required:
- quadrantChart
- xyChart
- requirement
- architecture
- mindmap
- gitGraph
- c4
@@ -226,6 +227,11 @@ properties:
fall back to legacy rendering for KaTeX.
type: boolean
default: false
iconLibraries:
description: |
This option specifies an object contianing a mappig of SVG icon names to a resolver that returns the svg code.
For supported diagrams (i.e., Architecture), their syntax allows refering to key names in this object to display the corresponding SVG icon in the rendered diagram.
tsType: Array<import('./rendering-util/svgRegister.js').IconLibrary | import('./rendering-util/svg/index.js').IconNamespaceKeys>
forceLegacyMathML:
description: |
This option forces Mermaid to rely on KaTeX's own stylesheet for rendering MathML. Due to differences between OS
@@ -274,6 +280,8 @@ properties:
$ref: '#/$defs/XYChartConfig'
requirement:
$ref: '#/$defs/RequirementDiagramConfig'
architecture:
$ref: '#/$defs/ArchitectureDiagramConfig'
mindmap:
$ref: '#/$defs/MindmapDiagramConfig'
gitGraph:
@@ -921,6 +929,28 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: number
default: 20
ArchitectureDiagramConfig:
title: Architecture Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
description: The object containing configurations specific for architecture diagrams
type: object
unevaluatedProperties: false
required:
- useMaxWidth
- padding
- iconSize
- fontSize
properties:
padding:
type: number
default: 40
iconSize:
type: number
default: 80
fontSize:
type: number
default: 16
MindmapDiagramConfig:
title: Mindmap Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]

View File

@@ -220,6 +220,13 @@ class Theme {
this.pieOuterStrokeColor = this.pieOuterStrokeColor || 'black';
this.pieOpacity = this.pieOpacity || '0.7';
/* architecture */
this.archEdgeColor = this.archEdgeColor || '#777';
this.archEdgeArrowColor = this.archEdgeArrowColor || '#777';
this.archEdgeWidth = this.archEdgeWidth || '3';
this.archGroupBorderColor = this.archGroupBorderColor || '#000';
this.archGroupBorderWidth = this.archGroupBorderWidth || '2px';
/* quadrant-graph */
this.quadrant1Fill = this.quadrant1Fill || this.primaryColor;
this.quadrant2Fill = this.quadrant2Fill || adjust(this.primaryColor, { r: 5, g: 5, b: 5 });

View File

@@ -84,6 +84,13 @@ class Theme {
this.personBorder = this.primaryBorderColor;
this.personBkg = this.mainBkg;
/* Architecture Diagram variables */
this.archEdgeColor = 'calculated';
this.archEdgeArrowColor = 'calculated';
this.archEdgeWidth = '3';
this.archGroupBorderColor = this.primaryBorderColor;
this.archGroupBorderWidth = '2px';
/* state colors */
this.labelColor = 'calculated';
@@ -132,6 +139,10 @@ class Theme {
this.doneTaskBkgColor = this.mainContrastColor;
this.taskTextDarkColor = this.darkTextColor;
/* Architecture Diagram variables */
this.archEdgeColor = this.lineColor;
this.archEdgeArrowColor = this.lineColor;
/* state colors */
this.transitionColor = this.transitionColor || this.lineColor;
this.transitionLabelColor = this.transitionLabelColor || this.textColor;

View File

@@ -112,6 +112,13 @@ class Theme {
this.personBorder = this.primaryBorderColor;
this.personBkg = this.mainBkg;
/* Architecture Diagram variables */
this.archEdgeColor = 'calculated';
this.archEdgeArrowColor = 'calculated';
this.archEdgeWidth = '3';
this.archGroupBorderColor = this.primaryBorderColor;
this.archGroupBorderWidth = '2px';
/* state colors */
this.labelColor = 'black';
this.errorBkgColor = '#552222';
@@ -194,6 +201,10 @@ class Theme {
this.taskTextColor = this.taskTextLightColor;
this.taskTextOutsideColor = this.taskTextDarkColor;
/* Architecture Diagram variables */
this.archEdgeColor = this.lineColor;
this.archEdgeArrowColor = this.lineColor;
/* state colors */
this.transitionColor = this.transitionColor || this.lineColor;
this.transitionLabelColor = this.transitionLabelColor || this.textColor;

View File

@@ -86,6 +86,13 @@ class Theme {
this.personBorder = this.primaryBorderColor;
this.personBkg = this.mainBkg;
/* Architecture Diagram variables */
this.archEdgeColor = 'calculated';
this.archEdgeArrowColor = 'calculated';
this.archEdgeWidth = '3';
this.archGroupBorderColor = this.primaryBorderColor;
this.archGroupBorderWidth = '2px';
/* state colors */
this.labelColor = 'black';
@@ -162,6 +169,10 @@ class Theme {
this.activeTaskBorderColor = this.taskBorderColor;
this.activeTaskBkgColor = this.mainBkg;
/* Architecture Diagram variables */
this.archEdgeColor = this.lineColor;
this.archEdgeArrowColor = this.lineColor;
/* state colors */
this.transitionColor = this.transitionColor || this.lineColor;
this.transitionLabelColor = this.transitionLabelColor || this.textColor;

View File

@@ -98,6 +98,13 @@ class Theme {
this.personBorder = this.primaryBorderColor;
this.personBkg = this.mainBkg;
/* Architecture Diagram variables */
this.archEdgeColor = 'calculated';
this.archEdgeArrowColor = 'calculated';
this.archEdgeWidth = '3';
this.archGroupBorderColor = this.primaryBorderColor;
this.archGroupBorderWidth = '2px';
/* state colors */
this.labelColor = 'black';
@@ -199,6 +206,10 @@ class Theme {
this.todayLineColor = this.critBkgColor;
/* Architecture Diagram variables */
this.archEdgeColor = this.lineColor;
this.archEdgeArrowColor = this.lineColor;
/* state colors */
this.transitionColor = this.transitionColor || '#000';
this.transitionLabelColor = this.transitionLabelColor || this.textColor;

View File

@@ -4,22 +4,42 @@
{
"id": "info",
"grammar": "src/language/info/info.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "packet",
"grammar": "src/language/packet/packet.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "pie",
"grammar": "src/language/pie/pie.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "architecture",
"grammar": "src/language/architecture/architecture.langium",
"fileExtensions": [
".mmd",
".mermaid"
]
},
{
"id": "gitGraph",
"grammar": "src/language/gitGraph/gitGraph.langium",
"fileExtensions": [".mmd", ".mermaid"]
"fileExtensions": [
".mmd",
".mermaid"
]
}
],
"mode": "production",

View File

@@ -0,0 +1,55 @@
grammar Architecture
import "../common/common";
entry Architecture:
NEWLINE*
"architecture-beta"
(
NEWLINE* TitleAndAccessibilities
| NEWLINE* Statement*
| NEWLINE*
)
;
fragment Statement:
groups+=Group
| services+=Service
| junctions+=Junction
| edges+=Edge
;
fragment LeftPort:
':'lhsDir=ARROW_DIRECTION
;
fragment RightPort:
rhsDir=ARROW_DIRECTION':'
;
fragment Arrow:
LeftPort lhsInto?=ARROW_INTO? ('--' | '-' title=ARCH_TITLE '-') rhsInto?=ARROW_INTO? RightPort
;
Group:
'group' id=ARCH_ID icon=ARCH_ICON? title=ARCH_TITLE? ('in' in=ARCH_ID)? EOL
;
Service:
'service' id=ARCH_ID (iconText=ARCH_TEXT_ICON | icon=ARCH_ICON)? title=ARCH_TITLE? ('in' in=ARCH_ID)? EOL
;
Junction:
'junction' id=ARCH_ID ('in' in=ARCH_ID)? EOL
;
Edge:
lhsId=ARCH_ID lhsGroup?=ARROW_GROUP? Arrow rhsId=ARCH_ID rhsGroup?=ARROW_GROUP? EOL
;
terminal ARROW_DIRECTION: 'L' | 'R' | 'T' | 'B';
terminal ARCH_ID: /[\w]+/;
terminal ARCH_TEXT_ICON: /\("[^"]+"\)/;
terminal ARCH_ICON: /\([\w:]+\)/;
terminal ARCH_TITLE: /\[[\w ]+\]/;
terminal ARROW_GROUP: /\{group\}/;
terminal ARROW_INTO: /<|>/;

View File

@@ -0,0 +1 @@
export * from './module.js';

View File

@@ -0,0 +1,79 @@
import type {
DefaultSharedCoreModuleContext,
LangiumCoreServices,
LangiumSharedCoreServices,
Module,
PartialLangiumCoreServices,
} from 'langium';
import {
EmptyFileSystem,
createDefaultCoreModule,
createDefaultSharedCoreModule,
inject,
} from 'langium';
import { MermaidGeneratedSharedModule, ArchitectureGeneratedModule } from '../generated/module.js';
import { ArchitectureTokenBuilder } from './tokenBuilder.js';
import { ArchitectureValueConverter } from './valueConverter.js';
/**
* Declaration of `Architecture` services.
*/
interface ArchitectureAddedServices {
parser: {
TokenBuilder: ArchitectureTokenBuilder;
ValueConverter: ArchitectureValueConverter;
};
}
/**
* Union of Langium default services and `Architecture` services.
*/
export type ArchitectureServices = LangiumCoreServices & ArchitectureAddedServices;
/**
* Dependency injection module that overrides Langium default services and
* contributes the declared `Architecture` services.
*/
export const ArchitectureModule: Module<
ArchitectureServices,
PartialLangiumCoreServices & ArchitectureAddedServices
> = {
parser: {
TokenBuilder: () => new ArchitectureTokenBuilder(),
ValueConverter: () => new ArchitectureValueConverter(),
},
};
/**
* Create the full set of services required by Langium.
*
* First inject the shared services by merging two modules:
* - Langium default shared services
* - Services generated by langium-cli
*
* Then inject the language-specific services by merging three modules:
* - Langium default language-specific services
* - Services generated by langium-cli
* - Services specified in this file
* @param context - Optional module context with the LSP connection
* @returns An object wrapping the shared services and the language-specific services
*/
export function createArchitectureServices(
context: DefaultSharedCoreModuleContext = EmptyFileSystem
): {
shared: LangiumSharedCoreServices;
Architecture: ArchitectureServices;
} {
const shared: LangiumSharedCoreServices = inject(
createDefaultSharedCoreModule(context),
MermaidGeneratedSharedModule
);
const Architecture: ArchitectureServices = inject(
createDefaultCoreModule({ shared }),
ArchitectureGeneratedModule,
ArchitectureModule
);
shared.ServiceRegistry.register(Architecture);
return { shared, Architecture };
}

View File

@@ -0,0 +1,7 @@
import { AbstractMermaidTokenBuilder } from '../common/index.js';
export class ArchitectureTokenBuilder extends AbstractMermaidTokenBuilder {
public constructor() {
super(['architecture']);
}
}

View File

@@ -0,0 +1,20 @@
import type { CstNode, GrammarAST, ValueType } from 'langium';
import { AbstractMermaidValueConverter } from '../common/index.js';
export class ArchitectureValueConverter extends AbstractMermaidValueConverter {
protected runCustomConverter(
rule: GrammarAST.AbstractRule,
input: string,
_cstNode: CstNode
): ValueType | undefined {
if (rule.name === 'ARCH_ICON') {
return input.replace(/[()]/g, '').trim();
} else if (rule.name === 'ARCH_TEXT_ICON') {
return input.replace(/["()]/g, '');
} else if (rule.name === 'ARCH_TITLE') {
return input.replace(/[[\]]/g, '').trim();
}
return undefined;
}
}

View File

@@ -5,6 +5,7 @@ export {
PacketBlock,
Pie,
PieSection,
Architecture,
GitGraph,
Branch,
Commit,
@@ -16,16 +17,19 @@ export {
isPacketBlock,
isPie,
isPieSection,
isArchitecture,
isGitGraph,
isBranch,
isCommit,
isMerge,
} from './generated/ast.js';
export {
InfoGeneratedModule,
MermaidGeneratedSharedModule,
PacketGeneratedModule,
PieGeneratedModule,
ArchitectureGeneratedModule,
GitGraphGeneratedModule,
} from './generated/module.js';
@@ -34,3 +38,4 @@ export * from './common/index.js';
export * from './info/index.js';
export * from './packet/index.js';
export * from './pie/index.js';
export * from './architecture/index.js';

View File

@@ -6,7 +6,6 @@ export class PieValueConverter extends AbstractMermaidValueConverter {
protected runCustomConverter(
rule: GrammarAST.AbstractRule,
input: string,
_cstNode: CstNode
): ValueType | undefined {
if (rule.name !== 'PIE_SECTION_LABEL') {

View File

@@ -1,8 +1,8 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info, Packet, Pie, GitGraph } from './index.js';
import type { Info, Packet, Pie, Architecture, GitGraph } from './index.js';
export type DiagramAST = Info | Packet | Pie | GitGraph;
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
@@ -21,6 +21,11 @@ const initializers = {
const parser = createPieServices().Pie.parser.LangiumParser;
parsers.pie = parser;
},
architecture: async () => {
const { createArchitectureServices } = await import('./language/architecture/index.js');
const parser = createArchitectureServices().Architecture.parser.LangiumParser;
parsers.architecture = parser;
},
gitGraph: async () => {
const { createGitGraphServices } = await import('./language/gitGraph/index.js');
const parser = createGitGraphServices().GitGraph.parser.LangiumParser;
@@ -31,6 +36,7 @@ const initializers = {
export async function parse(diagramType: 'info', text: string): Promise<Info>;
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
export async function parse(diagramType: 'architecture', text: string): Promise<Architecture>;
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
export async function parse<T extends DiagramAST>(

1430
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff