mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-07 08:09:39 +02:00
Merge branch '5237-unified-layout-common-renderer' of github.com:mermaid-js/mermaid into alanaV11
This commit is contained in:
@@ -22,9 +22,9 @@ export const packageOptions = {
|
|||||||
packageName: 'mermaid-zenuml',
|
packageName: 'mermaid-zenuml',
|
||||||
file: 'detector.ts',
|
file: 'detector.ts',
|
||||||
},
|
},
|
||||||
'mermaid-flowchart-elk': {
|
'mermaid-layout-elk': {
|
||||||
name: 'mermaid-flowchart-elk',
|
name: 'mermaid-layout-elk',
|
||||||
packageName: 'mermaid-flowchart-elk',
|
packageName: 'mermaid-layout-elk',
|
||||||
file: 'detector.ts',
|
file: 'layouts.ts',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import jison from 'jison';
|
import jison from 'jison';
|
||||||
|
|
||||||
export const transformJison = (src: string): string => {
|
export const transformJison = (src: string): string => {
|
||||||
|
// @ts-ignore - Jison is not typed properly
|
||||||
const parser = new jison.Generator(src, {
|
const parser = new jison.Generator(src, {
|
||||||
moduleType: 'js',
|
moduleType: 'js',
|
||||||
'token-stack': true,
|
'token-stack': true,
|
||||||
|
@@ -27,6 +27,7 @@ controly
|
|||||||
CSSCLASS
|
CSSCLASS
|
||||||
CYLINDEREND
|
CYLINDEREND
|
||||||
CYLINDERSTART
|
CYLINDERSTART
|
||||||
|
DAGA
|
||||||
datakey
|
datakey
|
||||||
DEND
|
DEND
|
||||||
descr
|
descr
|
||||||
@@ -89,6 +90,7 @@ reqs
|
|||||||
rewritelinks
|
rewritelinks
|
||||||
rgba
|
rgba
|
||||||
RIGHTOF
|
RIGHTOF
|
||||||
|
roughjs
|
||||||
sankey
|
sankey
|
||||||
sequencenumber
|
sequencenumber
|
||||||
shrc
|
shrc
|
||||||
|
@@ -54,6 +54,7 @@ presetAttributify
|
|||||||
pyplot
|
pyplot
|
||||||
redmine
|
redmine
|
||||||
rehype
|
rehype
|
||||||
|
roughjs
|
||||||
rscratch
|
rscratch
|
||||||
sparkline
|
sparkline
|
||||||
sphinxcontrib
|
sphinxcontrib
|
||||||
|
@@ -9,6 +9,7 @@ elems
|
|||||||
gantt
|
gantt
|
||||||
gitgraph
|
gitgraph
|
||||||
gzipped
|
gzipped
|
||||||
|
handdrawn
|
||||||
knsv
|
knsv
|
||||||
Knut
|
Knut
|
||||||
marginx
|
marginx
|
||||||
@@ -17,6 +18,7 @@ Markdownish
|
|||||||
mermaidjs
|
mermaidjs
|
||||||
mindmap
|
mindmap
|
||||||
mindmaps
|
mindmaps
|
||||||
|
mrtree
|
||||||
multigraph
|
multigraph
|
||||||
nodesep
|
nodesep
|
||||||
NOTEGROUP
|
NOTEGROUP
|
||||||
|
@@ -1 +1,4 @@
|
|||||||
|
circo
|
||||||
|
handdrawnSeed
|
||||||
|
neato
|
||||||
newbranch
|
newbranch
|
||||||
|
@@ -7,8 +7,8 @@ import { MermaidBuildOptions, defaultOptions, getBuildConfig } from './util.js';
|
|||||||
const shouldVisualize = process.argv.includes('--visualize');
|
const shouldVisualize = process.argv.includes('--visualize');
|
||||||
|
|
||||||
const buildPackage = async (entryName: keyof typeof packageOptions) => {
|
const buildPackage = async (entryName: keyof typeof packageOptions) => {
|
||||||
const commonOptions = { ...defaultOptions, entryName } as const;
|
const commonOptions: MermaidBuildOptions = { ...defaultOptions, entryName } as const;
|
||||||
const buildConfigs = [
|
const buildConfigs: MermaidBuildOptions[] = [
|
||||||
// package.mjs
|
// package.mjs
|
||||||
{ ...commonOptions },
|
{ ...commonOptions },
|
||||||
// package.min.mjs
|
// package.min.mjs
|
||||||
|
@@ -8,7 +8,7 @@ import { jisonPlugin } from './jisonPlugin.js';
|
|||||||
|
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
export interface MermaidBuildOptions {
|
export interface MermaidBuildOptions extends BuildOptions {
|
||||||
minify: boolean;
|
minify: boolean;
|
||||||
core: boolean;
|
core: boolean;
|
||||||
metafile: boolean;
|
metafile: boolean;
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,7 +35,7 @@ cypress/snapshots/
|
|||||||
.tsbuildinfo
|
.tsbuildinfo
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
knsv*.html
|
#knsv*.html
|
||||||
local*.html
|
local*.html
|
||||||
stats/
|
stats/
|
||||||
|
|
||||||
|
@@ -16,3 +16,5 @@ generated/
|
|||||||
# Ignore the files creates in /demos/dev except for example.html
|
# Ignore the files creates in /demos/dev except for example.html
|
||||||
demos/dev/**
|
demos/dev/**
|
||||||
!/demos/dev/example.html
|
!/demos/dev/example.html
|
||||||
|
# TODO: Lots of errors to fix
|
||||||
|
cypress/platform/state-refactor.html
|
||||||
|
433
cypress/platform/knsv-4442.html
Normal file
433
cypress/platform/knsv-4442.html
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||||
|
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
/* background: rgb(221, 208, 208); */
|
||||||
|
/* background:#333; */
|
||||||
|
font-family: 'Arial';
|
||||||
|
/* font-size: 18px !important; */
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
.mermaid2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mermaid svg {
|
||||||
|
/* font-size: 18px !important; */
|
||||||
|
background-color: #efefef;
|
||||||
|
background-image: radial-gradient(#fff 51%, transparent 91%),
|
||||||
|
radial-gradient(#fff 51%, transparent 91%);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
10px 10px;
|
||||||
|
background-repeat: repeat;
|
||||||
|
}
|
||||||
|
.malware {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 150px;
|
||||||
|
background: red;
|
||||||
|
color: black;
|
||||||
|
display: flex;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 72px;
|
||||||
|
}
|
||||||
|
/* tspan {
|
||||||
|
font-size: 6px !important;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<pre id="diagram" class="mermaid">
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Still
|
||||||
|
Still --> [*]
|
||||||
|
Still --> Moving
|
||||||
|
Moving --> Still
|
||||||
|
Moving --> Crash
|
||||||
|
Crash --> [*] </pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
flowchart RL
|
||||||
|
subgraph "`one`"
|
||||||
|
a1 -- l1 --> a2
|
||||||
|
a1 -- l2 --> a2
|
||||||
|
end
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram" class="mermaid">
|
||||||
|
flowchart RL
|
||||||
|
subgraph "`one`"
|
||||||
|
a1 -- l1 --> a2
|
||||||
|
a1 -- l2 --> a2
|
||||||
|
end
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
flowchart
|
||||||
|
id["`A root with a long text that wraps to keep the node size in check. A root with a long text that wraps to keep the node size in check`"]</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
A[A text that needs to be wrapped wraps to another line]
|
||||||
|
B[A text that needs to be<br/>wrapped wraps to another line]
|
||||||
|
C["`A text that needs to be wrapped to another line`"]</pre>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
C["`A text
|
||||||
|
that needs
|
||||||
|
to be wrapped
|
||||||
|
in another
|
||||||
|
way`"]
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid">
|
||||||
|
classDiagram-v2
|
||||||
|
note "I love this diagram!\nDo you love it?"
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram" class="mermaid">
|
||||||
|
stateDiagram-v2
|
||||||
|
State1: The state with a note with minus - and plus + in it
|
||||||
|
note left of State1
|
||||||
|
Important information! You can write
|
||||||
|
notes with . and in them.
|
||||||
|
end note </pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
mindmap
|
||||||
|
root
|
||||||
|
Child3(A node with an icon and with a long text that wraps to keep the node size in check)
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
%%{init: {"theme": "forest"} }%%
|
||||||
|
mindmap
|
||||||
|
id1[**Start2**<br/>end]
|
||||||
|
id2[**Start2**<br />end]
|
||||||
|
%% Another comment
|
||||||
|
id3[**Start2**<br>end] %% Comment
|
||||||
|
id4[**Start2**<br >end<br >the very end]
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
mindmap
|
||||||
|
id1["`**Start2**
|
||||||
|
second line 😎 with long text that is wrapping to the next line`"]
|
||||||
|
id2["`Child **with bold** text`"]
|
||||||
|
id3["`Children of which some
|
||||||
|
is using *italic type of* text`"]
|
||||||
|
id4[Child]
|
||||||
|
id5["`Child
|
||||||
|
Row
|
||||||
|
and another
|
||||||
|
`"]
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
mindmap
|
||||||
|
id1("`**Root**`"]
|
||||||
|
id2["`A formatted text... with **bold** and *italics*`"]
|
||||||
|
id3[Regular labels works as usual]
|
||||||
|
id4["`Emojis and unicode works too: 🤓
|
||||||
|
शान्तिः سلام 和平 `"]
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
<pre id="diagram" class="mermaid">
|
||||||
|
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
||||||
|
flowchart TB
|
||||||
|
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||||
|
subgraph ibm[IBM Espresso CPU]
|
||||||
|
core0[IBM PowerPC Broadway Core 0]
|
||||||
|
core1[IBM PowerPC Broadway Core 1]
|
||||||
|
core2[IBM PowerPC Broadway Core 2]
|
||||||
|
|
||||||
|
rom[16 KB ROM]
|
||||||
|
|
||||||
|
core0 --- core2
|
||||||
|
|
||||||
|
rom --> core2
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph amd["`**AMD** Latte GPU`"]
|
||||||
|
mem[Memory & I/O Bridge]
|
||||||
|
dram[DRAM Controller]
|
||||||
|
edram[32 MB EDRAM MEM1]
|
||||||
|
rom[512 B SEEPROM]
|
||||||
|
|
||||||
|
sata[SATA IF]
|
||||||
|
exi[EXI]
|
||||||
|
|
||||||
|
subgraph gx[GX]
|
||||||
|
sram[3 MB 1T-SRAM]
|
||||||
|
end
|
||||||
|
|
||||||
|
radeon[AMD Radeon R7xx GX2]
|
||||||
|
|
||||||
|
mem --- gx
|
||||||
|
mem --- radeon
|
||||||
|
|
||||||
|
rom --- mem
|
||||||
|
|
||||||
|
mem --- sata
|
||||||
|
mem --- exi
|
||||||
|
|
||||||
|
dram --- sata
|
||||||
|
dram --- exi
|
||||||
|
end
|
||||||
|
|
||||||
|
ddr3[2 GB DDR3 RAM MEM2]
|
||||||
|
|
||||||
|
mem --- ddr3
|
||||||
|
dram --- ddr3
|
||||||
|
edram --- ddr3
|
||||||
|
|
||||||
|
core1 --- mem
|
||||||
|
|
||||||
|
exi --- rtc
|
||||||
|
rtc{{rtc}}
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
%%{init: {"flowchart": {"defaultRenderer": "elk", "htmlLabels": false}} }%%
|
||||||
|
flowchart TB
|
||||||
|
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||||
|
subgraph ibm[IBM Espresso CPU]
|
||||||
|
core0[IBM PowerPC Broadway Core 0]
|
||||||
|
core1[IBM PowerPC Broadway Core 1]
|
||||||
|
core2[IBM PowerPC Broadway Core 2]
|
||||||
|
|
||||||
|
rom[16 KB ROM]
|
||||||
|
|
||||||
|
core0 --- core2
|
||||||
|
|
||||||
|
rom --> core2
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph amd["`**AMD** Latte GPU`"]
|
||||||
|
mem[Memory & I/O Bridge]
|
||||||
|
dram[DRAM Controller]
|
||||||
|
edram[32 MB EDRAM MEM1]
|
||||||
|
rom[512 B SEEPROM]
|
||||||
|
|
||||||
|
sata[SATA IF]
|
||||||
|
exi[EXI]
|
||||||
|
|
||||||
|
subgraph gx[GX]
|
||||||
|
sram[3 MB 1T-SRAM]
|
||||||
|
end
|
||||||
|
|
||||||
|
radeon[AMD Radeon R7xx GX2]
|
||||||
|
|
||||||
|
mem --- gx
|
||||||
|
mem --- radeon
|
||||||
|
|
||||||
|
rom --- mem
|
||||||
|
|
||||||
|
mem --- sata
|
||||||
|
mem --- exi
|
||||||
|
|
||||||
|
dram --- sata
|
||||||
|
dram --- exi
|
||||||
|
end
|
||||||
|
|
||||||
|
ddr3[2 GB DDR3 RAM MEM2]
|
||||||
|
|
||||||
|
mem --- ddr3
|
||||||
|
dram --- ddr3
|
||||||
|
edram --- ddr3
|
||||||
|
|
||||||
|
core1 --- mem
|
||||||
|
|
||||||
|
exi --- rtc
|
||||||
|
rtc{{rtc}}
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
flowchart TB
|
||||||
|
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||||
|
subgraph ibm[IBM Espresso CPU]
|
||||||
|
core0[IBM PowerPC Broadway Core 0]
|
||||||
|
core1[IBM PowerPC Broadway Core 1]
|
||||||
|
core2[IBM PowerPC Broadway Core 2]
|
||||||
|
|
||||||
|
rom[16 KB ROM]
|
||||||
|
|
||||||
|
core0 --- core2
|
||||||
|
|
||||||
|
rom --> core2
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph amd[AMD Latte GPU]
|
||||||
|
mem[Memory & I/O Bridge]
|
||||||
|
dram[DRAM Controller]
|
||||||
|
edram[32 MB EDRAM MEM1]
|
||||||
|
rom[512 B SEEPROM]
|
||||||
|
|
||||||
|
sata[SATA IF]
|
||||||
|
exi[EXI]
|
||||||
|
|
||||||
|
subgraph gx[GX]
|
||||||
|
sram[3 MB 1T-SRAM]
|
||||||
|
end
|
||||||
|
|
||||||
|
radeon[AMD Radeon R7xx GX2]
|
||||||
|
|
||||||
|
mem --- gx
|
||||||
|
mem --- radeon
|
||||||
|
|
||||||
|
rom --- mem
|
||||||
|
|
||||||
|
mem --- sata
|
||||||
|
mem --- exi
|
||||||
|
|
||||||
|
dram --- sata
|
||||||
|
dram --- exi
|
||||||
|
end
|
||||||
|
|
||||||
|
ddr3[2 GB DDR3 RAM MEM2]
|
||||||
|
|
||||||
|
mem --- ddr3
|
||||||
|
dram --- ddr3
|
||||||
|
edram --- ddr3
|
||||||
|
|
||||||
|
core1 --- mem
|
||||||
|
|
||||||
|
exi --- rtc
|
||||||
|
rtc{{rtc}}
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
flowchart LR
|
||||||
|
B1 --be be--x B2
|
||||||
|
B1 --bo bo--o B3
|
||||||
|
subgraph Ugge
|
||||||
|
B2
|
||||||
|
B3
|
||||||
|
subgraph inner
|
||||||
|
B4
|
||||||
|
B5
|
||||||
|
end
|
||||||
|
subgraph inner2
|
||||||
|
subgraph deeper
|
||||||
|
C4
|
||||||
|
C5
|
||||||
|
end
|
||||||
|
C6
|
||||||
|
end
|
||||||
|
|
||||||
|
B4 --> C4
|
||||||
|
|
||||||
|
B3 -- X --> B4
|
||||||
|
B2 --> inner
|
||||||
|
|
||||||
|
C4 --> C5
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph outer
|
||||||
|
B6
|
||||||
|
end
|
||||||
|
B6 --> B5
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
sequenceDiagram
|
||||||
|
Customer->>+Stripe: Makes a payment request
|
||||||
|
Stripe->>+Bank: Forwards the payment request to the bank
|
||||||
|
Bank->>+Customer: Asks for authorization
|
||||||
|
Customer->>+Bank: Provides authorization
|
||||||
|
Bank->>+Stripe: Sends a response with payment details
|
||||||
|
Stripe->>+Merchant: Sends a notification of payment receipt
|
||||||
|
Merchant->>+Stripe: Confirms the payment
|
||||||
|
Stripe->>+Customer: Sends a confirmation of payment
|
||||||
|
Customer->>+Merchant: Receives goods or services
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
mindmap
|
||||||
|
root((mindmap))
|
||||||
|
Origins
|
||||||
|
Long history
|
||||||
|
::icon(fa fa-book)
|
||||||
|
Popularisation
|
||||||
|
British popular psychology author Tony Buzan
|
||||||
|
Research
|
||||||
|
On effectiveness<br/>and features
|
||||||
|
On Automatic creation
|
||||||
|
Uses
|
||||||
|
Creative techniques
|
||||||
|
Strategic planning
|
||||||
|
Argument mapping
|
||||||
|
Tools
|
||||||
|
Pen and paper
|
||||||
|
Mermaid
|
||||||
|
</pre>
|
||||||
|
<br />
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
example-diagram
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<!-- <div id="cy"></div> -->
|
||||||
|
<!-- <script src="http://localhost:9000/packages/mermaid-mindmap/dist/mermaid-mindmap-detector.js"></script> -->
|
||||||
|
<!-- <script src="./mermaid-example-diagram-detector.js"></script> -->
|
||||||
|
<!-- <script src="//cdn.jsdelivr.net/npm/mermaid@9.1.7/dist/mermaid.min.js"></script> -->
|
||||||
|
<!-- <script src="./mermaid.js"></script> -->
|
||||||
|
|
||||||
|
<scrpt>
|
||||||
|
// import mindmap from '../../packages/mermaid-mindmap/src/detector'; // import example from
|
||||||
|
'../../packages/mermaid-example-diagram/src/mermaid-example-diagram.core.mjs'; import mermaid
|
||||||
|
from './mermaid.esm.mjs'; // await mermaid.registerExternalDiagrams([example]);
|
||||||
|
mermaid.parseError = function (err, hash) { // console.error('Mermaid error: ', err); };
|
||||||
|
mermaid.initialize({ // theme: 'forest', startOnLoad: true, logLevel: 0, flowchart: { //
|
||||||
|
defaultRenderer: 'elk', useMaxWidth: false, // htmlLabels: false, htmlLabels: true, }, //
|
||||||
|
htmlLabels: false, gantt: { useMaxWidth: false, }, useMaxWidth: false, }); function callback()
|
||||||
|
{ alert('It worked'); } mermaid.parseError = function (err, hash) { console.error('In parse
|
||||||
|
error:'); console.error(err); }; // mermaid.test1('first_slow', 1200).then((r) =>
|
||||||
|
console.info(r)); // mermaid.test1('second_fast', 200).then((r) => console.info(r)); //
|
||||||
|
mermaid.test1('third_fast', 200).then((r) => console.info(r)); // mermaid.test1('forth_slow',
|
||||||
|
1200).then((r) => console.info(r));
|
||||||
|
</scrpt>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="https://cdn.jsdelivr.net/npm/mermaid@10.2.0/dist/mermaid.min.js"
|
||||||
|
></script>
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10.2.0/dist/mermaid.min.js';
|
||||||
|
(function () {
|
||||||
|
mermaid.initialize({ startOnLoad: false });
|
||||||
|
const elements = document.getElementsByClassName('mermaid');
|
||||||
|
console.log(elements);
|
||||||
|
let id = 0;
|
||||||
|
[...elements].forEach((elem) => {
|
||||||
|
const insertSvg = function (svgCode) {
|
||||||
|
elem.innerHTML = svgCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(atob(elem.innerText));
|
||||||
|
|
||||||
|
mermaid.render(`graphDiv-${id++}`, atob(elem.innerText), insertSvg);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -4,7 +4,7 @@
|
|||||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/font-awesome.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||||
@@ -14,33 +14,45 @@
|
|||||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
/* background: rgb(221, 208, 208); */
|
/* background: rgb(221, 208, 208); */
|
||||||
background: #333;
|
/* background: #333; */
|
||||||
font-family: 'Arial';
|
font-family: 'Arial';
|
||||||
/* font-size: 18px !important; */
|
/* font-size: 18px !important; */
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
.mermaid {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
.mermaid2 {
|
.mermaid2 {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.mermaid svg {
|
.mermaid svg {
|
||||||
/* font-size: 18px !important; */
|
/* font-size: 18px !important; */
|
||||||
/* background-color: #efefef; */
|
|
||||||
background-color: #333;
|
/* background-color: #efefef;
|
||||||
background-image: radial-gradient(#333 51%, transparent 91%),
|
background-image: radial-gradient(#fff 51%, transparent 91%),
|
||||||
radial-gradient(#333 51%, transparent 91%);
|
radial-gradient(#fff 51%, transparent 91%);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-position: 0 0, 10px 10px;
|
background-position:
|
||||||
background-repeat: repeat;
|
0 0,
|
||||||
border: 2px solid rgb(131, 142, 205);
|
10px 10px;
|
||||||
|
background-repeat: repeat; */
|
||||||
}
|
}
|
||||||
.malware {
|
.malware {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -58,582 +70,198 @@
|
|||||||
font-size: 72px;
|
font-size: 72px;
|
||||||
}
|
}
|
||||||
/* tspan {
|
/* tspan {
|
||||||
font-size: 6px !important;
|
font-size: 6px !important;
|
||||||
} */
|
} */
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<pre id="diagram" class="mermaid">
|
<pre id="diagram" class="mermaid2">
|
||||||
|
%%{
|
||||||
|
init: {
|
||||||
|
"theme":"base",
|
||||||
|
"fontFamily": "Kalam",
|
||||||
|
"themeVariables": {
|
||||||
|
"background": "#FFFFFF",
|
||||||
|
"primaryColor": "#7bdfa7",
|
||||||
|
"primaryTextColor": "#3c3c3b",
|
||||||
|
"secondaryColor": "#642470",
|
||||||
|
"secondaryTextColor": "#3c3c3b",
|
||||||
|
"tertiaryColor": "#1c736D",
|
||||||
|
"tertiaryTextColor": "#3c3c3b",
|
||||||
|
"noteBkgColor": "#9fd8ef",
|
||||||
|
"loopTextColor": "#636362",
|
||||||
|
"labelBoxBkgColor": "#642470",
|
||||||
|
"labelBoxBorderColor": "#642470",
|
||||||
|
"labelTextColor": "#d4d4d4",
|
||||||
|
"signalTextColor": "#636362",
|
||||||
|
"signalColor": "#642470"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}%%
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
alt apa
|
|
||||||
Alice->>+John: Hello John, how are you?
|
Alice->>+John: Hello John, how are you?
|
||||||
Alice->>+John: John, can you hear me?
|
Alice->>+John: John, can you hear me?
|
||||||
note right of Alice: John thinks\nabout it
|
|
||||||
John-->>-Alice: Hi Alice, I can hear you!
|
John-->>-Alice: Hi Alice, I can hear you!
|
||||||
John-->>-Alice: I feel great!
|
John-->>-Alice: I feel great!
|
||||||
end
|
</pre
|
||||||
|
>
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
<pre id="diagram" class="mermaid2">
|
||||||
%%{ init: {}}%%
|
%%{
|
||||||
|
init: {
|
||||||
|
"theme":"base",
|
||||||
|
"fontFamily": "Forth Bold",
|
||||||
|
"themeVariables": {
|
||||||
|
"background": "#FFFFFF",
|
||||||
|
"primaryColor": "#7bdfa7",
|
||||||
|
"primaryTextColor": "#3c3c3b",
|
||||||
|
"secondaryColor": "#642470",
|
||||||
|
"secondaryTextColor": "#3c3c3b",
|
||||||
|
"tertiaryColor": "#1c736D",
|
||||||
|
"tertiaryTextColor": "#3c3c3b",
|
||||||
|
"noteBkgColor": "#9fd8ef",
|
||||||
|
"loopTextColor": "#636362",
|
||||||
|
"labelBoxBkgColor": "#642470",
|
||||||
|
"labelBoxBorderColor": "#642470",
|
||||||
|
"labelTextColor": "#d4d4d4",
|
||||||
|
"signalTextColor": "#636362",
|
||||||
|
"signalColor": "#642470"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}%%
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant H as H
|
Alice->>+John: Hello John, how are you?
|
||||||
participant Alice as Alice
|
Alice->>+John: John, can you hear me?
|
||||||
participant Bob as Bob
|
John-->>-Alice: Hi Alice, I can hear you!
|
||||||
|
John-->>-Alice: I feel great!
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
|
||||||
Alice ->> Bob: Hello Bob, how are you ?
|
<pre id="diagram" class="mermaid">
|
||||||
Bob ->> Alice: Fine, thank you. And you?
|
%%{init: {"layout": "elk", "mergeEdges": true} }%%
|
||||||
create participant Carl as Carl
|
stateDiagram
|
||||||
Alice ->> Carl: Hi Carl!
|
direction TB
|
||||||
create actor D as Donald
|
T00 --> T0
|
||||||
Carl ->> D: Hi!
|
T00 --> T1
|
||||||
D ->> H: sss
|
</pre
|
||||||
destroy Carl
|
>
|
||||||
Alice -x Carl: We are too many
|
<pre id="diagram" class="mermaid">
|
||||||
destroy Bob
|
%%{init: {"layout": "elk", "mergeEdges": false, "elk.nodePlacement.strategy": "NETWORK_SIMPLEX"} }%%
|
||||||
Bob ->> Alice: I agree
|
stateDiagram
|
||||||
|
State T0 {
|
||||||
</pre>
|
direction LR
|
||||||
<pre id="diagram" class="mermaid2">
|
A --> B
|
||||||
flowchart
|
}
|
||||||
A --> B
|
State T1 {
|
||||||
|
[*] --> NumLockOff
|
||||||
</pre>
|
NumLockOff --> NumLockOn : EvNumLockPressed
|
||||||
<pre id="diagram" class="mermaid2">
|
NumLockOn --> NumLockOff : EvNumLockPressed
|
||||||
block-beta
|
}
|
||||||
blockArrowId<["Label"]>(right)
|
</pre
|
||||||
blockArrowId2<["Label"]>(left)
|
|
||||||
blockArrowId3<["Label"]>(up)
|
|
||||||
blockArrowId4<["Label"]>(down)
|
|
||||||
blockArrowId5<["Label"]>(x)
|
|
||||||
blockArrowId6<["Label"]>(y)
|
|
||||||
blockArrowId6<["Label"]>(x, down)
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
block:e:4
|
|
||||||
columns 2
|
|
||||||
f
|
|
||||||
g
|
|
||||||
end
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
block:e:4
|
|
||||||
columns 2
|
|
||||||
f
|
|
||||||
g
|
|
||||||
h
|
|
||||||
end
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 4
|
|
||||||
a b c d
|
|
||||||
block:e:4
|
|
||||||
columns 2
|
|
||||||
f
|
|
||||||
g
|
|
||||||
h
|
|
||||||
end
|
|
||||||
i:4
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart LR
|
|
||||||
X-- "y" -->z
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 5
|
|
||||||
A space B
|
|
||||||
A --x B
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 3
|
|
||||||
a["A wide one"] b:2 c:2 d
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
block:e
|
|
||||||
f
|
|
||||||
end
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 3
|
|
||||||
a:3
|
|
||||||
block:e:3
|
|
||||||
f
|
|
||||||
end
|
|
||||||
g
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 3
|
|
||||||
a:3
|
|
||||||
block:e:3
|
|
||||||
f
|
|
||||||
g
|
|
||||||
end
|
|
||||||
h
|
|
||||||
i
|
|
||||||
j
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 3
|
|
||||||
a b:2
|
|
||||||
block:e:3
|
|
||||||
f
|
|
||||||
end
|
|
||||||
g h i
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 3
|
|
||||||
a b c
|
|
||||||
e:3
|
|
||||||
f g h
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 1
|
|
||||||
db(("DB"))
|
|
||||||
blockArrowId6<[" "]>(down)
|
|
||||||
block:ID
|
|
||||||
A
|
|
||||||
B["A wide one in the middle"]
|
|
||||||
C
|
|
||||||
end
|
|
||||||
space
|
|
||||||
D
|
|
||||||
ID --> D
|
|
||||||
C --> D
|
|
||||||
style B fill:#f9F,stroke:#333,stroke-width:4px
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
columns 5
|
|
||||||
A1:3
|
|
||||||
A2:1
|
|
||||||
A3
|
|
||||||
B1 B2 B3:3
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
block
|
|
||||||
D
|
|
||||||
E
|
|
||||||
end
|
|
||||||
db("This is the text in the box")
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
|
|
||||||
block
|
|
||||||
D
|
|
||||||
end
|
|
||||||
A["A: I am a wide one"]
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
A["square"]
|
|
||||||
B("rounded")
|
|
||||||
C(("circle"))
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
A>"rect_left_inv_arrow"]
|
|
||||||
B{"diamond"}
|
|
||||||
C{{"hexagon"}}
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
A(["stadium"])
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
%% A[["subroutine"]]
|
|
||||||
%% B[("cylinder")]
|
|
||||||
C>"surprise"]
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
block-beta
|
|
||||||
A[/"lean right"/]
|
|
||||||
B[\"lean left"\]
|
|
||||||
C[/"trapezoid"\]
|
|
||||||
D[\"trapezoid"/]
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart
|
|
||||||
B
|
|
||||||
style B fill:#f9F,stroke:#333,stroke-width:4px
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart LR
|
|
||||||
a1 -- apa --> b1
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart RL
|
|
||||||
subgraph "`one`"
|
|
||||||
id
|
|
||||||
end
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart RL
|
|
||||||
subgraph "`one`"
|
|
||||||
a1 -- l1 --> a2
|
|
||||||
a1 -- l2 --> a2
|
|
||||||
end
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart
|
|
||||||
id["`A root with a long text that wraps to keep the node size in check. A root with a long text that wraps to keep the node size in check`"]</pre
|
|
||||||
>
|
>
|
||||||
<pre id="diagram" class="mermaid2">
|
<pre id="diagram" class="mermaid2">
|
||||||
flowchart LR
|
%%{init: {"layout": "dagre", "mergeEdges": true} }%%
|
||||||
A[A text that needs to be wrapped wraps to another line]
|
stateDiagram
|
||||||
B[A text that needs to be<br/>wrapped wraps to another line]
|
direction TB
|
||||||
C["`A text that needs to be wrapped to another line`"]</pre>
|
State T1 {
|
||||||
<pre id="diagram" class="mermaid2">
|
T11
|
||||||
flowchart LR
|
}
|
||||||
C["`A text
|
</pre
|
||||||
that needs
|
|
||||||
to be wrapped
|
|
||||||
in another
|
|
||||||
way`"]
|
|
||||||
</pre
|
|
||||||
>
|
>
|
||||||
<pre id="diagram" class="mermaid2">
|
<pre id="diagram" class="mermaid2">
|
||||||
classDiagram-v2
|
%%{init: {"layout": "dagre", "mergeEdges": true} }%%
|
||||||
note "I love this diagram!\nDo you love it?"
|
stateDiagram
|
||||||
</pre>
|
State T1 {
|
||||||
|
T21
|
||||||
|
--
|
||||||
|
T22
|
||||||
|
}
|
||||||
|
</pre
|
||||||
|
>
|
||||||
<pre id="diagram" class="mermaid2">
|
<pre id="diagram" class="mermaid2">
|
||||||
stateDiagram-v2
|
%%{init: {"layout": "elk", "mergeEdges": true} }%%
|
||||||
State1: The state with a note with minus - and plus + in it
|
stateDiagram
|
||||||
note left of State1
|
direction TB
|
||||||
|
State T1 {
|
||||||
|
T11
|
||||||
|
}
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
%%{init: {"layout": "elk", "mergeEdges": true} }%%
|
||||||
|
stateDiagram
|
||||||
|
State T1 {
|
||||||
|
T21
|
||||||
|
--
|
||||||
|
T22
|
||||||
|
}
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
%%{init: {"layout": "elk", "mergeEdges": true} }%%
|
||||||
|
stateDiagram
|
||||||
|
[*] --> T1
|
||||||
|
T1 --> T2
|
||||||
|
T1 --> T3
|
||||||
|
T1 --> T4
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
%%{init: {"layout": "elk"} }%%
|
||||||
|
stateDiagram
|
||||||
|
[*] --> T1
|
||||||
|
T1 --> T2
|
||||||
|
T2 --> T3
|
||||||
|
T3 --> T1
|
||||||
|
T1 --> T3
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<pre id="diagram" class="mermaid2">
|
||||||
|
stateDiagram
|
||||||
|
State1: The state with a note
|
||||||
|
note right of State1
|
||||||
Important information! You can write
|
Important information! You can write
|
||||||
notes with . and in them.
|
notes.
|
||||||
end note </pre
|
end note
|
||||||
|
</pre
|
||||||
>
|
>
|
||||||
<pre id="diagram" class="mermaid2">
|
<pre id="diagram" class="mermaid2">
|
||||||
mindmap
|
stateDiagram-v2
|
||||||
root
|
direction LR
|
||||||
Child3(A node with an icon and with a long text that wraps to keep the node size in check)
|
[*] --> Active
|
||||||
</pre
|
|
||||||
|
state Active {
|
||||||
|
direction BT
|
||||||
|
[*] --> Inner
|
||||||
|
Inner --> NumLockOn : EvNumLockPressed
|
||||||
|
}
|
||||||
|
%% Outer --> Inner
|
||||||
|
</pre
|
||||||
>
|
>
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
%%{init: {"theme": "forest"} }%%
|
|
||||||
mindmap
|
|
||||||
id1[**Start2**<br/>end]
|
|
||||||
id2[**Start2**<br />end]
|
|
||||||
%% Another comment
|
|
||||||
id3[**Start2**<br>end] %% Comment
|
|
||||||
id4[**Start2**<br >end<br >the very end]
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
mindmap
|
|
||||||
id1["`**Start2**
|
|
||||||
second line 😎 with long text that is wrapping to the next line`"]
|
|
||||||
id2["`Child **with bold** text`"]
|
|
||||||
id3["`Children of which some
|
|
||||||
is using *italic type of* text`"]
|
|
||||||
id4[Child]
|
|
||||||
id5["`Child
|
|
||||||
Row
|
|
||||||
and another
|
|
||||||
`"]
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
mindmap
|
|
||||||
id1("`**Root**`"]
|
|
||||||
id2["`A formatted text... with **bold** and *italics*`"]
|
|
||||||
id3[Regular labels works as usual]
|
|
||||||
id4["`Emojis and unicode works too: 🤓
|
|
||||||
शान्तिः سلام 和平 `"]
|
|
||||||
|
|
||||||
</pre>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
|
||||||
flowchart TB
|
|
||||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
|
||||||
subgraph ibm[IBM Espresso CPU]
|
|
||||||
core0[IBM PowerPC Broadway Core 0]
|
|
||||||
core1[IBM PowerPC Broadway Core 1]
|
|
||||||
core2[IBM PowerPC Broadway Core 2]
|
|
||||||
|
|
||||||
rom[16 KB ROM]
|
|
||||||
|
|
||||||
core0 --- core2
|
|
||||||
|
|
||||||
rom --> core2
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph amd["`**AMD** Latte GPU`"]
|
|
||||||
mem[Memory & I/O Bridge]
|
|
||||||
dram[DRAM Controller]
|
|
||||||
edram[32 MB EDRAM MEM1]
|
|
||||||
rom[512 B SEEPROM]
|
|
||||||
|
|
||||||
sata[SATA IF]
|
|
||||||
exi[EXI]
|
|
||||||
|
|
||||||
subgraph gx[GX]
|
|
||||||
sram[3 MB 1T-SRAM]
|
|
||||||
end
|
|
||||||
|
|
||||||
radeon[AMD Radeon R7xx GX2]
|
|
||||||
|
|
||||||
mem --- gx
|
|
||||||
mem --- radeon
|
|
||||||
|
|
||||||
rom --- mem
|
|
||||||
|
|
||||||
mem --- sata
|
|
||||||
mem --- exi
|
|
||||||
|
|
||||||
dram --- sata
|
|
||||||
dram --- exi
|
|
||||||
end
|
|
||||||
|
|
||||||
ddr3[2 GB DDR3 RAM MEM2]
|
|
||||||
|
|
||||||
mem --- ddr3
|
|
||||||
dram --- ddr3
|
|
||||||
edram --- ddr3
|
|
||||||
|
|
||||||
core1 --- mem
|
|
||||||
|
|
||||||
exi --- rtc
|
|
||||||
rtc{{rtc}}
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
%%{init: {"flowchart": {"defaultRenderer": "elk", "htmlLabels": false}} }%%
|
|
||||||
flowchart TB
|
|
||||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
|
||||||
subgraph ibm[IBM Espresso CPU]
|
|
||||||
core0[IBM PowerPC Broadway Core 0]
|
|
||||||
core1[IBM PowerPC Broadway Core 1]
|
|
||||||
core2[IBM PowerPC Broadway Core 2]
|
|
||||||
|
|
||||||
rom[16 KB ROM]
|
|
||||||
|
|
||||||
core0 --- core2
|
|
||||||
|
|
||||||
rom --> core2
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph amd["`**AMD** Latte GPU`"]
|
|
||||||
mem[Memory & I/O Bridge]
|
|
||||||
dram[DRAM Controller]
|
|
||||||
edram[32 MB EDRAM MEM1]
|
|
||||||
rom[512 B SEEPROM]
|
|
||||||
|
|
||||||
sata[SATA IF]
|
|
||||||
exi[EXI]
|
|
||||||
|
|
||||||
subgraph gx[GX]
|
|
||||||
sram[3 MB 1T-SRAM]
|
|
||||||
end
|
|
||||||
|
|
||||||
radeon[AMD Radeon R7xx GX2]
|
|
||||||
|
|
||||||
mem --- gx
|
|
||||||
mem --- radeon
|
|
||||||
|
|
||||||
rom --- mem
|
|
||||||
|
|
||||||
mem --- sata
|
|
||||||
mem --- exi
|
|
||||||
|
|
||||||
dram --- sata
|
|
||||||
dram --- exi
|
|
||||||
end
|
|
||||||
|
|
||||||
ddr3[2 GB DDR3 RAM MEM2]
|
|
||||||
|
|
||||||
mem --- ddr3
|
|
||||||
dram --- ddr3
|
|
||||||
edram --- ddr3
|
|
||||||
|
|
||||||
core1 --- mem
|
|
||||||
|
|
||||||
exi --- rtc
|
|
||||||
rtc{{rtc}}
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart TB
|
|
||||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
|
||||||
subgraph ibm[IBM Espresso CPU]
|
|
||||||
core0[IBM PowerPC Broadway Core 0]
|
|
||||||
core1[IBM PowerPC Broadway Core 1]
|
|
||||||
core2[IBM PowerPC Broadway Core 2]
|
|
||||||
|
|
||||||
rom[16 KB ROM]
|
|
||||||
|
|
||||||
core0 --- core2
|
|
||||||
|
|
||||||
rom --> core2
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph amd[AMD Latte GPU]
|
|
||||||
mem[Memory & I/O Bridge]
|
|
||||||
dram[DRAM Controller]
|
|
||||||
edram[32 MB EDRAM MEM1]
|
|
||||||
rom[512 B SEEPROM]
|
|
||||||
|
|
||||||
sata[SATA IF]
|
|
||||||
exi[EXI]
|
|
||||||
|
|
||||||
subgraph gx[GX]
|
|
||||||
sram[3 MB 1T-SRAM]
|
|
||||||
end
|
|
||||||
|
|
||||||
radeon[AMD Radeon R7xx GX2]
|
|
||||||
|
|
||||||
mem --- gx
|
|
||||||
mem --- radeon
|
|
||||||
|
|
||||||
rom --- mem
|
|
||||||
|
|
||||||
mem --- sata
|
|
||||||
mem --- exi
|
|
||||||
|
|
||||||
dram --- sata
|
|
||||||
dram --- exi
|
|
||||||
end
|
|
||||||
|
|
||||||
ddr3[2 GB DDR3 RAM MEM2]
|
|
||||||
|
|
||||||
mem --- ddr3
|
|
||||||
dram --- ddr3
|
|
||||||
edram --- ddr3
|
|
||||||
|
|
||||||
core1 --- mem
|
|
||||||
|
|
||||||
exi --- rtc
|
|
||||||
rtc{{rtc}}
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
flowchart LR
|
|
||||||
B1 --be be--x B2
|
|
||||||
B1 --bo bo--o B3
|
|
||||||
subgraph Ugge
|
|
||||||
B2
|
|
||||||
B3
|
|
||||||
subgraph inner
|
|
||||||
B4
|
|
||||||
B5
|
|
||||||
end
|
|
||||||
subgraph inner2
|
|
||||||
subgraph deeper
|
|
||||||
C4
|
|
||||||
C5
|
|
||||||
end
|
|
||||||
C6
|
|
||||||
end
|
|
||||||
|
|
||||||
B4 --> C4
|
|
||||||
|
|
||||||
B3 -- X --> B4
|
|
||||||
B2 --> inner
|
|
||||||
|
|
||||||
C4 --> C5
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph outer
|
|
||||||
B6
|
|
||||||
end
|
|
||||||
B6 --> B5
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
sequenceDiagram
|
|
||||||
Customer->>+Stripe: Makes a payment request
|
|
||||||
Stripe->>+Bank: Forwards the payment request to the bank
|
|
||||||
Bank->>+Customer: Asks for authorization
|
|
||||||
Customer->>+Bank: Provides authorization
|
|
||||||
Bank->>+Stripe: Sends a response with payment details
|
|
||||||
Stripe->>+Merchant: Sends a notification of payment receipt
|
|
||||||
Merchant->>+Stripe: Confirms the payment
|
|
||||||
Stripe->>+Customer: Sends a confirmation of payment
|
|
||||||
Customer->>+Merchant: Receives goods or services
|
|
||||||
</pre
|
|
||||||
>
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
mindmap
|
|
||||||
root((mindmap))
|
|
||||||
Origins
|
|
||||||
Long history
|
|
||||||
::icon(fa fa-book)
|
|
||||||
Popularisation
|
|
||||||
British popular psychology author Tony Buzan
|
|
||||||
Research
|
|
||||||
On effectiveness<br/>and features
|
|
||||||
On Automatic creation
|
|
||||||
Uses
|
|
||||||
Creative techniques
|
|
||||||
Strategic planning
|
|
||||||
Argument mapping
|
|
||||||
Tools
|
|
||||||
Pen and paper
|
|
||||||
Mermaid
|
|
||||||
</pre>
|
|
||||||
<br />
|
|
||||||
<pre id="diagram" class="mermaid2">
|
|
||||||
example-diagram
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<!-- <div id="cy"></div> -->
|
|
||||||
<!-- <script src="http://localhost:9000/packages/mermaid-mindmap/dist/mermaid-mindmap-detector.js"></script> -->
|
|
||||||
<!-- <script src="./mermaid-example-diagram-detector.js"></script> -->
|
|
||||||
<!-- <script src="//cdn.jsdelivr.net/npm/mermaid@9.1.7/dist/mermaid.min.js"></script> -->
|
|
||||||
<!-- <script src="./mermaid.js"></script> -->
|
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
// import mindmap from '../../packages/mermaid-mindmap/src/detector';
|
|
||||||
// import example from '../../packages/mermaid-example-diagram/src/mermaid-example-diagram.core.mjs';
|
|
||||||
import mermaid from './mermaid.esm.mjs';
|
import mermaid from './mermaid.esm.mjs';
|
||||||
// await mermaid.registerExternalDiagrams([example]);
|
import { layouts } from './mermaid-layout-elk.esm.mjs';
|
||||||
|
mermaid.registerLayoutLoaders(layouts);
|
||||||
mermaid.parseError = function (err, hash) {
|
mermaid.parseError = function (err, hash) {
|
||||||
// console.error('Mermaid error: ', err);
|
console.error('Mermaid error: ', err);
|
||||||
};
|
};
|
||||||
// mermaid.initialize({
|
|
||||||
// // theme: 'forest',
|
|
||||||
// startOnLoad: true,
|
|
||||||
// logLevel: 0,
|
|
||||||
// flowchart: {
|
|
||||||
// // defaultRenderer: 'elk',
|
|
||||||
// useMaxWidth: false,
|
|
||||||
// // htmlLabels: false,
|
|
||||||
// htmlLabels: true,
|
|
||||||
// },
|
|
||||||
// // htmlLabels: false,
|
|
||||||
// gantt: {
|
|
||||||
// useMaxWidth: false,
|
|
||||||
// },
|
|
||||||
// useMaxWidth: false,
|
|
||||||
// });
|
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
theme: 'dark',
|
theme: 'base',
|
||||||
startOnLoad: true,
|
handdrawnSeed: 12,
|
||||||
|
look: 'handdrawn',
|
||||||
|
'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||||
|
// layout: 'dagre',
|
||||||
|
layout: 'elk',
|
||||||
|
flowchart: { titleTopMargin: 10 },
|
||||||
|
// fontFamily: 'Caveat',
|
||||||
|
// fontFamily: 'Kalam',
|
||||||
|
fontFamily: 'courier',
|
||||||
|
sequence: {
|
||||||
|
actorFontFamily: 'courier',
|
||||||
|
noteFontFamily: 'courier',
|
||||||
|
messageFontFamily: 'courier',
|
||||||
|
},
|
||||||
|
fontSize: 12,
|
||||||
logLevel: 0,
|
logLevel: 0,
|
||||||
});
|
});
|
||||||
function callback() {
|
function callback() {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
1270
cypress/platform/state-refactor.html
Normal file
1270
cypress/platform/state-refactor.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -118,7 +118,7 @@ The siteConfig
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[config.ts:218](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L218)
|
[config.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L221)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
43
packages/mermaid-layout-elk/package.json
Normal file
43
packages/mermaid-layout-elk/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@mermaid-js/layout-elk",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "ELK layout engine for mermaid",
|
||||||
|
"module": "dist/mermaid-layout-elk.core.mjs",
|
||||||
|
"types": "dist/packages/mermaid-layout-elk/src/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/mermaid-layout-elk.core.mjs",
|
||||||
|
"types": "./dist/packages/mermaid-layout-elk/src/index.d.ts"
|
||||||
|
},
|
||||||
|
"./*": "./*"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"diagram",
|
||||||
|
"markdown",
|
||||||
|
"elk",
|
||||||
|
"mermaid"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "pnpm -w run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mermaid-js/mermaid"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
"Knut Sveidqvist",
|
||||||
|
"Sidharth Vinod"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"elkjs": "^0.9.3",
|
||||||
|
"d3": "^7.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"mermaid": "workspace:^"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
25
packages/mermaid-layout-elk/src/find-common-ancestor.ts
Normal file
25
packages/mermaid-layout-elk/src/find-common-ancestor.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export interface TreeData {
|
||||||
|
parentById: Record<string, string>;
|
||||||
|
childrenById: Record<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findCommonAncestor = (id1: string, id2: string, treeData: TreeData) => {
|
||||||
|
const { parentById } = treeData;
|
||||||
|
const visited = new Set();
|
||||||
|
let currentId = id1;
|
||||||
|
while (currentId) {
|
||||||
|
visited.add(currentId);
|
||||||
|
if (currentId === id2) {
|
||||||
|
return currentId;
|
||||||
|
}
|
||||||
|
currentId = parentById[currentId];
|
||||||
|
}
|
||||||
|
currentId = id2;
|
||||||
|
while (currentId) {
|
||||||
|
if (visited.has(currentId)) {
|
||||||
|
return currentId;
|
||||||
|
}
|
||||||
|
currentId = parentById[currentId];
|
||||||
|
}
|
||||||
|
return 'root';
|
||||||
|
};
|
17
packages/mermaid-layout-elk/src/layouts.ts
Normal file
17
packages/mermaid-layout-elk/src/layouts.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { LayoutLoaderDefinition } from 'mermaid';
|
||||||
|
|
||||||
|
const loader = async () => await import(`./render.js`);
|
||||||
|
const algos = ['elk.stress', 'elk.force', 'elk.mrtree', 'elk.sporeOverlap'];
|
||||||
|
|
||||||
|
export const layouts: LayoutLoaderDefinition[] = [
|
||||||
|
{
|
||||||
|
name: 'elk',
|
||||||
|
loader,
|
||||||
|
algorithm: 'elk.layered',
|
||||||
|
},
|
||||||
|
...algos.map((algo) => ({
|
||||||
|
name: algo,
|
||||||
|
loader,
|
||||||
|
algorithm: algo,
|
||||||
|
})),
|
||||||
|
];
|
588
packages/mermaid-layout-elk/src/render.ts
Normal file
588
packages/mermaid-layout-elk/src/render.ts
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
// @ts-nocheck File not ready to check types
|
||||||
|
import { curveLinear } from 'd3';
|
||||||
|
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||||
|
import mermaid from 'mermaid';
|
||||||
|
import { findCommonAncestor } from './find-common-ancestor.js';
|
||||||
|
import config from '../../mermaid/src/defaultConfig';
|
||||||
|
|
||||||
|
const {
|
||||||
|
common,
|
||||||
|
getConfig,
|
||||||
|
insertCluster,
|
||||||
|
insertEdge,
|
||||||
|
insertEdgeLabel,
|
||||||
|
insertMarkers,
|
||||||
|
insertNode,
|
||||||
|
interpolateToCurve,
|
||||||
|
labelHelper,
|
||||||
|
log,
|
||||||
|
positionEdgeLabel,
|
||||||
|
} = mermaid.internalHelpers;
|
||||||
|
|
||||||
|
const nodeDb = {};
|
||||||
|
const portPos = {};
|
||||||
|
const clusterDb = {};
|
||||||
|
|
||||||
|
export const addVertex = async (nodeEl, graph, nodeArr, node) => {
|
||||||
|
const labelData = { width: 0, height: 0 };
|
||||||
|
const ports = [
|
||||||
|
{
|
||||||
|
id: node.id + '-west',
|
||||||
|
layoutOptions: {
|
||||||
|
'port.side': 'WEST',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: node.id + '-east',
|
||||||
|
layoutOptions: {
|
||||||
|
'port.side': 'EAST',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: node.id + '-south',
|
||||||
|
layoutOptions: {
|
||||||
|
'port.side': 'SOUTH',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: node.id + '-north',
|
||||||
|
layoutOptions: {
|
||||||
|
'port.side': 'NORTH',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let boundingBox;
|
||||||
|
const child = {
|
||||||
|
...node,
|
||||||
|
ports: node.shape === 'diamond' ? ports : [],
|
||||||
|
};
|
||||||
|
graph.children.push(child);
|
||||||
|
nodeDb[node.id] = child;
|
||||||
|
|
||||||
|
// // Add the element to the DOM
|
||||||
|
if (!node.isGroup) {
|
||||||
|
const childNodeEl = await insertNode(nodeEl, node, node.dir);
|
||||||
|
boundingBox = childNodeEl.node().getBBox();
|
||||||
|
child.domId = childNodeEl;
|
||||||
|
child.width = boundingBox.width;
|
||||||
|
child.height = boundingBox.height;
|
||||||
|
} else {
|
||||||
|
child.children = [];
|
||||||
|
await addVertices(nodeEl, nodeArr, child, node.id);
|
||||||
|
|
||||||
|
if (node.label) {
|
||||||
|
const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true);
|
||||||
|
labelData.width = bbox.width;
|
||||||
|
labelData.wrappingWidth = getConfig().flowchart.wrappingWidth;
|
||||||
|
labelData.height = bbox.height - 8;
|
||||||
|
labelData.labelNode = shapeSvg.node();
|
||||||
|
// We need the label hight to be able to size the subgraph;
|
||||||
|
shapeSvg.remove();
|
||||||
|
} else {
|
||||||
|
// Subgraph without label
|
||||||
|
labelData.width = 0;
|
||||||
|
labelData.height = 0;
|
||||||
|
}
|
||||||
|
child.labelData = labelData;
|
||||||
|
child.domId = nodeEl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addVertices = async function (nodeEl, nodeArr, graph, parentId) {
|
||||||
|
const siblings = nodeArr.filter((node) => node.parentId === parentId);
|
||||||
|
log.info('addVertices DAGA', siblings, parentId);
|
||||||
|
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||||
|
await Promise.all(
|
||||||
|
siblings.map(async (node) => {
|
||||||
|
await addVertex(nodeEl, graph, nodeArr, node);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return graph;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, depth) => {
|
||||||
|
nodeArray.forEach(function (node) {
|
||||||
|
if (node) {
|
||||||
|
nodeDb[node.id] = node;
|
||||||
|
nodeDb[node.id].offset = {
|
||||||
|
posX: node.x + relX,
|
||||||
|
posY: node.y + relY,
|
||||||
|
x: relX,
|
||||||
|
y: relY,
|
||||||
|
depth,
|
||||||
|
width: node.width,
|
||||||
|
height: node.height,
|
||||||
|
};
|
||||||
|
if (node.isGroup) {
|
||||||
|
log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
|
||||||
|
const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph');
|
||||||
|
// TODO use faster way of cloning
|
||||||
|
const clusterNode = JSON.parse(JSON.stringify(node));
|
||||||
|
clusterNode.x = node.offset.posX + node.width / 2;
|
||||||
|
clusterNode.y = node.offset.posY + node.height / 2;
|
||||||
|
const cluster = insertCluster(subgraphEl, clusterNode);
|
||||||
|
|
||||||
|
log.info('Id (UGH)= ', node.shape, node.labels);
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'Id NODE = ',
|
||||||
|
node.id,
|
||||||
|
node.x,
|
||||||
|
node.y,
|
||||||
|
relX,
|
||||||
|
relY,
|
||||||
|
node.domId.node(),
|
||||||
|
`translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})`
|
||||||
|
);
|
||||||
|
node.domId.attr(
|
||||||
|
'transform',
|
||||||
|
`translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nodeArray.forEach(function (node) {
|
||||||
|
if (node && node.isGroup) {
|
||||||
|
drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, depth + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextPort = (node, edgeDirection, graphDirection) => {
|
||||||
|
log.info('getNextPort abc88', { node, edgeDirection, graphDirection });
|
||||||
|
if (!portPos[node]) {
|
||||||
|
switch (graphDirection) {
|
||||||
|
case 'TB':
|
||||||
|
case 'TD':
|
||||||
|
portPos[node] = {
|
||||||
|
inPosition: 'north',
|
||||||
|
outPosition: 'south',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'BT':
|
||||||
|
portPos[node] = {
|
||||||
|
inPosition: 'south',
|
||||||
|
outPosition: 'north',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'RL':
|
||||||
|
portPos[node] = {
|
||||||
|
inPosition: 'east',
|
||||||
|
outPosition: 'west',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'LR':
|
||||||
|
portPos[node] = {
|
||||||
|
inPosition: 'west',
|
||||||
|
outPosition: 'east',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition;
|
||||||
|
|
||||||
|
if (edgeDirection === 'in') {
|
||||||
|
portPos[node].inPosition = getNextPosition(
|
||||||
|
portPos[node].inPosition,
|
||||||
|
edgeDirection,
|
||||||
|
graphDirection
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
portPos[node].outPosition = getNextPosition(
|
||||||
|
portPos[node].outPosition,
|
||||||
|
edgeDirection,
|
||||||
|
graphDirection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSubGraphs = function (nodeArr) {
|
||||||
|
const parentLookupDb = { parentById: {}, childrenById: {} };
|
||||||
|
const subgraphs = nodeArr.filter((node) => node.isGroup);
|
||||||
|
log.info('Subgraphs - ', subgraphs);
|
||||||
|
subgraphs.forEach(function (subgraph) {
|
||||||
|
const children = nodeArr.filter((node) => node.parentId === subgraph.id);
|
||||||
|
children.forEach(function (node) {
|
||||||
|
parentLookupDb.parentById[node.id] = subgraph.id;
|
||||||
|
if (parentLookupDb.childrenById[subgraph.id] === undefined) {
|
||||||
|
parentLookupDb.childrenById[subgraph.id] = [];
|
||||||
|
}
|
||||||
|
parentLookupDb.childrenById[subgraph.id].push(node);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
subgraphs.forEach(function (subgraph) {
|
||||||
|
const data = { id: subgraph.id };
|
||||||
|
if (parentLookupDb.parentById[subgraph.id] !== undefined) {
|
||||||
|
data.parent = parentLookupDb.parentById[subgraph.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parentLookupDb;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEdgeStartEndPoint = (edge, dir) => {
|
||||||
|
let source = edge.start;
|
||||||
|
let target = edge.end;
|
||||||
|
|
||||||
|
// Save the original source and target
|
||||||
|
const sourceId = source;
|
||||||
|
const targetId = target;
|
||||||
|
|
||||||
|
const startNode = nodeDb[edge.start.id];
|
||||||
|
const endNode = nodeDb[edge.end.id];
|
||||||
|
|
||||||
|
if (!startNode || !endNode) {
|
||||||
|
return { source, target };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startNode.shape === 'diamond') {
|
||||||
|
source = `${source}-${getNextPort(source, 'out', dir)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endNode.shape === 'diamond') {
|
||||||
|
target = `${target}-${getNextPort(target, 'in', dir)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the edge to the graph
|
||||||
|
return { source, target, sourceId, targetId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcOffset = function (src, dest, parentLookupDb) {
|
||||||
|
const ancestor = findCommonAncestor(src, dest, parentLookupDb);
|
||||||
|
if (ancestor === undefined || ancestor === 'root') {
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ancestorOffset = nodeDb[ancestor].offset;
|
||||||
|
return { x: ancestorOffset.posX, y: ancestorOffset.posY };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add edges to graph based on parsed graph definition
|
||||||
|
*/
|
||||||
|
export const addEdges = function (dataForLayout, graph, svg) {
|
||||||
|
log.info('abc78 DAGA edges = ', dataForLayout);
|
||||||
|
const edges = dataForLayout.edges;
|
||||||
|
const labelsEl = svg.insert('g').attr('class', 'edgeLabels');
|
||||||
|
const linkIdCnt = {};
|
||||||
|
const dir = dataForLayout.direction || 'DOWN';
|
||||||
|
let defaultStyle;
|
||||||
|
let defaultLabelStyle;
|
||||||
|
|
||||||
|
edges.forEach(function (edge) {
|
||||||
|
// Identify Link
|
||||||
|
const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end;
|
||||||
|
// count the links from+to the same node to give unique id
|
||||||
|
if (linkIdCnt[linkIdBase] === undefined) {
|
||||||
|
linkIdCnt[linkIdBase] = 0;
|
||||||
|
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
|
||||||
|
} else {
|
||||||
|
linkIdCnt[linkIdBase]++;
|
||||||
|
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
|
||||||
|
}
|
||||||
|
const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase];
|
||||||
|
edge.id = linkId;
|
||||||
|
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
|
||||||
|
const linkNameStart = 'LS_' + edge.start;
|
||||||
|
const linkNameEnd = 'LE_' + edge.end;
|
||||||
|
|
||||||
|
const edgeData = { style: '', labelStyle: '' };
|
||||||
|
edgeData.minlen = edge.length || 1;
|
||||||
|
edge.text = edge.label;
|
||||||
|
// Set link type for rendering
|
||||||
|
if (edge.type === 'arrow_open') {
|
||||||
|
edgeData.arrowhead = 'none';
|
||||||
|
} else {
|
||||||
|
edgeData.arrowhead = 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check of arrow types, placed here in order not to break old rendering
|
||||||
|
edgeData.arrowTypeStart = 'arrow_open';
|
||||||
|
edgeData.arrowTypeEnd = 'arrow_open';
|
||||||
|
|
||||||
|
/* eslint-disable no-fallthrough */
|
||||||
|
switch (edge.type) {
|
||||||
|
case 'double_arrow_cross':
|
||||||
|
edgeData.arrowTypeStart = 'arrow_cross';
|
||||||
|
case 'arrow_cross':
|
||||||
|
edgeData.arrowTypeEnd = 'arrow_cross';
|
||||||
|
break;
|
||||||
|
case 'double_arrow_point':
|
||||||
|
edgeData.arrowTypeStart = 'arrow_point';
|
||||||
|
case 'arrow_point':
|
||||||
|
edgeData.arrowTypeEnd = 'arrow_point';
|
||||||
|
break;
|
||||||
|
case 'double_arrow_circle':
|
||||||
|
edgeData.arrowTypeStart = 'arrow_circle';
|
||||||
|
case 'arrow_circle':
|
||||||
|
edgeData.arrowTypeEnd = 'arrow_circle';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = '';
|
||||||
|
let labelStyle = '';
|
||||||
|
|
||||||
|
switch (edge.stroke) {
|
||||||
|
case 'normal':
|
||||||
|
style = 'fill:none;';
|
||||||
|
if (defaultStyle !== undefined) {
|
||||||
|
style = defaultStyle;
|
||||||
|
}
|
||||||
|
if (defaultLabelStyle !== undefined) {
|
||||||
|
labelStyle = defaultLabelStyle;
|
||||||
|
}
|
||||||
|
edgeData.thickness = 'normal';
|
||||||
|
edgeData.pattern = 'solid';
|
||||||
|
break;
|
||||||
|
case 'dotted':
|
||||||
|
edgeData.thickness = 'normal';
|
||||||
|
edgeData.pattern = 'dotted';
|
||||||
|
edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||||
|
break;
|
||||||
|
case 'thick':
|
||||||
|
edgeData.thickness = 'thick';
|
||||||
|
edgeData.pattern = 'solid';
|
||||||
|
edgeData.style = 'stroke-width: 3.5px;fill:none;';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// if (edge.style !== undefined) {
|
||||||
|
// const styles = getStylesFromArray(edge.style);
|
||||||
|
// style = styles.style;
|
||||||
|
// labelStyle = styles.labelStyle;
|
||||||
|
// }
|
||||||
|
|
||||||
|
edgeData.style = edgeData.style += style;
|
||||||
|
edgeData.labelStyle = edgeData.labelStyle += labelStyle;
|
||||||
|
|
||||||
|
const conf = getConfig();
|
||||||
|
if (edge.interpolate !== undefined) {
|
||||||
|
edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
|
||||||
|
} else if (edges.defaultInterpolate !== undefined) {
|
||||||
|
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear);
|
||||||
|
} else {
|
||||||
|
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edge.text === undefined) {
|
||||||
|
if (edge.style !== undefined) {
|
||||||
|
edgeData.arrowheadStyle = 'fill: #333';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edgeData.arrowheadStyle = 'fill: #333';
|
||||||
|
edgeData.labelpos = 'c';
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeData.labelType = edge.labelType;
|
||||||
|
edgeData.label = (edge?.text || '').replace(common.lineBreakRegex, '\n');
|
||||||
|
|
||||||
|
if (edge.style === undefined) {
|
||||||
|
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
|
||||||
|
|
||||||
|
edgeData.id = linkId;
|
||||||
|
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
|
||||||
|
|
||||||
|
const labelEl = insertEdgeLabel(labelsEl, edgeData);
|
||||||
|
|
||||||
|
// calculate start and end points of the edge, note that the source and target
|
||||||
|
// can be modified for shapes that have ports
|
||||||
|
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir);
|
||||||
|
log.debug('abc78 source and target', source, target);
|
||||||
|
// Add the edge to the graph
|
||||||
|
graph.edges.push({
|
||||||
|
id: 'e' + edge.start + edge.end,
|
||||||
|
...edge,
|
||||||
|
sources: [source],
|
||||||
|
targets: [target],
|
||||||
|
sourceId,
|
||||||
|
targetId,
|
||||||
|
labelEl: labelEl,
|
||||||
|
labels: [
|
||||||
|
{
|
||||||
|
width: edgeData.width,
|
||||||
|
height: edgeData.height,
|
||||||
|
orgWidth: edgeData.width,
|
||||||
|
orgHeight: edgeData.height,
|
||||||
|
text: edgeData.label,
|
||||||
|
layoutOptions: {
|
||||||
|
'edgeLabels.inline': 'true',
|
||||||
|
'edgeLabels.placement': 'CENTER',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edgeData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return graph;
|
||||||
|
};
|
||||||
|
|
||||||
|
function dir2ElkDirection(dir) {
|
||||||
|
switch (dir) {
|
||||||
|
case 'LR':
|
||||||
|
return 'RIGHT';
|
||||||
|
case 'RL':
|
||||||
|
return 'LEFT';
|
||||||
|
case 'TB':
|
||||||
|
return 'DOWN';
|
||||||
|
case 'BT':
|
||||||
|
return 'UP';
|
||||||
|
default:
|
||||||
|
return 'DOWN';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIncludeChildrenPolicy(nodeId: string, ancestorId: string) {
|
||||||
|
const node = nodeDb[nodeId];
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node?.layoutOptions === undefined) {
|
||||||
|
node.layoutOptions = {};
|
||||||
|
}
|
||||||
|
node.layoutOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN';
|
||||||
|
if (node.id !== ancestorId) {
|
||||||
|
setIncludeChildrenPolicy(node.parentId, ancestorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const render = async (data4Layout, svg, element, algorithm) => {
|
||||||
|
const elk = new ELK();
|
||||||
|
|
||||||
|
// Add the arrowheads to the svg
|
||||||
|
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||||
|
|
||||||
|
// Setup the graph with the layout options and the data for the layout
|
||||||
|
let elkGraph = {
|
||||||
|
id: 'root',
|
||||||
|
layoutOptions: {
|
||||||
|
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
||||||
|
'elk.algorithm': algorithm,
|
||||||
|
'nodePlacement.strategy': data4Layout.config['elk.nodePlacement.strategy'],
|
||||||
|
'elk.layered.mergeEdges': data4Layout.config.mergeEdges,
|
||||||
|
'elk.direction': 'DOWN',
|
||||||
|
'spacing.baseValue': 30,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info('Drawing flowchart using v4 renderer', elk);
|
||||||
|
|
||||||
|
// Set the direction of the graph based on the parsed information
|
||||||
|
const dir = data4Layout.direction || 'DOWN';
|
||||||
|
elkGraph.layoutOptions['elk.direction'] = dir2ElkDirection(dir);
|
||||||
|
|
||||||
|
// Create the lookup db for the subgraphs and their children to used when creating
|
||||||
|
// the tree structured graph
|
||||||
|
const parentLookupDb = addSubGraphs(data4Layout.nodes);
|
||||||
|
|
||||||
|
// Add elements in the svg to be used to hold the subgraphs container
|
||||||
|
// elements and the nodes
|
||||||
|
const subGraphsEl = svg.insert('g').attr('class', 'subgraphs');
|
||||||
|
const nodeEl = svg.insert('g').attr('class', 'nodes');
|
||||||
|
|
||||||
|
// Add the nodes to the graph, this will entail creating the actual nodes
|
||||||
|
// in order to get the size of the node. You can't get the size of a node
|
||||||
|
// that is not in the dom so we need to add it to the dom, get the size
|
||||||
|
// we will position the nodes when we get the layout from elkjs
|
||||||
|
elkGraph = await addVertices(nodeEl, data4Layout.nodes, elkGraph);
|
||||||
|
// Time for the edges, we start with adding an element in the node to hold the edges
|
||||||
|
const edgesEl = svg.insert('g').attr('class', 'edges edgePath');
|
||||||
|
|
||||||
|
// Add the edges to the elk graph, this will entail creating the actual edges
|
||||||
|
elkGraph = addEdges(data4Layout, elkGraph, svg);
|
||||||
|
|
||||||
|
// Iterate through all nodes and add the top level nodes to the graph
|
||||||
|
const nodes = data4Layout.nodes;
|
||||||
|
nodes.forEach((n) => {
|
||||||
|
const node = nodeDb[n.id];
|
||||||
|
|
||||||
|
// Subgraph
|
||||||
|
if (parentLookupDb.childrenById[node.id] !== undefined) {
|
||||||
|
log.trace('Subgraph XCX', node.id, node);
|
||||||
|
node.labels = [
|
||||||
|
{
|
||||||
|
text: node.labelText,
|
||||||
|
layoutOptions: {
|
||||||
|
'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]',
|
||||||
|
},
|
||||||
|
width: node?.labelData?.width || 0,
|
||||||
|
height: node?.labelData?.height || 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
node.layoutOptions = {
|
||||||
|
'spacing.baseValue': 30,
|
||||||
|
};
|
||||||
|
if (node.dir) {
|
||||||
|
node.layoutOptions = {
|
||||||
|
...node.layoutOptions,
|
||||||
|
'elk.direction': dir2ElkDirection(node.dir),
|
||||||
|
'elk.hierarchyHandling': 'SEPARATE_CHILDREN',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
delete node.x;
|
||||||
|
delete node.y;
|
||||||
|
delete node.width;
|
||||||
|
delete node.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
elkGraph.edges.forEach((edge) => {
|
||||||
|
const source = edge.sources[0];
|
||||||
|
const target = edge.targets[0];
|
||||||
|
|
||||||
|
if (nodeDb[source].parentId !== nodeDb[target].parentId) {
|
||||||
|
const ancestorId = findCommonAncestor(source, target, parentLookupDb);
|
||||||
|
// an edge that breaks a subgraph has been identified, set configuration accordingly
|
||||||
|
setIncludeChildrenPolicy(source, ancestorId);
|
||||||
|
setIncludeChildrenPolicy(target, ancestorId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.trace('before layout', JSON.stringify(elkGraph, null, 2));
|
||||||
|
const g = await elk.layout(elkGraph);
|
||||||
|
log.info('after layout', JSON.stringify(g));
|
||||||
|
|
||||||
|
// debugger;
|
||||||
|
drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
|
||||||
|
g.edges?.map((edge) => {
|
||||||
|
// (elem, edge, clusterDb, diagramType, graph, id)
|
||||||
|
edge.start = nodeDb[edge.sources[0]];
|
||||||
|
edge.end = nodeDb[edge.targets[0]];
|
||||||
|
const sourceId = edge.start.id;
|
||||||
|
const targetId = edge.end.id;
|
||||||
|
|
||||||
|
const offset = calcOffset(sourceId, targetId, parentLookupDb);
|
||||||
|
|
||||||
|
if (edge.sections) {
|
||||||
|
const src = edge.sections[0].startPoint;
|
||||||
|
const dest = edge.sections[0].endPoint;
|
||||||
|
const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : [];
|
||||||
|
|
||||||
|
const segPoints = segments.map((segment) => {
|
||||||
|
return { x: segment.x + offset.x, y: segment.y + offset.y };
|
||||||
|
});
|
||||||
|
edge.points = [
|
||||||
|
{ x: src.x + offset.x, y: src.y + offset.y },
|
||||||
|
...segPoints,
|
||||||
|
{ x: dest.x + offset.x, y: dest.y + offset.y },
|
||||||
|
];
|
||||||
|
const paths = insertEdge(
|
||||||
|
edgesEl,
|
||||||
|
edge,
|
||||||
|
clusterDb,
|
||||||
|
data4Layout.type,
|
||||||
|
g,
|
||||||
|
data4Layout.diagramId
|
||||||
|
);
|
||||||
|
|
||||||
|
edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2;
|
||||||
|
edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;
|
||||||
|
positionEdgeLabel(edge, paths);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
10
packages/mermaid-layout-elk/tsconfig.json
Normal file
10
packages/mermaid-layout-elk/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"types": ["vitest/importMeta", "vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*.ts"],
|
||||||
|
"typeRoots": ["./src/types"]
|
||||||
|
}
|
@@ -77,11 +77,11 @@
|
|||||||
"dagre-d3-es": "7.0.10",
|
"dagre-d3-es": "7.0.10",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dompurify": "^3.0.11",
|
"dompurify": "^3.0.11",
|
||||||
"elkjs": "^0.9.2",
|
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"khroma": "^2.1.0",
|
"khroma": "^2.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mdast-util-from-markdown": "^2.0.0",
|
"mdast-util-from-markdown": "^2.0.0",
|
||||||
|
"roughjs": "^4.6.6",
|
||||||
"stylis": "^4.3.1",
|
"stylis": "^4.3.1",
|
||||||
"ts-dedent": "^2.2.0",
|
"ts-dedent": "^2.2.0",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
|
@@ -190,7 +190,10 @@ export const addDirective = (directive: MermaidConfig) => {
|
|||||||
|
|
||||||
// If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables
|
// If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables
|
||||||
if (directive.fontFamily && (!directive.themeVariables || !directive.themeVariables.fontFamily)) {
|
if (directive.fontFamily && (!directive.themeVariables || !directive.themeVariables.fontFamily)) {
|
||||||
directive.themeVariables = { fontFamily: directive.fontFamily };
|
directive.themeVariables = {
|
||||||
|
...directive.themeVariables,
|
||||||
|
fontFamily: directive.fontFamily,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
directives.push(directive);
|
directives.push(directive);
|
||||||
|
@@ -64,6 +64,21 @@ export interface MermaidConfig {
|
|||||||
theme?: 'default' | 'forest' | 'dark' | 'neutral' | 'null';
|
theme?: 'default' | 'forest' | 'dark' | 'neutral' | 'null';
|
||||||
themeVariables?: any;
|
themeVariables?: any;
|
||||||
themeCSS?: string;
|
themeCSS?: string;
|
||||||
|
/**
|
||||||
|
* Defines which main look to use for the diagram.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
look?: 'classic' | 'handdrawn' | 'slick';
|
||||||
|
/**
|
||||||
|
* Defines the seed to be used when using handdrawn look. This is important for the automated tests as they will always find differences without the seed. The default value is 0 which gives a random seed.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
handdrawnSeed?: number;
|
||||||
|
/**
|
||||||
|
* Defines which layout algorithm to use for rendering the diagram.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
layout?: string;
|
||||||
/**
|
/**
|
||||||
* The maximum allowed size of the users text diagram
|
* The maximum allowed size of the users text diagram
|
||||||
*/
|
*/
|
||||||
@@ -73,6 +88,16 @@ export interface MermaidConfig {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
maxEdges?: number;
|
maxEdges?: number;
|
||||||
|
/**
|
||||||
|
* Elk specific option that allows edge egdes to share path where it convenient. It can make for pretty diagrams but can also make it harder to read the diagram.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'elk.mergeEdges'?: boolean;
|
||||||
|
/**
|
||||||
|
* Elk specific option affedcting how nodes are placed.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'elk.nodePlacement.strategy'?: 'SIMPLE' | 'NETWORK_SIMPLEX' | 'LINEAR_SEGMENTS' | 'BRANDES_KOEPF';
|
||||||
darkMode?: boolean;
|
darkMode?: boolean;
|
||||||
htmlLabels?: boolean;
|
htmlLabels?: boolean;
|
||||||
/**
|
/**
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { select } from 'd3';
|
import { select } from 'd3';
|
||||||
import { log } from '../logger.js';
|
|
||||||
import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util.js';
|
|
||||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||||
import intersect from './intersect/index.js';
|
|
||||||
import createLabel from './createLabel.js';
|
|
||||||
import note from './shapes/note.js';
|
|
||||||
import { evaluate } from '../diagrams/common/common.js';
|
import { evaluate } from '../diagrams/common/common.js';
|
||||||
|
import { log } from '../logger.js';
|
||||||
import { getArrowPoints } from './blockArrowHelper.js';
|
import { getArrowPoints } from './blockArrowHelper.js';
|
||||||
|
import createLabel from './createLabel.js';
|
||||||
|
import intersect from './intersect/index.js';
|
||||||
|
import note from './shapes/note.js';
|
||||||
|
import { insertPolygonShape, labelHelper, updateNodeBounds } from './shapes/util.js';
|
||||||
|
|
||||||
const formatClass = (str) => {
|
const formatClass = (str) => {
|
||||||
if (str) {
|
if (str) {
|
||||||
@@ -395,6 +395,7 @@ const rect = async (parent, node) => {
|
|||||||
// add the rect
|
// add the rect
|
||||||
const rect = shapeSvg.insert('rect', ':first-child');
|
const rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
|
||||||
|
// console.log('Rect node:', node, 'bbox:', bbox, 'halfPadding:', halfPadding, 'node.padding:', node.padding);
|
||||||
// const totalWidth = bbox.width + node.padding * 2;
|
// const totalWidth = bbox.width + node.padding * 2;
|
||||||
// const totalHeight = bbox.height + node.padding * 2;
|
// const totalHeight = bbox.height + node.padding * 2;
|
||||||
const totalWidth = node.positioned ? node.width : bbox.width + node.padding;
|
const totalWidth = node.positioned ? node.width : bbox.width + node.padding;
|
||||||
@@ -1134,6 +1135,8 @@ export const insertNode = async (elem, node, dir) => {
|
|||||||
let newEl;
|
let newEl;
|
||||||
let el;
|
let el;
|
||||||
|
|
||||||
|
// console.log('insertNode element', elem, elem.node());
|
||||||
|
// debugger;
|
||||||
// Add link when appropriate
|
// Add link when appropriate
|
||||||
if (node.link) {
|
if (node.link) {
|
||||||
let target;
|
let target;
|
||||||
|
@@ -15,6 +15,8 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
|||||||
classes = _classes;
|
classes = _classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log('parentY', parent.node());
|
||||||
|
|
||||||
// Add outer g element
|
// Add outer g element
|
||||||
const shapeSvg = parent
|
const shapeSvg = parent
|
||||||
.insert('g')
|
.insert('g')
|
||||||
@@ -33,6 +35,7 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const textNode = label.node();
|
const textNode = label.node();
|
||||||
|
// console.log('parentX', parent, 'node',node,'labelText',labelText, textNode, node.labelType, 'label', label.node());
|
||||||
let text;
|
let text;
|
||||||
if (node.labelType === 'markdown') {
|
if (node.labelType === 'markdown') {
|
||||||
// text = textNode;
|
// text = textNode;
|
||||||
|
@@ -29,6 +29,8 @@ export const setConf = function (cnf) {
|
|||||||
*/
|
*/
|
||||||
export const addVertices = async function (vert, g, svgId, root, doc, diagObj) {
|
export const addVertices = async function (vert, g, svgId, root, doc, diagObj) {
|
||||||
const svg = root.select(`[id="${svgId}"]`);
|
const svg = root.select(`[id="${svgId}"]`);
|
||||||
|
// console.log('SVG:', svg, svg.node(), 'root:', root, root.node());
|
||||||
|
|
||||||
const keys = vert.keys();
|
const keys = vert.keys();
|
||||||
|
|
||||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||||
|
394
packages/mermaid/src/diagrams/state/dataFetcher.js
Normal file
394
packages/mermaid/src/diagrams/state/dataFetcher.js
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import common from '../common/common.js';
|
||||||
|
import {
|
||||||
|
CSS_DIAGRAM_CLUSTER,
|
||||||
|
CSS_DIAGRAM_CLUSTER_ALT,
|
||||||
|
CSS_DIAGRAM_NOTE,
|
||||||
|
CSS_DIAGRAM_STATE,
|
||||||
|
CSS_EDGE,
|
||||||
|
CSS_EDGE_NOTE_EDGE,
|
||||||
|
DEFAULT_NESTED_DOC_DIR,
|
||||||
|
DEFAULT_STATE_TYPE,
|
||||||
|
DIVIDER_TYPE,
|
||||||
|
DOMID_STATE,
|
||||||
|
DOMID_TYPE_SPACER,
|
||||||
|
G_EDGE_ARROWHEADSTYLE,
|
||||||
|
G_EDGE_LABELPOS,
|
||||||
|
G_EDGE_LABELTYPE,
|
||||||
|
G_EDGE_STYLE,
|
||||||
|
G_EDGE_THICKNESS,
|
||||||
|
NOTE,
|
||||||
|
NOTE_ID,
|
||||||
|
PARENT,
|
||||||
|
PARENT_ID,
|
||||||
|
SHAPE_DIVIDER,
|
||||||
|
SHAPE_END,
|
||||||
|
SHAPE_GROUP,
|
||||||
|
SHAPE_NOTE,
|
||||||
|
SHAPE_NOTEGROUP,
|
||||||
|
SHAPE_START,
|
||||||
|
SHAPE_STATE,
|
||||||
|
SHAPE_STATE_WITH_DESC,
|
||||||
|
STMT_RELATION,
|
||||||
|
STMT_STATE,
|
||||||
|
} from './stateCommon.js';
|
||||||
|
|
||||||
|
// List of nodes created from the parsed diagram statement items
|
||||||
|
let nodeDb = {};
|
||||||
|
|
||||||
|
let graphItemCount = 0; // used to construct ids, etc.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a standard string for the dom ID of an item.
|
||||||
|
* If a type is given, insert that before the counter, preceded by the type spacer
|
||||||
|
*
|
||||||
|
* @param itemId
|
||||||
|
* @param counter
|
||||||
|
* @param {string | null} type
|
||||||
|
* @param typeSpacer
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function stateDomId(itemId = '', counter = 0, type = '', typeSpacer = DOMID_TYPE_SPACER) {
|
||||||
|
const typeStr = type !== null && type.length > 0 ? `${typeSpacer}${type}` : '';
|
||||||
|
return `${DOMID_STATE}-${itemId}${typeStr}-${counter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, useRough) => {
|
||||||
|
// graphItemCount = 0;
|
||||||
|
log.trace('items', doc);
|
||||||
|
doc.forEach((item) => {
|
||||||
|
switch (item.stmt) {
|
||||||
|
case STMT_STATE:
|
||||||
|
dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, useRough);
|
||||||
|
break;
|
||||||
|
case DEFAULT_STATE_TYPE:
|
||||||
|
dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, useRough);
|
||||||
|
break;
|
||||||
|
case STMT_RELATION:
|
||||||
|
{
|
||||||
|
dataFetcher(
|
||||||
|
parentParsedItem,
|
||||||
|
item.state1,
|
||||||
|
diagramStates,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
altFlag,
|
||||||
|
useRough
|
||||||
|
);
|
||||||
|
dataFetcher(
|
||||||
|
parentParsedItem,
|
||||||
|
item.state2,
|
||||||
|
diagramStates,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
altFlag,
|
||||||
|
useRough
|
||||||
|
);
|
||||||
|
const edgeData = {
|
||||||
|
id: 'edge' + graphItemCount,
|
||||||
|
start: item.state1.id,
|
||||||
|
end: item.state2.id,
|
||||||
|
arrowhead: 'normal',
|
||||||
|
arrowTypeEnd: 'arrow_barb',
|
||||||
|
style: G_EDGE_STYLE,
|
||||||
|
labelStyle: '',
|
||||||
|
label: common.sanitizeText(item.description, getConfig()),
|
||||||
|
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
|
||||||
|
labelpos: G_EDGE_LABELPOS,
|
||||||
|
labelType: G_EDGE_LABELTYPE,
|
||||||
|
thickness: G_EDGE_THICKNESS,
|
||||||
|
classes: CSS_EDGE,
|
||||||
|
useRough,
|
||||||
|
};
|
||||||
|
edges.push(edgeData);
|
||||||
|
//g.setEdge(item.state1.id, item.state2.id, edgeData, graphItemCount);
|
||||||
|
graphItemCount++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the direction from the statement items.
|
||||||
|
* Look through all of the documents (docs) in the parsedItems
|
||||||
|
* Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction.
|
||||||
|
* @param {object[]} parsedItem - the parsed statement item to look through
|
||||||
|
* @param [defaultDir] - the direction to use if none is found
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
|
||||||
|
let dir = defaultDir;
|
||||||
|
if (parsedItem.doc) {
|
||||||
|
for (let i = 0; i < parsedItem.doc.length; i++) {
|
||||||
|
const parsedItemDoc = parsedItem.doc[i];
|
||||||
|
if (parsedItemDoc.stmt === 'dir') {
|
||||||
|
dir = parsedItemDoc.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new list of classes.
|
||||||
|
* In the future, this can be replaced with a class common to all diagrams.
|
||||||
|
* ClassDef information = { id: id, styles: [], textStyles: [] }
|
||||||
|
*
|
||||||
|
* @returns {{}}
|
||||||
|
*/
|
||||||
|
function newClassesList() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// let direction = DEFAULT_DIAGRAM_DIRECTION;
|
||||||
|
// let rootDoc = [];
|
||||||
|
let cssClasses = newClassesList(); // style classes defined by a classDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param nodes
|
||||||
|
* @param nodeData
|
||||||
|
*/
|
||||||
|
function insertOrUpdateNode(nodes, nodeData) {
|
||||||
|
if (!nodeData.id || nodeData.id === '</join></fork>' || nodeData.id === '</choice>') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Populate node style attributes if nodeData has classes defined
|
||||||
|
if (nodeData.cssClasses) {
|
||||||
|
nodeData.cssClasses.split(' ').forEach((cssClass) => {
|
||||||
|
if (cssClasses[cssClass]) {
|
||||||
|
cssClasses[cssClass].styles.forEach((style) => {
|
||||||
|
// Populate nodeData with style attributes specifically to be used by rough.js
|
||||||
|
if (style && style.startsWith('fill:')) {
|
||||||
|
nodeData.backgroundColor = style.replace('fill:', '');
|
||||||
|
}
|
||||||
|
if (style && style.startsWith('stroke:')) {
|
||||||
|
nodeData.borderColor = style.replace('stroke:', '');
|
||||||
|
}
|
||||||
|
if (style && style.startsWith('stroke-width:')) {
|
||||||
|
nodeData.borderWidth = style.replace('stroke-width:', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeData.cssStyles += style + ';';
|
||||||
|
});
|
||||||
|
cssClasses[cssClass].textStyles.forEach((style) => {
|
||||||
|
nodeData.labelStyle += style + ';';
|
||||||
|
if (style && style.startsWith('fill:')) {
|
||||||
|
nodeData.labelTextColor = style.replace('fill:', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const existingNodeData = nodes.find((node) => node.id === nodeData.id);
|
||||||
|
if (existingNodeData) {
|
||||||
|
//update the existing nodeData
|
||||||
|
Object.assign(existingNodeData, nodeData);
|
||||||
|
} else {
|
||||||
|
nodes.push(nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get classes from the db for the info item.
|
||||||
|
* If there aren't any or if dbInfoItem isn't defined, return an empty string.
|
||||||
|
* Else create 1 string from the list of classes found
|
||||||
|
*
|
||||||
|
* @param {undefined | null | object} dbInfoItem
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getClassesFromDbInfo(dbInfoItem) {
|
||||||
|
if (dbInfoItem === undefined || dbInfoItem === null) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
if (dbInfoItem.cssClasses) {
|
||||||
|
return dbInfoItem.cssClasses.join(' ');
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const dataFetcher = (parent, parsedItem, diagramStates, nodes, edges, altFlag, useRough) => {
|
||||||
|
const itemId = parsedItem.id;
|
||||||
|
const classStr = getClassesFromDbInfo(diagramStates[itemId]);
|
||||||
|
|
||||||
|
if (itemId !== 'root') {
|
||||||
|
let shape = SHAPE_STATE;
|
||||||
|
if (parsedItem.start === true) {
|
||||||
|
shape = SHAPE_START;
|
||||||
|
}
|
||||||
|
if (parsedItem.start === false) {
|
||||||
|
shape = SHAPE_END;
|
||||||
|
}
|
||||||
|
if (parsedItem.type !== DEFAULT_STATE_TYPE) {
|
||||||
|
shape = parsedItem.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the node to our list (nodeDb)
|
||||||
|
if (!nodeDb[itemId]) {
|
||||||
|
nodeDb[itemId] = {
|
||||||
|
id: itemId,
|
||||||
|
shape,
|
||||||
|
description: common.sanitizeText(itemId, getConfig()),
|
||||||
|
cssClasses: `${classStr} ${CSS_DIAGRAM_STATE}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNode = nodeDb[itemId];
|
||||||
|
|
||||||
|
// Save data for description and group so that for instance a statement without description overwrites
|
||||||
|
// one with description @todo TODO What does this mean? If important, add a test for it
|
||||||
|
|
||||||
|
// Build of the array of description strings
|
||||||
|
if (parsedItem.description) {
|
||||||
|
if (Array.isArray(newNode.description)) {
|
||||||
|
// There already is an array of strings,add to it
|
||||||
|
newNode.shape = SHAPE_STATE_WITH_DESC;
|
||||||
|
newNode.description.push(parsedItem.description);
|
||||||
|
} else {
|
||||||
|
if (newNode.description?.length > 0) {
|
||||||
|
// if there is a description already transform it to an array
|
||||||
|
newNode.shape = SHAPE_STATE_WITH_DESC;
|
||||||
|
if (newNode.description === itemId) {
|
||||||
|
// If the previous description was this, remove it
|
||||||
|
newNode.description = [parsedItem.description];
|
||||||
|
} else {
|
||||||
|
newNode.description = [newNode.description, parsedItem.description];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newNode.shape = SHAPE_STATE;
|
||||||
|
newNode.description = parsedItem.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newNode.description = common.sanitizeTextOrArray(newNode.description, getConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's only 1 description entry, just use a regular state shape
|
||||||
|
if (newNode.description?.length === 1 && newNode.shape === SHAPE_STATE_WITH_DESC) {
|
||||||
|
newNode.shape = SHAPE_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// group
|
||||||
|
if (!newNode.type && parsedItem.doc) {
|
||||||
|
log.info('Setting cluster for XCX', itemId, getDir(parsedItem));
|
||||||
|
newNode.type = 'group';
|
||||||
|
newNode.isGroup = true;
|
||||||
|
newNode.dir = getDir(parsedItem);
|
||||||
|
newNode.shape = parsedItem.type === DIVIDER_TYPE ? SHAPE_DIVIDER : SHAPE_GROUP;
|
||||||
|
newNode.cssClasses =
|
||||||
|
newNode.cssClasses +
|
||||||
|
' ' +
|
||||||
|
CSS_DIAGRAM_CLUSTER +
|
||||||
|
' ' +
|
||||||
|
(altFlag ? CSS_DIAGRAM_CLUSTER_ALT : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is what will be added to the graph
|
||||||
|
const nodeData = {
|
||||||
|
labelStyle: '',
|
||||||
|
shape: newNode.shape,
|
||||||
|
label: newNode.description,
|
||||||
|
cssClasses: newNode.cssClasses,
|
||||||
|
cssStyles: '',
|
||||||
|
id: itemId,
|
||||||
|
dir: newNode.dir,
|
||||||
|
domId: stateDomId(itemId, graphItemCount),
|
||||||
|
type: newNode.type,
|
||||||
|
isGroup: newNode.type === 'group',
|
||||||
|
padding: 8,
|
||||||
|
rx: 10,
|
||||||
|
ry: 10,
|
||||||
|
useRough,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear the label for dividers who have no description
|
||||||
|
if (nodeData.shape === SHAPE_DIVIDER) {
|
||||||
|
nodeData.label = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent && parent.id !== 'root') {
|
||||||
|
log.trace('Setting node ', itemId, ' to be child of its parent ', parent.id);
|
||||||
|
nodeData.parentId = parent.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeData.centerLabel = true;
|
||||||
|
|
||||||
|
if (parsedItem.note) {
|
||||||
|
// Todo: set random id
|
||||||
|
const noteData = {
|
||||||
|
labelStyle: '',
|
||||||
|
shape: SHAPE_NOTE,
|
||||||
|
label: parsedItem.note.text,
|
||||||
|
cssClasses: CSS_DIAGRAM_NOTE,
|
||||||
|
// useHtmlLabels: false,
|
||||||
|
cssStyles: '', // styles.style,
|
||||||
|
id: itemId + NOTE_ID + '-' + graphItemCount,
|
||||||
|
domId: stateDomId(itemId, graphItemCount, NOTE),
|
||||||
|
type: newNode.type,
|
||||||
|
isGroup: newNode.type === 'group',
|
||||||
|
padding: 0, //getConfig().flowchart.padding
|
||||||
|
useRough,
|
||||||
|
};
|
||||||
|
const groupData = {
|
||||||
|
labelStyle: '',
|
||||||
|
shape: SHAPE_NOTEGROUP,
|
||||||
|
label: parsedItem.note.text,
|
||||||
|
cssClasses: newNode.cssClasses,
|
||||||
|
cssStyles: '', // styles.style,
|
||||||
|
id: itemId + PARENT_ID,
|
||||||
|
domId: stateDomId(itemId, graphItemCount, PARENT),
|
||||||
|
type: 'group',
|
||||||
|
isGroup: true,
|
||||||
|
padding: 16, //getConfig().flowchart.padding
|
||||||
|
useRough,
|
||||||
|
};
|
||||||
|
graphItemCount++;
|
||||||
|
|
||||||
|
const parentNodeId = itemId + PARENT_ID;
|
||||||
|
|
||||||
|
//add parent id to groupData
|
||||||
|
groupData.id = parentNodeId;
|
||||||
|
//add parent id to noteData
|
||||||
|
noteData.parentId = parentNodeId;
|
||||||
|
|
||||||
|
//insert groupData
|
||||||
|
insertOrUpdateNode(nodes, groupData);
|
||||||
|
//insert noteData
|
||||||
|
insertOrUpdateNode(nodes, noteData);
|
||||||
|
//insert nodeData
|
||||||
|
insertOrUpdateNode(nodes, nodeData);
|
||||||
|
|
||||||
|
let from = itemId;
|
||||||
|
let to = noteData.id;
|
||||||
|
|
||||||
|
if (parsedItem.note.position === 'left of') {
|
||||||
|
from = noteData.id;
|
||||||
|
to = itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
edges.push({
|
||||||
|
id: from + '-' + to,
|
||||||
|
start: from,
|
||||||
|
end: to,
|
||||||
|
arrowhead: 'none',
|
||||||
|
arrowTypeEnd: '',
|
||||||
|
style: G_EDGE_STYLE,
|
||||||
|
labelStyle: '',
|
||||||
|
classes: CSS_EDGE_NOTE_EDGE,
|
||||||
|
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
|
||||||
|
labelpos: G_EDGE_LABELPOS,
|
||||||
|
labelType: G_EDGE_LABELTYPE,
|
||||||
|
thickness: G_EDGE_THICKNESS,
|
||||||
|
useRough,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
insertOrUpdateNode(nodes, nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parsedItem.doc) {
|
||||||
|
log.trace('Adding nodes children ');
|
||||||
|
setupDoc(parsedItem, parsedItem.doc, diagramStates, nodes, edges, !altFlag, useRough);
|
||||||
|
}
|
||||||
|
};
|
@@ -20,6 +20,44 @@ export const STMT_APPLYCLASS = 'applyClass';
|
|||||||
export const DEFAULT_STATE_TYPE = 'default';
|
export const DEFAULT_STATE_TYPE = 'default';
|
||||||
export const DIVIDER_TYPE = 'divider';
|
export const DIVIDER_TYPE = 'divider';
|
||||||
|
|
||||||
|
// Graph edge settings
|
||||||
|
export const G_EDGE_STYLE = 'fill:none';
|
||||||
|
export const G_EDGE_ARROWHEADSTYLE = 'fill: #333';
|
||||||
|
export const G_EDGE_LABELPOS = 'c';
|
||||||
|
export const G_EDGE_LABELTYPE = 'text';
|
||||||
|
export const G_EDGE_THICKNESS = 'normal';
|
||||||
|
|
||||||
|
export const SHAPE_STATE = 'rect';
|
||||||
|
export const SHAPE_STATE_WITH_DESC = 'rectWithTitle';
|
||||||
|
export const SHAPE_START = 'stateStart';
|
||||||
|
export const SHAPE_END = 'stateEnd';
|
||||||
|
export const SHAPE_DIVIDER = 'divider';
|
||||||
|
export const SHAPE_GROUP = 'roundedWithTitle';
|
||||||
|
export const SHAPE_NOTE = 'note';
|
||||||
|
export const SHAPE_NOTEGROUP = 'noteGroup';
|
||||||
|
|
||||||
|
// CSS classes
|
||||||
|
export const CSS_DIAGRAM = 'statediagram';
|
||||||
|
export const CSS_STATE = 'state';
|
||||||
|
export const CSS_DIAGRAM_STATE = `${CSS_DIAGRAM}-${CSS_STATE}`;
|
||||||
|
export const CSS_EDGE = 'transition';
|
||||||
|
export const CSS_NOTE = 'note';
|
||||||
|
export const CSS_NOTE_EDGE = 'note-edge';
|
||||||
|
export const CSS_EDGE_NOTE_EDGE = `${CSS_EDGE} ${CSS_NOTE_EDGE}`;
|
||||||
|
export const CSS_DIAGRAM_NOTE = `${CSS_DIAGRAM}-${CSS_NOTE}`;
|
||||||
|
export const CSS_CLUSTER = 'cluster';
|
||||||
|
export const CSS_DIAGRAM_CLUSTER = `${CSS_DIAGRAM}-${CSS_CLUSTER}`;
|
||||||
|
export const CSS_CLUSTER_ALT = 'cluster-alt';
|
||||||
|
export const CSS_DIAGRAM_CLUSTER_ALT = `${CSS_DIAGRAM}-${CSS_CLUSTER_ALT}`;
|
||||||
|
|
||||||
|
export const PARENT = 'parent';
|
||||||
|
export const NOTE = 'note';
|
||||||
|
export const DOMID_STATE = 'state';
|
||||||
|
export const DOMID_TYPE_SPACER = '----';
|
||||||
|
export const NOTE_ID = `${DOMID_TYPE_SPACER}${NOTE}`;
|
||||||
|
export const PARENT_ID = `${DOMID_TYPE_SPACER}${PARENT}`;
|
||||||
|
// --------------------------------------
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
DEFAULT_DIAGRAM_DIRECTION,
|
DEFAULT_DIAGRAM_DIRECTION,
|
||||||
DEFAULT_NESTED_DOC_DIR,
|
DEFAULT_NESTED_DOC_DIR,
|
||||||
@@ -29,4 +67,35 @@ export default {
|
|||||||
STMT_APPLYCLASS,
|
STMT_APPLYCLASS,
|
||||||
DEFAULT_STATE_TYPE,
|
DEFAULT_STATE_TYPE,
|
||||||
DIVIDER_TYPE,
|
DIVIDER_TYPE,
|
||||||
|
G_EDGE_STYLE,
|
||||||
|
G_EDGE_ARROWHEADSTYLE,
|
||||||
|
G_EDGE_LABELPOS,
|
||||||
|
G_EDGE_LABELTYPE,
|
||||||
|
G_EDGE_THICKNESS,
|
||||||
|
CSS_EDGE,
|
||||||
|
CSS_DIAGRAM,
|
||||||
|
SHAPE_STATE,
|
||||||
|
SHAPE_STATE_WITH_DESC,
|
||||||
|
SHAPE_START,
|
||||||
|
SHAPE_END,
|
||||||
|
SHAPE_DIVIDER,
|
||||||
|
SHAPE_GROUP,
|
||||||
|
SHAPE_NOTE,
|
||||||
|
SHAPE_NOTEGROUP,
|
||||||
|
CSS_STATE,
|
||||||
|
CSS_DIAGRAM_STATE,
|
||||||
|
CSS_NOTE,
|
||||||
|
CSS_NOTE_EDGE,
|
||||||
|
CSS_EDGE_NOTE_EDGE,
|
||||||
|
CSS_DIAGRAM_NOTE,
|
||||||
|
CSS_CLUSTER,
|
||||||
|
CSS_DIAGRAM_CLUSTER,
|
||||||
|
CSS_CLUSTER_ALT,
|
||||||
|
CSS_DIAGRAM_CLUSTER_ALT,
|
||||||
|
PARENT,
|
||||||
|
NOTE,
|
||||||
|
DOMID_STATE,
|
||||||
|
DOMID_TYPE_SPACER,
|
||||||
|
NOTE_ID,
|
||||||
|
PARENT_ID,
|
||||||
};
|
};
|
||||||
|
@@ -11,6 +11,7 @@ import {
|
|||||||
setDiagramTitle,
|
setDiagramTitle,
|
||||||
getDiagramTitle,
|
getDiagramTitle,
|
||||||
} from '../common/commonDb.js';
|
} from '../common/commonDb.js';
|
||||||
|
import { dataFetcher } from './dataFetcher.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_DIAGRAM_DIRECTION,
|
DEFAULT_DIAGRAM_DIRECTION,
|
||||||
@@ -20,7 +21,34 @@ import {
|
|||||||
STMT_APPLYCLASS,
|
STMT_APPLYCLASS,
|
||||||
DEFAULT_STATE_TYPE,
|
DEFAULT_STATE_TYPE,
|
||||||
DIVIDER_TYPE,
|
DIVIDER_TYPE,
|
||||||
|
G_EDGE_STYLE,
|
||||||
|
G_EDGE_ARROWHEADSTYLE,
|
||||||
|
G_EDGE_LABELPOS,
|
||||||
|
G_EDGE_LABELTYPE,
|
||||||
|
G_EDGE_THICKNESS,
|
||||||
|
CSS_EDGE,
|
||||||
|
DEFAULT_NESTED_DOC_DIR,
|
||||||
|
SHAPE_DIVIDER,
|
||||||
|
SHAPE_GROUP,
|
||||||
|
CSS_DIAGRAM_CLUSTER,
|
||||||
|
CSS_DIAGRAM_CLUSTER_ALT,
|
||||||
|
CSS_DIAGRAM_STATE,
|
||||||
|
SHAPE_STATE_WITH_DESC,
|
||||||
|
SHAPE_STATE,
|
||||||
|
SHAPE_START,
|
||||||
|
SHAPE_END,
|
||||||
|
SHAPE_NOTE,
|
||||||
|
SHAPE_NOTEGROUP,
|
||||||
|
CSS_DIAGRAM_NOTE,
|
||||||
|
DOMID_TYPE_SPACER,
|
||||||
|
DOMID_STATE,
|
||||||
|
NOTE_ID,
|
||||||
|
PARENT_ID,
|
||||||
|
NOTE,
|
||||||
|
PARENT,
|
||||||
|
CSS_EDGE_NOTE_EDGE,
|
||||||
} from './stateCommon.js';
|
} from './stateCommon.js';
|
||||||
|
import { node } from 'stylis';
|
||||||
|
|
||||||
const START_NODE = '[*]';
|
const START_NODE = '[*]';
|
||||||
const START_TYPE = 'start';
|
const START_TYPE = 'start';
|
||||||
@@ -47,6 +75,8 @@ let direction = DEFAULT_DIAGRAM_DIRECTION;
|
|||||||
let rootDoc = [];
|
let rootDoc = [];
|
||||||
let classes = newClassesList(); // style classes defined by a classDef
|
let classes = newClassesList(); // style classes defined by a classDef
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
|
||||||
const newDoc = () => {
|
const newDoc = () => {
|
||||||
return {
|
return {
|
||||||
/** @type {{ id1: string, id2: string, relationTitle: string }[]} */
|
/** @type {{ id1: string, id2: string, relationTitle: string }[]} */
|
||||||
@@ -540,8 +570,27 @@ const setDirection = (dir) => {
|
|||||||
|
|
||||||
const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
|
const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
|
||||||
|
|
||||||
|
export const getData = () => {
|
||||||
|
const nodes = [];
|
||||||
|
const edges = [];
|
||||||
|
|
||||||
|
// for (const key in currentDocument.states) {
|
||||||
|
// if (currentDocument.states.hasOwnProperty(key)) {
|
||||||
|
// nodes.push({...currentDocument.states[key]});
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
extract(getRootDocV2());
|
||||||
|
const diagramStates = getStates();
|
||||||
|
const config = getConfig();
|
||||||
|
const useRough = config.look === 'handdrawn';
|
||||||
|
dataFetcher(undefined, getRootDocV2(), diagramStates, nodes, edges, true, useRough);
|
||||||
|
|
||||||
|
return { nodes, edges, other: {}, config };
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getConfig: () => getConfig().state,
|
getConfig: () => getConfig().state,
|
||||||
|
getData,
|
||||||
addState,
|
addState,
|
||||||
clear,
|
clear,
|
||||||
getState,
|
getState,
|
||||||
|
@@ -3,7 +3,8 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
|
|||||||
import parser from './parser/stateDiagram.jison';
|
import parser from './parser/stateDiagram.jison';
|
||||||
import db from './stateDb.js';
|
import db from './stateDb.js';
|
||||||
import styles from './styles.js';
|
import styles from './styles.js';
|
||||||
import renderer from './stateRenderer-v2.js';
|
//import renderer from './stateRenderer-v2.js';
|
||||||
|
import renderer from './stateRenderer-v3-unified.js';
|
||||||
|
|
||||||
export const diagram: DiagramDefinition = {
|
export const diagram: DiagramDefinition = {
|
||||||
parser,
|
parser,
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
|
// @ts-ignore: JISON doesn't support types
|
||||||
|
import parser from './parser/stateDiagram.jison';
|
||||||
|
import db from './stateDb.js';
|
||||||
|
import styles from './styles.js';
|
||||||
|
import renderer from './stateRenderer-v3-unified.js';
|
||||||
|
|
||||||
|
export const diagram: DiagramDefinition = {
|
||||||
|
parser,
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
styles,
|
||||||
|
init: (cnf) => {
|
||||||
|
if (!cnf.state) {
|
||||||
|
cnf.state = {};
|
||||||
|
}
|
||||||
|
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||||
|
db.clear();
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,93 @@
|
|||||||
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
|
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import { getDiagramElements } from '../../rendering-util/insertElementsForSize.js';
|
||||||
|
import { render } from '../../rendering-util/render.js';
|
||||||
|
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||||
|
import type { LayoutData } from '../../rendering-util/types.js';
|
||||||
|
import utils from '../../utils.js';
|
||||||
|
import { CSS_DIAGRAM, DEFAULT_NESTED_DOC_DIR } from './stateCommon.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the direction from the statement items.
|
||||||
|
* Look through all of the documents (docs) in the parsedItems
|
||||||
|
* Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction.
|
||||||
|
* @param parsedItem - the parsed statement item to look through
|
||||||
|
* @param defaultDir - the direction to use if none is found
|
||||||
|
* @returns The direction to use
|
||||||
|
*/
|
||||||
|
const getDir = (parsedItem: any, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
|
||||||
|
let dir = defaultDir;
|
||||||
|
if (parsedItem.doc) {
|
||||||
|
for (let i = 0; i < parsedItem.doc.length; i++) {
|
||||||
|
const parsedItemDoc = parsedItem.doc[i];
|
||||||
|
if (parsedItemDoc.stmt === 'dir') {
|
||||||
|
dir = parsedItemDoc.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getClasses = function (
|
||||||
|
text: string,
|
||||||
|
diagramObj: any
|
||||||
|
): Map<string, DiagramStyleClassDef> {
|
||||||
|
diagramObj.db.extract(diagramObj.db.getRootDocV2());
|
||||||
|
return diagramObj.db.getClasses();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const draw = async function (text: string, id: string, _version: string, diag: any) {
|
||||||
|
log.info('REF0:');
|
||||||
|
log.info('Drawing state diagram (v2)', id);
|
||||||
|
const { securityLevel, state: conf, layout } = getConfig();
|
||||||
|
// Extracting the data from the parsed structure into a more usable form
|
||||||
|
// Not related to the refactoring, but this is the first step in the rendering process
|
||||||
|
diag.db.extract(diag.db.getRootDocV2());
|
||||||
|
|
||||||
|
const DIR = getDir(diag.db.getRootDocV2());
|
||||||
|
|
||||||
|
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||||
|
// into the Layout data format
|
||||||
|
const data4Layout = diag.db.getData() as LayoutData;
|
||||||
|
// Create the root SVG - the element is the div containing the SVG element
|
||||||
|
const { element, svg } = getDiagramElements(id, securityLevel);
|
||||||
|
|
||||||
|
// // For some diagrams this call is not needed, but in the state diagram it is
|
||||||
|
// await insertElementsForSize(element, data4Layout);
|
||||||
|
|
||||||
|
// console.log('data4Layout:', data4Layout);
|
||||||
|
|
||||||
|
// // Now we have layout data with real sizes, we can perform the layout
|
||||||
|
// const data4Rendering = doLayout(data4Layout, id, _version, 'dagre-wrapper');
|
||||||
|
|
||||||
|
// // The performRender method provided in all supported diagrams is used to render the data
|
||||||
|
// performRender(data4Rendering);
|
||||||
|
|
||||||
|
data4Layout.type = diag.type;
|
||||||
|
data4Layout.layoutAlgorithm = layout;
|
||||||
|
data4Layout.direction = DIR;
|
||||||
|
|
||||||
|
// TODO: Should we move these two to baseConfig? These types are not there in StateConfig.
|
||||||
|
// @ts-expect-error TODO: Will be fixed after config refactor
|
||||||
|
data4Layout.nodeSpacing = conf?.nodeSpacing || 50;
|
||||||
|
// @ts-expect-error TODO: Will be fixed after config refactor
|
||||||
|
data4Layout.rankSpacing = conf?.rankSpacing || 50;
|
||||||
|
data4Layout.markers = ['barb'];
|
||||||
|
data4Layout.diagramId = id;
|
||||||
|
// console.log('REF1:', data4Layout);
|
||||||
|
await render(data4Layout, svg, element);
|
||||||
|
const padding = 8;
|
||||||
|
utils.insertTitle(
|
||||||
|
element,
|
||||||
|
'statediagramTitleText',
|
||||||
|
conf?.titleTopMargin ?? 25,
|
||||||
|
diag.db.getDiagramTitle()
|
||||||
|
);
|
||||||
|
setupViewPortForSVG(svg, padding, CSS_DIAGRAM, conf?.useMaxWidth ?? true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getClasses,
|
||||||
|
draw,
|
||||||
|
};
|
31
packages/mermaid/src/internals.ts
Normal file
31
packages/mermaid/src/internals.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { getConfig } from './config.js';
|
||||||
|
import common from './diagrams/common/common.js';
|
||||||
|
import { log } from './logger.js';
|
||||||
|
import { insertCluster } from './rendering-util/rendering-elements/clusters.js';
|
||||||
|
import {
|
||||||
|
insertEdge,
|
||||||
|
insertEdgeLabel,
|
||||||
|
positionEdgeLabel,
|
||||||
|
} from './rendering-util/rendering-elements/edges.js';
|
||||||
|
import insertMarkers from './rendering-util/rendering-elements/markers.js';
|
||||||
|
import { insertNode } from './rendering-util/rendering-elements/nodes.js';
|
||||||
|
import { labelHelper } from './rendering-util/rendering-elements/shapes/util.js';
|
||||||
|
import { interpolateToCurve } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal helpers for mermaid
|
||||||
|
* @deprecated - This should not be used by external packages, as the definitions will change without notice.
|
||||||
|
*/
|
||||||
|
export const internalHelpers = {
|
||||||
|
common,
|
||||||
|
getConfig,
|
||||||
|
insertCluster,
|
||||||
|
insertEdge,
|
||||||
|
insertEdgeLabel,
|
||||||
|
insertMarkers,
|
||||||
|
insertNode,
|
||||||
|
interpolateToCurve,
|
||||||
|
labelHelper,
|
||||||
|
log,
|
||||||
|
positionEdgeLabel,
|
||||||
|
};
|
@@ -17,6 +17,9 @@ import type { DetailedError } from './utils.js';
|
|||||||
import type { ExternalDiagramDefinition } from './diagram-api/types.js';
|
import type { ExternalDiagramDefinition } from './diagram-api/types.js';
|
||||||
import type { UnknownDiagramError } from './errors.js';
|
import type { UnknownDiagramError } from './errors.js';
|
||||||
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
|
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
|
||||||
|
import { registerLayoutLoaders } from './rendering-util/render.js';
|
||||||
|
import type { LayoutLoaderDefinition } from './rendering-util/render.js';
|
||||||
|
import { internalHelpers } from './internals.js';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
MermaidConfig,
|
MermaidConfig,
|
||||||
@@ -26,6 +29,7 @@ export type {
|
|||||||
RenderResult,
|
RenderResult,
|
||||||
ParseOptions,
|
ParseOptions,
|
||||||
UnknownDiagramError,
|
UnknownDiagramError,
|
||||||
|
LayoutLoaderDefinition,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RunOptions {
|
export interface RunOptions {
|
||||||
@@ -423,11 +427,17 @@ export interface Mermaid {
|
|||||||
render: typeof render;
|
render: typeof render;
|
||||||
init: typeof init;
|
init: typeof init;
|
||||||
run: typeof run;
|
run: typeof run;
|
||||||
|
registerLayoutLoaders: typeof registerLayoutLoaders;
|
||||||
registerExternalDiagrams: typeof registerExternalDiagrams;
|
registerExternalDiagrams: typeof registerExternalDiagrams;
|
||||||
initialize: typeof initialize;
|
initialize: typeof initialize;
|
||||||
contentLoaded: typeof contentLoaded;
|
contentLoaded: typeof contentLoaded;
|
||||||
setParseErrorHandler: typeof setParseErrorHandler;
|
setParseErrorHandler: typeof setParseErrorHandler;
|
||||||
detectType: typeof detectType;
|
detectType: typeof detectType;
|
||||||
|
/**
|
||||||
|
* Internal helpers for mermaid
|
||||||
|
* @deprecated - This should not be used by external packages, as the definitions will change without notice.
|
||||||
|
*/
|
||||||
|
internalHelpers: typeof internalHelpers;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mermaid: Mermaid = {
|
const mermaid: Mermaid = {
|
||||||
@@ -438,11 +448,13 @@ const mermaid: Mermaid = {
|
|||||||
init,
|
init,
|
||||||
run,
|
run,
|
||||||
registerExternalDiagrams,
|
registerExternalDiagrams,
|
||||||
|
registerLayoutLoaders,
|
||||||
initialize,
|
initialize,
|
||||||
parseError: undefined,
|
parseError: undefined,
|
||||||
contentLoaded,
|
contentLoaded,
|
||||||
setParseErrorHandler,
|
setParseErrorHandler,
|
||||||
detectType,
|
detectType,
|
||||||
|
internalHelpers,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default mermaid;
|
export default mermaid;
|
||||||
|
15
packages/mermaid/src/rendering-util/doLayout.ts
Normal file
15
packages/mermaid/src/rendering-util/doLayout.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import type { LayoutData, LayoutMethod, RenderData } from './types.js';
|
||||||
|
|
||||||
|
const layoutAlgorithms = {} as Record<string, any>;
|
||||||
|
|
||||||
|
const performLayout = (
|
||||||
|
layoutData: LayoutData,
|
||||||
|
id: string,
|
||||||
|
_version: string,
|
||||||
|
layoutMethod: LayoutMethod
|
||||||
|
): RenderData => {
|
||||||
|
log.info('Performing layout', layoutData, id, _version, layoutMethod);
|
||||||
|
return { items: [] };
|
||||||
|
};
|
||||||
|
export default performLayout;
|
59
packages/mermaid/src/rendering-util/insertElementsForSize.js
Normal file
59
packages/mermaid/src/rendering-util/insertElementsForSize.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// import type { LayoutData } from './types';
|
||||||
|
import { select } from 'd3';
|
||||||
|
import { insertNode } from '../dagre-wrapper/nodes.js';
|
||||||
|
|
||||||
|
// export const getDiagramElements = (id: string, securityLevel: any) => {
|
||||||
|
export const getDiagramElements = (id, securityLevel) => {
|
||||||
|
let sandboxElement;
|
||||||
|
if (securityLevel === 'sandbox') {
|
||||||
|
sandboxElement = select('#i' + id);
|
||||||
|
}
|
||||||
|
const root =
|
||||||
|
securityLevel === 'sandbox'
|
||||||
|
? select(sandboxElement.nodes()[0].contentDocument.body)
|
||||||
|
: select('body');
|
||||||
|
|
||||||
|
const svg = root.select(`[id="${id}"]`);
|
||||||
|
|
||||||
|
// Run the renderer. This is what draws the final graph.
|
||||||
|
|
||||||
|
// @ts-ignore todo: fix this
|
||||||
|
const element = root.select('#' + id + ' g');
|
||||||
|
return { svg, element };
|
||||||
|
};
|
||||||
|
|
||||||
|
// export function insertElementsForSize(el: SVGElement, data: LayoutData): void {
|
||||||
|
export function insertElementsForSize(el, data) {
|
||||||
|
const nodesElem = el.insert('g').attr('class', 'nodes');
|
||||||
|
const edgesElem = el.insert('g').attr('class', 'edges');
|
||||||
|
data.nodes.forEach(async (item) => {
|
||||||
|
item.shape = 'rect';
|
||||||
|
const e = await insertNode(nodesElem, {
|
||||||
|
...item,
|
||||||
|
class: 'default flowchart-label',
|
||||||
|
labelStyle: '',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
rx: 0,
|
||||||
|
ry: 0,
|
||||||
|
height: 100,
|
||||||
|
shape: 'rect',
|
||||||
|
padding: 8,
|
||||||
|
});
|
||||||
|
// Create a new DOM element
|
||||||
|
// const element = document.createElement('div');
|
||||||
|
|
||||||
|
// // Set the content of the element to the name of the item
|
||||||
|
// element.textContent = item.name;
|
||||||
|
|
||||||
|
// // Set the size of the element to the size of the item
|
||||||
|
// element.style.width = `${item.size}px`;
|
||||||
|
// element.style.height = `${item.size}px`;
|
||||||
|
|
||||||
|
// Append the element to the body of the document
|
||||||
|
// document.body.appendChild(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default insertElementsForSize;
|
@@ -0,0 +1,299 @@
|
|||||||
|
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
|
||||||
|
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
||||||
|
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||||
|
import insertMarkers from '../../rendering-elements/markers.js';
|
||||||
|
import { updateNodeBounds } from '../../rendering-elements/shapes/util.js';
|
||||||
|
import {
|
||||||
|
clear as clearGraphlib,
|
||||||
|
clusterDb,
|
||||||
|
adjustClustersAndEdges,
|
||||||
|
findNonClusterChild,
|
||||||
|
sortNodesByHierarchy,
|
||||||
|
} from './mermaid-graphlib.js';
|
||||||
|
import {
|
||||||
|
insertNode,
|
||||||
|
positionNode,
|
||||||
|
clear as clearNodes,
|
||||||
|
setNodeElem,
|
||||||
|
} from '../../rendering-elements/nodes.js';
|
||||||
|
import { insertCluster, clear as clearClusters } from '../../rendering-elements/clusters.js';
|
||||||
|
import {
|
||||||
|
insertEdgeLabel,
|
||||||
|
positionEdgeLabel,
|
||||||
|
insertEdge,
|
||||||
|
clear as clearEdges,
|
||||||
|
} from '../../rendering-elements/edges.js';
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { getSubGraphTitleMargins } from '../../../utils/subGraphTitleMargins.js';
|
||||||
|
import { getConfig } from '../../../diagram-api/diagramAPI.js';
|
||||||
|
|
||||||
|
const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, siteConfig) => {
|
||||||
|
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
|
||||||
|
const dir = graph.graph().rankdir;
|
||||||
|
log.trace('Dir in recursive render - dir:', dir);
|
||||||
|
|
||||||
|
const elem = _elem.insert('g').attr('class', 'root');
|
||||||
|
if (!graph.nodes()) {
|
||||||
|
log.info('No nodes found for', graph);
|
||||||
|
} else {
|
||||||
|
log.info('Recursive render XXX', graph.nodes());
|
||||||
|
}
|
||||||
|
if (graph.edges().length > 0) {
|
||||||
|
log.info('Recursive edges', graph.edge(graph.edges()[0]));
|
||||||
|
}
|
||||||
|
const clusters = elem.insert('g').attr('class', 'clusters');
|
||||||
|
const edgePaths = elem.insert('g').attr('class', 'edgePaths');
|
||||||
|
const edgeLabels = elem.insert('g').attr('class', 'edgeLabels');
|
||||||
|
const nodes = elem.insert('g').attr('class', 'nodes');
|
||||||
|
|
||||||
|
// Insert nodes, this will insert them into the dom and each node will get a size. The size is updated
|
||||||
|
// to the abstract node and is later used by dagre for the layout
|
||||||
|
await Promise.all(
|
||||||
|
graph.nodes().map(async function (v) {
|
||||||
|
const node = graph.node(v);
|
||||||
|
if (parentCluster !== undefined) {
|
||||||
|
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
|
||||||
|
// data.clusterPositioning = true;
|
||||||
|
log.trace(
|
||||||
|
'Setting data for parent cluster XXX\n Node.id = ',
|
||||||
|
v,
|
||||||
|
'\n data=',
|
||||||
|
data.height,
|
||||||
|
'\nParent cluster',
|
||||||
|
parentCluster.height
|
||||||
|
);
|
||||||
|
graph.setNode(parentCluster.id, data);
|
||||||
|
if (!graph.parent(v)) {
|
||||||
|
log.trace('Setting parent', v, parentCluster.id);
|
||||||
|
graph.setParent(v, parentCluster.id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info('(Insert) Node XXX' + v + ': ' + JSON.stringify(graph.node(v)));
|
||||||
|
if (node && node.clusterNode) {
|
||||||
|
// const children = graph.children(v);
|
||||||
|
log.info('Cluster identified XXX', v, node.width, graph.node(v));
|
||||||
|
// "o" will contain the full cluster not just the children
|
||||||
|
const o = await recursiveRender(
|
||||||
|
nodes,
|
||||||
|
node.graph,
|
||||||
|
diagramType,
|
||||||
|
id,
|
||||||
|
graph.node(v),
|
||||||
|
siteConfig
|
||||||
|
);
|
||||||
|
const newEl = o.elem;
|
||||||
|
updateNodeBounds(node, newEl);
|
||||||
|
node.diff = o.diff || 0;
|
||||||
|
log.trace(
|
||||||
|
'New compound node after recursive render XAX',
|
||||||
|
v,
|
||||||
|
'width',
|
||||||
|
// node,
|
||||||
|
node.width,
|
||||||
|
'height',
|
||||||
|
node.height
|
||||||
|
// node.x,
|
||||||
|
// node.y
|
||||||
|
);
|
||||||
|
setNodeElem(newEl, node);
|
||||||
|
} else {
|
||||||
|
if (graph.children(v).length > 0) {
|
||||||
|
// This is a cluster but not to be rendered recursively
|
||||||
|
// Render as before
|
||||||
|
log.info('Cluster - the non recursive path XXX', v, node.id, node, graph);
|
||||||
|
log.info(findNonClusterChild(node.id, graph));
|
||||||
|
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node };
|
||||||
|
// insertCluster(clusters, graph.node(v));
|
||||||
|
} else {
|
||||||
|
log.trace('Node - the non recursive path XAX', v, node.id, node);
|
||||||
|
await insertNode(nodes, graph.node(v), dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert labels, this will insert them into the dom so that the width can be calculated
|
||||||
|
// Also figure out which edges point to/from clusters and adjust them accordingly
|
||||||
|
// Edges from/to clusters really points to the first child in the cluster.
|
||||||
|
// TODO: pick optimal child in the cluster to us as link anchor
|
||||||
|
graph.edges().forEach(function (e) {
|
||||||
|
const edge = graph.edge(e.v, e.w, e.name);
|
||||||
|
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||||
|
log.info('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e)));
|
||||||
|
|
||||||
|
// Check if link is either from or to a cluster
|
||||||
|
log.info('Fix', clusterDb, 'ids:', e.v, e.w, 'Translating: ', clusterDb[e.v], clusterDb[e.w]);
|
||||||
|
insertEdgeLabel(edgeLabels, edge);
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.edges().forEach(function (e) {
|
||||||
|
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info('############################################# XXX');
|
||||||
|
log.info('### Layout ### XXX');
|
||||||
|
log.info('############################################# XXX');
|
||||||
|
|
||||||
|
dagreLayout(graph);
|
||||||
|
|
||||||
|
log.info('Graph after layout:', graphlibJson.write(graph));
|
||||||
|
// Move the nodes to the correct place
|
||||||
|
let diff = 0;
|
||||||
|
log.info('Need the size here XAX', graph.node('T1')?.height);
|
||||||
|
let { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
|
||||||
|
subGraphTitleTotalMargin = 0;
|
||||||
|
sortNodesByHierarchy(graph).forEach(function (v) {
|
||||||
|
const node = graph.node(v);
|
||||||
|
const p = graph.node(node?.parentId);
|
||||||
|
subGraphTitleTotalMargin = p?.offsetY || subGraphTitleTotalMargin;
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'Position XAX' + v + ': (' + node.x,
|
||||||
|
',' + node.y,
|
||||||
|
') width: ',
|
||||||
|
node.width,
|
||||||
|
' height: ',
|
||||||
|
node.height
|
||||||
|
);
|
||||||
|
if (node && node.clusterNode) {
|
||||||
|
const parentId = graph.parent(v);
|
||||||
|
// Adjust for padding when on root level
|
||||||
|
node.y = parentId ? node.y + 2 : node.y - 8;
|
||||||
|
node.x -= 8;
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'A tainted cluster node XBX',
|
||||||
|
v,
|
||||||
|
node.id,
|
||||||
|
node.width,
|
||||||
|
node.height,
|
||||||
|
node.x,
|
||||||
|
node.y,
|
||||||
|
graph.parent(v)
|
||||||
|
);
|
||||||
|
clusterDb[node.id].node = node;
|
||||||
|
// node.y += subGraphTitleTotalMargin - 10;
|
||||||
|
node.y -= (node.offsetY || 0) / 2;
|
||||||
|
positionNode(node);
|
||||||
|
} else {
|
||||||
|
// Non cluster node
|
||||||
|
if (graph.children(v).length > 0) {
|
||||||
|
node.height += 0;
|
||||||
|
const parent = graph.node(node.parentId);
|
||||||
|
node.y += (node.offsetY || 0) / 2;
|
||||||
|
insertCluster(clusters, node);
|
||||||
|
|
||||||
|
// A cluster in the non-recursive way
|
||||||
|
log.info(
|
||||||
|
'A pure cluster node with children XBX',
|
||||||
|
v,
|
||||||
|
node.id,
|
||||||
|
node.width,
|
||||||
|
node.height,
|
||||||
|
node.x,
|
||||||
|
node.y,
|
||||||
|
'offset',
|
||||||
|
parent?.offsetY
|
||||||
|
);
|
||||||
|
clusterDb[node.id].node = node;
|
||||||
|
} else {
|
||||||
|
const parent = graph.node(node.parentId);
|
||||||
|
node.y += (parent?.offsetY || 0) / 2;
|
||||||
|
log.info(
|
||||||
|
'A regular node XBX - using the padding',
|
||||||
|
v,
|
||||||
|
node.id,
|
||||||
|
'parent',
|
||||||
|
node.parentId,
|
||||||
|
node.width,
|
||||||
|
node.height,
|
||||||
|
node.x,
|
||||||
|
node.y,
|
||||||
|
'offsetY',
|
||||||
|
node.offsetY,
|
||||||
|
'parent',
|
||||||
|
parent,
|
||||||
|
node
|
||||||
|
);
|
||||||
|
|
||||||
|
positionNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move the edge labels to the correct place after layout
|
||||||
|
graph.edges().forEach(function (e) {
|
||||||
|
const edge = graph.edge(e);
|
||||||
|
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
||||||
|
|
||||||
|
edge.points.forEach((point) => (point.y += subGraphTitleTotalMargin / 2));
|
||||||
|
const paths = insertEdge(edgePaths, edge, clusterDb, diagramType, graph, id);
|
||||||
|
positionEdgeLabel(edge, paths);
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes().forEach(function (v) {
|
||||||
|
const n = graph.node(v);
|
||||||
|
log.info(v, n.type, n.diff);
|
||||||
|
if (n.isGroup) {
|
||||||
|
diff = n.diff;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.trace('Returning from recursive render XAX', elem, diff);
|
||||||
|
return { elem, diff };
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* ###############################################################
|
||||||
|
* Render the graph
|
||||||
|
* ###############################################################
|
||||||
|
*/
|
||||||
|
export const render = async (data4Layout, svg, element) => {
|
||||||
|
// Create the input mermaid.graph
|
||||||
|
const graph = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true,
|
||||||
|
})
|
||||||
|
.setGraph({
|
||||||
|
rankdir: data4Layout.direction,
|
||||||
|
nodesep: data4Layout.nodeSpacing,
|
||||||
|
ranksep: data4Layout.rankSpacing,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8,
|
||||||
|
})
|
||||||
|
.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Org
|
||||||
|
|
||||||
|
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||||
|
clearNodes();
|
||||||
|
clearEdges();
|
||||||
|
clearClusters();
|
||||||
|
clearGraphlib();
|
||||||
|
|
||||||
|
// Add the nodes and edges to the graph
|
||||||
|
data4Layout.nodes.forEach((node) => {
|
||||||
|
graph.setNode(node.id, { ...node });
|
||||||
|
if (node.parentId) {
|
||||||
|
graph.setParent(node.id, node.parentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.debug('Edges:', data4Layout.edges);
|
||||||
|
data4Layout.edges.forEach((edge) => {
|
||||||
|
graph.setEdge(edge.start, edge.end, { ...edge });
|
||||||
|
});
|
||||||
|
|
||||||
|
log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph)));
|
||||||
|
adjustClustersAndEdges(graph);
|
||||||
|
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
|
||||||
|
const siteConfig = getConfig();
|
||||||
|
await recursiveRender(
|
||||||
|
element,
|
||||||
|
graph,
|
||||||
|
data4Layout.type,
|
||||||
|
data4Layout.diagramId,
|
||||||
|
undefined,
|
||||||
|
siteConfig
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,467 @@
|
|||||||
|
/** Decorates with functions required by mermaids dagre-wrapper. */
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||||
|
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
||||||
|
|
||||||
|
export let clusterDb = {};
|
||||||
|
let descendants = {};
|
||||||
|
let parents = {};
|
||||||
|
|
||||||
|
export const clear = () => {
|
||||||
|
descendants = {};
|
||||||
|
parents = {};
|
||||||
|
clusterDb = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDescendant = (id, ancestorId) => {
|
||||||
|
log.trace('In isDescendant', ancestorId, ' ', id, ' = ', descendants[ancestorId].includes(id));
|
||||||
|
return descendants[ancestorId].includes(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const edgeInCluster = (edge, clusterId) => {
|
||||||
|
log.info('Descendants of ', clusterId, ' is ', descendants[clusterId]);
|
||||||
|
log.info('Edge is ', edge);
|
||||||
|
// Edges to/from the cluster is not in the cluster, they are in the parent
|
||||||
|
if (edge.v === clusterId || edge.w === clusterId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!descendants[clusterId]) {
|
||||||
|
log.debug('Tilt, ', clusterId, ',not in descendants');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
descendants[clusterId].includes(edge.v) ||
|
||||||
|
isDescendant(edge.v, clusterId) ||
|
||||||
|
isDescendant(edge.w, clusterId) ||
|
||||||
|
descendants[clusterId].includes(edge.w)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = (clusterId, graph, newGraph, rootId) => {
|
||||||
|
log.warn(
|
||||||
|
'Copying children of ',
|
||||||
|
clusterId,
|
||||||
|
'root',
|
||||||
|
rootId,
|
||||||
|
'data',
|
||||||
|
graph.node(clusterId),
|
||||||
|
rootId
|
||||||
|
);
|
||||||
|
const nodes = graph.children(clusterId) || [];
|
||||||
|
|
||||||
|
// Include cluster node if it is not the root
|
||||||
|
if (clusterId !== rootId) {
|
||||||
|
nodes.push(clusterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn('Copying (nodes) clusterId', clusterId, 'nodes', nodes);
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (graph.children(node).length > 0) {
|
||||||
|
copy(node, graph, newGraph, rootId);
|
||||||
|
} else {
|
||||||
|
const data = graph.node(node);
|
||||||
|
log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId);
|
||||||
|
newGraph.setNode(node, data);
|
||||||
|
if (rootId !== graph.parent(node)) {
|
||||||
|
log.warn('Setting parent', node, graph.parent(node));
|
||||||
|
newGraph.setParent(node, graph.parent(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clusterId !== rootId && node !== clusterId) {
|
||||||
|
log.debug('Setting parent', node, clusterId);
|
||||||
|
newGraph.setParent(node, clusterId);
|
||||||
|
} else {
|
||||||
|
log.info('In copy ', clusterId, 'root', rootId, 'data', graph.node(clusterId), rootId);
|
||||||
|
log.debug(
|
||||||
|
'Not Setting parent for node=',
|
||||||
|
node,
|
||||||
|
'cluster!==rootId',
|
||||||
|
clusterId !== rootId,
|
||||||
|
'node!==clusterId',
|
||||||
|
node !== clusterId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const edges = graph.edges(node);
|
||||||
|
log.debug('Copying Edges', edges);
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
log.info('Edge', edge);
|
||||||
|
const data = graph.edge(edge.v, edge.w, edge.name);
|
||||||
|
log.info('Edge data', data, rootId);
|
||||||
|
try {
|
||||||
|
// Do not copy edges in and out of the root cluster, they belong to the parent graph
|
||||||
|
if (edgeInCluster(edge, rootId)) {
|
||||||
|
log.info('Copying as ', edge.v, edge.w, data, edge.name);
|
||||||
|
newGraph.setEdge(edge.v, edge.w, data, edge.name);
|
||||||
|
log.info('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0]));
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'Skipping copy of edge ',
|
||||||
|
edge.v,
|
||||||
|
'-->',
|
||||||
|
edge.w,
|
||||||
|
' rootId: ',
|
||||||
|
rootId,
|
||||||
|
' clusterId:',
|
||||||
|
clusterId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.debug('Removing node', node);
|
||||||
|
graph.removeNode(node);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const extractDescendants = (id, graph) => {
|
||||||
|
// log.debug('Extracting ', id);
|
||||||
|
const children = graph.children(id);
|
||||||
|
let res = [...children];
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
parents[child] = id;
|
||||||
|
res = [...res, ...extractDescendants(child, graph)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the graph, checking that all parent child relation points to existing nodes and that
|
||||||
|
* edges between nodes also ia correct. When not correct the function logs the discrepancies.
|
||||||
|
*
|
||||||
|
* @param graph
|
||||||
|
*/
|
||||||
|
export const validate = (graph) => {
|
||||||
|
const edges = graph.edges();
|
||||||
|
log.trace('Edges: ', edges);
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (graph.children(edge.v).length > 0) {
|
||||||
|
log.trace('The node ', edge.v, ' is part of and edge even though it has children');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (graph.children(edge.w).length > 0) {
|
||||||
|
log.trace('The node ', edge.w, ' is part of and edge even though it has children');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a child that is not a cluster. When faking an edge between a node and a cluster.
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @param {any} graph
|
||||||
|
*/
|
||||||
|
export const findNonClusterChild = (id, graph) => {
|
||||||
|
// const node = graph.node(id);
|
||||||
|
log.trace('Searching', id);
|
||||||
|
// const children = graph.children(id).reverse();
|
||||||
|
const children = graph.children(id); //.reverse();
|
||||||
|
log.trace('Searching children of id ', id, children);
|
||||||
|
if (children.length < 1) {
|
||||||
|
log.trace('This is a valid node', id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
for (const child of children) {
|
||||||
|
const _id = findNonClusterChild(child, graph);
|
||||||
|
if (_id) {
|
||||||
|
log.trace('Found replacement for', id, ' => ', _id);
|
||||||
|
return _id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnchorId = (id) => {
|
||||||
|
if (!clusterDb[id]) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
// If the cluster has no external connections
|
||||||
|
if (!clusterDb[id].externalConnections) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the replacement node
|
||||||
|
if (clusterDb[id]) {
|
||||||
|
return clusterDb[id].id;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adjustClustersAndEdges = (graph, depth) => {
|
||||||
|
if (!graph || depth > 10) {
|
||||||
|
log.debug('Opting out, no graph ');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log.debug('Opting in, graph ');
|
||||||
|
}
|
||||||
|
// Go through the nodes and for each cluster found, save a replacement node, this can be used when
|
||||||
|
// faking a link to a cluster
|
||||||
|
graph.nodes().forEach(function (id) {
|
||||||
|
const children = graph.children(id);
|
||||||
|
if (children.length > 0) {
|
||||||
|
log.warn(
|
||||||
|
'Cluster identified',
|
||||||
|
id,
|
||||||
|
' Replacement id in edges: ',
|
||||||
|
findNonClusterChild(id, graph)
|
||||||
|
);
|
||||||
|
descendants[id] = extractDescendants(id, graph);
|
||||||
|
clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check incoming and outgoing edges for each cluster
|
||||||
|
graph.nodes().forEach(function (id) {
|
||||||
|
const children = graph.children(id);
|
||||||
|
const edges = graph.edges();
|
||||||
|
if (children.length > 0) {
|
||||||
|
log.debug('Cluster identified', id, descendants);
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
// log.debug('Edge, descendants: ', edge, descendants[id]);
|
||||||
|
|
||||||
|
// Check if any edge leaves the cluster (not the actual cluster, that's a link from the box)
|
||||||
|
if (edge.v !== id && edge.w !== id) {
|
||||||
|
// Any edge where either the one of the nodes is descending to the cluster but not the other
|
||||||
|
// if (descendants[id].indexOf(edge.v) < 0 && descendants[id].indexOf(edge.w) < 0) {
|
||||||
|
|
||||||
|
const d1 = isDescendant(edge.v, id);
|
||||||
|
const d2 = isDescendant(edge.w, id);
|
||||||
|
|
||||||
|
// d1 xor d2 - if either d1 is true and d2 is false or the other way around
|
||||||
|
if (d1 ^ d2) {
|
||||||
|
log.warn('Edge: ', edge, ' leaves cluster ', id);
|
||||||
|
log.warn('Descendants of XXX ', id, ': ', descendants[id]);
|
||||||
|
clusterDb[id].externalConnections = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.debug('Not a cluster ', id, descendants);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let id of Object.keys(clusterDb)) {
|
||||||
|
const nonClusterChild = clusterDb[id].id;
|
||||||
|
const parent = graph.parent(nonClusterChild);
|
||||||
|
|
||||||
|
// Change replacement node of id to parent of current replacement node if valid
|
||||||
|
if (parent !== id && clusterDb[parent] && !clusterDb[parent].externalConnections) {
|
||||||
|
clusterDb[id].id = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For clusters with incoming and/or outgoing edges translate those edges to a real node
|
||||||
|
// in the cluster in order to fake the edge
|
||||||
|
graph.edges().forEach(function (e) {
|
||||||
|
const edge = graph.edge(e);
|
||||||
|
log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||||
|
log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
|
||||||
|
|
||||||
|
let v = e.v;
|
||||||
|
let w = e.w;
|
||||||
|
// Check if link is either from or to a cluster
|
||||||
|
log.warn(
|
||||||
|
'Fix XXX',
|
||||||
|
clusterDb,
|
||||||
|
'ids:',
|
||||||
|
e.v,
|
||||||
|
e.w,
|
||||||
|
'Translating: ',
|
||||||
|
clusterDb[e.v],
|
||||||
|
' --- ',
|
||||||
|
clusterDb[e.w]
|
||||||
|
);
|
||||||
|
if (clusterDb[e.v] && clusterDb[e.w] && clusterDb[e.v] === clusterDb[e.w]) {
|
||||||
|
// cspell:ignore trixing
|
||||||
|
log.warn('Fixing and trixing link to self - removing XXX', e.v, e.w, e.name);
|
||||||
|
log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name);
|
||||||
|
v = getAnchorId(e.v);
|
||||||
|
w = getAnchorId(e.w);
|
||||||
|
graph.removeEdge(e.v, e.w, e.name);
|
||||||
|
const specialId = e.w + '---' + e.v;
|
||||||
|
graph.setNode(specialId, {
|
||||||
|
domId: specialId,
|
||||||
|
id: specialId,
|
||||||
|
labelStyle: '',
|
||||||
|
label: edge.label,
|
||||||
|
padding: 0,
|
||||||
|
shape: 'labelRect',
|
||||||
|
style: '',
|
||||||
|
});
|
||||||
|
const edge1 = structuredClone(edge);
|
||||||
|
const edge2 = structuredClone(edge);
|
||||||
|
edge1.label = '';
|
||||||
|
edge1.arrowTypeEnd = 'none';
|
||||||
|
edge2.label = '';
|
||||||
|
edge1.fromCluster = e.v;
|
||||||
|
edge2.toCluster = e.v;
|
||||||
|
|
||||||
|
graph.setEdge(v, specialId, edge1, e.name + '-cyclic-special');
|
||||||
|
graph.setEdge(specialId, w, edge2, e.name + '-cyclic-special');
|
||||||
|
} else if (clusterDb[e.v] || clusterDb[e.w]) {
|
||||||
|
log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name);
|
||||||
|
v = getAnchorId(e.v);
|
||||||
|
w = getAnchorId(e.w);
|
||||||
|
graph.removeEdge(e.v, e.w, e.name);
|
||||||
|
if (v !== e.v) {
|
||||||
|
const parent = graph.parent(v);
|
||||||
|
clusterDb[parent].externalConnections = true;
|
||||||
|
edge.fromCluster = e.v;
|
||||||
|
}
|
||||||
|
if (w !== e.w) {
|
||||||
|
const parent = graph.parent(w);
|
||||||
|
clusterDb[parent].externalConnections = true;
|
||||||
|
edge.toCluster = e.w;
|
||||||
|
}
|
||||||
|
log.warn('Fix Replacing with XXX', v, w, e.name);
|
||||||
|
graph.setEdge(v, w, edge, e.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.warn('Adjusted Graph', graphlibJson.write(graph));
|
||||||
|
extractor(graph, 0);
|
||||||
|
|
||||||
|
log.trace(clusterDb);
|
||||||
|
|
||||||
|
// Remove references to extracted cluster
|
||||||
|
// graph.edges().forEach(edge => {
|
||||||
|
// if (isDescendant(edge.v, clusterId) || isDescendant(edge.w, clusterId)) {
|
||||||
|
// graph.removeEdge(edge);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractor = (graph, depth) => {
|
||||||
|
log.warn('extractor - ', depth, graphlibJson.write(graph), graph.children('D'));
|
||||||
|
if (depth > 10) {
|
||||||
|
log.error('Bailing out');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// For clusters without incoming and/or outgoing edges, create a new cluster-node
|
||||||
|
// containing the nodes and edges in the custer in a new graph
|
||||||
|
// for (let i = 0;)
|
||||||
|
let nodes = graph.nodes();
|
||||||
|
let hasChildren = false;
|
||||||
|
for (const node of nodes) {
|
||||||
|
const children = graph.children(node);
|
||||||
|
hasChildren = hasChildren || children.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChildren) {
|
||||||
|
log.debug('Done, no node has children', graph.nodes());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// const clusters = Object.keys(clusterDb);
|
||||||
|
// clusters.forEach(clusterId => {
|
||||||
|
log.debug('Nodes = ', nodes, depth);
|
||||||
|
for (const node of nodes) {
|
||||||
|
log.debug(
|
||||||
|
'Extracting node',
|
||||||
|
node,
|
||||||
|
clusterDb,
|
||||||
|
clusterDb[node] && !clusterDb[node].externalConnections,
|
||||||
|
!graph.parent(node),
|
||||||
|
graph.node(node),
|
||||||
|
graph.children('D'),
|
||||||
|
' Depth ',
|
||||||
|
depth
|
||||||
|
);
|
||||||
|
// Note that the node might have been removed after the Object.keys call so better check
|
||||||
|
// that it still is in the game
|
||||||
|
if (!clusterDb[node]) {
|
||||||
|
// Skip if the node is not a cluster
|
||||||
|
log.debug('Not a cluster', node, depth);
|
||||||
|
// break;
|
||||||
|
} else if (
|
||||||
|
!clusterDb[node].externalConnections &&
|
||||||
|
// !graph.parent(node) &&
|
||||||
|
graph.children(node) &&
|
||||||
|
graph.children(node).length > 0
|
||||||
|
) {
|
||||||
|
log.warn(
|
||||||
|
'Cluster without external connections, without a parent and with children',
|
||||||
|
node,
|
||||||
|
depth
|
||||||
|
);
|
||||||
|
|
||||||
|
const graphSettings = graph.graph();
|
||||||
|
let dir = graphSettings.rankdir === 'TB' ? 'LR' : 'TB';
|
||||||
|
if (clusterDb[node] && clusterDb[node].clusterData && clusterDb[node].clusterData.dir) {
|
||||||
|
dir = clusterDb[node].clusterData.dir;
|
||||||
|
log.warn('Fixing dir', clusterDb[node].clusterData.dir, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clusterGraph = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true,
|
||||||
|
})
|
||||||
|
.setGraph({
|
||||||
|
rankdir: dir, // Todo: set proper spacing
|
||||||
|
nodesep: 50,
|
||||||
|
ranksep: 50,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8,
|
||||||
|
})
|
||||||
|
.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
log.warn('Old graph before copy', graphlibJson.write(graph));
|
||||||
|
copy(node, graph, clusterGraph, node);
|
||||||
|
graph.setNode(node, {
|
||||||
|
clusterNode: true,
|
||||||
|
id: node,
|
||||||
|
clusterData: clusterDb[node].clusterData,
|
||||||
|
label: clusterDb[node].label,
|
||||||
|
graph: clusterGraph,
|
||||||
|
});
|
||||||
|
log.warn('New graph after copy node: (', node, ')', graphlibJson.write(clusterGraph));
|
||||||
|
log.debug('Old graph after copy', graphlibJson.write(graph));
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
'Cluster ** ',
|
||||||
|
node,
|
||||||
|
' **not meeting the criteria !externalConnections:',
|
||||||
|
!clusterDb[node].externalConnections,
|
||||||
|
' no parent: ',
|
||||||
|
!graph.parent(node),
|
||||||
|
' children ',
|
||||||
|
graph.children(node) && graph.children(node).length > 0,
|
||||||
|
graph.children('D'),
|
||||||
|
depth
|
||||||
|
);
|
||||||
|
log.debug(clusterDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes = graph.nodes();
|
||||||
|
log.warn('New list of nodes', nodes);
|
||||||
|
for (const node of nodes) {
|
||||||
|
const data = graph.node(node);
|
||||||
|
log.warn(' Now next level', node, data);
|
||||||
|
if (data.clusterNode) {
|
||||||
|
extractor(data.graph, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sorter = (graph, nodes) => {
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let result = Object.assign(nodes);
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const children = graph.children(node);
|
||||||
|
const sorted = sorter(graph, children);
|
||||||
|
result = [...result, ...sorted];
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortNodesByHierarchy = (graph) => sorter(graph, graph.children());
|
@@ -0,0 +1,508 @@
|
|||||||
|
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
||||||
|
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||||
|
import {
|
||||||
|
validate,
|
||||||
|
adjustClustersAndEdges,
|
||||||
|
extractDescendants,
|
||||||
|
sortNodesByHierarchy,
|
||||||
|
} from './mermaid-graphlib.js';
|
||||||
|
import { setLogLevel, log } from '$root/logger.js';
|
||||||
|
|
||||||
|
describe('Graphlib decorations', () => {
|
||||||
|
let g;
|
||||||
|
beforeEach(function () {
|
||||||
|
setLogLevel(1);
|
||||||
|
g = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true,
|
||||||
|
});
|
||||||
|
g.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
nodesep: 10,
|
||||||
|
ranksep: 10,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8,
|
||||||
|
});
|
||||||
|
g.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validate', function () {
|
||||||
|
it('Validate should detect edges between clusters', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a --> b
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
c
|
||||||
|
end
|
||||||
|
C1 --> C2
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setParent('b', 'C1');
|
||||||
|
g.setParent('c', 'C2');
|
||||||
|
g.setEdge('a', 'b');
|
||||||
|
g.setEdge('C1', 'C2');
|
||||||
|
|
||||||
|
expect(validate(g)).toBe(false);
|
||||||
|
});
|
||||||
|
it('Validate should not detect edges between clusters after adjustment', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a --> b
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
c
|
||||||
|
end
|
||||||
|
C1 --> C2
|
||||||
|
*/
|
||||||
|
g.setNode('a', {});
|
||||||
|
g.setNode('b', {});
|
||||||
|
g.setNode('c', {});
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setParent('b', 'C1');
|
||||||
|
g.setParent('c', 'C2');
|
||||||
|
g.setEdge('a', 'b');
|
||||||
|
g.setEdge('C1', 'C2');
|
||||||
|
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
log.info(g.edges());
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Validate should detect edges between clusters and transform clusters GLB4', function () {
|
||||||
|
/*
|
||||||
|
a --> b
|
||||||
|
subgraph C1
|
||||||
|
subgraph C2
|
||||||
|
a
|
||||||
|
end
|
||||||
|
b
|
||||||
|
end
|
||||||
|
C1 --> c
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setNode('C1', { data: 4 });
|
||||||
|
g.setNode('C2', { data: 5 });
|
||||||
|
g.setParent('a', 'C2');
|
||||||
|
g.setParent('b', 'C1');
|
||||||
|
g.setParent('C2', 'C1');
|
||||||
|
g.setEdge('a', 'b', { name: 'C1-internal-link' });
|
||||||
|
g.setEdge('C1', 'c', { name: 'C1-external-link' });
|
||||||
|
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
log.info(g.nodes());
|
||||||
|
expect(g.nodes().length).toBe(2);
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
it('Validate should detect edges between clusters and transform clusters GLB5', function () {
|
||||||
|
/*
|
||||||
|
a --> b
|
||||||
|
subgraph C1
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph C2
|
||||||
|
b
|
||||||
|
end
|
||||||
|
C1 -->
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setParent('b', 'C2');
|
||||||
|
// g.setEdge('a', 'b', { name: 'C1-internal-link' });
|
||||||
|
g.setEdge('C1', 'C2', { name: 'C1-external-link' });
|
||||||
|
|
||||||
|
log.info(g.nodes());
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
log.info(g.nodes());
|
||||||
|
expect(g.nodes().length).toBe(2);
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges GLB6', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a
|
||||||
|
end
|
||||||
|
C1 --> b
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('C1', { data: 3 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setEdge('C1', 'b', { data: 'link1' }, '1');
|
||||||
|
|
||||||
|
// log.info(g.edges())
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
log.info(g.edges());
|
||||||
|
expect(g.nodes()).toEqual(['b', 'C1']);
|
||||||
|
expect(g.edges().length).toBe(1);
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
expect(g.node('C1').clusterNode).toBe(true);
|
||||||
|
|
||||||
|
const C1Graph = g.node('C1').graph;
|
||||||
|
expect(C1Graph.nodes()).toEqual(['a']);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges GLB7', function () {
|
||||||
|
/*
|
||||||
|
subgraph C1
|
||||||
|
a
|
||||||
|
end
|
||||||
|
C1 --> b
|
||||||
|
C1 --> c
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'C1');
|
||||||
|
g.setNode('C1', { data: 4 });
|
||||||
|
g.setEdge('C1', 'b', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('C1', 'c', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
log.info(g.node('C1'));
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
log.info(g.edges());
|
||||||
|
expect(g.nodes()).toEqual(['b', 'c', 'C1']);
|
||||||
|
expect(g.nodes().length).toBe(3);
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
const edgeData = g.edge(g.edges()[1]);
|
||||||
|
expect(edgeData.data).toBe('link2');
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
|
||||||
|
const C1Graph = g.node('C1').graph;
|
||||||
|
expect(C1Graph.nodes()).toEqual(['a']);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges GLB8', function () {
|
||||||
|
/*
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph C
|
||||||
|
c
|
||||||
|
end
|
||||||
|
A --> B
|
||||||
|
A --> C
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'A');
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setParent('c', 'C');
|
||||||
|
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
// log.info(g.edges())
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
expect(g.nodes()).toEqual(['A', 'B', 'C']);
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
const edgeData = g.edge(g.edges()[1]);
|
||||||
|
expect(edgeData.data).toBe('link2');
|
||||||
|
expect(validate(g)).toBe(true);
|
||||||
|
|
||||||
|
const CGraph = g.node('C').graph;
|
||||||
|
expect(CGraph.nodes()).toEqual(['c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB10', function () {
|
||||||
|
/*
|
||||||
|
subgraph C
|
||||||
|
subgraph D
|
||||||
|
d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
*/
|
||||||
|
|
||||||
|
g.setNode('C', { data: 1 });
|
||||||
|
g.setNode('D', { data: 2 });
|
||||||
|
g.setNode('d', { data: 3 });
|
||||||
|
g.setParent('d', 'D');
|
||||||
|
g.setParent('D', 'C');
|
||||||
|
|
||||||
|
// log.info('Graph before', g.node('D'))
|
||||||
|
// log.info('Graph before', graphlibJson.write(g))
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
// log.info('Graph after', graphlibJson.write(g), g.node('C').graph)
|
||||||
|
|
||||||
|
const CGraph = g.node('C').graph;
|
||||||
|
const DGraph = CGraph.node('D').graph;
|
||||||
|
|
||||||
|
expect(CGraph.nodes()).toEqual(['D']);
|
||||||
|
expect(DGraph.nodes()).toEqual(['d']);
|
||||||
|
|
||||||
|
expect(g.nodes()).toEqual(['C']);
|
||||||
|
expect(g.nodes().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB11', function () {
|
||||||
|
/*
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph C
|
||||||
|
subgraph D
|
||||||
|
d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
A --> B
|
||||||
|
A --> C
|
||||||
|
*/
|
||||||
|
|
||||||
|
g.setNode('C', { data: 1 });
|
||||||
|
g.setNode('D', { data: 2 });
|
||||||
|
g.setNode('d', { data: 3 });
|
||||||
|
g.setNode('B', { data: 4 });
|
||||||
|
g.setNode('b', { data: 5 });
|
||||||
|
g.setNode('A', { data: 6 });
|
||||||
|
g.setNode('a', { data: 7 });
|
||||||
|
g.setParent('a', 'A');
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setParent('d', 'D');
|
||||||
|
g.setParent('D', 'C');
|
||||||
|
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
log.info('Graph before', g.node('D'));
|
||||||
|
log.info('Graph before', graphlibJson.write(g));
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
log.trace('Graph after', graphlibJson.write(g));
|
||||||
|
expect(g.nodes()).toEqual(['C', 'B', 'A']);
|
||||||
|
expect(g.nodes().length).toBe(3);
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
const AGraph = g.node('A').graph;
|
||||||
|
const BGraph = g.node('B').graph;
|
||||||
|
const CGraph = g.node('C').graph;
|
||||||
|
// log.info(CGraph.nodes());
|
||||||
|
const DGraph = CGraph.node('D').graph;
|
||||||
|
// log.info('DG', CGraph.children('D'));
|
||||||
|
|
||||||
|
log.info('A', AGraph.nodes());
|
||||||
|
expect(AGraph.nodes().length).toBe(1);
|
||||||
|
expect(AGraph.nodes()).toEqual(['a']);
|
||||||
|
log.trace('Nodes', BGraph.nodes());
|
||||||
|
expect(BGraph.nodes().length).toBe(1);
|
||||||
|
expect(BGraph.nodes()).toEqual(['b']);
|
||||||
|
expect(CGraph.nodes()).toEqual(['D']);
|
||||||
|
expect(CGraph.nodes().length).toEqual(1);
|
||||||
|
|
||||||
|
expect(AGraph.edges().length).toBe(0);
|
||||||
|
expect(BGraph.edges().length).toBe(0);
|
||||||
|
expect(CGraph.edges().length).toBe(0);
|
||||||
|
expect(DGraph.nodes()).toEqual(['d']);
|
||||||
|
expect(DGraph.edges().length).toBe(0);
|
||||||
|
// expect(CGraph.node('D')).toEqual({ data: 2 });
|
||||||
|
expect(g.edges().length).toBe(2);
|
||||||
|
|
||||||
|
// expect(g.edges().length).toBe(2);
|
||||||
|
// const edgeData = g.edge(g.edges()[1]);
|
||||||
|
// expect(edgeData.data).toBe('link2');
|
||||||
|
// expect(validate(g)).toBe(true);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB20', function () {
|
||||||
|
/*
|
||||||
|
a --> b
|
||||||
|
subgraph b [Test]
|
||||||
|
c --> d -->e
|
||||||
|
end
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setNode('d', { data: 3 });
|
||||||
|
g.setNode('e', { data: 3 });
|
||||||
|
g.setParent('c', 'b');
|
||||||
|
g.setParent('d', 'b');
|
||||||
|
g.setParent('e', 'b');
|
||||||
|
g.setEdge('a', 'b', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('c', 'd', { data: 'link2' }, '2');
|
||||||
|
g.setEdge('d', 'e', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
log.info('Graph before', graphlibJson.write(g));
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
const bGraph = g.node('b').graph;
|
||||||
|
// log.trace('Graph after', graphlibJson.write(g))
|
||||||
|
log.info('Graph after', graphlibJson.write(bGraph));
|
||||||
|
expect(bGraph.nodes().length).toBe(3);
|
||||||
|
expect(bGraph.edges().length).toBe(2);
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB21', function () {
|
||||||
|
/*
|
||||||
|
state a {
|
||||||
|
state b {
|
||||||
|
state c {
|
||||||
|
e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setNode('e', { data: 3 });
|
||||||
|
g.setParent('b', 'a');
|
||||||
|
g.setParent('c', 'b');
|
||||||
|
g.setParent('e', 'c');
|
||||||
|
|
||||||
|
log.info('Graph before', graphlibJson.write(g));
|
||||||
|
adjustClustersAndEdges(g);
|
||||||
|
const aGraph = g.node('a').graph;
|
||||||
|
const bGraph = aGraph.node('b').graph;
|
||||||
|
log.info('Graph after', graphlibJson.write(aGraph));
|
||||||
|
const cGraph = bGraph.node('c').graph;
|
||||||
|
// log.trace('Graph after', graphlibJson.write(g))
|
||||||
|
expect(aGraph.nodes().length).toBe(1);
|
||||||
|
expect(bGraph.nodes().length).toBe(1);
|
||||||
|
expect(cGraph.nodes().length).toBe(1);
|
||||||
|
expect(bGraph.edges().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('adjustClustersAndEdges should handle nesting GLB77', function () {
|
||||||
|
/*
|
||||||
|
flowchart TB
|
||||||
|
subgraph A
|
||||||
|
b-->B
|
||||||
|
a-->c
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
c
|
||||||
|
end
|
||||||
|
*/
|
||||||
|
|
||||||
|
const exportedGraph = JSON.parse(
|
||||||
|
'{"options":{"directed":true,"multigraph":true,"compound":true},"nodes":[{"v":"A","value":{"labelStyle":"","shape":"rect","labelText":"A","rx":0,"ry":0,"cssClass":"default","style":"","id":"A","width":500,"type":"group","padding":15}},{"v":"B","value":{"labelStyle":"","shape":"rect","labelText":"B","rx":0,"ry":0,"class":"default","style":"","id":"B","width":500,"type":"group","padding":15},"parent":"A"},{"v":"b","value":{"labelStyle":"","shape":"rect","labelText":"b","rx":0,"ry":0,"class":"default","style":"","id":"b","padding":15},"parent":"A"},{"v":"c","value":{"labelStyle":"","shape":"rect","labelText":"c","rx":0,"ry":0,"class":"default","style":"","id":"c","padding":15},"parent":"B"},{"v":"a","value":{"labelStyle":"","shape":"rect","labelText":"a","rx":0,"ry":0,"class":"default","style":"","id":"a","padding":15},"parent":"A"}],"edges":[{"v":"b","w":"B","name":"1","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-b-B","cssClasses":"flowchart-link LS-b LE-B"}},{"v":"a","w":"c","name":"2","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-a-c","cssClasses":"flowchart-link LS-a LE-c"}}],"value":{"rankdir":"TB","nodesep":50,"ranksep":50,"marginx":8,"marginy":8}}'
|
||||||
|
);
|
||||||
|
const gr = graphlibJson.read(exportedGraph);
|
||||||
|
|
||||||
|
log.info('Graph before', graphlibJson.write(gr));
|
||||||
|
adjustClustersAndEdges(gr);
|
||||||
|
const aGraph = gr.node('A').graph;
|
||||||
|
const bGraph = aGraph.node('B').graph;
|
||||||
|
log.info('Graph after', graphlibJson.write(aGraph));
|
||||||
|
// log.trace('Graph after', graphlibJson.write(g))
|
||||||
|
expect(aGraph.parent('c')).toBe('B');
|
||||||
|
expect(aGraph.parent('B')).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('extractDescendants', function () {
|
||||||
|
let g;
|
||||||
|
beforeEach(function () {
|
||||||
|
setLogLevel(1);
|
||||||
|
g = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true,
|
||||||
|
});
|
||||||
|
g.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
nodesep: 10,
|
||||||
|
ranksep: 10,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8,
|
||||||
|
});
|
||||||
|
g.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Simple case of one level descendants GLB9', function () {
|
||||||
|
/*
|
||||||
|
subgraph A
|
||||||
|
a
|
||||||
|
end
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph C
|
||||||
|
c
|
||||||
|
end
|
||||||
|
A --> B
|
||||||
|
A --> C
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setNode('c', { data: 3 });
|
||||||
|
g.setParent('a', 'A');
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setParent('c', 'C');
|
||||||
|
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||||
|
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||||
|
|
||||||
|
// log.info(g.edges())
|
||||||
|
const d1 = extractDescendants('A', g);
|
||||||
|
const d2 = extractDescendants('B', g);
|
||||||
|
const d3 = extractDescendants('C', g);
|
||||||
|
|
||||||
|
expect(d1).toEqual(['a']);
|
||||||
|
expect(d2).toEqual(['b']);
|
||||||
|
expect(d3).toEqual(['c']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('sortNodesByHierarchy', function () {
|
||||||
|
let g;
|
||||||
|
beforeEach(function () {
|
||||||
|
setLogLevel(1);
|
||||||
|
g = new graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true,
|
||||||
|
});
|
||||||
|
g.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
nodesep: 10,
|
||||||
|
ranksep: 10,
|
||||||
|
marginx: 8,
|
||||||
|
marginy: 8,
|
||||||
|
});
|
||||||
|
g.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should sort proper en nodes are in reverse order', function () {
|
||||||
|
/*
|
||||||
|
a -->b
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph A
|
||||||
|
B
|
||||||
|
end
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setParent('B', 'A');
|
||||||
|
g.setEdge('a', 'b', '1');
|
||||||
|
expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
|
||||||
|
});
|
||||||
|
it('should sort proper en nodes are in correct order', function () {
|
||||||
|
/*
|
||||||
|
a -->b
|
||||||
|
subgraph B
|
||||||
|
b
|
||||||
|
end
|
||||||
|
subgraph A
|
||||||
|
B
|
||||||
|
end
|
||||||
|
*/
|
||||||
|
g.setNode('a', { data: 1 });
|
||||||
|
g.setParent('B', 'A');
|
||||||
|
g.setParent('b', 'B');
|
||||||
|
g.setNode('b', { data: 2 });
|
||||||
|
g.setEdge('a', 'b', '1');
|
||||||
|
expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
|
||||||
|
});
|
||||||
|
});
|
40
packages/mermaid/src/rendering-util/render.ts
Normal file
40
packages/mermaid/src/rendering-util/render.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export interface LayoutAlgorithm {
|
||||||
|
render(data4Layout: any, svg: any, element: any, algorithm?: string): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LayoutLoader = () => Promise<LayoutAlgorithm>;
|
||||||
|
export interface LayoutLoaderDefinition {
|
||||||
|
name: string;
|
||||||
|
loader: LayoutLoader;
|
||||||
|
algorithm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutAlgorithms: Record<string, LayoutLoaderDefinition> = {};
|
||||||
|
|
||||||
|
export const registerLayoutLoaders = (loaders: LayoutLoaderDefinition[]) => {
|
||||||
|
for (const loader of loaders) {
|
||||||
|
layoutAlgorithms[loader.name] = loader;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Should we load dagre without lazy loading?
|
||||||
|
const registerDefaultLayoutLoaders = () => {
|
||||||
|
registerLayoutLoaders([
|
||||||
|
{
|
||||||
|
name: 'dagre',
|
||||||
|
loader: async () => await import('./layout-algorithms/dagre/index.js'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerDefaultLayoutLoaders();
|
||||||
|
|
||||||
|
export const render = async (data4Layout: any, svg: any, element: any) => {
|
||||||
|
if (!(data4Layout.layoutAlgorithm in layoutAlgorithms)) {
|
||||||
|
throw new Error(`Unknown layout algorithm: ${data4Layout.layoutAlgorithm}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutDefinition = layoutAlgorithms[data4Layout.layoutAlgorithm];
|
||||||
|
const layoutRenderer = await layoutDefinition.loader();
|
||||||
|
return layoutRenderer.render(data4Layout, svg, element, layoutDefinition.algorithm);
|
||||||
|
};
|
@@ -0,0 +1,334 @@
|
|||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
import { evaluate } from '$root/diagrams/common/common.js';
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { getSubGraphTitleMargins } from '$root/utils/subGraphTitleMargins.js';
|
||||||
|
import { select } from 'd3';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
import { createText } from '../createText.ts';
|
||||||
|
import intersectRect from '../rendering-elements/intersect/intersect-rect.js';
|
||||||
|
import createLabel from './createLabel.js';
|
||||||
|
import { createRoundedRectPathD } from './shapes/roundedRectPath.ts';
|
||||||
|
|
||||||
|
const rect = (parent, node) => {
|
||||||
|
log.info('Creating subgraph rect for ', node.id, node);
|
||||||
|
const siteConfig = getConfig();
|
||||||
|
|
||||||
|
// Add outer g element
|
||||||
|
const shapeSvg = parent.insert('g').attr('class', 'cluster').attr('id', node.id);
|
||||||
|
|
||||||
|
// add the rect
|
||||||
|
const rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
|
||||||
|
const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
|
||||||
|
|
||||||
|
// Create the label and insert it after the rect
|
||||||
|
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||||
|
|
||||||
|
// const text = label
|
||||||
|
// .node()
|
||||||
|
// .appendChild(createLabel(node.label, node.labelStyle, undefined, true));
|
||||||
|
const text =
|
||||||
|
node.labelType === 'markdown'
|
||||||
|
? createText(labelEl, node.label, { style: node.labelStyle, useHtmlLabels })
|
||||||
|
: labelEl.node().appendChild(createLabel(node.label, node.labelStyle, undefined, true));
|
||||||
|
|
||||||
|
// Get the size of the label
|
||||||
|
let bbox = text.getBBox();
|
||||||
|
|
||||||
|
if (evaluate(siteConfig.flowchart.htmlLabels)) {
|
||||||
|
const div = text.children[0];
|
||||||
|
const dv = select(text);
|
||||||
|
bbox = div.getBoundingClientRect();
|
||||||
|
dv.attr('width', bbox.width);
|
||||||
|
dv.attr('height', bbox.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = 0 * node.padding;
|
||||||
|
const halfPadding = padding / 2;
|
||||||
|
|
||||||
|
const width = node.width <= bbox.width + padding ? bbox.width + padding : node.width;
|
||||||
|
if (node.width <= bbox.width + padding) {
|
||||||
|
node.diff = (bbox.width - node.width) / 2 - node.padding / 2;
|
||||||
|
} else {
|
||||||
|
node.diff = -node.padding / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace('Data ', node, JSON.stringify(node));
|
||||||
|
// center the rect around its coordinate
|
||||||
|
rect
|
||||||
|
.attr('style', node.cssStyles)
|
||||||
|
.attr('rx', node.rx)
|
||||||
|
.attr('ry', node.ry)
|
||||||
|
.attr('x', node.x - width / 2)
|
||||||
|
.attr('y', node.y - node.height / 2 - halfPadding)
|
||||||
|
.attr('width', width)
|
||||||
|
.attr('height', node.height + padding);
|
||||||
|
|
||||||
|
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
|
||||||
|
if (useHtmlLabels) {
|
||||||
|
labelEl.attr(
|
||||||
|
'transform',
|
||||||
|
// This puts the label on top of the box instead of inside it
|
||||||
|
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
labelEl.attr(
|
||||||
|
'transform',
|
||||||
|
// This puts the label on top of the box instead of inside it
|
||||||
|
`translate(${node.x}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Center the label
|
||||||
|
|
||||||
|
const rectBox = rect.node().getBBox();
|
||||||
|
node.width = rectBox.width;
|
||||||
|
node.height = rectBox.height;
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersectRect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non visible cluster where the note is group with its
|
||||||
|
*
|
||||||
|
* @param {any} parent
|
||||||
|
* @param {any} node
|
||||||
|
* @returns {any} ShapeSvg
|
||||||
|
*/
|
||||||
|
const noteGroup = (parent, node) => {
|
||||||
|
const { themeVariables } = getConfig();
|
||||||
|
const {
|
||||||
|
textColor,
|
||||||
|
clusterTextColor,
|
||||||
|
altBackground,
|
||||||
|
compositeBackground,
|
||||||
|
compositeTitleBackground,
|
||||||
|
compositeBorder,
|
||||||
|
noteBorderColor,
|
||||||
|
noteBkgColor,
|
||||||
|
nodeBorder,
|
||||||
|
mainBkg,
|
||||||
|
stateBorder,
|
||||||
|
} = themeVariables;
|
||||||
|
|
||||||
|
// Add outer g element
|
||||||
|
const shapeSvg = parent.insert('g').attr('class', 'note-cluster').attr('id', node.id);
|
||||||
|
|
||||||
|
// add the rect
|
||||||
|
const rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
|
||||||
|
const padding = 0 * node.padding;
|
||||||
|
const halfPadding = padding / 2;
|
||||||
|
|
||||||
|
// center the rect around its coordinate
|
||||||
|
rect
|
||||||
|
.attr('rx', node.rx)
|
||||||
|
.attr('ry', node.ry)
|
||||||
|
.attr('x', node.x - node.width / 2 - halfPadding)
|
||||||
|
.attr('y', node.y - node.height / 2 - halfPadding)
|
||||||
|
.attr('width', node.width + padding)
|
||||||
|
.attr('height', node.height + padding)
|
||||||
|
.attr('fill', 'none');
|
||||||
|
|
||||||
|
const rectBox = rect.node().getBBox();
|
||||||
|
node.width = rectBox.width;
|
||||||
|
node.height = rectBox.height;
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersectRect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
||||||
|
const roundedWithTitle = (parent, node) => {
|
||||||
|
const siteConfig = getConfig();
|
||||||
|
|
||||||
|
const { themeVariables, handdrawnSeed } = siteConfig;
|
||||||
|
const { altBackground, compositeBackground, compositeTitleBackground, nodeBorder } =
|
||||||
|
themeVariables;
|
||||||
|
|
||||||
|
// Add outer g element
|
||||||
|
const shapeSvg = parent.insert('g').attr('class', node.cssClasses).attr('id', node.id);
|
||||||
|
|
||||||
|
// add the rect
|
||||||
|
const outerRectG = shapeSvg.insert('g', ':first-child');
|
||||||
|
|
||||||
|
// Create the label and insert it after the rect
|
||||||
|
const label = shapeSvg.insert('g').attr('class', 'cluster-label');
|
||||||
|
let innerRect = shapeSvg.append('rect');
|
||||||
|
|
||||||
|
const text = label.node().appendChild(createLabel(node.label, node.labelStyle, undefined, true));
|
||||||
|
|
||||||
|
// Get the size of the label
|
||||||
|
let bbox = text.getBBox();
|
||||||
|
if (evaluate(siteConfig.flowchart.htmlLabels)) {
|
||||||
|
const div = text.children[0];
|
||||||
|
const dv = select(text);
|
||||||
|
bbox = div.getBoundingClientRect();
|
||||||
|
dv.attr('width', bbox.width);
|
||||||
|
dv.attr('height', bbox.height);
|
||||||
|
}
|
||||||
|
bbox = text.getBBox();
|
||||||
|
const padding = 0 * node.padding;
|
||||||
|
const halfPadding = padding / 2;
|
||||||
|
|
||||||
|
const width =
|
||||||
|
(node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width) + padding;
|
||||||
|
if (node.width <= bbox.width + node.padding) {
|
||||||
|
node.diff = (bbox.width + node.padding * 0 - node.width) / 2;
|
||||||
|
} else {
|
||||||
|
node.diff = -node.padding / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = node.x - width / 2 - halfPadding;
|
||||||
|
const y = node.y - node.height / 2 - halfPadding;
|
||||||
|
const innerY = node.y - node.height / 2 - halfPadding + bbox.height - 1;
|
||||||
|
const height = node.height + padding;
|
||||||
|
const innerHeight = node.height + padding - bbox.height - 3;
|
||||||
|
|
||||||
|
// add the rect
|
||||||
|
let rect;
|
||||||
|
if (node.useRough) {
|
||||||
|
const isAlt = node.cssClasses.includes('statediagram-cluster-alt');
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const roughOuterNode =
|
||||||
|
node.rx || node.ry
|
||||||
|
? rc.path(createRoundedRectPathD(x, y, width, height, 10), {
|
||||||
|
roughness: 0.7,
|
||||||
|
fill: compositeTitleBackground,
|
||||||
|
fillStyle: 'solid',
|
||||||
|
stroke: nodeBorder,
|
||||||
|
seed: handdrawnSeed,
|
||||||
|
})
|
||||||
|
: rc.rectangle(x, y, width, height, { seed: handdrawnSeed });
|
||||||
|
|
||||||
|
rect = shapeSvg.insert(() => roughOuterNode, ':first-child');
|
||||||
|
const roughInnerNode = rc.rectangle(x, innerY, width, innerHeight, {
|
||||||
|
fill: isAlt ? altBackground : compositeBackground,
|
||||||
|
fillStyle: isAlt ? 'hachure' : 'solid',
|
||||||
|
stroke: nodeBorder,
|
||||||
|
seed: handdrawnSeed,
|
||||||
|
});
|
||||||
|
|
||||||
|
rect = shapeSvg.insert(() => roughOuterNode, ':first-child');
|
||||||
|
innerRect = shapeSvg.insert(() => roughInnerNode);
|
||||||
|
} else {
|
||||||
|
rect = outerRectG.insert('rect', ':first-child');
|
||||||
|
// center the rect around its coordinate
|
||||||
|
rect
|
||||||
|
.attr('class', 'outer')
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y)
|
||||||
|
.attr('width', width)
|
||||||
|
.attr('height', node.height + padding);
|
||||||
|
innerRect
|
||||||
|
.attr('class', 'inner')
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', innerY)
|
||||||
|
.attr('width', width)
|
||||||
|
.attr('height', innerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
|
||||||
|
// Center the label
|
||||||
|
label.attr(
|
||||||
|
'transform',
|
||||||
|
`translate(${node.x - bbox.width / 2}, ${
|
||||||
|
node.y -
|
||||||
|
node.height / 2 -
|
||||||
|
node.padding / 3 +
|
||||||
|
(evaluate(siteConfig.flowchart.htmlLabels) ? 5 : 3) +
|
||||||
|
subGraphTitleTopMargin
|
||||||
|
})`
|
||||||
|
);
|
||||||
|
|
||||||
|
const rectBox = rect.node().getBBox();
|
||||||
|
node.height = rectBox.height;
|
||||||
|
node.offsetX = 0;
|
||||||
|
node.offsetY = 20;
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersectRect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const divider = (parent, node) => {
|
||||||
|
const { handdrawnSeed } = getConfig();
|
||||||
|
// Add outer g element
|
||||||
|
const shapeSvg = parent.insert('g').attr('class', node.cssClasses).attr('id', node.id);
|
||||||
|
|
||||||
|
// add the rect
|
||||||
|
let rect;
|
||||||
|
|
||||||
|
const padding = 0 * node.padding;
|
||||||
|
const halfPadding = padding / 2;
|
||||||
|
|
||||||
|
const x = node.x - node.width / 2 - halfPadding;
|
||||||
|
const y = node.y - node.height / 2;
|
||||||
|
const width = node.width + padding;
|
||||||
|
const height = node.height + padding;
|
||||||
|
if (node.useRough) {
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const roughNode = rc.rectangle(x, y, width, height, {
|
||||||
|
fill: 'lightgrey',
|
||||||
|
roughness: 0.5,
|
||||||
|
strokeLineDash: [5],
|
||||||
|
seed: handdrawnSeed,
|
||||||
|
});
|
||||||
|
|
||||||
|
rect = shapeSvg.insert(() => roughNode);
|
||||||
|
} else {
|
||||||
|
rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
// center the rect around its coordinate
|
||||||
|
rect
|
||||||
|
.attr('class', 'divider')
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y)
|
||||||
|
.attr('width', width)
|
||||||
|
.attr('height', height);
|
||||||
|
}
|
||||||
|
const rectBox = rect.node().getBBox();
|
||||||
|
node.width = rectBox.width;
|
||||||
|
node.height = rectBox.height - node.padding;
|
||||||
|
node.diff = 0; //-node.padding / 2;
|
||||||
|
node.offsetY = 0;
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersectRect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shapes = { rect, roundedWithTitle, noteGroup, divider };
|
||||||
|
|
||||||
|
let clusterElems = {};
|
||||||
|
|
||||||
|
export const insertCluster = (elem, node) => {
|
||||||
|
const shape = node.shape || 'rect';
|
||||||
|
const cluster = shapes[shape](elem, node);
|
||||||
|
clusterElems[node.id] = cluster;
|
||||||
|
return cluster;
|
||||||
|
};
|
||||||
|
export const getClusterTitleWidth = (elem, node) => {
|
||||||
|
const label = createLabel(node.label, node.labelStyle, undefined, true);
|
||||||
|
elem.node().appendChild(label);
|
||||||
|
const width = label.getBBox().width;
|
||||||
|
elem.node().removeChild(label);
|
||||||
|
return width;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clear = () => {
|
||||||
|
clusterElems = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const positionCluster = (node) => {
|
||||||
|
log.info('Position cluster (' + node.id + ', ' + node.x + ', ' + node.y + ')');
|
||||||
|
const el = clusterElems[node.id];
|
||||||
|
|
||||||
|
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||||
|
};
|
@@ -0,0 +1,101 @@
|
|||||||
|
import { select } from 'd3';
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
import { evaluate } from '$root/diagrams/common/common.js';
|
||||||
|
import { decodeEntities } from '$root/utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param dom
|
||||||
|
* @param styleFn
|
||||||
|
*/
|
||||||
|
function applyStyle(dom, styleFn) {
|
||||||
|
if (styleFn) {
|
||||||
|
dom.attr('style', styleFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} node
|
||||||
|
* @returns {SVGForeignObjectElement} Node
|
||||||
|
*/
|
||||||
|
function addHtmlLabel(node) {
|
||||||
|
const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
|
||||||
|
const div = fo.append('xhtml:div');
|
||||||
|
|
||||||
|
const label = node.label;
|
||||||
|
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
|
||||||
|
div.html(
|
||||||
|
'<span class="' +
|
||||||
|
labelClass +
|
||||||
|
'" ' +
|
||||||
|
(node.labelStyle ? 'style="' + node.labelStyle + '"' : '') +
|
||||||
|
'>' +
|
||||||
|
label +
|
||||||
|
'</span>'
|
||||||
|
);
|
||||||
|
|
||||||
|
applyStyle(div, node.labelStyle);
|
||||||
|
div.style('display', 'inline-block');
|
||||||
|
div.style('padding-right', '1px');
|
||||||
|
// Fix for firefox
|
||||||
|
div.style('white-space', 'nowrap');
|
||||||
|
div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
|
||||||
|
return fo.node();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param _vertexText
|
||||||
|
* @param style
|
||||||
|
* @param isTitle
|
||||||
|
* @param isNode
|
||||||
|
* @deprecated svg-util/createText instead
|
||||||
|
*/
|
||||||
|
const createLabel = (_vertexText, style, isTitle, isNode) => {
|
||||||
|
let vertexText = _vertexText || '';
|
||||||
|
if (typeof vertexText === 'object') {
|
||||||
|
vertexText = vertexText[0];
|
||||||
|
}
|
||||||
|
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||||
|
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||||
|
vertexText = vertexText.replace(/\\n|\n/g, '<br />');
|
||||||
|
log.info('vertexText' + vertexText);
|
||||||
|
const node = {
|
||||||
|
isNode,
|
||||||
|
label: decodeEntities(vertexText).replace(
|
||||||
|
/fa[blrs]?:fa-[\w-]+/g,
|
||||||
|
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||||
|
),
|
||||||
|
labelStyle: style ? style.replace('fill:', 'color:') : style,
|
||||||
|
};
|
||||||
|
let vertexNode = addHtmlLabel(node);
|
||||||
|
// vertexNode.parentNode.removeChild(vertexNode);
|
||||||
|
return vertexNode;
|
||||||
|
} else {
|
||||||
|
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
|
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
|
||||||
|
let rows = [];
|
||||||
|
if (typeof vertexText === 'string') {
|
||||||
|
rows = vertexText.split(/\\n|\n|<br\s*\/?>/gi);
|
||||||
|
} else if (Array.isArray(vertexText)) {
|
||||||
|
rows = vertexText;
|
||||||
|
} else {
|
||||||
|
rows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||||
|
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||||
|
tspan.setAttribute('dy', '1em');
|
||||||
|
tspan.setAttribute('x', '0');
|
||||||
|
if (isTitle) {
|
||||||
|
tspan.setAttribute('class', 'title-row');
|
||||||
|
} else {
|
||||||
|
tspan.setAttribute('class', 'row');
|
||||||
|
}
|
||||||
|
tspan.textContent = row.trim();
|
||||||
|
svgLabel.appendChild(tspan);
|
||||||
|
}
|
||||||
|
return svgLabel;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createLabel;
|
@@ -0,0 +1,79 @@
|
|||||||
|
import type { SVG } from '$root/diagram-api/types.js';
|
||||||
|
import type { Mocked } from 'vitest';
|
||||||
|
import { addEdgeMarkers } from './edgeMarker.js';
|
||||||
|
|
||||||
|
describe('addEdgeMarker', () => {
|
||||||
|
const svgPath = {
|
||||||
|
attr: vitest.fn(),
|
||||||
|
} as unknown as Mocked<SVG>;
|
||||||
|
const url = 'http://example.com';
|
||||||
|
const id = 'test';
|
||||||
|
const diagramType = 'test';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
svgPath.attr.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add markers for arrow_cross:arrow_point', () => {
|
||||||
|
const arrowTypeStart = 'arrow_cross';
|
||||||
|
const arrowTypeEnd = 'arrow_point';
|
||||||
|
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-start',
|
||||||
|
`url(${url}#${id}_${diagramType}-crossStart)`
|
||||||
|
);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-end',
|
||||||
|
`url(${url}#${id}_${diagramType}-pointEnd)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add markers for aggregation:arrow_point', () => {
|
||||||
|
const arrowTypeStart = 'aggregation';
|
||||||
|
const arrowTypeEnd = 'arrow_point';
|
||||||
|
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-start',
|
||||||
|
`url(${url}#${id}_${diagramType}-aggregationStart)`
|
||||||
|
);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-end',
|
||||||
|
`url(${url}#${id}_${diagramType}-pointEnd)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add markers for arrow_point:aggregation', () => {
|
||||||
|
const arrowTypeStart = 'arrow_point';
|
||||||
|
const arrowTypeEnd = 'aggregation';
|
||||||
|
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-start',
|
||||||
|
`url(${url}#${id}_${diagramType}-pointStart)`
|
||||||
|
);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-end',
|
||||||
|
`url(${url}#${id}_${diagramType}-aggregationEnd)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add markers for aggregation:composition', () => {
|
||||||
|
const arrowTypeStart = 'aggregation';
|
||||||
|
const arrowTypeEnd = 'composition';
|
||||||
|
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-start',
|
||||||
|
`url(${url}#${id}_${diagramType}-aggregationStart)`
|
||||||
|
);
|
||||||
|
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||||
|
'marker-end',
|
||||||
|
`url(${url}#${id}_${diagramType}-compositionEnd)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add invalid markers', () => {
|
||||||
|
const arrowTypeStart = 'this is an invalid marker';
|
||||||
|
const arrowTypeEnd = ') url(https://my-malicious-site.example)';
|
||||||
|
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||||
|
expect(svgPath.attr).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,57 @@
|
|||||||
|
import type { SVG } from '$root/diagram-api/types.js';
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import type { EdgeData } from '$root/types.js';
|
||||||
|
/**
|
||||||
|
* Adds SVG markers to a path element based on the arrow types specified in the edge.
|
||||||
|
*
|
||||||
|
* @param svgPath - The SVG path element to add markers to.
|
||||||
|
* @param edge - The edge data object containing the arrow types.
|
||||||
|
* @param url - The URL of the SVG marker definitions.
|
||||||
|
* @param id - The ID prefix for the SVG marker definitions.
|
||||||
|
* @param diagramType - The type of diagram being rendered.
|
||||||
|
*/
|
||||||
|
export const addEdgeMarkers = (
|
||||||
|
svgPath: SVG,
|
||||||
|
edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>,
|
||||||
|
url: string,
|
||||||
|
id: string,
|
||||||
|
diagramType: string
|
||||||
|
) => {
|
||||||
|
if (edge.arrowTypeStart) {
|
||||||
|
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType);
|
||||||
|
}
|
||||||
|
if (edge.arrowTypeEnd) {
|
||||||
|
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrowTypesMap = {
|
||||||
|
arrow_cross: 'cross',
|
||||||
|
arrow_point: 'point',
|
||||||
|
arrow_barb: 'barb',
|
||||||
|
arrow_circle: 'circle',
|
||||||
|
aggregation: 'aggregation',
|
||||||
|
extension: 'extension',
|
||||||
|
composition: 'composition',
|
||||||
|
dependency: 'dependency',
|
||||||
|
lollipop: 'lollipop',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const addEdgeMarker = (
|
||||||
|
svgPath: SVG,
|
||||||
|
position: 'start' | 'end',
|
||||||
|
arrowType: string,
|
||||||
|
url: string,
|
||||||
|
id: string,
|
||||||
|
diagramType: string
|
||||||
|
) => {
|
||||||
|
const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
|
||||||
|
|
||||||
|
if (!endMarkerType) {
|
||||||
|
log.warn(`Unknown arrow type: ${arrowType}`);
|
||||||
|
return; // unknown arrow type, ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = position === 'start' ? 'Start' : 'End';
|
||||||
|
svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`);
|
||||||
|
};
|
668
packages/mermaid/src/rendering-util/rendering-elements/edges.js
Normal file
668
packages/mermaid/src/rendering-util/rendering-elements/edges.js
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
import { evaluate } from '$root/diagrams/common/common.js';
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { createText } from '$root/rendering-util/createText.ts';
|
||||||
|
import utils from '$root/utils.js';
|
||||||
|
import { getLineFunctionsWithOffset } from '$root/utils/lineWithOffset.js';
|
||||||
|
import { getSubGraphTitleMargins } from '$root/utils/subGraphTitleMargins.js';
|
||||||
|
import { curveBasis, line, select } from 'd3';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
import createLabel from './createLabel.js';
|
||||||
|
import { addEdgeMarkers } from './edgeMarker.ts';
|
||||||
|
//import type { Edge } from '$root/rendering-util/types.d.ts';
|
||||||
|
|
||||||
|
let edgeLabels = {};
|
||||||
|
let terminalLabels = {};
|
||||||
|
|
||||||
|
export const clear = () => {
|
||||||
|
edgeLabels = {};
|
||||||
|
terminalLabels = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertEdgeLabel = (elem, edge) => {
|
||||||
|
const useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||||
|
// Create the actual text element
|
||||||
|
const labelElement =
|
||||||
|
edge.labelType === 'markdown'
|
||||||
|
? createText(elem, edge.label, {
|
||||||
|
style: edge.labelStyle,
|
||||||
|
useHtmlLabels,
|
||||||
|
addSvgBackground: true,
|
||||||
|
})
|
||||||
|
: createLabel(edge.label, edge.labelStyle);
|
||||||
|
log.info('abc82', edge, edge.labelType);
|
||||||
|
|
||||||
|
// Create outer g, edgeLabel, this will be positioned after graph layout
|
||||||
|
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
|
||||||
|
|
||||||
|
// Create inner g, label, this will be positioned now for centering the text
|
||||||
|
const label = edgeLabel.insert('g').attr('class', 'label');
|
||||||
|
label.node().appendChild(labelElement);
|
||||||
|
|
||||||
|
// Center the label
|
||||||
|
let bbox = labelElement.getBBox();
|
||||||
|
if (useHtmlLabels) {
|
||||||
|
const div = labelElement.children[0];
|
||||||
|
const dv = select(labelElement);
|
||||||
|
bbox = div.getBoundingClientRect();
|
||||||
|
dv.attr('width', bbox.width);
|
||||||
|
dv.attr('height', bbox.height);
|
||||||
|
}
|
||||||
|
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||||
|
|
||||||
|
// Make element accessible by id for positioning
|
||||||
|
edgeLabels[edge.id] = edgeLabel;
|
||||||
|
|
||||||
|
// Update the abstract data of the edge with the new information about its width and height
|
||||||
|
edge.width = bbox.width;
|
||||||
|
edge.height = bbox.height;
|
||||||
|
|
||||||
|
let fo;
|
||||||
|
if (edge.startLabelLeft) {
|
||||||
|
// Create the actual text element
|
||||||
|
const startLabelElement = createLabel(edge.startLabelLeft, edge.labelStyle);
|
||||||
|
const startEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
|
const inner = startEdgeLabelLeft.insert('g').attr('class', 'inner');
|
||||||
|
fo = inner.node().appendChild(startLabelElement);
|
||||||
|
const slBox = startLabelElement.getBBox();
|
||||||
|
inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')');
|
||||||
|
if (!terminalLabels[edge.id]) {
|
||||||
|
terminalLabels[edge.id] = {};
|
||||||
|
}
|
||||||
|
terminalLabels[edge.id].startLeft = startEdgeLabelLeft;
|
||||||
|
setTerminalWidth(fo, edge.startLabelLeft);
|
||||||
|
}
|
||||||
|
if (edge.startLabelRight) {
|
||||||
|
// Create the actual text element
|
||||||
|
const startLabelElement = createLabel(edge.startLabelRight, edge.labelStyle);
|
||||||
|
const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
|
const inner = startEdgeLabelRight.insert('g').attr('class', 'inner');
|
||||||
|
fo = startEdgeLabelRight.node().appendChild(startLabelElement);
|
||||||
|
inner.node().appendChild(startLabelElement);
|
||||||
|
const slBox = startLabelElement.getBBox();
|
||||||
|
inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')');
|
||||||
|
|
||||||
|
if (!terminalLabels[edge.id]) {
|
||||||
|
terminalLabels[edge.id] = {};
|
||||||
|
}
|
||||||
|
terminalLabels[edge.id].startRight = startEdgeLabelRight;
|
||||||
|
setTerminalWidth(fo, edge.startLabelRight);
|
||||||
|
}
|
||||||
|
if (edge.endLabelLeft) {
|
||||||
|
// Create the actual text element
|
||||||
|
const endLabelElement = createLabel(edge.endLabelLeft, edge.labelStyle);
|
||||||
|
const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
|
const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner');
|
||||||
|
fo = inner.node().appendChild(endLabelElement);
|
||||||
|
const slBox = endLabelElement.getBBox();
|
||||||
|
inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')');
|
||||||
|
|
||||||
|
endEdgeLabelLeft.node().appendChild(endLabelElement);
|
||||||
|
|
||||||
|
if (!terminalLabels[edge.id]) {
|
||||||
|
terminalLabels[edge.id] = {};
|
||||||
|
}
|
||||||
|
terminalLabels[edge.id].endLeft = endEdgeLabelLeft;
|
||||||
|
setTerminalWidth(fo, edge.endLabelLeft);
|
||||||
|
}
|
||||||
|
if (edge.endLabelRight) {
|
||||||
|
// Create the actual text element
|
||||||
|
const endLabelElement = createLabel(edge.endLabelRight, edge.labelStyle);
|
||||||
|
const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
|
const inner = endEdgeLabelRight.insert('g').attr('class', 'inner');
|
||||||
|
|
||||||
|
fo = inner.node().appendChild(endLabelElement);
|
||||||
|
const slBox = endLabelElement.getBBox();
|
||||||
|
inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')');
|
||||||
|
|
||||||
|
endEdgeLabelRight.node().appendChild(endLabelElement);
|
||||||
|
if (!terminalLabels[edge.id]) {
|
||||||
|
terminalLabels[edge.id] = {};
|
||||||
|
}
|
||||||
|
terminalLabels[edge.id].endRight = endEdgeLabelRight;
|
||||||
|
setTerminalWidth(fo, edge.endLabelRight);
|
||||||
|
}
|
||||||
|
return labelElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} fo
|
||||||
|
* @param {any} value
|
||||||
|
*/
|
||||||
|
function setTerminalWidth(fo, value) {
|
||||||
|
if (getConfig().flowchart.htmlLabels && fo) {
|
||||||
|
fo.style.width = value.length * 9 + 'px';
|
||||||
|
fo.style.height = '12px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const positionEdgeLabel = (edge, paths) => {
|
||||||
|
log.info('Moving label abc78 ', edge.id, edge.label, edgeLabels[edge.id]);
|
||||||
|
let path = paths.updatedPath ? paths.updatedPath : paths.originalPath;
|
||||||
|
const siteConfig = getConfig();
|
||||||
|
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
|
||||||
|
if (edge.label) {
|
||||||
|
const el = edgeLabels[edge.id];
|
||||||
|
let x = edge.x;
|
||||||
|
let y = edge.y;
|
||||||
|
if (path) {
|
||||||
|
// // debugger;
|
||||||
|
const pos = utils.calcLabelPosition(path);
|
||||||
|
log.info(
|
||||||
|
'Moving label ' + edge.label + ' from (',
|
||||||
|
x,
|
||||||
|
',',
|
||||||
|
y,
|
||||||
|
') to (',
|
||||||
|
pos.x,
|
||||||
|
',',
|
||||||
|
pos.y,
|
||||||
|
') abc78'
|
||||||
|
);
|
||||||
|
if (paths.updatedPath) {
|
||||||
|
x = pos.x;
|
||||||
|
y = pos.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el.attr('transform', `translate(${x}, ${y + subGraphTitleTotalMargin / 2})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//let path = paths.updatedPath ? paths.updatedPath : paths.originalPath;
|
||||||
|
if (edge.startLabelLeft) {
|
||||||
|
const el = terminalLabels[edge.id].startLeft;
|
||||||
|
let x = edge.x;
|
||||||
|
let y = edge.y;
|
||||||
|
if (path) {
|
||||||
|
// debugger;
|
||||||
|
const pos = utils.calcTerminalLabelPosition(edge.arrowTypeStart ? 10 : 0, 'start_left', path);
|
||||||
|
x = pos.x;
|
||||||
|
y = pos.y;
|
||||||
|
}
|
||||||
|
el.attr('transform', `translate(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
if (edge.startLabelRight) {
|
||||||
|
const el = terminalLabels[edge.id].startRight;
|
||||||
|
let x = edge.x;
|
||||||
|
let y = edge.y;
|
||||||
|
if (path) {
|
||||||
|
// debugger;
|
||||||
|
const pos = utils.calcTerminalLabelPosition(
|
||||||
|
edge.arrowTypeStart ? 10 : 0,
|
||||||
|
'start_right',
|
||||||
|
path
|
||||||
|
);
|
||||||
|
x = pos.x;
|
||||||
|
y = pos.y;
|
||||||
|
}
|
||||||
|
el.attr('transform', `translate(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
if (edge.endLabelLeft) {
|
||||||
|
const el = terminalLabels[edge.id].endLeft;
|
||||||
|
let x = edge.x;
|
||||||
|
let y = edge.y;
|
||||||
|
if (path) {
|
||||||
|
// debugger;
|
||||||
|
const pos = utils.calcTerminalLabelPosition(edge.arrowTypeEnd ? 10 : 0, 'end_left', path);
|
||||||
|
x = pos.x;
|
||||||
|
y = pos.y;
|
||||||
|
}
|
||||||
|
el.attr('transform', `translate(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
if (edge.endLabelRight) {
|
||||||
|
const el = terminalLabels[edge.id].endRight;
|
||||||
|
let x = edge.x;
|
||||||
|
let y = edge.y;
|
||||||
|
if (path) {
|
||||||
|
// debugger;
|
||||||
|
const pos = utils.calcTerminalLabelPosition(edge.arrowTypeEnd ? 10 : 0, 'end_right', path);
|
||||||
|
x = pos.x;
|
||||||
|
y = pos.y;
|
||||||
|
}
|
||||||
|
el.attr('transform', `translate(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const outsideNode = (node, point) => {
|
||||||
|
// log.warn('Checking bounds ', node, point);
|
||||||
|
const x = node.x;
|
||||||
|
const y = node.y;
|
||||||
|
const dx = Math.abs(point.x - x);
|
||||||
|
const dy = Math.abs(point.y - y);
|
||||||
|
const w = node.width / 2;
|
||||||
|
const h = node.height / 2;
|
||||||
|
if (dx >= w || dy >= h) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const intersection = (node, outsidePoint, insidePoint) => {
|
||||||
|
log.warn(`intersection calc abc89:
|
||||||
|
outsidePoint: ${JSON.stringify(outsidePoint)}
|
||||||
|
insidePoint : ${JSON.stringify(insidePoint)}
|
||||||
|
node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`);
|
||||||
|
const x = node.x;
|
||||||
|
const y = node.y;
|
||||||
|
|
||||||
|
const dx = Math.abs(x - insidePoint.x);
|
||||||
|
// const dy = Math.abs(y - insidePoint.y);
|
||||||
|
const w = node.width / 2;
|
||||||
|
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||||
|
const h = node.height / 2;
|
||||||
|
|
||||||
|
// const edges = {
|
||||||
|
// x1: x - w,
|
||||||
|
// x2: x + w,
|
||||||
|
// y1: y - h,
|
||||||
|
// y2: y + h
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// outsidePoint.x === edges.x1 ||
|
||||||
|
// outsidePoint.x === edges.x2 ||
|
||||||
|
// outsidePoint.y === edges.y1 ||
|
||||||
|
// outsidePoint.y === edges.y2
|
||||||
|
// ) {
|
||||||
|
// log.warn('abc89 calc equals on edge', outsidePoint, edges);
|
||||||
|
// return outsidePoint;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||||
|
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||||
|
// log.warn();
|
||||||
|
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
||||||
|
// Intersection is top or bottom of rect.
|
||||||
|
// let q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||||
|
let q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||||
|
r = (R * q) / Q;
|
||||||
|
const res = {
|
||||||
|
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
|
||||||
|
y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (r === 0) {
|
||||||
|
res.x = outsidePoint.x;
|
||||||
|
res.y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
if (R === 0) {
|
||||||
|
res.x = outsidePoint.x;
|
||||||
|
}
|
||||||
|
if (Q === 0) {
|
||||||
|
res.y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(`abc89 top/bot calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
// Intersection onn sides of rect
|
||||||
|
if (insidePoint.x < outsidePoint.x) {
|
||||||
|
r = outsidePoint.x - w - x;
|
||||||
|
} else {
|
||||||
|
// r = outsidePoint.x - w - x;
|
||||||
|
r = x - w - outsidePoint.x;
|
||||||
|
}
|
||||||
|
let q = (Q * r) / R;
|
||||||
|
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w;
|
||||||
|
// OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
|
||||||
|
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
||||||
|
// let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
|
||||||
|
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
||||||
|
log.warn(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y });
|
||||||
|
if (r === 0) {
|
||||||
|
_x = outsidePoint.x;
|
||||||
|
_y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
if (R === 0) {
|
||||||
|
_x = outsidePoint.x;
|
||||||
|
}
|
||||||
|
if (Q === 0) {
|
||||||
|
_y = outsidePoint.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: _x, y: _y };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This function will page a path and node where the last point(s) in the path is inside the node
|
||||||
|
* and return an update path ending by the border of the node.
|
||||||
|
*
|
||||||
|
* @param {Array} _points
|
||||||
|
* @param {any} boundaryNode
|
||||||
|
* @returns {Array} Points
|
||||||
|
*/
|
||||||
|
const cutPathAtIntersect = (_points, boundaryNode) => {
|
||||||
|
log.warn('abc88 cutPathAtIntersect', _points, boundaryNode);
|
||||||
|
let points = [];
|
||||||
|
let lastPointOutside = _points[0];
|
||||||
|
let isInside = false;
|
||||||
|
_points.forEach((point) => {
|
||||||
|
// const node = clusterDb[edge.toCluster].node;
|
||||||
|
log.info('abc88 checking point', point, boundaryNode);
|
||||||
|
|
||||||
|
// check if point is inside the boundary rect
|
||||||
|
if (!outsideNode(boundaryNode, point) && !isInside) {
|
||||||
|
// First point inside the rect found
|
||||||
|
// Calc the intersection coord between the point anf the last point outside the rect
|
||||||
|
const inter = intersection(boundaryNode, lastPointOutside, point);
|
||||||
|
log.warn('abc88 inside', point, lastPointOutside, inter);
|
||||||
|
log.warn('abc88 intersection', inter);
|
||||||
|
|
||||||
|
// // Check case where the intersection is the same as the last point
|
||||||
|
let pointPresent = false;
|
||||||
|
points.forEach((p) => {
|
||||||
|
pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
|
||||||
|
});
|
||||||
|
// // if (!pointPresent) {
|
||||||
|
if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
|
||||||
|
points.push(inter);
|
||||||
|
} else {
|
||||||
|
log.warn('abc88 no intersect', inter, points);
|
||||||
|
}
|
||||||
|
// points.push(inter);
|
||||||
|
isInside = true;
|
||||||
|
} else {
|
||||||
|
// Outside
|
||||||
|
log.warn('abc88 outside', point, lastPointOutside);
|
||||||
|
lastPointOutside = point;
|
||||||
|
// points.push(point);
|
||||||
|
if (!isInside) {
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.warn('abc88 returning points', points);
|
||||||
|
return points;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an edge, this function will return the corner points of the edge. This is defined as:
|
||||||
|
* one point that has a previous point and a next point such as the angle between the previous
|
||||||
|
* point and the next point is 90 degrees. Meaning that the previous point has the same x coordinate
|
||||||
|
* as the center point and at the same time the next point has the same y coordinate or vice versa.
|
||||||
|
* @param points
|
||||||
|
*/
|
||||||
|
function extractCornerPoints(points) {
|
||||||
|
const cornerPoints = [];
|
||||||
|
const cornerPointPositions = [];
|
||||||
|
for (let i = 1; i < points.length - 1; i++) {
|
||||||
|
const prev = points[i - 1];
|
||||||
|
const curr = points[i];
|
||||||
|
const next = points[i + 1];
|
||||||
|
if (
|
||||||
|
prev.x === curr.x &&
|
||||||
|
curr.y === next.y &&
|
||||||
|
Math.abs(curr.x - next.x) > 5 &&
|
||||||
|
Math.abs(curr.y - prev.y) > 5
|
||||||
|
) {
|
||||||
|
cornerPoints.push(curr);
|
||||||
|
cornerPointPositions.push(i);
|
||||||
|
} else if (
|
||||||
|
prev.y === curr.y &&
|
||||||
|
curr.x === next.x &&
|
||||||
|
Math.abs(curr.x - prev.x) > 5 &&
|
||||||
|
Math.abs(curr.y - next.y) > 5
|
||||||
|
) {
|
||||||
|
cornerPoints.push(curr);
|
||||||
|
cornerPointPositions.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { cornerPoints, cornerPointPositions };
|
||||||
|
}
|
||||||
|
|
||||||
|
const findAdjacentPoint = function (pointA, pointB, distance) {
|
||||||
|
const xDiff = pointB.x - pointA.x;
|
||||||
|
const yDiff = pointB.y - pointA.y;
|
||||||
|
const length = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
|
||||||
|
const ratio = distance / length;
|
||||||
|
return { x: pointB.x - ratio * xDiff, y: pointB.y - ratio * yDiff };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array of points, this function will return a new array of points where the corners have been removed and replaced with
|
||||||
|
* adjacent points in each direction. SO a corder will be replaced with a point before and the point after the corner.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fixCorners = function (lineData) {
|
||||||
|
const { cornerPoints, cornerPointPositions } = extractCornerPoints(lineData);
|
||||||
|
const newLineData = [];
|
||||||
|
let lastCorner = 0;
|
||||||
|
for (let i = 0; i < lineData.length; i++) {
|
||||||
|
if (cornerPointPositions.includes(i)) {
|
||||||
|
const prevPoint = lineData[i - 1];
|
||||||
|
const nextPoint = lineData[i + 1];
|
||||||
|
const cornerPoint = lineData[i];
|
||||||
|
|
||||||
|
// Find a new point on the line point 5 points back and push it to the new array
|
||||||
|
const newPrevPoint = findAdjacentPoint(prevPoint, cornerPoint, 5);
|
||||||
|
const newNextPoint = findAdjacentPoint(nextPoint, cornerPoint, 5);
|
||||||
|
newLineData.push(newPrevPoint);
|
||||||
|
|
||||||
|
const xDiff = newNextPoint.x - newPrevPoint.x;
|
||||||
|
const yDiff = newNextPoint.y - newPrevPoint.y;
|
||||||
|
|
||||||
|
const a = Math.sqrt(2) * 2;
|
||||||
|
let newCornerPoint = { x: cornerPoint.x, y: cornerPoint.y };
|
||||||
|
if (cornerPoint.x === newPrevPoint.x) {
|
||||||
|
// if (yDiff > 0) {
|
||||||
|
newCornerPoint = {
|
||||||
|
x: xDiff < 0 ? newPrevPoint.x - 5 + a : newPrevPoint.x + 5 - a,
|
||||||
|
y: yDiff < 0 ? newPrevPoint.y - a : newPrevPoint.y + a,
|
||||||
|
};
|
||||||
|
// } else {
|
||||||
|
// newCornerPoint = { x: newPrevPoint.x - a, y: newPrevPoint.y + a };
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
// if (yDiff > 0) {
|
||||||
|
// newCornerPoint = { x: newPrevPoint.x - 5 + a, y: newPrevPoint.y + a };
|
||||||
|
// } else {
|
||||||
|
newCornerPoint = {
|
||||||
|
x: xDiff < 0 ? newPrevPoint.x - a : newPrevPoint.x + a,
|
||||||
|
y: yDiff < 0 ? newPrevPoint.y - 5 + a : newPrevPoint.y + 5 - a,
|
||||||
|
};
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// newLineData.push(cornerPoint);
|
||||||
|
newLineData.push(newCornerPoint, newNextPoint);
|
||||||
|
} else {
|
||||||
|
newLineData.push(lineData[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newLineData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a line, this function will return a new line where the corners are rounded.
|
||||||
|
* @param lineData
|
||||||
|
*/
|
||||||
|
function roundedCornersLine(lineData) {
|
||||||
|
const newLineData = fixCorners(lineData);
|
||||||
|
let path = '';
|
||||||
|
for (let i = 0; i < newLineData.length; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
path += 'M' + newLineData[i].x + ',' + newLineData[i].y;
|
||||||
|
} else if (i === newLineData.length - 1) {
|
||||||
|
path += 'L' + newLineData[i].x + ',' + newLineData[i].y;
|
||||||
|
} else {
|
||||||
|
path += 'L' + newLineData[i].x + ',' + newLineData[i].y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
export const insertEdge = function (elem, edge, clusterDb, diagramType, graph, id) {
|
||||||
|
const { handdrawnSeed } = getConfig();
|
||||||
|
let points = edge.points;
|
||||||
|
let pointsHasChanged = false;
|
||||||
|
const tail = edge.start;
|
||||||
|
var head = edge.end;
|
||||||
|
|
||||||
|
log.info('abc88 InsertEdge: ', points);
|
||||||
|
if (head.intersect && tail.intersect) {
|
||||||
|
log.info('abc88 InsertEdge: 0.5', points);
|
||||||
|
// points = points.slice(1, edge.points.length - 1);
|
||||||
|
log.info('abc88 InsertEdge: 0.7', points);
|
||||||
|
// points.unshift(tail.intersect(points[0]));
|
||||||
|
// log.info(
|
||||||
|
// 'Last point abc88',
|
||||||
|
// points[points.length - 1],
|
||||||
|
// head,
|
||||||
|
// head.intersect(points[points.length - 1])
|
||||||
|
// );
|
||||||
|
// points.push(head.intersect(points[points.length - 1]));
|
||||||
|
}
|
||||||
|
log.info('abc88 InsertEdge 2: ', points);
|
||||||
|
if (edge.toCluster) {
|
||||||
|
log.info('to cluster abc88', clusterDb[edge.toCluster]);
|
||||||
|
points = cutPathAtIntersect(edge.points, clusterDb[edge.toCluster].node);
|
||||||
|
|
||||||
|
pointsHasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edge.fromCluster) {
|
||||||
|
log.info('from cluster abc88', clusterDb[edge.fromCluster]);
|
||||||
|
points = cutPathAtIntersect(points.reverse(), clusterDb[edge.fromCluster].node).reverse();
|
||||||
|
|
||||||
|
pointsHasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The data for our line
|
||||||
|
let lineData = points.filter((p) => !Number.isNaN(p.y));
|
||||||
|
const { cornerPoints, cornerPointPositions } = extractCornerPoints(lineData);
|
||||||
|
lineData = fixCorners(lineData);
|
||||||
|
let lastPoint = lineData[0];
|
||||||
|
if (lineData.length > 1) {
|
||||||
|
lastPoint = lineData[lineData.length - 1];
|
||||||
|
const secondLastPoint = lineData[lineData.length - 2];
|
||||||
|
// Calculate the mid point of the last two points
|
||||||
|
const diffX = (lastPoint.x - secondLastPoint.x) / 4;
|
||||||
|
const diffY = (lastPoint.y - secondLastPoint.y) / 4;
|
||||||
|
const midPoint = { x: secondLastPoint.x + 3 * diffX, y: secondLastPoint.y + 3 * diffY };
|
||||||
|
lineData.splice(-1, 0, midPoint);
|
||||||
|
}
|
||||||
|
// This is the accessor function we talked about above
|
||||||
|
let curve;
|
||||||
|
curve = curveBasis;
|
||||||
|
// curve = curveCardinal;
|
||||||
|
// curve = curveLinear;
|
||||||
|
// curve = curveNatural;
|
||||||
|
// curve = curveCatmullRom.alpha(0.5);
|
||||||
|
// curve = curveCatmullRom;
|
||||||
|
// curve = curveCardinal.tension(0.7);
|
||||||
|
// curve = curveMonotoneY;
|
||||||
|
// let curve = interpolateToCurve([5], curveNatural, 0.01, 10);
|
||||||
|
// Currently only flowcharts get the curve from the settings, perhaps this should
|
||||||
|
// be expanded to a common setting? Restricting it for now in order not to cause side-effects that
|
||||||
|
// have not been thought through
|
||||||
|
if (edge.curve) {
|
||||||
|
curve = edge.curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = getLineFunctionsWithOffset(edge);
|
||||||
|
// const lineFunction = edge.curve ? line().x(x).y(y).curve(curve) : roundedCornersLine;
|
||||||
|
const lineFunction = line().x(x).y(y).curve(curve);
|
||||||
|
|
||||||
|
// Construct stroke classes based on properties
|
||||||
|
let strokeClasses;
|
||||||
|
switch (edge.thickness) {
|
||||||
|
case 'normal':
|
||||||
|
strokeClasses = 'edge-thickness-normal';
|
||||||
|
break;
|
||||||
|
case 'thick':
|
||||||
|
strokeClasses = 'edge-thickness-thick';
|
||||||
|
break;
|
||||||
|
case 'invisible':
|
||||||
|
strokeClasses = 'edge-thickness-thick';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
strokeClasses = 'edge-thickness-normal';
|
||||||
|
}
|
||||||
|
switch (edge.pattern) {
|
||||||
|
case 'solid':
|
||||||
|
strokeClasses += ' edge-pattern-solid';
|
||||||
|
break;
|
||||||
|
case 'dotted':
|
||||||
|
strokeClasses += ' edge-pattern-dotted';
|
||||||
|
break;
|
||||||
|
case 'dashed':
|
||||||
|
strokeClasses += ' edge-pattern-dashed';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
strokeClasses += ' edge-pattern-solid';
|
||||||
|
}
|
||||||
|
let useRough = edge.useRough;
|
||||||
|
let svgPath;
|
||||||
|
let path = '';
|
||||||
|
|
||||||
|
if (useRough) {
|
||||||
|
const rc = rough.svg(elem);
|
||||||
|
const ld = Object.assign([], lineData);
|
||||||
|
const svgPathNode = rc.path(lineFunction(ld.splice(0, ld.length - 1)), {
|
||||||
|
roughness: 0.3,
|
||||||
|
seed: handdrawnSeed,
|
||||||
|
});
|
||||||
|
|
||||||
|
strokeClasses += ' transition';
|
||||||
|
|
||||||
|
svgPath = select(svgPathNode)
|
||||||
|
.select('path')
|
||||||
|
// .attr('d', lineFunction(lineData))
|
||||||
|
.attr('id', edge.id)
|
||||||
|
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
|
||||||
|
.attr('style', edge.style);
|
||||||
|
let d = svgPath.attr('d');
|
||||||
|
d = d + ' L ' + lastPoint.x + ' ' + lastPoint.y;
|
||||||
|
svgPath.attr('d', d);
|
||||||
|
elem.node().appendChild(svgPath.node());
|
||||||
|
} else {
|
||||||
|
svgPath = elem
|
||||||
|
.append('path')
|
||||||
|
.attr('d', lineFunction(lineData))
|
||||||
|
.attr('id', edge.id)
|
||||||
|
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
|
||||||
|
.attr('style', edge.style);
|
||||||
|
}
|
||||||
|
// DEBUG code, adds a red circle at each edge coordinate
|
||||||
|
// cornerPoints.forEach((point) => {
|
||||||
|
// elem
|
||||||
|
// .append('circle')
|
||||||
|
// .style('stroke', 'blue')
|
||||||
|
// .style('fill', 'blue')
|
||||||
|
// .attr('r', 3)
|
||||||
|
// .attr('cx', point.x)
|
||||||
|
// .attr('cy', point.y);
|
||||||
|
// });
|
||||||
|
// lineData.forEach((point) => {
|
||||||
|
// elem
|
||||||
|
// .append('circle')
|
||||||
|
// .style('stroke', 'red')
|
||||||
|
// .style('fill', 'red')
|
||||||
|
// .attr('r', 1)
|
||||||
|
// .attr('cx', point.x)
|
||||||
|
// .attr('cy', point.y);
|
||||||
|
// });
|
||||||
|
|
||||||
|
let url = '';
|
||||||
|
// // TODO: Can we load this config only from the rendered graph type?
|
||||||
|
if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
|
||||||
|
url =
|
||||||
|
window.location.protocol +
|
||||||
|
'//' +
|
||||||
|
window.location.host +
|
||||||
|
window.location.pathname +
|
||||||
|
window.location.search;
|
||||||
|
url = url.replace(/\(/g, '\\(');
|
||||||
|
url = url.replace(/\)/g, '\\)');
|
||||||
|
}
|
||||||
|
log.info('arrowTypeStart', edge.arrowTypeStart);
|
||||||
|
log.info('arrowTypeEnd', edge.arrowTypeEnd);
|
||||||
|
|
||||||
|
addEdgeMarkers(svgPath, edge, url, id, diagramType);
|
||||||
|
|
||||||
|
let paths = {};
|
||||||
|
if (pointsHasChanged) {
|
||||||
|
paths.updatedPath = points;
|
||||||
|
}
|
||||||
|
paths.originalPath = edge.points;
|
||||||
|
return paths;
|
||||||
|
};
|
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Borrowed with love from from dagre-d3. Many thanks to cpettitt!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import node from './intersect-node.js';
|
||||||
|
import circle from './intersect-circle.js';
|
||||||
|
import ellipse from './intersect-ellipse.js';
|
||||||
|
import polygon from './intersect-polygon.js';
|
||||||
|
import rect from './intersect-rect.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
node,
|
||||||
|
circle,
|
||||||
|
ellipse,
|
||||||
|
polygon,
|
||||||
|
rect,
|
||||||
|
};
|
@@ -0,0 +1,12 @@
|
|||||||
|
import intersectEllipse from './intersect-ellipse.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param node
|
||||||
|
* @param rx
|
||||||
|
* @param point
|
||||||
|
*/
|
||||||
|
function intersectCircle(node, rx, point) {
|
||||||
|
return intersectEllipse(node, rx, rx, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default intersectCircle;
|
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @param node
|
||||||
|
* @param rx
|
||||||
|
* @param ry
|
||||||
|
* @param point
|
||||||
|
*/
|
||||||
|
function intersectEllipse(node, rx, ry, point) {
|
||||||
|
// Formulae from: https://mathworld.wolfram.com/Ellipse-LineIntersection.html
|
||||||
|
|
||||||
|
var cx = node.x;
|
||||||
|
var cy = node.y;
|
||||||
|
|
||||||
|
var px = cx - point.x;
|
||||||
|
var py = cy - point.y;
|
||||||
|
|
||||||
|
var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px);
|
||||||
|
|
||||||
|
var dx = Math.abs((rx * ry * px) / det);
|
||||||
|
if (point.x < cx) {
|
||||||
|
dx = -dx;
|
||||||
|
}
|
||||||
|
var dy = Math.abs((rx * ry * py) / det);
|
||||||
|
if (point.y < cy) {
|
||||||
|
dy = -dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: cx + dx, y: cy + dy };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default intersectEllipse;
|
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
|
||||||
|
*
|
||||||
|
* @param p1
|
||||||
|
* @param p2
|
||||||
|
* @param q1
|
||||||
|
* @param q2
|
||||||
|
*/
|
||||||
|
function intersectLine(p1, p2, q1, q2) {
|
||||||
|
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
||||||
|
// p7 and p473.
|
||||||
|
|
||||||
|
var a1, a2, b1, b2, c1, c2;
|
||||||
|
var r1, r2, r3, r4;
|
||||||
|
var denom, offset, num;
|
||||||
|
var x, y;
|
||||||
|
|
||||||
|
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
||||||
|
// b1 y + c1 = 0.
|
||||||
|
a1 = p2.y - p1.y;
|
||||||
|
b1 = p1.x - p2.x;
|
||||||
|
c1 = p2.x * p1.y - p1.x * p2.y;
|
||||||
|
|
||||||
|
// Compute r3 and r4.
|
||||||
|
r3 = a1 * q1.x + b1 * q1.y + c1;
|
||||||
|
r4 = a1 * q2.x + b1 * q2.y + c1;
|
||||||
|
|
||||||
|
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
||||||
|
// same side of line 1, the line segments do not intersect.
|
||||||
|
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
|
||||||
|
return /*DON'T_INTERSECT*/;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
||||||
|
a2 = q2.y - q1.y;
|
||||||
|
b2 = q1.x - q2.x;
|
||||||
|
c2 = q2.x * q1.y - q1.x * q2.y;
|
||||||
|
|
||||||
|
// Compute r1 and r2
|
||||||
|
r1 = a2 * p1.x + b2 * p1.y + c2;
|
||||||
|
r2 = a2 * p2.x + b2 * p2.y + c2;
|
||||||
|
|
||||||
|
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
||||||
|
// on same side of second line segment, the line segments do
|
||||||
|
// not intersect.
|
||||||
|
if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
|
||||||
|
return /*DON'T_INTERSECT*/;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line segments intersect: compute intersection point.
|
||||||
|
denom = a1 * b2 - a2 * b1;
|
||||||
|
if (denom === 0) {
|
||||||
|
return /*COLLINEAR*/;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = Math.abs(denom / 2);
|
||||||
|
|
||||||
|
// The denom/2 is to get rounding instead of truncating. It
|
||||||
|
// is added or subtracted to the numerator, depending upon the
|
||||||
|
// sign of the numerator.
|
||||||
|
num = b1 * c2 - b2 * c1;
|
||||||
|
x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||||
|
|
||||||
|
num = a2 * c1 - a1 * c2;
|
||||||
|
y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||||
|
|
||||||
|
return { x: x, y: y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param r1
|
||||||
|
* @param r2
|
||||||
|
*/
|
||||||
|
function sameSign(r1, r2) {
|
||||||
|
return r1 * r2 > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default intersectLine;
|
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @param node
|
||||||
|
* @param point
|
||||||
|
*/
|
||||||
|
function intersectNode(node, point) {
|
||||||
|
return node.intersect(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default intersectNode;
|
@@ -0,0 +1,69 @@
|
|||||||
|
/* eslint "no-console": off */
|
||||||
|
|
||||||
|
import intersectLine from './intersect-line.js';
|
||||||
|
|
||||||
|
export default intersectPolygon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the point ({x, y}) at which the point argument intersects with the node argument assuming
|
||||||
|
* that it has the shape specified by polygon.
|
||||||
|
*
|
||||||
|
* @param node
|
||||||
|
* @param polyPoints
|
||||||
|
* @param point
|
||||||
|
*/
|
||||||
|
function intersectPolygon(node, polyPoints, point) {
|
||||||
|
var x1 = node.x;
|
||||||
|
var y1 = node.y;
|
||||||
|
|
||||||
|
var intersections = [];
|
||||||
|
|
||||||
|
var minX = Number.POSITIVE_INFINITY;
|
||||||
|
var minY = Number.POSITIVE_INFINITY;
|
||||||
|
if (typeof polyPoints.forEach === 'function') {
|
||||||
|
polyPoints.forEach(function (entry) {
|
||||||
|
minX = Math.min(minX, entry.x);
|
||||||
|
minY = Math.min(minY, entry.y);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
minX = Math.min(minX, polyPoints.x);
|
||||||
|
minY = Math.min(minY, polyPoints.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = x1 - node.width / 2 - minX;
|
||||||
|
var top = y1 - node.height / 2 - minY;
|
||||||
|
|
||||||
|
for (var i = 0; i < polyPoints.length; i++) {
|
||||||
|
var p1 = polyPoints[i];
|
||||||
|
var p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
|
||||||
|
var intersect = intersectLine(
|
||||||
|
node,
|
||||||
|
point,
|
||||||
|
{ x: left + p1.x, y: top + p1.y },
|
||||||
|
{ x: left + p2.x, y: top + p2.y }
|
||||||
|
);
|
||||||
|
if (intersect) {
|
||||||
|
intersections.push(intersect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intersections.length) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersections.length > 1) {
|
||||||
|
// More intersections, find the one nearest to edge end point
|
||||||
|
intersections.sort(function (p, q) {
|
||||||
|
var pdx = p.x - point.x;
|
||||||
|
var pdy = p.y - point.y;
|
||||||
|
var distp = Math.sqrt(pdx * pdx + pdy * pdy);
|
||||||
|
|
||||||
|
var qdx = q.x - point.x;
|
||||||
|
var qdy = q.y - point.y;
|
||||||
|
var distq = Math.sqrt(qdx * qdx + qdy * qdy);
|
||||||
|
|
||||||
|
return distp < distq ? -1 : distp === distq ? 0 : 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return intersections[0];
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
const intersectRect = (node, point) => {
|
||||||
|
var x = node.x;
|
||||||
|
var y = node.y;
|
||||||
|
|
||||||
|
// Rectangle intersection algorithm from:
|
||||||
|
// https://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
||||||
|
var dx = point.x - x;
|
||||||
|
var dy = point.y - y;
|
||||||
|
var w = node.width / 2;
|
||||||
|
var h = node.height / 2;
|
||||||
|
|
||||||
|
var sx, sy;
|
||||||
|
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
||||||
|
// Intersection is top or bottom of rect.
|
||||||
|
if (dy < 0) {
|
||||||
|
h = -h;
|
||||||
|
}
|
||||||
|
sx = dy === 0 ? 0 : (h * dx) / dy;
|
||||||
|
sy = h;
|
||||||
|
} else {
|
||||||
|
// Intersection is left or right of rect.
|
||||||
|
if (dx < 0) {
|
||||||
|
w = -w;
|
||||||
|
}
|
||||||
|
sx = w;
|
||||||
|
sy = dx === 0 ? 0 : (w * dy) / dx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: x + sx, y: y + sy };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default intersectRect;
|
@@ -0,0 +1,293 @@
|
|||||||
|
/** Setup arrow head and define the marker. The result is appended to the svg. */
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
|
||||||
|
// Only add the number of markers that the diagram needs
|
||||||
|
const insertMarkers = (elem, markerArray, type, id) => {
|
||||||
|
markerArray.forEach((markerName) => {
|
||||||
|
markers[markerName](elem, type, id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const extension = (elem, type, id) => {
|
||||||
|
log.trace('Making markers for ', id);
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-extensionStart')
|
||||||
|
.attr('class', 'marker extension ' + type)
|
||||||
|
.attr('refX', 18)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 190)
|
||||||
|
.attr('markerHeight', 240)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 1,7 L18,13 V 1 Z');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-extensionEnd')
|
||||||
|
.attr('class', 'marker extension ' + type)
|
||||||
|
.attr('refX', 1)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 28)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
|
||||||
|
};
|
||||||
|
|
||||||
|
const composition = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-compositionStart')
|
||||||
|
.attr('class', 'marker composition ' + type)
|
||||||
|
.attr('refX', 18)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 190)
|
||||||
|
.attr('markerHeight', 240)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-compositionEnd')
|
||||||
|
.attr('class', 'marker composition ' + type)
|
||||||
|
.attr('refX', 1)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 28)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||||
|
};
|
||||||
|
const aggregation = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-aggregationStart')
|
||||||
|
.attr('class', 'marker aggregation ' + type)
|
||||||
|
.attr('refX', 18)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 190)
|
||||||
|
.attr('markerHeight', 240)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-aggregationEnd')
|
||||||
|
.attr('class', 'marker aggregation ' + type)
|
||||||
|
.attr('refX', 1)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 28)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||||
|
};
|
||||||
|
const dependency = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-dependencyStart')
|
||||||
|
.attr('class', 'marker dependency ' + type)
|
||||||
|
.attr('refX', 6)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 190)
|
||||||
|
.attr('markerHeight', 240)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-dependencyEnd')
|
||||||
|
.attr('class', 'marker dependency ' + type)
|
||||||
|
.attr('refX', 13)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 28)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
|
||||||
|
};
|
||||||
|
const lollipop = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-lollipopStart')
|
||||||
|
.attr('class', 'marker lollipop ' + type)
|
||||||
|
.attr('refX', 13)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 190)
|
||||||
|
.attr('markerHeight', 240)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('circle')
|
||||||
|
.attr('stroke', 'black')
|
||||||
|
.attr('fill', 'transparent')
|
||||||
|
.attr('cx', 7)
|
||||||
|
.attr('cy', 7)
|
||||||
|
.attr('r', 6);
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-lollipopEnd')
|
||||||
|
.attr('class', 'marker lollipop ' + type)
|
||||||
|
.attr('refX', 1)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 190)
|
||||||
|
.attr('markerHeight', 240)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('circle')
|
||||||
|
.attr('stroke', 'black')
|
||||||
|
.attr('fill', 'transparent')
|
||||||
|
.attr('cx', 7)
|
||||||
|
.attr('cy', 7)
|
||||||
|
.attr('r', 6);
|
||||||
|
};
|
||||||
|
const point = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-pointEnd')
|
||||||
|
.attr('class', 'marker ' + type)
|
||||||
|
.attr('viewBox', '0 0 10 10')
|
||||||
|
.attr('refX', 6)
|
||||||
|
.attr('refY', 5)
|
||||||
|
.attr('markerUnits', 'userSpaceOnUse')
|
||||||
|
.attr('markerWidth', 12)
|
||||||
|
.attr('markerHeight', 12)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
|
||||||
|
.attr('class', 'arrowMarkerPath')
|
||||||
|
.style('stroke-width', 1)
|
||||||
|
.style('stroke-dasharray', '1,0');
|
||||||
|
elem
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-pointStart')
|
||||||
|
.attr('class', 'marker ' + type)
|
||||||
|
.attr('viewBox', '0 0 10 10')
|
||||||
|
.attr('refX', 4.5)
|
||||||
|
.attr('refY', 5)
|
||||||
|
.attr('markerUnits', 'userSpaceOnUse')
|
||||||
|
.attr('markerWidth', 12)
|
||||||
|
.attr('markerHeight', 12)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 0 5 L 10 10 L 10 0 z')
|
||||||
|
.attr('class', 'arrowMarkerPath')
|
||||||
|
.style('stroke-width', 1)
|
||||||
|
.style('stroke-dasharray', '1,0');
|
||||||
|
};
|
||||||
|
const circle = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-circleEnd')
|
||||||
|
.attr('class', 'marker ' + type)
|
||||||
|
.attr('viewBox', '0 0 10 10')
|
||||||
|
.attr('refX', 11)
|
||||||
|
.attr('refY', 5)
|
||||||
|
.attr('markerUnits', 'userSpaceOnUse')
|
||||||
|
.attr('markerWidth', 11)
|
||||||
|
.attr('markerHeight', 11)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('circle')
|
||||||
|
.attr('cx', '5')
|
||||||
|
.attr('cy', '5')
|
||||||
|
.attr('r', '5')
|
||||||
|
.attr('class', 'arrowMarkerPath')
|
||||||
|
.style('stroke-width', 1)
|
||||||
|
.style('stroke-dasharray', '1,0');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-circleStart')
|
||||||
|
.attr('class', 'marker ' + type)
|
||||||
|
.attr('viewBox', '0 0 10 10')
|
||||||
|
.attr('refX', -1)
|
||||||
|
.attr('refY', 5)
|
||||||
|
.attr('markerUnits', 'userSpaceOnUse')
|
||||||
|
.attr('markerWidth', 11)
|
||||||
|
.attr('markerHeight', 11)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('circle')
|
||||||
|
.attr('cx', '5')
|
||||||
|
.attr('cy', '5')
|
||||||
|
.attr('r', '5')
|
||||||
|
.attr('class', 'arrowMarkerPath')
|
||||||
|
.style('stroke-width', 1)
|
||||||
|
.style('stroke-dasharray', '1,0');
|
||||||
|
};
|
||||||
|
const cross = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-crossEnd')
|
||||||
|
.attr('class', 'marker cross ' + type)
|
||||||
|
.attr('viewBox', '0 0 11 11')
|
||||||
|
.attr('refX', 12)
|
||||||
|
.attr('refY', 5.2)
|
||||||
|
.attr('markerUnits', 'userSpaceOnUse')
|
||||||
|
.attr('markerWidth', 11)
|
||||||
|
.attr('markerHeight', 11)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
// .attr('stroke', 'black')
|
||||||
|
.attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9')
|
||||||
|
.attr('class', 'arrowMarkerPath')
|
||||||
|
.style('stroke-width', 2)
|
||||||
|
.style('stroke-dasharray', '1,0');
|
||||||
|
|
||||||
|
elem
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-crossStart')
|
||||||
|
.attr('class', 'marker cross ' + type)
|
||||||
|
.attr('viewBox', '0 0 11 11')
|
||||||
|
.attr('refX', -1)
|
||||||
|
.attr('refY', 5.2)
|
||||||
|
.attr('markerUnits', 'userSpaceOnUse')
|
||||||
|
.attr('markerWidth', 11)
|
||||||
|
.attr('markerHeight', 11)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
// .attr('stroke', 'black')
|
||||||
|
.attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9')
|
||||||
|
.attr('class', 'arrowMarkerPath')
|
||||||
|
.style('stroke-width', 2)
|
||||||
|
.style('stroke-dasharray', '1,0');
|
||||||
|
};
|
||||||
|
const barb = (elem, type, id) => {
|
||||||
|
elem
|
||||||
|
.append('defs')
|
||||||
|
.append('marker')
|
||||||
|
.attr('id', id + '_' + type + '-barbEnd')
|
||||||
|
.attr('refX', 19)
|
||||||
|
.attr('refY', 7)
|
||||||
|
.attr('markerWidth', 20)
|
||||||
|
.attr('markerHeight', 14)
|
||||||
|
.attr('markerUnits', 'strokeWidth')
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z');
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO rename the class diagram markers to something shape descriptive and semantic free
|
||||||
|
const markers = {
|
||||||
|
extension,
|
||||||
|
composition,
|
||||||
|
aggregation,
|
||||||
|
dependency,
|
||||||
|
lollipop,
|
||||||
|
point,
|
||||||
|
circle,
|
||||||
|
cross,
|
||||||
|
barb,
|
||||||
|
};
|
||||||
|
export default insertMarkers;
|
@@ -0,0 +1,89 @@
|
|||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { rect } from './shapes/rect.ts';
|
||||||
|
import { stateStart } from './shapes/stateStart.ts';
|
||||||
|
import { stateEnd } from './shapes/stateEnd.ts';
|
||||||
|
import { forkJoin } from './shapes/forkJoin.ts';
|
||||||
|
import { choice } from './shapes/choice.ts';
|
||||||
|
import { note } from './shapes/note.ts';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
|
||||||
|
const formatClass = (str) => {
|
||||||
|
if (str) {
|
||||||
|
return ' ' + str;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const shapes = {
|
||||||
|
rect,
|
||||||
|
stateStart,
|
||||||
|
stateEnd,
|
||||||
|
fork: forkJoin,
|
||||||
|
join: forkJoin,
|
||||||
|
choice,
|
||||||
|
note,
|
||||||
|
};
|
||||||
|
|
||||||
|
let nodeElems = {};
|
||||||
|
|
||||||
|
export const insertNode = async (elem, node, dir) => {
|
||||||
|
let newEl;
|
||||||
|
let el;
|
||||||
|
|
||||||
|
// debugger;
|
||||||
|
// Add link when appropriate
|
||||||
|
if (node.link) {
|
||||||
|
let target;
|
||||||
|
if (getConfig().securityLevel === 'sandbox') {
|
||||||
|
target = '_top';
|
||||||
|
} else if (node.linkTarget) {
|
||||||
|
target = node.linkTarget || '_blank';
|
||||||
|
}
|
||||||
|
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
|
||||||
|
el = await shapes[node.shape](newEl, node, dir);
|
||||||
|
} else {
|
||||||
|
el = await shapes[node.shape](elem, node, dir);
|
||||||
|
newEl = el;
|
||||||
|
}
|
||||||
|
if (node.tooltip) {
|
||||||
|
el.attr('title', node.tooltip);
|
||||||
|
}
|
||||||
|
// if (node.class) {
|
||||||
|
// el.attr('class', 'node default ' + node.class);
|
||||||
|
// }
|
||||||
|
|
||||||
|
nodeElems[node.id] = newEl;
|
||||||
|
|
||||||
|
if (node.haveCallback) {
|
||||||
|
nodeElems[node.id].attr('class', nodeElems[node.id].attr('class') + ' clickable');
|
||||||
|
}
|
||||||
|
return newEl;
|
||||||
|
};
|
||||||
|
export const setNodeElem = (elem, node) => {
|
||||||
|
nodeElems[node.id] = elem;
|
||||||
|
};
|
||||||
|
export const clear = () => {
|
||||||
|
nodeElems = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const positionNode = (node) => {
|
||||||
|
const el = nodeElems[node.id];
|
||||||
|
|
||||||
|
log.trace(
|
||||||
|
'Transforming node',
|
||||||
|
node.diff,
|
||||||
|
node,
|
||||||
|
'translate(' + (node.x - node.width / 2 - 5) + ', ' + node.width / 2 + ')'
|
||||||
|
);
|
||||||
|
|
||||||
|
const diff = 0;
|
||||||
|
if (node.clusterNode) {
|
||||||
|
el.attr(
|
||||||
|
'transform',
|
||||||
|
'translate(' + (node.x + diff - node.width / 2) + ', ' + (node.y - node.height / 2) + ')'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||||
|
}
|
||||||
|
return diff;
|
||||||
|
};
|
@@ -0,0 +1,55 @@
|
|||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import type { Node } from '$root/rendering-util/types.d.ts';
|
||||||
|
import type { SVG } from '$root/diagram-api/types.js';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
import { solidStateFill } from './handdrawnStyles.js';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
|
||||||
|
export const choice = (parent: SVG, node: Node) => {
|
||||||
|
const { themeVariables } = getConfig();
|
||||||
|
const { lineColor } = themeVariables;
|
||||||
|
const shapeSvg = parent
|
||||||
|
.insert('g')
|
||||||
|
.attr('class', 'node default')
|
||||||
|
.attr('id', node.domId || node.id);
|
||||||
|
|
||||||
|
const s = 28;
|
||||||
|
const points = [
|
||||||
|
{ x: 0, y: s / 2 },
|
||||||
|
{ x: s / 2, y: 0 },
|
||||||
|
{ x: 0, y: -s / 2 },
|
||||||
|
{ x: -s / 2, y: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let choice;
|
||||||
|
if (node.useRough) {
|
||||||
|
// @ts-ignore TODO: Fix rough typings
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const pointArr = points.map(function (d) {
|
||||||
|
return [d.x, d.y];
|
||||||
|
});
|
||||||
|
const roughNode = rc.polygon(pointArr, solidStateFill(lineColor));
|
||||||
|
choice = shapeSvg.insert(() => roughNode);
|
||||||
|
} else {
|
||||||
|
choice = shapeSvg.insert('polygon', ':first-child').attr(
|
||||||
|
'points',
|
||||||
|
points
|
||||||
|
.map(function (d) {
|
||||||
|
return d.x + ',' + d.y;
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// center the circle around its coordinate
|
||||||
|
// @ts-ignore TODO: Fix rough typings
|
||||||
|
choice.attr('class', 'state-start').attr('r', 7).attr('width', 28).attr('height', 28);
|
||||||
|
node.width = 28;
|
||||||
|
node.height = 28;
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.circle(node, 14, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
@@ -0,0 +1,65 @@
|
|||||||
|
import { updateNodeBounds } from './util.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import type { Node } from '$root/rendering-util/types.d.ts';
|
||||||
|
import type { SVG } from '$root/diagram-api/types.js';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
import { solidStateFill } from './handdrawnStyles.js';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
|
||||||
|
export const forkJoin = (parent: SVG, node: Node, dir: string) => {
|
||||||
|
const { themeVariables } = getConfig();
|
||||||
|
const { lineColor } = themeVariables;
|
||||||
|
const shapeSvg = parent
|
||||||
|
.insert('g')
|
||||||
|
.attr('class', 'node default')
|
||||||
|
.attr('id', node.domId || node.id);
|
||||||
|
|
||||||
|
let width = 70;
|
||||||
|
let height = 10;
|
||||||
|
|
||||||
|
if (dir === 'LR') {
|
||||||
|
width = 10;
|
||||||
|
height = 70;
|
||||||
|
}
|
||||||
|
const x = (-1 * width) / 2;
|
||||||
|
const y = (-1 * height) / 2;
|
||||||
|
|
||||||
|
let shape;
|
||||||
|
if (node.useRough) {
|
||||||
|
// @ts-ignore TODO: Fix rough typings
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const roughNode = rc.rectangle(x, y, width, height, solidStateFill(lineColor));
|
||||||
|
shape = shapeSvg.insert(() => roughNode);
|
||||||
|
} else {
|
||||||
|
shape = shapeSvg
|
||||||
|
.append('rect')
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y)
|
||||||
|
.attr('width', width)
|
||||||
|
.attr('height', height)
|
||||||
|
.attr('class', 'fork-join');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodeBounds(node, shape);
|
||||||
|
let nodeHeight = 0;
|
||||||
|
let nodeWidth = 0;
|
||||||
|
let nodePadding = 10;
|
||||||
|
if (node.height) {
|
||||||
|
nodeHeight = node.height;
|
||||||
|
}
|
||||||
|
if (node.width) {
|
||||||
|
nodeWidth = node.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.padding) {
|
||||||
|
nodePadding = node.padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.height = nodeHeight + nodePadding / 2;
|
||||||
|
node.width = nodeWidth + nodePadding / 2;
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.rect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
import type { Node } from '$root/rendering-util/types.d.ts';
|
||||||
|
|
||||||
|
// Striped fill like start or fork nodes in state diagrams
|
||||||
|
export const solidStateFill = (color: string) => {
|
||||||
|
const { handdrawnSeed } = getConfig();
|
||||||
|
return {
|
||||||
|
fill: color,
|
||||||
|
hachureAngle: 120, // angle of hachure,
|
||||||
|
hachureGap: 4,
|
||||||
|
fillWeight: 2,
|
||||||
|
roughness: 0.7,
|
||||||
|
stroke: color,
|
||||||
|
seed: handdrawnSeed,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Striped fill like start or fork nodes in state diagrams
|
||||||
|
// TODO remove any
|
||||||
|
export const userNodeOverrides = (node: Node, options: any) => {
|
||||||
|
const result = Object.assign({}, options);
|
||||||
|
result.fill = node.backgroundColor || options.fill;
|
||||||
|
result.stroke = node.borderColor || options.stroke;
|
||||||
|
return result;
|
||||||
|
};
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { labelHelper, updateNodeBounds } from './util.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
import type { Node } from '$root/rendering-util/types.d.ts';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
|
||||||
|
export const note = async (parent: SVGAElement, node: Node) => {
|
||||||
|
const { themeVariables, handdrawnSeed } = getConfig();
|
||||||
|
const { noteBorderColor, noteBkgColor } = themeVariables;
|
||||||
|
|
||||||
|
const useHtmlLabels = node.useHtmlLabels;
|
||||||
|
if (!useHtmlLabels) {
|
||||||
|
node.centerLabel = true;
|
||||||
|
}
|
||||||
|
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||||
|
parent,
|
||||||
|
node,
|
||||||
|
'node ' + node.cssClasses,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info('Classes = ', node.cssClasses);
|
||||||
|
const { cssStyles, useRough } = node;
|
||||||
|
let rect;
|
||||||
|
const totalWidth = bbox.width + node.padding;
|
||||||
|
const totalHeight = bbox.height + node.padding;
|
||||||
|
const x = -bbox.width / 2 - halfPadding;
|
||||||
|
const y = -bbox.height / 2 - halfPadding;
|
||||||
|
|
||||||
|
if (useRough) {
|
||||||
|
// add the rect
|
||||||
|
// @ts-ignore TODO: Fix rough typings
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const roughNode = rc.rectangle(x, y, totalWidth, totalHeight, {
|
||||||
|
roughness: 0.7,
|
||||||
|
fill: noteBkgColor,
|
||||||
|
fillWeight: 3,
|
||||||
|
seed: handdrawnSeed,
|
||||||
|
// fillStyle: 'solid', // solid fill'
|
||||||
|
stroke: noteBorderColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
rect = shapeSvg.insert(() => roughNode, ':first-child');
|
||||||
|
rect.attr('class', 'basic label-container').attr('style', cssStyles);
|
||||||
|
} else {
|
||||||
|
rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
rect
|
||||||
|
.attr('rx', node.rx)
|
||||||
|
.attr('ry', node.ry)
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y)
|
||||||
|
.attr('width', totalWidth)
|
||||||
|
.attr('height', totalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodeBounds(node, rect);
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.rect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
@@ -0,0 +1,156 @@
|
|||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { labelHelper, updateNodeBounds } from './util.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import type { Node } from '$root/rendering-util/types.d.ts';
|
||||||
|
import { createRoundedRectPathD } from './roundedRectPath.js';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
import { userNodeOverrides } from '$root/rendering-util/rendering-elements/shapes/handdrawnStyles.js';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
|
||||||
|
// function applyNodePropertyBorders(
|
||||||
|
// rect: d3.Selection<SVGRectElement, unknown, null, undefined>,
|
||||||
|
// borders: string | undefined,
|
||||||
|
// totalWidth: number,
|
||||||
|
// totalHeight: number
|
||||||
|
// ) {
|
||||||
|
// if (!borders) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const strokeDashArray: number[] = [];
|
||||||
|
// const addBorder = (length: number) => {
|
||||||
|
// strokeDashArray.push(length, 0);
|
||||||
|
// };
|
||||||
|
// const skipBorder = (length: number) => {
|
||||||
|
// strokeDashArray.push(0, length);
|
||||||
|
// };
|
||||||
|
// if (borders.includes('t')) {
|
||||||
|
// log.debug('add top border');
|
||||||
|
// addBorder(totalWidth);
|
||||||
|
// } else {
|
||||||
|
// skipBorder(totalWidth);
|
||||||
|
// }
|
||||||
|
// if (borders.includes('r')) {
|
||||||
|
// log.debug('add right border');
|
||||||
|
// addBorder(totalHeight);
|
||||||
|
// } else {
|
||||||
|
// skipBorder(totalHeight);
|
||||||
|
// }
|
||||||
|
// if (borders.includes('b')) {
|
||||||
|
// log.debug('add bottom border');
|
||||||
|
// addBorder(totalWidth);
|
||||||
|
// } else {
|
||||||
|
// skipBorder(totalWidth);
|
||||||
|
// }
|
||||||
|
// if (borders.includes('l')) {
|
||||||
|
// log.debug('add left border');
|
||||||
|
// addBorder(totalHeight);
|
||||||
|
// } else {
|
||||||
|
// skipBorder(totalHeight);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// rect.attr('stroke-dasharray', strokeDashArray.join(' '));
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const rect = async (parent: SVGAElement, node: Node) => {
|
||||||
|
const { themeVariables, handdrawnSeed } = getConfig();
|
||||||
|
const { nodeBorder, mainBkg } = themeVariables;
|
||||||
|
|
||||||
|
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||||
|
parent,
|
||||||
|
node,
|
||||||
|
'node ' + node.cssClasses, // + ' ' + node.class,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalWidth = bbox.width + node.padding;
|
||||||
|
const totalHeight = bbox.height + node.padding;
|
||||||
|
const x = -bbox.width / 2 - halfPadding;
|
||||||
|
const y = -bbox.height / 2 - halfPadding;
|
||||||
|
|
||||||
|
let rect;
|
||||||
|
const { rx, ry, cssStyles, useRough } = node;
|
||||||
|
if (useRough) {
|
||||||
|
// @ts-ignore TODO: Fix rough typings
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const options = userNodeOverrides(node, {
|
||||||
|
roughness: 0.7,
|
||||||
|
fill: mainBkg,
|
||||||
|
// fillStyle: 'solid', // solid fill'
|
||||||
|
fillStyle: 'hachure', // solid fill'
|
||||||
|
fillWeight: 3.5,
|
||||||
|
stroke: nodeBorder,
|
||||||
|
seed: handdrawnSeed,
|
||||||
|
});
|
||||||
|
const roughNode =
|
||||||
|
rx || ry
|
||||||
|
? rc.path(createRoundedRectPathD(x, y, totalWidth, totalHeight, rx || 0), options)
|
||||||
|
: rc.rectangle(x, y, totalWidth, totalHeight, options);
|
||||||
|
|
||||||
|
rect = shapeSvg.insert(() => roughNode, ':first-child');
|
||||||
|
rect.attr('class', 'basic label-container').attr('style', cssStyles);
|
||||||
|
} else {
|
||||||
|
rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
|
||||||
|
rect
|
||||||
|
.attr('class', 'basic label-container')
|
||||||
|
.attr('style', cssStyles)
|
||||||
|
.attr('rx', rx)
|
||||||
|
.attr('ry', ry)
|
||||||
|
.attr('x', x)
|
||||||
|
.attr('y', y)
|
||||||
|
.attr('width', totalWidth)
|
||||||
|
.attr('height', totalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (node.props) {
|
||||||
|
// const propKeys = new Set(Object.keys(node.props));
|
||||||
|
// if (node.props.borders) {
|
||||||
|
// applyNodePropertyBorders(rect, node.props.borders + '', totalWidth, totalHeight);
|
||||||
|
// propKeys.delete('borders');
|
||||||
|
// }
|
||||||
|
// propKeys.forEach((propKey) => {
|
||||||
|
// log.warn(`Unknown node property ${propKey}`);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
updateNodeBounds(node, rect);
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.rect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const labelRect = async (parent: SVGElement, node: Node) => {
|
||||||
|
const { shapeSvg } = await labelHelper(parent, node, 'label', true);
|
||||||
|
|
||||||
|
// log.trace('Classes = ', node.class);
|
||||||
|
// add the rect
|
||||||
|
const rect = shapeSvg.insert('rect', ':first-child');
|
||||||
|
|
||||||
|
// Hide the rect we are only after the label
|
||||||
|
const totalWidth = 0;
|
||||||
|
const totalHeight = 0;
|
||||||
|
rect.attr('width', totalWidth).attr('height', totalHeight);
|
||||||
|
shapeSvg.attr('class', 'label edgeLabel');
|
||||||
|
|
||||||
|
// if (node.props) {
|
||||||
|
// const propKeys = new Set(Object.keys(node.props));
|
||||||
|
// if (node.props.borders) {
|
||||||
|
// applyNodePropertyBorders(rect, node.borders, totalWidth, totalHeight);
|
||||||
|
// propKeys.delete('borders');
|
||||||
|
// }
|
||||||
|
// propKeys.forEach((propKey) => {
|
||||||
|
// log.warn(`Unknown node property ${propKey}`);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
updateNodeBounds(node, rect);
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.rect(node, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
@@ -0,0 +1,53 @@
|
|||||||
|
export const createRoundedRectPathD = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
totalWidth: number,
|
||||||
|
totalHeight: number,
|
||||||
|
radius: number
|
||||||
|
) =>
|
||||||
|
[
|
||||||
|
'M',
|
||||||
|
x + radius,
|
||||||
|
y, // Move to the first point
|
||||||
|
'H',
|
||||||
|
x + totalWidth - radius, // Draw horizontal line to the beginning of the right corner
|
||||||
|
'A',
|
||||||
|
radius,
|
||||||
|
radius,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
x + totalWidth,
|
||||||
|
y + radius, // Draw arc to the right top corner
|
||||||
|
'V',
|
||||||
|
y + totalHeight - radius, // Draw vertical line down to the beginning of the right bottom corner
|
||||||
|
'A',
|
||||||
|
radius,
|
||||||
|
radius,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
x + totalWidth - radius,
|
||||||
|
y + totalHeight, // Draw arc to the right bottom corner
|
||||||
|
'H',
|
||||||
|
x + radius, // Draw horizontal line to the beginning of the left bottom corner
|
||||||
|
'A',
|
||||||
|
radius,
|
||||||
|
radius,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
x,
|
||||||
|
y + totalHeight - radius, // Draw arc to the left bottom corner
|
||||||
|
'V',
|
||||||
|
y + radius, // Draw vertical line up to the beginning of the left top corner
|
||||||
|
'A',
|
||||||
|
radius,
|
||||||
|
radius,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
x + radius,
|
||||||
|
y, // Draw arc to the left top corner
|
||||||
|
'Z', // Close the path
|
||||||
|
].join(' ');
|
@@ -0,0 +1,43 @@
|
|||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { updateNodeBounds } from './util.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import type { Node } from '$root/rendering-util/types.d.ts';
|
||||||
|
import type { SVG } from '$root/diagram-api/types.js';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
import { solidStateFill } from './handdrawnStyles.js';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
|
||||||
|
export const stateEnd = (parent: SVG, node: Node) => {
|
||||||
|
const { themeVariables } = getConfig();
|
||||||
|
const { lineColor } = themeVariables;
|
||||||
|
const shapeSvg = parent
|
||||||
|
.insert('g')
|
||||||
|
.attr('class', 'node default')
|
||||||
|
.attr('id', node.domId || node.id);
|
||||||
|
|
||||||
|
let circle;
|
||||||
|
let innerCircle;
|
||||||
|
if (node.useRough) {
|
||||||
|
// @ts-ignore TODO: Fix rough typings
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const roughNode = rc.circle(0, 0, 14, { ...solidStateFill(lineColor), roughness: 0.5 });
|
||||||
|
const roughInnerNode = rc.circle(0, 0, 5, { ...solidStateFill(lineColor), fillStyle: 'solid' });
|
||||||
|
circle = shapeSvg.insert(() => roughNode);
|
||||||
|
innerCircle = shapeSvg.insert(() => roughInnerNode);
|
||||||
|
} else {
|
||||||
|
innerCircle = shapeSvg.insert('circle', ':first-child');
|
||||||
|
circle = shapeSvg.insert('circle', ':first-child');
|
||||||
|
|
||||||
|
circle.attr('class', 'state-start').attr('r', 7).attr('width', 14).attr('height', 14);
|
||||||
|
|
||||||
|
innerCircle.attr('class', 'state-end').attr('r', 5).attr('width', 10).attr('height', 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodeBounds(node, circle);
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.circle(node, 7, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { log } from '$root/logger.js';
|
||||||
|
import { updateNodeBounds } from './util.js';
|
||||||
|
import intersect from '../intersect/index.js';
|
||||||
|
import type { Node } from '$root/rendering-util/types.d.ts';
|
||||||
|
import type { SVG } from '$root/diagram-api/types.js';
|
||||||
|
import rough from 'roughjs';
|
||||||
|
import { solidStateFill } from './handdrawnStyles.js';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
|
||||||
|
export const stateStart = (parent: SVG, node: Node) => {
|
||||||
|
const { themeVariables } = getConfig();
|
||||||
|
const { lineColor } = themeVariables;
|
||||||
|
|
||||||
|
const shapeSvg = parent
|
||||||
|
.insert('g')
|
||||||
|
.attr('class', 'node default')
|
||||||
|
.attr('id', node.domId || node.id);
|
||||||
|
|
||||||
|
let circle;
|
||||||
|
if (node.useRough) {
|
||||||
|
// @ts-ignore TODO: Fix rough typings
|
||||||
|
const rc = rough.svg(shapeSvg);
|
||||||
|
const roughNode = rc.circle(0, 0, 14, solidStateFill(lineColor));
|
||||||
|
circle = shapeSvg.insert(() => roughNode);
|
||||||
|
} else {
|
||||||
|
circle = shapeSvg.insert('circle', ':first-child');
|
||||||
|
}
|
||||||
|
|
||||||
|
// center the circle around its coordinate
|
||||||
|
// @ts-ignore TODO: Fix typings
|
||||||
|
circle.attr('class', 'state-start').attr('r', 7).attr('width', 14).attr('height', 14);
|
||||||
|
|
||||||
|
updateNodeBounds(node, circle);
|
||||||
|
|
||||||
|
node.intersect = function (point) {
|
||||||
|
return intersect.circle(node, 7, point);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapeSvg;
|
||||||
|
};
|
@@ -0,0 +1,141 @@
|
|||||||
|
import createLabel from '../createLabel.js';
|
||||||
|
import { createText } from '$root/rendering-util/createText.ts';
|
||||||
|
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||||
|
import { select } from 'd3';
|
||||||
|
import { evaluate, sanitizeText } from '$root/diagrams/common/common.js';
|
||||||
|
import { decodeEntities } from '$root/utils.js';
|
||||||
|
|
||||||
|
export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||||
|
let cssClasses;
|
||||||
|
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig().flowchart.htmlLabels);
|
||||||
|
if (!_classes) {
|
||||||
|
cssClasses = 'node default';
|
||||||
|
} else {
|
||||||
|
cssClasses = _classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add outer g element
|
||||||
|
const shapeSvg = parent
|
||||||
|
.insert('g')
|
||||||
|
.attr('class', cssClasses)
|
||||||
|
.attr('id', node.domId || node.id);
|
||||||
|
|
||||||
|
// Create the label and insert it after the rect
|
||||||
|
const labelEl = shapeSvg.insert('g').attr('class', 'label').attr('style', node.labelStyle);
|
||||||
|
|
||||||
|
// Replace label with default value if undefined
|
||||||
|
let label;
|
||||||
|
if (node.label === undefined) {
|
||||||
|
label = '';
|
||||||
|
} else {
|
||||||
|
label = typeof node.label === 'string' ? node.label : node.label[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const textNode = labelEl.node();
|
||||||
|
let text;
|
||||||
|
if (node.labelType === 'markdown') {
|
||||||
|
// text = textNode;
|
||||||
|
text = createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {
|
||||||
|
useHtmlLabels,
|
||||||
|
width: node.width || getConfig().flowchart.wrappingWidth,
|
||||||
|
cssClasses: 'markdown-node-label',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
text = textNode.appendChild(
|
||||||
|
createLabel(sanitizeText(decodeEntities(label), getConfig()), node.labelStyle, false, isNode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Get the size of the label
|
||||||
|
let bbox = text.getBBox();
|
||||||
|
const halfPadding = node.padding / 2;
|
||||||
|
|
||||||
|
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||||
|
const div = text.children[0];
|
||||||
|
const dv = select(text);
|
||||||
|
|
||||||
|
// if there are images, need to wait for them to load before getting the bounding box
|
||||||
|
const images = div.getElementsByTagName('img');
|
||||||
|
if (images) {
|
||||||
|
const noImgText = label.replace(/<img[^>]*>/g, '').trim() === '';
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...images].map(
|
||||||
|
(img) =>
|
||||||
|
new Promise((res) => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function setupImage() {
|
||||||
|
img.style.display = 'flex';
|
||||||
|
img.style.flexDirection = 'column';
|
||||||
|
|
||||||
|
if (noImgText) {
|
||||||
|
// default size if no text
|
||||||
|
const bodyFontSize = getConfig().fontSize
|
||||||
|
? getConfig().fontSize
|
||||||
|
: window.getComputedStyle(document.body).fontSize;
|
||||||
|
const enlargingFactor = 5;
|
||||||
|
const width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px';
|
||||||
|
img.style.minWidth = width;
|
||||||
|
img.style.maxWidth = width;
|
||||||
|
} else {
|
||||||
|
img.style.width = '100%';
|
||||||
|
}
|
||||||
|
res(img);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (img.complete) {
|
||||||
|
setupImage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
img.addEventListener('error', setupImage);
|
||||||
|
img.addEventListener('load', setupImage);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bbox = div.getBoundingClientRect();
|
||||||
|
dv.attr('width', bbox.width);
|
||||||
|
dv.attr('height', bbox.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the label
|
||||||
|
if (useHtmlLabels) {
|
||||||
|
labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||||
|
} else {
|
||||||
|
labelEl.attr('transform', 'translate(' + 0 + ', ' + -bbox.height / 2 + ')');
|
||||||
|
}
|
||||||
|
if (node.centerLabel) {
|
||||||
|
labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||||
|
}
|
||||||
|
labelEl.insert('rect', ':first-child');
|
||||||
|
return { shapeSvg, bbox, halfPadding, label: labelEl };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateNodeBounds = (node, element) => {
|
||||||
|
const bbox = element.node().getBBox();
|
||||||
|
node.width = bbox.width;
|
||||||
|
node.height = bbox.height;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param parent
|
||||||
|
* @param w
|
||||||
|
* @param h
|
||||||
|
* @param points
|
||||||
|
*/
|
||||||
|
export function insertPolygonShape(parent, w, h, points) {
|
||||||
|
return parent
|
||||||
|
.insert('polygon', ':first-child')
|
||||||
|
.attr(
|
||||||
|
'points',
|
||||||
|
points
|
||||||
|
.map(function (d) {
|
||||||
|
return d.x + ',' + d.y;
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
)
|
||||||
|
.attr('class', 'label-container')
|
||||||
|
.attr('transform', 'translate(' + -w / 2 + ',' + h / 2 + ')');
|
||||||
|
}
|
40
packages/mermaid/src/rendering-util/setupViewPortForSVG.ts
Normal file
40
packages/mermaid/src/rendering-util/setupViewPortForSVG.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { configureSvgSize } from '$root/setupGraphViewbox.js';
|
||||||
|
import type { SVG } from '$root/diagram-api/types.js';
|
||||||
|
import { log } from '$root/logger.js';
|
||||||
|
|
||||||
|
export const setupViewPortForSVG = (
|
||||||
|
svg: SVG,
|
||||||
|
padding: number,
|
||||||
|
cssDiagram: string,
|
||||||
|
useMaxWidth: boolean
|
||||||
|
) => {
|
||||||
|
// Initialize the SVG element and set the diagram class
|
||||||
|
svg.attr('class', cssDiagram);
|
||||||
|
|
||||||
|
// Calculate the dimensions and position with padding
|
||||||
|
const { width, height, x, y } = calculateDimensionsWithPadding(svg, padding);
|
||||||
|
|
||||||
|
// Configure the size and aspect ratio of the SVG
|
||||||
|
configureSvgSize(svg, height, width, useMaxWidth);
|
||||||
|
|
||||||
|
// Update the viewBox to ensure all content is visible with padding
|
||||||
|
const viewBox = createViewBox(x, y, width, height, padding);
|
||||||
|
svg.attr('viewBox', viewBox);
|
||||||
|
|
||||||
|
// Log the viewBox configuration for debugging
|
||||||
|
log.debug(`viewBox configured: ${viewBox}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateDimensionsWithPadding = (svg: SVG, padding: number) => {
|
||||||
|
const bounds = svg.node()?.getBBox() || { width: 0, height: 0, x: 0, y: 0 };
|
||||||
|
return {
|
||||||
|
width: bounds.width + padding * 2,
|
||||||
|
height: bounds.height + padding * 2,
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createViewBox = (x: number, y: number, width: number, height: number, padding: number) => {
|
||||||
|
return `${x - padding} ${y - padding} ${width} ${height}`;
|
||||||
|
};
|
150
packages/mermaid/src/rendering-util/types.d.ts
vendored
150
packages/mermaid/src/rendering-util/types.d.ts
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
import type { MermaidConfig } from '$root/config.type.ts';
|
||||||
|
|
||||||
export type MarkdownWordType = 'normal' | 'strong' | 'emphasis';
|
export type MarkdownWordType = 'normal' | 'strong' | 'emphasis';
|
||||||
export interface MarkdownWord {
|
export interface MarkdownWord {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -6,3 +8,151 @@ export interface MarkdownWord {
|
|||||||
export type MarkdownLine = MarkdownWord[];
|
export type MarkdownLine = MarkdownWord[];
|
||||||
/** Returns `true` if the line fits a constraint (e.g. it's under 𝑛 chars) */
|
/** Returns `true` if the line fits a constraint (e.g. it's under 𝑛 chars) */
|
||||||
export type CheckFitFunction = (text: MarkdownLine) => boolean;
|
export type CheckFitFunction = (text: MarkdownLine) => boolean;
|
||||||
|
|
||||||
|
// Common properties for any node in the system
|
||||||
|
interface Node {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
parentId?: string;
|
||||||
|
position?: string; // Keep, this is for notes 'left of', 'right of', etc. Move into nodeNode
|
||||||
|
cssStyles?: string; // Renamed from `styles` to `cssStyles`
|
||||||
|
cssClasses?: string; // Renamed from `classes` to `cssClasses`
|
||||||
|
// style?: string; //REMOVE ✅
|
||||||
|
// class?: string; //REMOVE ✅
|
||||||
|
// labelText?: string; //REMOVE, use `label` instead ✅
|
||||||
|
// props?: Record<string, unknown>; //REMOVE ✅
|
||||||
|
// type: string; // REMOVE, replace with isGroup: boolean, default false ✅
|
||||||
|
// borders?: string; //REMOVE ✅
|
||||||
|
labelStyle?: string;
|
||||||
|
|
||||||
|
// Flowchart specific properties
|
||||||
|
labelType?: string; // REMOVE? Always use markdown string, need to check for KaTeX - ⏳ wait with this one
|
||||||
|
domId: string;
|
||||||
|
// Rendering specific properties for both Flowchart and State Diagram nodes
|
||||||
|
dir?: string; // Only relevant for isGroup true, i.e. a sub-graph or composite state.
|
||||||
|
haveCallback?: boolean;
|
||||||
|
link?: string;
|
||||||
|
linkTarget?: string;
|
||||||
|
padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific
|
||||||
|
shape?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
isGroup: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
// Specific properties for State Diagram nodes TODO remove and use generic properties
|
||||||
|
intersect?: (point: any) => any;
|
||||||
|
|
||||||
|
// Non-generic properties
|
||||||
|
rx?: number; // Used for rounded corners in Rect, Ellipse, etc.Maybe it to specialized RectNode, EllipseNode, etc.
|
||||||
|
ry?: number;
|
||||||
|
|
||||||
|
useRough?: boolean;
|
||||||
|
useHtmlLabels?: boolean;
|
||||||
|
centerLabel?: boolean; //keep for now.
|
||||||
|
//Candidate for removal, maybe rely on labelStyle or a specific property labelPosition: Top, Center, Bottom
|
||||||
|
|
||||||
|
//Node style properties
|
||||||
|
backgroundColor?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
borderStyle?: string;
|
||||||
|
borderWidth?: number;
|
||||||
|
labelTextColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common properties for any edge in the system
|
||||||
|
interface Edge {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
classes?: string;
|
||||||
|
style?: string;
|
||||||
|
// Properties common to both Flowchart and State Diagram edges
|
||||||
|
arrowhead?: string;
|
||||||
|
arrowheadStyle?: string;
|
||||||
|
arrowTypeEnd?: string;
|
||||||
|
arrowTypeStart?: string;
|
||||||
|
// Flowchart specific properties
|
||||||
|
defaultInterpolate?: string;
|
||||||
|
end?: string;
|
||||||
|
interpolate?: string;
|
||||||
|
labelType?: string;
|
||||||
|
length?: number;
|
||||||
|
start?: string;
|
||||||
|
stroke?: string;
|
||||||
|
text?: string;
|
||||||
|
type: string;
|
||||||
|
// Rendering specific properties
|
||||||
|
curve?: string;
|
||||||
|
labelpos?: string;
|
||||||
|
labelStyle?: string;
|
||||||
|
minlen?: number;
|
||||||
|
pattern?: string;
|
||||||
|
thickness?: number;
|
||||||
|
useRough?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extending the Node interface for specific types if needed
|
||||||
|
interface ClassDiagramNode extends Node {
|
||||||
|
memberData: any; // Specific property for class diagram nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific interfaces for layout and render data
|
||||||
|
export interface LayoutData {
|
||||||
|
nodes: Node[];
|
||||||
|
edges: Edge[];
|
||||||
|
config: MermaidConfig;
|
||||||
|
[key: string]: any; // Additional properties not yet defined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderData {
|
||||||
|
items: (Node | Edge)[];
|
||||||
|
[key: string]: any; // Additional properties not yet defined
|
||||||
|
}
|
||||||
|
|
||||||
|
// This refactored approach ensures that common properties are included in the base `Node` and `Edge` interfaces, with specific types extending these bases with additional properties as needed. This maintains flexibility while ensuring type safety and reducing redundancy.
|
||||||
|
|
||||||
|
export type LayoutMethod =
|
||||||
|
| 'dagre'
|
||||||
|
| 'dagre-wrapper'
|
||||||
|
| 'elk'
|
||||||
|
| 'neato'
|
||||||
|
| 'dot'
|
||||||
|
| 'circo'
|
||||||
|
| 'fdp'
|
||||||
|
| 'osage'
|
||||||
|
| 'grid';
|
||||||
|
|
||||||
|
export function createDomElement(node: Node): Node {
|
||||||
|
// Create a new DOM element. Assuming we're creating a div as an example
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
// Check if node.domId is set, if not generate a unique identifier for it
|
||||||
|
if (!node.domId) {
|
||||||
|
// This is a simplistic approach to generate a unique ID
|
||||||
|
// In a real application, you might want to use a more robust method
|
||||||
|
node.domId = `node-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the ID of the DOM element
|
||||||
|
element.id = node.domId;
|
||||||
|
|
||||||
|
// Optional: Apply styles and classes to the element
|
||||||
|
if (node.cssStyles) {
|
||||||
|
element.style.cssText = node.cssStyles;
|
||||||
|
}
|
||||||
|
if (node.classes) {
|
||||||
|
element.className = node.classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Add content or additional attributes to the element
|
||||||
|
// This can be based on other properties of the node
|
||||||
|
if (node.label) {
|
||||||
|
element.textContent = node.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the newly created element to the document body or a specific container
|
||||||
|
// This is just an example; in a real application, you might append it somewhere specific
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
// Return the updated node with its domId set
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
@@ -71,6 +71,25 @@ properties:
|
|||||||
tsType: any
|
tsType: any
|
||||||
themeCSS:
|
themeCSS:
|
||||||
type: string
|
type: string
|
||||||
|
look:
|
||||||
|
description: |
|
||||||
|
Defines which main look to use for the diagram.
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- classic
|
||||||
|
- handdrawn
|
||||||
|
- slick
|
||||||
|
default: 'classic'
|
||||||
|
handdrawnSeed:
|
||||||
|
description: |
|
||||||
|
Defines the seed to be used when using handdrawn look. This is important for the automated tests as they will always find differences without the seed. The default value is 0 which gives a random seed.
|
||||||
|
type: number
|
||||||
|
default: 0
|
||||||
|
layout:
|
||||||
|
description: |
|
||||||
|
Defines which layout algorithm to use for rendering the diagram.
|
||||||
|
type: string
|
||||||
|
default: 'dagre'
|
||||||
maxTextSize:
|
maxTextSize:
|
||||||
description: The maximum allowed size of the users text diagram
|
description: The maximum allowed size of the users text diagram
|
||||||
type: number
|
type: number
|
||||||
@@ -81,6 +100,21 @@ properties:
|
|||||||
type: integer
|
type: integer
|
||||||
default: 500
|
default: 500
|
||||||
minimum: 0
|
minimum: 0
|
||||||
|
elk.mergeEdges:
|
||||||
|
description: |
|
||||||
|
Elk specific option that allows edge egdes to share path where it convenient. It can make for pretty diagrams but can also make it harder to read the diagram.
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
elk.nodePlacement.strategy:
|
||||||
|
description: |
|
||||||
|
Elk specific option affedcting how nodes are placed.
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- SIMPLE
|
||||||
|
- NETWORK_SIMPLEX
|
||||||
|
- LINEAR_SEGMENTS
|
||||||
|
- BRANDES_KOEPF
|
||||||
|
default: SIMPLE
|
||||||
darkMode:
|
darkMode:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
@@ -182,7 +216,7 @@ properties:
|
|||||||
default: false
|
default: false
|
||||||
forceLegacyMathML:
|
forceLegacyMathML:
|
||||||
description: |
|
description: |
|
||||||
This option forces Mermaid to rely on KaTeX's own stylesheet for rendering MathML. Due to differences between OS
|
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.
|
fonts and browser's MathML implementation, this option is recommended if consistent rendering is important.
|
||||||
If set to true, ignores legacyMathML.
|
If set to true, ignores legacyMathML.
|
||||||
type: boolean
|
type: boolean
|
||||||
|
@@ -6,7 +6,6 @@ class Theme {
|
|||||||
this.background = '#333';
|
this.background = '#333';
|
||||||
this.primaryColor = '#1f2020';
|
this.primaryColor = '#1f2020';
|
||||||
this.secondaryColor = lighten(this.primaryColor, 16);
|
this.secondaryColor = lighten(this.primaryColor, 16);
|
||||||
|
|
||||||
this.tertiaryColor = adjust(this.primaryColor, { h: -160 });
|
this.tertiaryColor = adjust(this.primaryColor, { h: -160 });
|
||||||
this.primaryBorderColor = invert(this.background);
|
this.primaryBorderColor = invert(this.background);
|
||||||
this.secondaryBorderColor = mkBorder(this.secondaryColor, this.darkMode);
|
this.secondaryBorderColor = mkBorder(this.secondaryColor, this.darkMode);
|
||||||
@@ -22,7 +21,7 @@ class Theme {
|
|||||||
this.mainContrastColor = 'lightgrey';
|
this.mainContrastColor = 'lightgrey';
|
||||||
this.darkTextColor = lighten(invert('#323D47'), 10);
|
this.darkTextColor = lighten(invert('#323D47'), 10);
|
||||||
this.lineColor = 'calculated';
|
this.lineColor = 'calculated';
|
||||||
this.border1 = '#81B1DB';
|
this.border1 = '#ccc';
|
||||||
this.border2 = rgba(255, 255, 255, 0.25);
|
this.border2 = rgba(255, 255, 255, 0.25);
|
||||||
this.arrowheadColor = 'calculated';
|
this.arrowheadColor = 'calculated';
|
||||||
this.fontFamily = '"trebuchet ms", verdana, arial, sans-serif';
|
this.fontFamily = '"trebuchet ms", verdana, arial, sans-serif';
|
||||||
@@ -333,6 +332,8 @@ class Theme {
|
|||||||
this.attributeBackgroundColorEven =
|
this.attributeBackgroundColorEven =
|
||||||
this.attributeBackgroundColorEven || lighten(this.background, 2);
|
this.attributeBackgroundColorEven || lighten(this.background, 2);
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
|
this.nodeBorder = this.nodeBorder || '#999';
|
||||||
}
|
}
|
||||||
calculate(overrides) {
|
calculate(overrides) {
|
||||||
if (typeof overrides !== 'object') {
|
if (typeof overrides !== 'object') {
|
||||||
|
@@ -3,7 +3,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"types": ["vitest/importMeta", "vitest/globals"]
|
"types": ["vitest/importMeta", "vitest/globals"],
|
||||||
|
"baseUrl": ".", // This must be set if "paths" is set
|
||||||
|
"paths": {
|
||||||
|
"$root/*": ["src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts", "./package.json"]
|
"include": ["./src/**/*.ts", "./package.json"]
|
||||||
}
|
}
|
||||||
|
19842
pnpm-lock.yaml
generated
19842
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -66,7 +66,7 @@
|
|||||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
"noEmitOnError": false /* Disable emitting files if any type checking errors are reported. */,
|
||||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
@@ -2,15 +2,19 @@ import jison from './.vite/jisonPlugin.js';
|
|||||||
import jsonSchemaPlugin from './.vite/jsonSchemaPlugin.js';
|
import jsonSchemaPlugin from './.vite/jsonSchemaPlugin.js';
|
||||||
import typescript from '@rollup/plugin-typescript';
|
import typescript from '@rollup/plugin-typescript';
|
||||||
import { defaultExclude, defineConfig } from 'vitest/config';
|
import { defaultExclude, defineConfig } from 'vitest/config';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js'],
|
extensions: ['.js'],
|
||||||
|
alias: {
|
||||||
|
// Define your alias here
|
||||||
|
'$root/*': path.resolve(__dirname, 'src/*'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
jison(),
|
jison(),
|
||||||
jsonSchemaPlugin(), // handles .schema.yaml JSON Schema files
|
jsonSchemaPlugin(), // handles .schema.yaml JSON Schema files
|
||||||
// @ts-expect-error According to the type definitions, rollup plugins are incompatible with vite
|
|
||||||
typescript({ compilerOptions: { declaration: false } }),
|
typescript({ compilerOptions: { declaration: false } }),
|
||||||
],
|
],
|
||||||
test: {
|
test: {
|
||||||
|
Reference in New Issue
Block a user