diff --git a/.build/jsonSchema.ts b/.build/jsonSchema.ts index 6fd8ca3f54..50b9ff097b 100644 --- a/.build/jsonSchema.ts +++ b/.build/jsonSchema.ts @@ -25,6 +25,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'sankey', 'block', 'packet', + 'architecture', ] as const; /** diff --git a/.changeset/nice-flowers-yawn.md b/.changeset/nice-flowers-yawn.md new file mode 100644 index 0000000000..31c0e81aff --- /dev/null +++ b/.changeset/nice-flowers-yawn.md @@ -0,0 +1,8 @@ +--- +"mermaid": minor +"@mermaid-js/docs": patch +--- + +New Diagram: Architecture + +Adds architecture diagrams which allows users to show relations between services. diff --git a/.cspell/code-terms.txt b/.cspell/code-terms.txt index d0e40e8f21..8e4c02261a 100644 --- a/.cspell/code-terms.txt +++ b/.cspell/code-terms.txt @@ -55,6 +55,7 @@ GENERICTYPE getBoundarys grammr graphtype +halign iife interp introdcued @@ -66,6 +67,7 @@ Kaufmann keyify LABELPOS LABELTYPE +layoutstop lcov LEFTOF Lexa diff --git a/.cspell/libraries.txt b/.cspell/libraries.txt index 3bfec1d5f4..ad0e3e7011 100644 --- a/.cspell/libraries.txt +++ b/.cspell/libraries.txt @@ -24,6 +24,7 @@ Doctave DokuWiki dompurify elkjs +fcose fontawesome Foswiki Gitea diff --git a/cypress/integration/rendering/architecture.spec.ts b/cypress/integration/rendering/architecture.spec.ts new file mode 100644 index 0000000000..4599319793 --- /dev/null +++ b/cypress/integration/rendering/architecture.spec.ts @@ -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} + ` + ); + }); +}); diff --git a/demos/architecture.html b/demos/architecture.html new file mode 100644 index 0000000000..fc65b6bebf --- /dev/null +++ b/demos/architecture.html @@ -0,0 +1,309 @@ + + + + + + Architecture Mermaid Quick Test Page + + + + + +

Architecture diagram demo

+

Simple diagram with groups

+
+      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
+    
+
+ +

Groups within groups

+
+      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
+    
+
+ +

Default icon (?) from unknown icon name

+
+      architecture-beta
+        service unknown(iconnamedoesntexist)[Unknown Icon]
+    
+
+ +

Split Direction

+
+      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
+    
+
+ +

Arrow Tests

+
+      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
+    
+
+      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
+    
+
+ +

Group Edges

+
+      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}
+  
+
+ +

Edge Label Test

+
+      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
+    
+
+      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
+    
+ +
+

Junction Demo

+
+      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
+    
+
+ +

Junction Demo Groups

+
+      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}
+    
+
+ +

AWS Icon Demo

+
+    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]
+  
+ + + + diff --git a/demos/index.html b/demos/index.html index 61a86a2aa0..07b51a3136 100644 --- a/demos/index.html +++ b/demos/index.html @@ -88,6 +88,9 @@

Packet

  • Layered Blocks

  • +
  • +

    Architecture

    +
  • diff --git a/docs/config/icons.md b/docs/config/icons.md new file mode 100644 index 0000000000..d0a80f2d83 --- /dev/null +++ b/docs/config/icons.md @@ -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; +// createIcon: (icon: string, originalSize: number) => IconResolver +const myIconLibrary: IconLibrary = { + defaultCloudExample: createIcon( + ` + + + `, + 80 + ) +}; + +export default myIconLibrary +``` diff --git a/docs/config/setup/interfaces/mermaid.Mermaid.md b/docs/config/setup/interfaces/mermaid.Mermaid.md index 09fab149cd..04f5bcc939 100644 --- a/docs/config/setup/interfaces/mermaid.Mermaid.md +++ b/docs/config/setup/interfaces/mermaid.Mermaid.md @@ -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) diff --git a/docs/config/setup/interfaces/mermaid.MermaidConfig.md b/docs/config/setup/interfaces/mermaid.MermaidConfig.md index 846918ef46..95de778d48 100644 --- a/docs/config/setup/interfaces/mermaid.MermaidConfig.md +++ b/docs/config/setup/interfaces/mermaid.MermaidConfig.md @@ -20,6 +20,16 @@ --- +### architecture + +• `Optional` **architecture**: `ArchitectureDiagramConfig` + +#### Defined in + +[packages/mermaid/src/config.type.ts:193](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L193) + +--- + ### arrowMarkerAbsolute • `Optional` **arrowMarkerAbsolute**: `boolean` @@ -59,6 +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) --- @@ -233,6 +244,19 @@ 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:152](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L152) + +--- + ### journey • `Optional` **journey**: `JourneyDiagramConfig` diff --git a/docs/config/setup/interfaces/mermaid.RunOptions.md b/docs/config/setup/interfaces/mermaid.RunOptions.md index aae004d6d5..6d45d35493 100644 --- a/docs/config/setup/interfaces/mermaid.RunOptions.md +++ b/docs/config/setup/interfaces/mermaid.RunOptions.md @@ -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) diff --git a/docs/config/setup/modules/mermaid.md b/docs/config/setup/modules/mermaid.md index bdaeb05e1e..8e3b025b74 100644 --- a/docs/config/setup/modules/mermaid.md +++ b/docs/config/setup/modules/mermaid.md @@ -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) diff --git a/docs/syntax/architecture.md b/docs/syntax/architecture.md new file mode 100644 index 0000000000..2d71d84fba --- /dev/null +++ b/docs/syntax/architecture.md @@ -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\+) + +> 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 diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 8956eb1e5c..3fee53a0da 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -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", diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 0236de3ae9..0459faf52b 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -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 * diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 55d05c9aaa..d68a1c4982 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -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 ); }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts new file mode 100644 index 0000000000..f019d1eff9 --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -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 = + DEFAULT_CONFIG.architecture; + +const state = new ImperativeState(() => ({ + 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) { + 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(isArchitectureService); + +const addJunction = function ({ id, in: parent }: Omit) { + state.records.registeredIds[id] = 'node'; + + state.records.nodes[id] = { + id, + type: 'junction', + edges: [], + in: parent, + }; +}; + +const getJunctions = (): ArchitectureJunction[] => + Object.values(state.records.nodes).filter(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) { + 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 + >((prevOuter, [id, service]) => { + prevOuter[id] = service.edges.reduce((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 + ); + + // 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( + field: T +): Required[T] { + const arch = getConfig().architecture; + if (arch?.[field]) { + return arch[field] as Required[T]; + } + return DEFAULT_ARCHITECTURE_CONFIG[field]; +} diff --git a/packages/mermaid/src/diagrams/architecture/architectureDetector.ts b/packages/mermaid/src/diagrams/architecture/architectureDetector.ts new file mode 100644 index 0000000000..c15b474abc --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureDetector.ts @@ -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; diff --git a/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts new file mode 100644 index 0000000000..82dacd3e19 --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts @@ -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, +}; diff --git a/packages/mermaid/src/diagrams/architecture/architectureParser.ts b/packages/mermaid/src/diagrams/architecture/architectureParser.ts new file mode 100644 index 0000000000..a7159d9076 --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureParser.ts @@ -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 => { + const ast: Architecture = await parse('architecture', input); + log.debug(ast); + populateDb(ast, db); + }, +}; diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts new file mode 100644 index 0000000000..f23c1894bd --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -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 = {}; + const verticalAlignments: Record = {}; + // 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 = {}; + const directions: Record = { + 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 { + 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 }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureStyles.ts b/packages/mermaid/src/diagrams/architecture/architectureStyles.ts new file mode 100644 index 0000000000..7f494ecd13 --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureStyles.ts @@ -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; diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts new file mode 100644 index 0000000000..b3ef55ec69 --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -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; +export type ArchitectureDirectionY = Extract; + +/** + * 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
    { + 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) => void; + getServices: () => ArchitectureService[]; + addJunction: (service: Omit) => 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; +export type ArchitectureSpatialMap = Record; +export interface ArchitectureDataStructures { + adjList: ArchitectureAdjacencyList; + spatialMaps: ArchitectureSpatialMap[]; +} + +export interface ArchitectureState extends Record { + nodes: Record; + groups: Record; + edges: ArchitectureEdge[]; + registeredIds: Record; + dataStructures?: ArchitectureDataStructures; + elements: Record; + 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(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(key: T): NodeSingularData[T]; +} diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts new file mode 100644 index 0000000000..cbe385f27d --- /dev/null +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -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 { + 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); + }); +}; diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index 940fc6940a..ebbab35c71 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -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' }, diff --git a/packages/mermaid/src/docs/config/icons.md b/packages/mermaid/src/docs/config/icons.md new file mode 100644 index 0000000000..0b9c78d541 --- /dev/null +++ b/packages/mermaid/src/docs/config/icons.md @@ -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; +// createIcon: (icon: string, originalSize: number) => IconResolver +const myIconLibrary: IconLibrary = { + defaultCloudExample: createIcon( + ` + + + `, + 80 + ) +}; + +export default myIconLibrary +``` diff --git a/packages/mermaid/src/docs/syntax/architecture.md b/packages/mermaid/src/docs/syntax/architecture.md new file mode 100644 index 0000000000..e74995ce33 --- /dev/null +++ b/packages/mermaid/src/docs/syntax/architecture.md @@ -0,0 +1,156 @@ +# Architecture Diagrams Documentation (v+) + +> 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 diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index 43fc5bd31b..0cc94bcc24 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -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"`. diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index e1c4412b9d..af2fbd04dc 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -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( diff --git a/packages/mermaid/src/rendering-util/svg/aws/awsCommon.ts b/packages/mermaid/src/rendering-util/svg/aws/awsCommon.ts new file mode 100644 index 0000000000..0f33914143 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/aws/awsCommon.ts @@ -0,0 +1,237 @@ +import { createIcon } from '../../svgRegister.js'; +// cSpell:disable +const awsIcons = { + 'aws:amplify': createIcon( + ` + + + + +`, + 80 + ), + 'aws:api-gateway': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:aurora': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloudformation': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloudfront': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloudwatch': createIcon( + ` + + + + +`, + 80 + ), + 'aws:documentdb': createIcon( + ` + + + + +`, + 80 + ), + 'aws:dynamodb': createIcon( + ` + + + + +`, + 80 + ), + 'aws:ec2': createIcon( + ` + + + + +`, + 80 + ), + 'aws:efs': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:elastic-beanstalk': createIcon( + ` + + + + +`, + 80 + ), + 'aws:fargate': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iam': createIcon( + ` + + + + +`, + 80 + ), + 'aws:kms': createIcon( + ` + + + + +`, + 80 + ), + 'aws:kinesis': createIcon( + ` + + + + +`, + 80 + ), + 'aws:lambda': createIcon( + ` + + + + +`, + 80 + ), + 'aws:lightsail': createIcon( + ` + + + + +`, + 80 + ), + 'aws:neptune': createIcon( + ` + + + + +`, + 80 + ), + 'aws:rds': createIcon( + ` + + + + +`, + 80 + ), + 'aws:redshift': createIcon( + ` + + + + +`, + 80 + ), + 'aws:route-53': createIcon( + ` + + + + +`, + 80 + ), + 'aws:secrets-manager': createIcon( + ` + + + + +`, + 80 + ), + 'aws:s3': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:timestream': createIcon( + ` + + + + +`, + 80 + ), + 'aws:vpc': createIcon( + ` + + + + +`, + 80 + ), +}; + +export default awsIcons; diff --git a/packages/mermaid/src/rendering-util/svg/aws/awsFull.ts b/packages/mermaid/src/rendering-util/svg/aws/awsFull.ts new file mode 100644 index 0000000000..cae1de757d --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/aws/awsFull.ts @@ -0,0 +1,2816 @@ +import { createIcon } from '../../svgRegister.js'; +// cSpell:disable +const awsIcons = { + 'aws:activate': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:alexa-for-business': createIcon( + ` + + + + +`, + 80 + ), + 'aws:apache-mxnet-on-aws': createIcon( + ` + + + + +`, + 80 + ), + 'aws:app-mesh': createIcon( + ` + + + + +`, + 80 + ), + 'aws:app-runner': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:appconfig': createIcon( + ` + + + + +`, + 80 + ), + 'aws:appfabric': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:appflow': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:application-auto-scaling': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:application-composer': createIcon( + ` + + + + +`, + 80 + ), + 'aws:application-cost-profiler': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:application-discovery-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:application-migration-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:appstream': createIcon( + ` + + + + +`, + 80 + ), + 'aws:appsync': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:artifact': createIcon( + ` + + + + +`, + 80 + ), + 'aws:athena': createIcon( + ` + + + + +`, + 80 + ), + 'aws:audit-manager': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:augmented-ai-a2i': createIcon( + ` + + + + +`, + 80 + ), + 'aws:auto-scaling': createIcon( + ` + + + + +`, + 80 + ), + 'aws:b2b-data-interchange': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:backint-agent': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:backup': createIcon( + ` + + + + +`, + 80 + ), + 'aws:batch': createIcon( + ` + + + + +`, + 80 + ), + 'aws:bedrock': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:billing-conductor': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:bottlerocket': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:braket': createIcon( + ` + + + + +`, + 80 + ), + 'aws:budgets': createIcon( + ` + + + + +`, + 80 + ), + 'aws:certificate-manager': createIcon( + ` + + + + +`, + 80 + ), + 'aws:chatbot': createIcon( + ` + + + + +`, + 80 + ), + 'aws:chime-sdk': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:chime': createIcon( + ` + + + + +`, + 80 + ), + 'aws:clean-rooms': createIcon( + ` + + + + +`, + 80 + ), + 'aws:client-vpn': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloud-control-api': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:cloud-development-kit': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloud-directory': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:cloud-map': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloud-wan': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloud9': createIcon( + ` + + + + +`, + 80 + ), + + 'aws:cloudhsm': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloudsearch': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cloudshell': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:cloudtrail': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codeartifact': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:codebuild': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codecatalyst': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codecommit': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codedeploy': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codeguru': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codepipeline': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codestar': createIcon( + ` + + + + +`, + 80 + ), + 'aws:codewhisperer': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:cognito': createIcon( + ` + + + + +`, + 80 + ), + 'aws:command-line-interface': createIcon( + ` + + + + +`, + 80 + ), + 'aws:comprehend-medical': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:comprehend': createIcon( + ` + + + + +`, + 80 + ), + 'aws:compute-optimizer': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:config': createIcon( + ` + + + + +`, + 80 + ), + 'aws:connect': createIcon( + ` + + + + +`, + 80 + ), + 'aws:console-mobile-application': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:control-tower': createIcon( + ` + + + + +`, + 80 + ), + 'aws:corretto': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:cost-and-usage-report': createIcon( + ` + + + + +`, + 80 + ), + 'aws:cost-explorer': createIcon( + ` + + + + +`, + 80 + ), + 'aws:data-exchange': createIcon( + ` + + + + +`, + 80 + ), + 'aws:data-pipeline': createIcon( + ` + + + + +`, + 80 + ), + 'aws:database-migration-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:datasync': createIcon( + ` + + + + +`, + 80 + ), + 'aws:datazone': createIcon( + ` + + + + +`, + 80 + ), + 'aws:deep-learning-amis': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:deep-learning-containers': createIcon( + ` + + + + +`, + 80 + ), + 'aws:deepcomposer': createIcon( + ` + + + + +`, + 80 + ), + 'aws:deeplens': createIcon( + ` + + + + +`, + 80 + ), + 'aws:deepracer': createIcon( + ` + + + + +`, + 80 + ), + 'aws:detective': createIcon( + ` + + + + +`, + 80 + ), + 'aws:device-farm': createIcon( + ` + + + + +`, + 80 + ), + 'aws:devops-guru': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:direct-connect': createIcon( + ` + + + + +`, + 80 + ), + 'aws:directory-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:distro-for-opentelemetry': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:ec2-auto-scaling': createIcon( + ` + + + + +`, + 80 + ), + 'aws:ec2-image-builder': createIcon( + ` + + + + +`, + 80 + ), + 'aws:ecs-anywhere': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:eks-anywhere': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:eks-cloud': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:eks-distro': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:elastic-block-store': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elastic-container-registry': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elastic-container-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elastic-disaster-recovery': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elastic-fabric-adapter': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:elastic-inference': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elastic-kubernetes-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elastic-load-balancing': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elastic-transcoder': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elasticache': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-appliances-&-software': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-conductor': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-delta': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-link': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:elemental-live': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-mediaconnect': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-mediaconvert': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-medialive': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-mediapackage': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-mediastore': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-mediatailor': createIcon( + ` + + + + +`, + 80 + ), + 'aws:elemental-server': createIcon( + ` + + + + +`, + 80 + ), + 'aws:emr': createIcon( + ` + + + + +`, + 80 + ), + 'aws:entity-resolution': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:eventbridge': createIcon( + ` + + + + +`, + 80 + ), + 'aws:express-workflows': createIcon( + ` + + + + +`, + 80 + ), + 'aws:fault-injection-simulator': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:file-cache': createIcon( + ` + + + + +`, + 80 + ), + 'aws:finspace': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:firewall-manager': createIcon( + ` + + + + +`, + 80 + ), + 'aws:forecast': createIcon( + ` + + + + +`, + 80 + ), + 'aws:fraud-detector': createIcon( + ` + + + + +`, + 80 + ), + 'aws:freertos': createIcon( + ` + + + + +`, + 80 + ), + 'aws:fsx-for-lustre': createIcon( + ` + + + + +`, + 80 + ), + 'aws:fsx-for-netapp-ontap': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:fsx-for-openzfs': createIcon( + ` + + + + +`, + 80 + ), + 'aws:fsx-for-wfs': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:fsx': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:gamekit': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:gamelift': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:gamesparks': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:genomics-cli': createIcon( + ` + + + + +`, + 80 + ), + 'aws:global-accelerator': createIcon( + ` + + + + +`, + 80 + ), + 'aws:glue-databrew': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:glue-elastic-views': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:glue': createIcon( + ` + + + + +`, + 80 + ), + 'aws:ground-station': createIcon( + ` + + + + +`, + 80 + ), + 'aws:guardduty': createIcon( + ` + + + + +`, + 80 + ), + 'aws:health-dashboard': createIcon( + ` + + + + +`, + 80 + ), + 'aws:healthimaging': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:healthlake': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:healthomics': createIcon( + ` + + + + +`, + 80 + ), + 'aws:healthscribe': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:honeycode': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:iam-identity-center': createIcon( + ` + + + + +`, + 80 + ), + 'aws:inspector': createIcon( + ` + + + + +`, + 80 + ), + 'aws:interactive-video-service': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:iot-1-click': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-analytics': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-button': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-core': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-device-defender': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-device-management': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-events': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-expresslink': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:iot-fleetwise': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:iot-greengrass': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-roborunner': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:iot-sitewise': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-things-graph': createIcon( + ` + + + + +`, + 80 + ), + 'aws:iot-twinmaker': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:iq': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:kendra': createIcon( + ` + + + + +`, + 80 + ), + 'aws:keyspaces': createIcon( + ` + + + + +`, + 80 + ), + 'aws:kinesis-data-firehose': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:kinesis-data-streams': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:kinesis-video-streams': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:lake-formation': createIcon( + ` + + + + +`, + 80 + ), + 'aws:launch-wizard': createIcon( + ` + + + + +`, + 80 + ), + 'aws:lex': createIcon( + ` + + + + +`, + 80 + ), + 'aws:license-manager': createIcon( + ` + + + + +`, + 80 + ), + 'aws:lightsail-for-research': createIcon( + ` + + + + +`, + 80 + ), + 'aws:local-zones': createIcon( + ` + + + + +`, + 80 + ), + 'aws:location-service': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:lookout-for-equipment': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:lookout-for-metrics': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:lookout-for-vision': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:macie': createIcon( + ` + + + + +`, + 80 + ), + 'aws:mainframe-modernization': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:managed-blockchain': createIcon( + ` + + + + +`, + 80 + ), + 'aws:managed-grafana': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:managed-service-for-apache-flink': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:managed-service-for-prometheus': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:managed-services': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:managed-streaming-for-apache-kafka': createIcon( + ` + + + + +`, + 80 + ), + 'aws:managed-workflows-for-apache-airflow': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:management-console': createIcon( + ` + + + + +`, + 80 + ), + 'aws:marketplace_dark': createIcon( + ` + + + + +`, + 80 + ), + 'aws:marketplace_light': createIcon( + ` + + + + +`, + 80 + ), + 'aws:memorydb-for-redis': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:migration-evaluator': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:migration-hub': createIcon( + ` + + + + +`, + 80 + ), + 'aws:monitron': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:mq': createIcon( + ` + + + + +`, + 80 + ), + 'aws:network-firewall': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:neuron': createIcon( + ` + + + + +`, + 80 + ), + 'aws:nice-dcv': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:nice-enginframe': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:nimble-studio': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:nitro-enclaves': createIcon( + ` + + + + +`, + 80 + ), + 'aws:open-3d-engine': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:opensearch-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:opsworks': createIcon( + ` + + + + +`, + 80 + ), + 'aws:organizations': createIcon( + ` + + + + +`, + 80 + ), + 'aws:outposts-family': createIcon( + ` + + + + +`, + 80 + ), + 'aws:outposts-rack': createIcon( + ` + + + + +`, + 80 + ), + 'aws:outposts-servers': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:panorama': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:parallel-cluster': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:payment-cryptography': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:personalize': createIcon( + ` + + + + +`, + 80 + ), + 'aws:pinpoint-apis': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:pinpoint': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:polly': createIcon( + ` + + + + +`, + 80 + ), + 'aws:private-5g': createIcon( + ` + + + + +`, + 80 + ), + 'aws:private-certificate-authority': createIcon( + ` + + + + +`, + 80 + ), + 'aws:privatelink': createIcon( + ` + + + + +`, + 80 + ), + 'aws:professional-services': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:proton': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:q': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:quantum-ledger-database': createIcon( + ` + + + + +`, + 80 + ), + 'aws:quicksight': createIcon( + ` + + + + +`, + 80 + ), + 'aws:rds-on-vmware': createIcon( + ` + + + + +`, + 80 + ), + 'aws:red-hat-openshift-service-on-aws': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:rekognition': createIcon( + ` + + + + +`, + 80 + ), + 'aws:repost-private': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:repost': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:reserved-instance-reporting': createIcon( + ` + + + + +`, + 80 + ), + 'aws:resilience-hub': createIcon( + ` + + + + +`, + 80 + ), + 'aws:resource-access-manager': createIcon( + ` + + + + +`, + 80 + ), + 'aws:resource-explorer': createIcon( + ` + + + + +`, + 80 + ), + 'aws:robomaker': createIcon( + ` + + + + +`, + 80 + ), + 'aws:s3-on-outposts': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:sagemaker-ground-truth': createIcon( + ` + + + + +`, + 80 + ), + 'aws:sagemaker-studio-lab': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:sagemaker': createIcon( + ` + + + + +`, + 80 + ), + 'aws:savings-plans': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:security-hub': createIcon( + ` + + + + +`, + 80 + ), + 'aws:security-lake': createIcon( + ` + + + + +`, + 80 + ), + 'aws:serverless-application-repository': createIcon( + ` + + + + +`, + 80 + ), + 'aws:service-catalog': createIcon( + ` + + + + +`, + 80 + ), + 'aws:service-management-connector': createIcon( + ` + + + + +`, + 80 + ), + 'aws:shield': createIcon( + ` + + + + +`, + 80 + ), + 'aws:signer': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:simple-email-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:simple-notification-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:simple-queue-service': createIcon( + ` + + + + +`, + 80 + ), + 'aws:simple-storage-service-glacier': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:simspace-weaver': createIcon( + ` + + + + +`, + 80 + ), + 'aws:site-to-site-vpn': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:snowball-edge': createIcon( + ` + + + + +`, + 80 + ), + 'aws:snowball': createIcon( + ` + + + + +`, + 80 + ), + 'aws:snowcone': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:snowmobile': createIcon( + ` + + + + +`, + 80 + ), + 'aws:step-functions': createIcon( + ` + + + + +`, + 80 + ), + 'aws:storage-gateway': createIcon( + ` + + + + +`, + 80 + ), + 'aws:supply-chain': createIcon( + ` + + + + +`, + 80 + ), + 'aws:support': createIcon( + ` + + + + +`, + 80 + ), + 'aws:systems-manager': createIcon( + ` + + + + +`, + 80 + ), + 'aws:telco-network-builder': createIcon( + ` + + + + +`, + 80 + ), + 'aws:tensorflow-on-aws': createIcon( + ` + + + + +`, + 80 + ), + 'aws:textract': createIcon( + ` + + + + +`, + 80 + ), + 'aws:thinkbox-deadline': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:thinkbox-frost': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:thinkbox-krakatoa': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:thinkbox-sequoia': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:thinkbox-stoke': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:thinkbox-xmesh': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:tools-and-sdks': createIcon( + ` + + + + +`, + 80 + ), + 'aws:torchserve': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:training-certification': createIcon( + ` + + + + +`, + 80 + ), + 'aws:transcribe': createIcon( + ` + + + + +`, + 80 + ), + 'aws:transfer-family': createIcon( + ` + + + + +`, + 80 + ), + 'aws:transit-gateway': createIcon( + ` + + + + +`, + 80 + ), + 'aws:translate': createIcon( + ` + + + + +`, + 80 + ), + 'aws:trusted-advisor': createIcon( + ` + + + + +`, + 80 + ), + 'aws:verified-access': createIcon( + ` + + + + +`, + 80 + ), + 'aws:verified-permissions': createIcon( + ` + + + + +`, + 80 + ), + 'aws:vmware-cloud-on-aws': createIcon( + ` + + + + +`, + 80 + ), + 'aws:vpc-lattice': createIcon( + ` + + + + +`, + 80 + ), + 'aws:waf': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:wavelength': createIcon( + ` + + + + +`, + 80 + ), + 'aws:well-architected-tool': createIcon( + ` + + + + +`, + 80 + ), + 'aws:wickr': createIcon( + ` + + + + +`, + 80 + ), + 'aws:workdocs-sdk': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:workdocs': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:worklink': createIcon( + ` + + + + +`, + 80 + ), + 'aws:workmail': createIcon( + ` + + + + +`, + 80 + ), + 'aws:workspaces-family': createIcon( + ` + + + + +`, + 80 + ), + 'aws:workspaces-thin-client': createIcon( + ` + + + + + + +`, + 80 + ), + 'aws:x-ray': createIcon( + ` + + + + +`, + 80 + ), +}; + +export default awsIcons; diff --git a/packages/mermaid/src/rendering-util/svg/default/blank.ts b/packages/mermaid/src/rendering-util/svg/default/blank.ts new file mode 100644 index 0000000000..53bf8b92c7 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/default/blank.ts @@ -0,0 +1,11 @@ +/** + * Designer: Nicolas Newman + */ +import { createIcon } from '../../svgRegister.js'; + +export default createIcon( + ` + +`, + 80 +); diff --git a/packages/mermaid/src/rendering-util/svg/default/cloud.ts b/packages/mermaid/src/rendering-util/svg/default/cloud.ts new file mode 100644 index 0000000000..ad4d860f5b --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/default/cloud.ts @@ -0,0 +1,13 @@ +/** + * Designer: Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from '../../svgRegister.js'; + +export default createIcon( + ` + + + `, + 80 +); diff --git a/packages/mermaid/src/rendering-util/svg/default/database.ts b/packages/mermaid/src/rendering-util/svg/default/database.ts new file mode 100644 index 0000000000..d0fb31296b --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/default/database.ts @@ -0,0 +1,18 @@ +/** + * Designer: Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from '../../svgRegister.js'; + +export default createIcon( + ` + + + + + + + +`, + 80 +); diff --git a/packages/mermaid/src/rendering-util/svg/default/disk.ts b/packages/mermaid/src/rendering-util/svg/default/disk.ts new file mode 100644 index 0000000000..6e21be9e64 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/default/disk.ts @@ -0,0 +1,20 @@ +/** + * Designer: Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from '../../svgRegister.js'; + +export default createIcon( + ` + + + + + + + + + +`, + 80 +); diff --git a/packages/mermaid/src/rendering-util/svg/default/internet.ts b/packages/mermaid/src/rendering-util/svg/default/internet.ts new file mode 100644 index 0000000000..fd23d2aca4 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/default/internet.ts @@ -0,0 +1,19 @@ +/** + * Designer: Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from '../../svgRegister.js'; + +export default createIcon( + ` + + + + + + + + +`, + 80 +); diff --git a/packages/mermaid/src/rendering-util/svg/default/server.ts b/packages/mermaid/src/rendering-util/svg/default/server.ts new file mode 100644 index 0000000000..e73e8c406b --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/default/server.ts @@ -0,0 +1,42 @@ +/** + * Designer: Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from '../../svgRegister.js'; + +export default createIcon( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`, + 80 +); diff --git a/packages/mermaid/src/rendering-util/svg/default/unknown.ts b/packages/mermaid/src/rendering-util/svg/default/unknown.ts new file mode 100644 index 0000000000..c6889be9b6 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/default/unknown.ts @@ -0,0 +1,13 @@ +/** + * Designer: Nicolas Newman + * @see https://github.com/NicolasNewman/IconLibrary + */ +import { createIcon } from '../../svgRegister.js'; + +export default createIcon( + ` + + ? +`, + 80 +); diff --git a/packages/mermaid/src/rendering-util/svg/digital-ocean/digitalOcean.ts b/packages/mermaid/src/rendering-util/svg/digital-ocean/digitalOcean.ts new file mode 100644 index 0000000000..bf2501dc59 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/digital-ocean/digitalOcean.ts @@ -0,0 +1,321 @@ +import { createIcon } from '../../svgRegister.js'; +// cSpell:disable +const digitalOceanIcons = { + 'do:api': createIcon( + ` + + + + + + + +`, + 41.61 + ), + 'do:cli': createIcon( + ` + + + + + + + + + +`, + 41.61 + ), + 'do:dns': createIcon( + ` + + + + + + + + + + + + +`, + 41.61 + ), + 'do:droplet': createIcon( + ` + + + + + + + + +`, + 41.61 + ), + 'do:kubernetes': createIcon( + ` + + + + + + + +`, + 41.61 + ), + 'do:local-ssd': createIcon( + ` + + + + + + + + +`, + 41.61 + ), + 'do:mysql': createIcon( + ` + + + + + + + + + + + + + + + +`, + 41.61 + ), + 'do:redis-copy': createIcon( + ` + + + + + + + + + + + + + + + + + + + +`, + 41.61 + ), + 'do:redis': createIcon( + ` + + + + + + + + + + + + + + + + +`, + 41.61 + ), + 'do:monitoring': createIcon( + ` + + + + + + + + + + + + +`, + 41.61 + ), + 'do:projects': createIcon( + ` + + + + + + + + + + + + + +`, + 41.61 + ), + 'do:spaces-cdn': createIcon( + ` + + + + + + + + + + + + +`, + 41.61 + ), + 'do:spaces-object-storage': createIcon( + ` + + + + + + + + + + + + +`, + 41.61 + ), + 'do:teams': createIcon( + ` + + + + + + + + + + + + +`, + 41.61 + ), + 'do:terraform-provider': createIcon( + ` + + + + + + + + + + +`, + 41.61 + ), + 'do:volumes-block-storage': createIcon( + ` + + + + + + + + + + + + + + + +`, + 41.61 + ), +}; + +export default digitalOceanIcons; diff --git a/packages/mermaid/src/rendering-util/svg/github/github.ts b/packages/mermaid/src/rendering-util/svg/github/github.ts new file mode 100644 index 0000000000..6b90573eb2 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/github/github.ts @@ -0,0 +1,26 @@ +import { createIcon } from '../../svgRegister.js'; + +const githubIcons = { + 'gh:action': createIcon( + ` + + + + + + + +`, + 200 + ), + 'gh:github': createIcon( + ` + +`, + 98 + ), +}; + +export default githubIcons; diff --git a/packages/mermaid/src/rendering-util/svg/index.ts b/packages/mermaid/src/rendering-util/svg/index.ts new file mode 100644 index 0000000000..22a8514a78 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svg/index.ts @@ -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; + +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 }; diff --git a/packages/mermaid/src/rendering-util/svgRegister.ts b/packages/mermaid/src/rendering-util/svgRegister.ts new file mode 100644 index 0000000000..6a21d5b8b2 --- /dev/null +++ b/packages/mermaid/src/rendering-util/svgRegister.ts @@ -0,0 +1,60 @@ +import type { Selection } from 'd3-selection'; + +type IconResolver = ( + parent: Selection, + width?: number +) => Selection; +type IconLibrary = Record; + +/** + * 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, + size: number = originalSize + ) => { + parent.html(`${icon}`); + 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, +}; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 0d2b610ae5..e902855746 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -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 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' }] diff --git a/packages/mermaid/src/themes/theme-base.js b/packages/mermaid/src/themes/theme-base.js index a92bd9e20d..6e572ea5f9 100644 --- a/packages/mermaid/src/themes/theme-base.js +++ b/packages/mermaid/src/themes/theme-base.js @@ -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 }); diff --git a/packages/mermaid/src/themes/theme-dark.js b/packages/mermaid/src/themes/theme-dark.js index 24ba128b50..cf223d9892 100644 --- a/packages/mermaid/src/themes/theme-dark.js +++ b/packages/mermaid/src/themes/theme-dark.js @@ -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; diff --git a/packages/mermaid/src/themes/theme-default.js b/packages/mermaid/src/themes/theme-default.js index 40acbb0f9a..e0023758ea 100644 --- a/packages/mermaid/src/themes/theme-default.js +++ b/packages/mermaid/src/themes/theme-default.js @@ -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; diff --git a/packages/mermaid/src/themes/theme-forest.js b/packages/mermaid/src/themes/theme-forest.js index 4bb7d24413..97c0682f3d 100644 --- a/packages/mermaid/src/themes/theme-forest.js +++ b/packages/mermaid/src/themes/theme-forest.js @@ -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; diff --git a/packages/mermaid/src/themes/theme-neutral.js b/packages/mermaid/src/themes/theme-neutral.js index 40963839e2..4a622cbccb 100644 --- a/packages/mermaid/src/themes/theme-neutral.js +++ b/packages/mermaid/src/themes/theme-neutral.js @@ -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; diff --git a/packages/parser/langium-config.json b/packages/parser/langium-config.json index af8a4cfe6e..b1ecf01f78 100644 --- a/packages/parser/langium-config.json +++ b/packages/parser/langium-config.json @@ -16,6 +16,10 @@ "grammar": "src/language/pie/pie.langium", "fileExtensions": [".mmd", ".mermaid"] }, + { + "id": "architecture", + "grammar": "src/language/architecture/architecture.langium", + }, { "id": "gitGraph", "grammar": "src/language/gitGraph/gitGraph.langium", diff --git a/packages/parser/src/language/architecture/architecture.langium b/packages/parser/src/language/architecture/architecture.langium new file mode 100644 index 0000000000..6246b423ed --- /dev/null +++ b/packages/parser/src/language/architecture/architecture.langium @@ -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: /<|>/; \ No newline at end of file diff --git a/packages/parser/src/language/architecture/index.ts b/packages/parser/src/language/architecture/index.ts new file mode 100644 index 0000000000..fd3c604b08 --- /dev/null +++ b/packages/parser/src/language/architecture/index.ts @@ -0,0 +1 @@ +export * from './module.js'; diff --git a/packages/parser/src/language/architecture/module.ts b/packages/parser/src/language/architecture/module.ts new file mode 100644 index 0000000000..d4e730c2c2 --- /dev/null +++ b/packages/parser/src/language/architecture/module.ts @@ -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 }; +} diff --git a/packages/parser/src/language/architecture/tokenBuilder.ts b/packages/parser/src/language/architecture/tokenBuilder.ts new file mode 100644 index 0000000000..6a7c6a37a4 --- /dev/null +++ b/packages/parser/src/language/architecture/tokenBuilder.ts @@ -0,0 +1,7 @@ +import { AbstractMermaidTokenBuilder } from '../common/index.js'; + +export class ArchitectureTokenBuilder extends AbstractMermaidTokenBuilder { + public constructor() { + super(['architecture']); + } +} diff --git a/packages/parser/src/language/architecture/valueConverter.ts b/packages/parser/src/language/architecture/valueConverter.ts new file mode 100644 index 0000000000..c9475657bb --- /dev/null +++ b/packages/parser/src/language/architecture/valueConverter.ts @@ -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; + } +} diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index 8e8dbce4f7..c85a5a8b60 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -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'; diff --git a/packages/parser/src/language/pie/valueConverter.ts b/packages/parser/src/language/pie/valueConverter.ts index 6e312e1721..cc412d7a1b 100644 --- a/packages/parser/src/language/pie/valueConverter.ts +++ b/packages/parser/src/language/pie/valueConverter.ts @@ -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') { diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 233faed00c..86713c2f1b 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -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 = {}; 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; export async function parse(diagramType: 'packet', text: string): Promise; export async function parse(diagramType: 'pie', text: string): Promise; +export async function parse(diagramType: 'architecture', text: string): Promise; export async function parse(diagramType: 'gitGraph', text: string): Promise; export async function parse( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d851e9d908..cfb8982606 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,7 +220,10 @@ importers: version: 3.30.2 cytoscape-cose-bilkent: specifier: ^4.1.0 - version: 4.1.0(cytoscape@3.30.2) + version: 4.1.0(cytoscape@3.30.1) + cytoscape-fcose: + specifier: ^2.2.0 + version: 2.2.0(cytoscape@3.30.1) d3: specifier: ^7.9.0 version: 7.9.0 @@ -266,7 +269,10 @@ importers: version: 8.0.2 '@types/cytoscape': specifier: ^3.21.4 - version: 3.21.7 + version: 3.21.5 + '@types/cytoscape-fcose': + specifier: ^2.2.4 + version: 2.2.4 '@types/d3': specifier: ^7.4.3 version: 7.4.3 @@ -494,6 +500,67 @@ importers: specifier: ^7.0.0 version: 7.1.0 + packages/mermaid/src/vitepress: + dependencies: + '@mdi/font': + specifier: ^7.0.0 + version: 7.4.47 + '@vueuse/core': + specifier: ^10.9.0 + version: 10.11.0(vue@3.4.35(typescript@5.4.5)) + font-awesome: + specifier: ^4.7.0 + version: 4.7.0 + jiti: + specifier: ^1.21.0 + version: 1.21.6 + mermaid: + specifier: workspace:^ + version: link:../.. + vue: + specifier: ^3.4.21 + version: 3.4.35(typescript@5.4.5) + devDependencies: + '@iconify-json/carbon': + specifier: ^1.1.31 + version: 1.1.37 + '@unocss/reset': + specifier: ^0.59.0 + version: 0.59.4 + '@vite-pwa/vitepress': + specifier: ^0.4.0 + version: 0.4.0(vite-plugin-pwa@0.19.8(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0)) + '@vitejs/plugin-vue': + specifier: ^5.0.0 + version: 5.1.2(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3))(vue@3.4.35(typescript@5.4.5)) + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + https-localhost: + specifier: ^4.7.1 + version: 4.7.1 + pathe: + specifier: ^1.1.2 + version: 1.1.2 + unocss: + specifier: ^0.59.0 + version: 0.59.4(postcss@8.4.40)(rollup@4.20.0)(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3)) + unplugin-vue-components: + specifier: ^0.26.0 + version: 0.26.0(@babel/parser@7.25.3)(rollup@4.20.0)(vue@3.4.35(typescript@5.4.5)) + vite: + specifier: ^5.0.0 + version: 5.3.5(@types/node@20.14.14)(terser@5.31.3) + vite-plugin-pwa: + specifier: ^0.19.7 + version: 0.19.8(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) + vitepress: + specifier: 1.1.4 + version: 1.1.4(@algolia/client-search@4.24.0)(@types/node@20.14.14)(axios@1.7.3)(postcss@8.4.40)(search-insights@2.15.0)(terser@5.31.3)(typescript@5.4.5) + workbox-window: + specifier: ^7.0.0 + version: 7.1.0 + packages/parser: dependencies: langium: @@ -2646,8 +2713,11 @@ packages: '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} - '@types/cytoscape@3.21.7': - resolution: {integrity: sha512-dP4UByJtfu5GjMJuv58yCIRxjCp4cP0Wp+Qd46L3Gom0hcV4OPmSOLqt83vArNcYRZLFCAyAk+lcC8oqQtcsqw==} + '@types/cytoscape-fcose@2.2.4': + resolution: {integrity: sha512-QwWtnT8HI9h+DHhG5krGc1ZY0Ex+cn85MvX96ZNAjSxuXiZDnjIZW/ypVkvvubTjIY4rSdkJY1D/Nsn8NDpmAw==} + + '@types/cytoscape@3.21.5': + resolution: {integrity: sha512-fzYT3vqY5J4gxVXDOsCgDpm0ZdU8bQq+wCv0ucS0MSTtvQdjs3lcb2VetJiUSAd4WBgouqizI+JT1f8Yc6eY7Q==} '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -4162,6 +4232,9 @@ packages: cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cp-file@10.0.0: resolution: {integrity: sha512-vy2Vi1r2epK5WqxOLnskeKeZkdZvTKfFZQCplE3XWsP+SUJyd5XAUFC9lFgTjjXJF2GMne/UML14iEmkAaDfFg==} engines: {node: '>=14.16'} @@ -4311,8 +4384,13 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.30.2: - resolution: {integrity: sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw==} + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.30.1: + resolution: {integrity: sha512-TRJc3HbBPkHd50u9YfJh2FxD1lDLZ+JXnJoyBn5LkncoeuT7fapO/Hq/Ed8TdFclaKshzInge2i30bg7VKeoPQ==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -6331,6 +6409,9 @@ packages: layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazy-ass@1.6.0: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} @@ -11721,7 +11802,11 @@ snapshots: dependencies: '@types/node': 20.16.2 - '@types/cytoscape@3.21.7': {} + '@types/cytoscape-fcose@2.2.4': + dependencies: + '@types/cytoscape': 3.21.5 + + '@types/cytoscape@3.21.5': {} '@types/d3-array@3.2.1': {} @@ -12221,6 +12306,16 @@ snapshots: transitivePeerDependencies: - rollup + '@unocss/astro@0.59.4(rollup@4.20.0)(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3))': + dependencies: + '@unocss/core': 0.59.4 + '@unocss/reset': 0.59.4 + '@unocss/vite': 0.59.4(rollup@4.20.0)(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3)) + optionalDependencies: + vite: 5.3.5(@types/node@20.14.14)(terser@5.31.3) + transitivePeerDependencies: + - rollup + '@unocss/cli@0.59.4(rollup@2.79.1)': dependencies: '@ampproject/remapping': 2.3.0 @@ -12239,6 +12334,24 @@ snapshots: transitivePeerDependencies: - rollup + '@unocss/cli@0.59.4(rollup@4.20.0)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) + '@unocss/config': 0.59.4 + '@unocss/core': 0.59.4 + '@unocss/preset-uno': 0.59.4 + cac: 6.7.14 + chokidar: 3.6.0 + colorette: 2.0.20 + consola: 3.2.3 + fast-glob: 3.3.2 + magic-string: 0.30.11 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + transitivePeerDependencies: + - rollup + '@unocss/config@0.59.4': dependencies: '@unocss/core': 0.59.4 @@ -12364,7 +12477,23 @@ snapshots: transitivePeerDependencies: - rollup - '@vite-pwa/vitepress@0.4.0(vite-plugin-pwa@0.19.8(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0))': + '@unocss/vite@0.59.4(rollup@4.20.0)(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) + '@unocss/config': 0.59.4 + '@unocss/core': 0.59.4 + '@unocss/inspector': 0.59.4 + '@unocss/scope': 0.59.4 + '@unocss/transformer-directives': 0.59.4 + chokidar: 3.6.0 + fast-glob: 3.3.2 + magic-string: 0.30.11 + vite: 5.3.5(@types/node@20.14.14)(terser@5.31.3) + transitivePeerDependencies: + - rollup + + '@vite-pwa/vitepress@0.4.0(vite-plugin-pwa@0.19.8(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0))': dependencies: vite-plugin-pwa: 0.19.8(vite@5.4.2(@types/node@22.5.1)(terser@5.31.6))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) @@ -13543,6 +13672,10 @@ snapshots: dependencies: layout-base: 1.0.2 + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cp-file@10.0.0: dependencies: graceful-fs: 4.2.11 @@ -13841,7 +13974,12 @@ snapshots: cose-base: 1.0.3 cytoscape: 3.30.2 - cytoscape@3.30.2: {} + cytoscape-fcose@2.2.0(cytoscape@3.30.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.30.1 + + cytoscape@3.30.1: {} d3-array@2.12.1: dependencies: @@ -16261,6 +16399,8 @@ snapshots: layout-base@1.0.2: {} + layout-base@2.0.1: {} + lazy-ass@1.6.0: {} leven@3.1.0: {} @@ -18738,6 +18878,35 @@ snapshots: - rollup - supports-color + unocss@0.59.4(postcss@8.4.40)(rollup@4.20.0)(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3)): + dependencies: + '@unocss/astro': 0.59.4(rollup@4.20.0)(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3)) + '@unocss/cli': 0.59.4(rollup@4.20.0) + '@unocss/core': 0.59.4 + '@unocss/extractor-arbitrary-variants': 0.59.4 + '@unocss/postcss': 0.59.4(postcss@8.4.40) + '@unocss/preset-attributify': 0.59.4 + '@unocss/preset-icons': 0.59.4 + '@unocss/preset-mini': 0.59.4 + '@unocss/preset-tagify': 0.59.4 + '@unocss/preset-typography': 0.59.4 + '@unocss/preset-uno': 0.59.4 + '@unocss/preset-web-fonts': 0.59.4 + '@unocss/preset-wind': 0.59.4 + '@unocss/reset': 0.59.4 + '@unocss/transformer-attributify-jsx': 0.59.4 + '@unocss/transformer-attributify-jsx-babel': 0.59.4 + '@unocss/transformer-compile-class': 0.59.4 + '@unocss/transformer-directives': 0.59.4 + '@unocss/transformer-variant-group': 0.59.4 + '@unocss/vite': 0.59.4(rollup@4.20.0)(vite@5.3.5(@types/node@20.14.14)(terser@5.31.3)) + optionalDependencies: + vite: 5.3.5(@types/node@20.14.14)(terser@5.31.3) + transitivePeerDependencies: + - postcss + - rollup + - supports-color + unpipe@1.0.0: {} unplugin-vue-components@0.26.0(@babel/parser@7.25.4)(rollup@2.79.1)(vue@3.4.38(typescript@5.4.5)): @@ -18759,6 +18928,25 @@ snapshots: - rollup - supports-color + unplugin-vue-components@0.26.0(@babel/parser@7.25.3)(rollup@4.20.0)(vue@3.4.35(typescript@5.4.5)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) + chokidar: 3.6.0 + debug: 4.3.6(supports-color@8.1.1) + fast-glob: 3.3.2 + local-pkg: 0.4.3 + magic-string: 0.30.11 + minimatch: 9.0.5 + resolve: 1.22.8 + unplugin: 1.12.0 + vue: 3.4.35(typescript@5.4.5) + optionalDependencies: + '@babel/parser': 7.25.3 + transitivePeerDependencies: + - rollup + - supports-color + unplugin@1.12.0: dependencies: acorn: 8.12.1