Merge branch '5237-unified-layout-common-renderer' of github.com:mermaid-js/mermaid into alanaV11

This commit is contained in:
Knut Sveidqvist
2024-05-24 17:00:31 +02:00
71 changed files with 16343 additions and 11534 deletions

View File

@@ -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;

View File

@@ -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,

View File

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

View File

@@ -54,6 +54,7 @@ presetAttributify
pyplot pyplot
redmine redmine
rehype rehype
roughjs
rscratch rscratch
sparkline sparkline
sphinxcontrib sphinxcontrib

View File

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

View File

@@ -1 +1,4 @@
circo
handdrawnSeed
neato
newbranch newbranch

View File

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

View File

@@ -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
View File

@@ -35,7 +35,7 @@ cypress/snapshots/
.tsbuildinfo .tsbuildinfo
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
knsv*.html #knsv*.html
local*.html local*.html
stats/ stats/

View File

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

View 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 />
&nbsp;
<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>

View File

@@ -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<["&nbsp;&nbsp;&nbsp;"]>(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 />
&nbsp;
<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() {

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

File diff suppressed because it is too large Load Diff

View File

@@ -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)
--- ---

View 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"
]
}

View 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';
};

View 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,
})),
];

View 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);
}
});
};

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
},
"include": ["./src/**/*.ts"],
"typeRoots": ["./src/types"]
}

View File

@@ -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"

View File

@@ -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);

View File

@@ -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;
/** /**

View File

@@ -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;

View File

@@ -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;

View File

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

View 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);
}
};

View File

@@ -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,
}; };

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();
},
};

View File

@@ -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,
};

View 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,
};

View File

@@ -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;

View 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;

View 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;

View File

@@ -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
);
};

View File

@@ -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());

View File

@@ -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']);
});
});

View 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);
};

View File

@@ -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 + ')');
};

View File

@@ -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;

View File

@@ -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();
});
});

View File

@@ -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})`);
};

View 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;
};

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
/**
* @param node
* @param point
*/
function intersectNode(node, point) {
return node.intersect(point);
}
export default intersectNode;

View File

@@ -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];
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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(' ');

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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 + ')');
}

View 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}`;
};

View File

@@ -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;
}

View File

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

View File

@@ -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') {

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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. */

View File

@@ -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: {