mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
Merge branch 'develop' into feature/4401_creating_langium_parser_gitGraph
This commit is contained in:
commit
73aae9e86e
@ -22,9 +22,9 @@ export const packageOptions = {
|
||||
packageName: 'mermaid-zenuml',
|
||||
file: 'detector.ts',
|
||||
},
|
||||
'mermaid-flowchart-elk': {
|
||||
name: 'mermaid-flowchart-elk',
|
||||
packageName: 'mermaid-flowchart-elk',
|
||||
file: 'detector.ts',
|
||||
'mermaid-layout-elk': {
|
||||
name: 'mermaid-layout-elk',
|
||||
packageName: 'mermaid-layout-elk',
|
||||
file: 'layouts.ts',
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import jison from 'jison';
|
||||
|
||||
export const transformJison = (src: string): string => {
|
||||
// @ts-ignore - Jison is not typed properly
|
||||
const parser = new jison.Generator(src, {
|
||||
moduleType: 'js',
|
||||
'token-stack': true,
|
||||
|
@ -28,6 +28,7 @@ controly
|
||||
CSSCLASS
|
||||
CYLINDEREND
|
||||
CYLINDERSTART
|
||||
DAGA
|
||||
datakey
|
||||
DEND
|
||||
descr
|
||||
@ -90,6 +91,7 @@ reqs
|
||||
rewritelinks
|
||||
rgba
|
||||
RIGHTOF
|
||||
roughjs
|
||||
sankey
|
||||
sequencenumber
|
||||
shrc
|
||||
@ -109,6 +111,7 @@ strikethrough
|
||||
stringifying
|
||||
struct
|
||||
STYLECLASS
|
||||
STYLEDEF
|
||||
STYLEOPTS
|
||||
subcomponent
|
||||
subcomponents
|
||||
|
@ -55,6 +55,7 @@ presetAttributify
|
||||
pyplot
|
||||
redmine
|
||||
rehype
|
||||
roughjs
|
||||
rscratch
|
||||
shiki
|
||||
sparkline
|
||||
|
@ -9,6 +9,7 @@ elems
|
||||
gantt
|
||||
gitgraph
|
||||
gzipped
|
||||
handDrawn
|
||||
knsv
|
||||
Knut
|
||||
marginx
|
||||
@ -17,6 +18,7 @@ Markdownish
|
||||
mermaidjs
|
||||
mindmap
|
||||
mindmaps
|
||||
mrtree
|
||||
multigraph
|
||||
nodesep
|
||||
NOTEGROUP
|
||||
|
@ -1,4 +1,6 @@
|
||||
BRANDES
|
||||
handdrawn
|
||||
circo
|
||||
handDrawn
|
||||
KOEPF
|
||||
neato
|
||||
newbranch
|
||||
|
@ -8,8 +8,8 @@ import { defaultOptions, getBuildConfig } from './util.js';
|
||||
const shouldVisualize = process.argv.includes('--visualize');
|
||||
|
||||
const buildPackage = async (entryName: keyof typeof packageOptions) => {
|
||||
const commonOptions = { ...defaultOptions, entryName } as const;
|
||||
const buildConfigs = [
|
||||
const commonOptions: MermaidBuildOptions = { ...defaultOptions, entryName } as const;
|
||||
const buildConfigs: MermaidBuildOptions[] = [
|
||||
// package.mjs
|
||||
{ ...commonOptions },
|
||||
// package.min.mjs
|
||||
|
@ -8,7 +8,7 @@ import { jisonPlugin } from './jisonPlugin.js';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
export interface MermaidBuildOptions {
|
||||
export interface MermaidBuildOptions extends BuildOptions {
|
||||
minify: boolean;
|
||||
core: boolean;
|
||||
metafile: boolean;
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -35,7 +35,7 @@ cypress/snapshots/
|
||||
.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
knsv*.html
|
||||
#knsv*.html
|
||||
local*.html
|
||||
stats/
|
||||
|
||||
|
@ -16,3 +16,5 @@ generated/
|
||||
# Ignore the files creates in /demos/dev except for example.html
|
||||
demos/dev/**
|
||||
!/demos/dev/example.html
|
||||
# TODO: Lots of errors to fix
|
||||
cypress/platform/state-refactor.html
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { urlSnapshotTest, openURLAndVerifyRendering } from '../../helpers/util.ts';
|
||||
|
||||
describe('Flowchart elk', () => {
|
||||
it('should use dagre as fallback', () => {
|
||||
urlSnapshotTest('http://localhost:9000/flow-elk.html', {
|
||||
name: 'flow-elk fallback to dagre',
|
||||
});
|
||||
});
|
||||
it('should allow overriding with external package', () => {
|
||||
urlSnapshotTest('http://localhost:9000/flow-elk.html?elk=true', {
|
||||
name: 'flow-elk overriding dagre with elk',
|
||||
});
|
||||
});
|
||||
});
|
@ -11,6 +11,27 @@ describe('Git Graph diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('Should render subgraphs with title margins and edge labels', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart LR
|
||||
|
||||
subgraph TOP
|
||||
direction TB
|
||||
subgraph B1
|
||||
direction RL
|
||||
i1 --lb1-->f1
|
||||
end
|
||||
subgraph B2
|
||||
direction BT
|
||||
i2 --lb2-->f2
|
||||
end
|
||||
end
|
||||
A --lb3--> TOP --lb4--> B
|
||||
B1 --lb5--> B2
|
||||
`,
|
||||
{ flowchart: { subGraphTitleMargin: { top: 10, bottom: 5 } } }
|
||||
);
|
||||
});
|
||||
// it(`ultraFastTest`, function () {
|
||||
// // Navigate to the url we want to test
|
||||
// // ⭐️ Note to see visual bugs, run the test using the above URL for the 1st run.
|
||||
|
@ -236,7 +236,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL16: width alignment - blocks shold be equal in width', () => {
|
||||
it('BL17: width alignment - blocks shold be equal in width', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
A("This is the text")
|
||||
@ -247,7 +247,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL17: block types 1 - square, rounded and circle', () => {
|
||||
it('BL18: block types 1 - square, rounded and circle', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
A["square"]
|
||||
@ -258,7 +258,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL18: block types 2 - odd, diamond and hexagon', () => {
|
||||
it('BL19: block types 2 - odd, diamond and hexagon', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
A>"rect_left_inv_arrow"]
|
||||
@ -269,7 +269,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL19: block types 3 - stadium', () => {
|
||||
it('BL20: block types 3 - stadium', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
A(["stadium"])
|
||||
@ -278,7 +278,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL20: block types 4 - lean right, lean left, trapezoid and inv trapezoid', () => {
|
||||
it('BL21: block types 4 - lean right, lean left, trapezoid and inv trapezoid', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
A[/"lean right"/]
|
||||
@ -290,7 +290,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL21: block types 1 - square, rounded and circle', () => {
|
||||
it('BL22: block types 1 - square, rounded and circle', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
A["square"]
|
||||
@ -301,7 +301,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL22: sizing - it should be possible to make a block wider', () => {
|
||||
it('BL23: sizing - it should be possible to make a block wider', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
A("rounded"):2
|
||||
@ -312,7 +312,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL23: sizing - it should be possible to make a composite block wider', () => {
|
||||
it('BL24: sizing - it should be possible to make a composite block wider', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
block:2
|
||||
@ -324,7 +324,7 @@ describe('Block diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('BL24: block in the middle with space on each side', () => {
|
||||
it('BL25: block in the middle with space on each side', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
columns 3
|
||||
@ -335,7 +335,7 @@ describe('Block diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('BL25: space and an edge', () => {
|
||||
it('BL26: space and an edge', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
columns 5
|
||||
@ -345,7 +345,7 @@ describe('Block diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('BL26: block sizes for regular blocks', () => {
|
||||
it('BL27: block sizes for regular blocks', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
columns 3
|
||||
@ -354,7 +354,7 @@ describe('Block diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('BL27: composite block with a set width - f should use the available space', () => {
|
||||
it('BL28: composite block with a set width - f should use the available space', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
columns 3
|
||||
@ -363,11 +363,12 @@ describe('Block diagram', () => {
|
||||
f
|
||||
end
|
||||
g
|
||||
`,
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('BL23: composite block with a set width - f and g should split the available space', () => {
|
||||
|
||||
it('BL29: composite block with a set width - f and g should split the available space', () => {
|
||||
imgSnapshotTest(
|
||||
`block-beta
|
||||
columns 3
|
||||
@ -379,7 +380,7 @@ describe('Block diagram', () => {
|
||||
h
|
||||
i
|
||||
j
|
||||
`,
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||
|
||||
describe('C4 diagram', () => {
|
||||
it('should render a simple C4Context diagram', () => {
|
||||
it('C4.1 should render a simple C4Context diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
C4Context
|
||||
@ -31,7 +31,7 @@ describe('C4 diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render a simple C4Container diagram', () => {
|
||||
it('C4.2 should render a simple C4Container diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
C4Container
|
||||
@ -50,7 +50,7 @@ describe('C4 diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render a simple C4Component diagram', () => {
|
||||
it('C4.3 should render a simple C4Component diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
C4Component
|
||||
@ -68,7 +68,7 @@ describe('C4 diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render a simple C4Dynamic diagram', () => {
|
||||
it('C4.4 should render a simple C4Dynamic diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
C4Dynamic
|
||||
@ -91,7 +91,7 @@ describe('C4 diagram', () => {
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should render a simple C4Deployment diagram', () => {
|
||||
it('C4.5 should render a simple C4Deployment diagram', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
C4Deployment
|
||||
|
@ -76,7 +76,7 @@ describe('Class diagram V2', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a simple class diagram with different visibilities', () => {
|
||||
it('2.1 should render a simple class diagram with different visibilities', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
classDiagram-v2
|
||||
@ -93,7 +93,7 @@ describe('Class diagram V2', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render multiple class diagrams', () => {
|
||||
it('3: should render multiple class diagrams', () => {
|
||||
imgSnapshotTest(
|
||||
[
|
||||
`
|
||||
|
@ -3,7 +3,7 @@ import { imgSnapshotTest } from '../../helpers/util';
|
||||
describe('Error Diagrams', () => {
|
||||
beforeEach(() => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
expect(err.message).to.include('Parse error');
|
||||
expect(err.message).to.include('error');
|
||||
// return false to prevent the error from
|
||||
// failing this test
|
||||
return false;
|
||||
|
@ -837,6 +837,26 @@ subgraph "\`**Two**\`"
|
||||
in the hat\`") -- "\`1o **ipa**\`" --> d("The dog in the hog")
|
||||
end
|
||||
|
||||
`,
|
||||
{ flowchart: { titleTopMargin: 0 } }
|
||||
);
|
||||
});
|
||||
it('Sub graphs and markdown strings', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
|
||||
flowchart LR
|
||||
subgraph subgraph_ko6czgs5u["Untitled subgraph"]
|
||||
D["Option 1"]
|
||||
end
|
||||
C{"Evaluate"} -- One --> D
|
||||
C -- Two --> E(("Option 2"))
|
||||
D --> E
|
||||
A["A"]
|
||||
|
||||
`,
|
||||
{ flowchart: { titleTopMargin: 0 } }
|
||||
);
|
||||
@ -855,7 +875,7 @@ describe('Title and arrow styling #4813', () => {
|
||||
flowchart LR
|
||||
A-->B
|
||||
A-->C`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
{ layout: 'elk' }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
const title = svg[0].querySelector('text');
|
||||
@ -871,15 +891,14 @@ describe('Title and arrow styling #4813', () => {
|
||||
B-.-oC
|
||||
C==xD
|
||||
D ~~~ A`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
{ layout: 'elk' }
|
||||
);
|
||||
cy.get('svg').should((svg) => {
|
||||
const edges = svg[0].querySelectorAll('.edges path');
|
||||
console.log(edges);
|
||||
expect(edges[0]).to.have.attr('pattern', 'solid');
|
||||
expect(edges[1]).to.have.attr('pattern', 'dotted');
|
||||
expect(edges[2]).to.have.css('stroke-width', '3.5px');
|
||||
expect(edges[3]).to.have.css('stroke-width', '1.5px');
|
||||
expect(edges[0].getAttribute('class')).to.contain('edge-pattern-solid');
|
||||
expect(edges[1].getAttribute('class')).to.contain('edge-pattern-dotted');
|
||||
expect(edges[2].getAttribute('class')).to.contain('edge-thickness-thick');
|
||||
expect(edges[3].getAttribute('class')).to.contain('edge-thickness-invisible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
1060
cypress/integration/rendering/flowchart-handDrawn.spec.js
Normal file
1060
cypress/integration/rendering/flowchart-handDrawn.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -99,7 +99,7 @@ describe('Flowchart v2', () => {
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||
expect(maxWidthValue).to.be.within(290 * 0.95 - 1, 290 * 1.05);
|
||||
expect(maxWidthValue).to.be.within(446 * 0.95 - 1, 446 * 1.05);
|
||||
});
|
||||
});
|
||||
it('8: should render a flowchart when useMaxWidth is false', () => {
|
||||
@ -118,7 +118,7 @@ describe('Flowchart v2', () => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
// use within because the absolute value can be slightly different depending on the environment ±5%
|
||||
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
|
||||
expect(width).to.be.within(290 * 0.95 - 1, 290 * 1.05);
|
||||
expect(width).to.be.within(446 * 0.95 - 1, 446 * 1.05);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
@ -1047,7 +1047,9 @@ end
|
||||
A --lb3--> TOP --lb4--> B
|
||||
B1 --lb5--> B2
|
||||
`,
|
||||
{ flowchart: { subGraphTitleMargin: { top: 10, bottom: 5 } } }
|
||||
{
|
||||
flowchart: { subGraphTitleMargin: { top: 10, bottom: 5 } },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -733,7 +733,7 @@ describe('Graph', () => {
|
||||
});
|
||||
it('38: should render a flowchart when useMaxWidth is true (default)', () => {
|
||||
renderGraph(
|
||||
`graph TD
|
||||
`flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
@ -751,7 +751,7 @@ describe('Graph', () => {
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||
expect(maxWidthValue).to.be.within(300 * 0.9, 300 * 1.1);
|
||||
expect(maxWidthValue).to.be.within(446 * 0.9, 446 * 1.1);
|
||||
});
|
||||
});
|
||||
it('39: should render a flowchart when useMaxWidth is false', () => {
|
||||
@ -770,7 +770,7 @@ describe('Graph', () => {
|
||||
const width = parseFloat(svg.attr('width'));
|
||||
// use within because the absolute value can be slightly different depending on the environment ±10%
|
||||
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
|
||||
expect(width).to.be.within(300 * 0.9, 300 * 1.1);
|
||||
expect(width).to.be.within(446 * 0.9, 446 * 1.1);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
@ -905,13 +905,16 @@ graph TD
|
||||
it('67: should be able to style default node independently', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
flowchart TD
|
||||
flowchart TD
|
||||
classDef default fill:#a34
|
||||
hello --> default
|
||||
|
||||
style default stroke:#000,stroke-width:4px
|
||||
`,
|
||||
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
|
||||
{
|
||||
flowchart: { htmlLabels: true },
|
||||
securityLevel: 'loose',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -542,6 +542,43 @@ stateDiagram-v2
|
||||
{ logLevel: 0, fontFamily: 'courier' }
|
||||
);
|
||||
});
|
||||
it(' can have styles applied ', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
AState
|
||||
style AState fill:#636,border:1px solid red,color:white;
|
||||
`,
|
||||
{ logLevel: 0, fontFamily: 'courier' }
|
||||
);
|
||||
});
|
||||
it(' should let styles take preceedence over classes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram-v2
|
||||
AState: Should NOT be white
|
||||
BState
|
||||
classDef exampleStyleClass fill:#fff,color: blue;
|
||||
class AState,BState exampleStyleClass
|
||||
style AState fill:#636,border:1px solid red,color:white;
|
||||
`,
|
||||
{ logLevel: 0, fontFamily: 'courier' }
|
||||
);
|
||||
});
|
||||
it(' should allow styles to take effect in stubgraphs', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
stateDiagram
|
||||
state roundWithTitle {
|
||||
C: Black with white text
|
||||
}
|
||||
D: Black with white text
|
||||
|
||||
style C,D stroke:#00f, fill:black, color:white
|
||||
`,
|
||||
{ logLevel: 0, fontFamily: 'courier' }
|
||||
);
|
||||
});
|
||||
});
|
||||
it('1433: should render a simple state diagram with a title', () => {
|
||||
imgSnapshotTest(
|
||||
@ -551,6 +588,20 @@ title: simple state diagram
|
||||
stateDiagram-v2
|
||||
[*] --> State1
|
||||
State1 --> [*]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('should align dividers correctly', () => {
|
||||
imgSnapshotTest(
|
||||
`stateDiagram-v2
|
||||
state s2 {
|
||||
s3
|
||||
--
|
||||
s4
|
||||
--
|
||||
55
|
||||
}
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
866
cypress/platform/flowchart-refactor.html
Normal file
866
cypress/platform/flowchart-refactor.html
Normal file
@ -0,0 +1,866 @@
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
body {
|
||||
font-family: 'Arial';
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 20px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.vertical-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
background-color: #f9f9f9;
|
||||
color: #444;
|
||||
cursor: pointer;
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.active,
|
||||
.collapsible:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.collapsible:after {
|
||||
content: '\002B';
|
||||
color: #777;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
content: '\2212';
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 5px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease-out;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.content .pre-scrollable {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<!-- Placeholder for the top left corner -->
|
||||
<th>Dagre</th>
|
||||
<th>Dagre with rough</th>
|
||||
<th>ELK</th>
|
||||
<th>ELK with rough</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Stadium shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1([This is the text in the box])
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram1" class="mermaid">
|
||||
flowchart LR
|
||||
id1([This is the text in the box])
|
||||
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram2" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1([This is the text in the box])
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram3" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1([This is the text in the box])
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1([This is the text in the box])
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Sub-Routine shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1[[This is the text in the box]]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram5" class="mermaid">
|
||||
flowchart LR
|
||||
id1[[This is the text in the box]]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram6" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1[[This is the text in the box]]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram7" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1[[This is the text in the box]]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram8" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1[[This is the text in the box]]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Cylindrical shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1[(Database)]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram9" class="mermaid">
|
||||
flowchart LR
|
||||
id1[(Database)]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram10" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1[(Database)]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram11" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1[(Database)]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram12" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1[(Database)]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Circle shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1((This is the text in the circle))
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram13" class="mermaid">
|
||||
flowchart LR
|
||||
id1((This is the text in the circle))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram14" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1((This is the text in the circle))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram15" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1((This is the text in the circle))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram16" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1((This is the text in the circle))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Double Circle shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart TD
|
||||
id1(((This is the text in the circle)))
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram17" class="mermaid">
|
||||
flowchart TD
|
||||
id1(((This is the text in the circle)))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram18" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart TD
|
||||
id1(((This is the text in the circle)))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram19" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
id1(((This is the text in the circle)))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram20" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
id1(((This is the text in the circle)))
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Asymmetric shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1>This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram21" class="mermaid">
|
||||
flowchart LR
|
||||
id1>This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram22" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1>This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram23" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1>This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram24" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1>This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Rhombus/Diamond/Question shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1{This is the text in the box}
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram25" class="mermaid">
|
||||
flowchart LR
|
||||
id1{This is the text in the box}
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram26" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1{This is the text in the box}
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram27" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1{This is the text in the box}
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram28" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1{This is the text in the box}
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Hexagon shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1{{This is the text in the box}}
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram29" class="mermaid">
|
||||
flowchart LR
|
||||
id1{{This is the text in the box}}
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram31" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1{{This is the text in the box}}
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram32" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1{{This is the text in the box}}
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Parallelogram shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart TD
|
||||
id1[/This is the text in the box/]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram33" class="mermaid">
|
||||
flowchart TD
|
||||
id1[/This is the text in the box/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram34" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart TD
|
||||
id1[/This is the text in the box/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram35" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
id1[/This is the text in the box/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram36" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
id1[/This is the text in the box/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Parallelogram Alt shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart TD
|
||||
id1[\This is the text in the box\]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram37" class="mermaid">
|
||||
flowchart TD
|
||||
id1[\This is the text in the box\]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram38" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart TD
|
||||
id1[\This is the text in the box\]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram39" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
id1[\This is the text in the box\]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram40" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
id1[\This is the text in the box\]
|
||||
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Trapezoid shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart TD
|
||||
A[/Christmas\]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram41" class="mermaid">
|
||||
flowchart TD
|
||||
A[/Christmas\]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram42" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart TD
|
||||
A[/Christmas\]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram43" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
A[/Christmas\]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram44" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
A[/Christmas\]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Trapezoid Alt shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart TD
|
||||
A[\Christmas/]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram45" class="mermaid">
|
||||
flowchart TD
|
||||
A[\Christmas/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram46" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart TD
|
||||
A[\Christmas/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram47" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
A[\Christmas/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram48" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart TD
|
||||
A[\Christmas/]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Rect with rounded corner</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1(This is the text in the box)
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram49" class="mermaid">
|
||||
flowchart LR
|
||||
id1(This is the text in the box)
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram50" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1(This is the text in the box)
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram51" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1(This is the text in the box)
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram52" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1(This is the text in the box)
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Rect with sharp corner</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1[This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram53" class="mermaid">
|
||||
flowchart LR
|
||||
id1[This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram54" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1[This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram55" class="mermaid">
|
||||
%%{init: {"handDrawn": false, "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1[This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram56" class="mermaid">
|
||||
%%{init: {"look": "handDrawn", "layout": "elk"} }%%
|
||||
flowchart LR
|
||||
id1[This is the text in the box]
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Separator row -->
|
||||
<tr class="separator">
|
||||
<td colspan="5"></td>
|
||||
<!-- This cell spans all columns including the vertical header -->
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import { layouts } from './mermaid-layout-elk.esm.mjs';
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {};
|
||||
|
||||
mermaid.initialize({
|
||||
handDrawn: false,
|
||||
mergeEdges: true,
|
||||
layout: 'dagre',
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
// fontFamily: 'Caveat',
|
||||
fontFamily: 'Kalam',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
messageFontFamily: 'courier',
|
||||
},
|
||||
fontSize: 16,
|
||||
logLevel: 0,
|
||||
});
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
|
||||
let coll = document.getElementsByClassName('collapsible');
|
||||
for (const element of coll) {
|
||||
element.addEventListener('click', function () {
|
||||
this.classList.toggle('active');
|
||||
let content = this.nextElementSibling;
|
||||
if (content.style.maxHeight) {
|
||||
content.style.maxHeight = null;
|
||||
} else {
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
191
cypress/platform/flowchart-sate.html
Normal file
191
cypress/platform/flowchart-sate.html
Normal file
@ -0,0 +1,191 @@
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
body {
|
||||
font-family: 'Arial';
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 20px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.vertical-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
background-color: #f9f9f9;
|
||||
color: #444;
|
||||
cursor: pointer;
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.active,
|
||||
.collapsible:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.collapsible:after {
|
||||
content: '\002B';
|
||||
color: #777;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
content: '\2212';
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 5px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease-out;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.content .pre-scrollable {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<!-- Placeholder for the top left corner -->
|
||||
<th>State rough</th>
|
||||
<th>Flowchart rough</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="vertical-header">
|
||||
<button class="collapsible">Stadium shape</button>
|
||||
<div class="content">
|
||||
<div class="pre-scrollable">
|
||||
<pre>
|
||||
flowchart LR
|
||||
id1([This is the text in the box])
|
||||
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<pre id="diagram1" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
stateDiagram-v2
|
||||
stateA
|
||||
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<pre id="diagram2" class="mermaid">
|
||||
%%{init: {"look": "handDrawn"} }%%
|
||||
flowchart LR
|
||||
id1[[This is the text in the box]]
|
||||
|
||||
|
||||
</pre
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import { layouts } from './mermaid-layout-elk.esm.mjs';
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {};
|
||||
|
||||
mermaid.initialize({
|
||||
handDrawn: false,
|
||||
mergeEdges: true,
|
||||
layout: 'dagre',
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
// fontFamily: 'Caveat',
|
||||
fontFamily: 'Kalam',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
messageFontFamily: 'courier',
|
||||
},
|
||||
fontSize: 16,
|
||||
logLevel: 0,
|
||||
});
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
}
|
||||
mermaid.parseError = function (err, hash) {
|
||||
console.error('In parse error:');
|
||||
console.error(err);
|
||||
};
|
||||
|
||||
let coll = document.getElementsByClassName('collapsible');
|
||||
for (const element of coll) {
|
||||
element.addEventListener('click', function () {
|
||||
this.classList.toggle('active');
|
||||
let content = this.nextElementSibling;
|
||||
if (content.style.maxHeight) {
|
||||
content.style.maxHeight = null;
|
||||
} else {
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
433
cypress/platform/knsv-4442.html
Normal file
433
cypress/platform/knsv-4442.html
Normal file
@ -0,0 +1,433 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
/* background: rgb(221, 208, 208); */
|
||||
/* background:#333; */
|
||||
font-family: 'Arial';
|
||||
/* font-size: 18px !important; */
|
||||
}
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
.mermaid svg {
|
||||
/* font-size: 18px !important; */
|
||||
background-color: #efefef;
|
||||
background-image: radial-gradient(#fff 51%, transparent 91%),
|
||||
radial-gradient(#fff 51%, transparent 91%);
|
||||
background-size: 20px 20px;
|
||||
background-position:
|
||||
0 0,
|
||||
10px 10px;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
.malware {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 150px;
|
||||
background: red;
|
||||
color: black;
|
||||
display: flex;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: monospace;
|
||||
font-size: 72px;
|
||||
}
|
||||
/* tspan {
|
||||
font-size: 6px !important;
|
||||
} */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="diagram" class="mermaid">
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*] </pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart RL
|
||||
subgraph "`one`"
|
||||
a1 -- l1 --> a2
|
||||
a1 -- l2 --> a2
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
flowchart RL
|
||||
subgraph "`one`"
|
||||
a1 -- l1 --> a2
|
||||
a1 -- l2 --> a2
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart
|
||||
id["`A root with a long text that wraps to keep the node size in check. A root with a long text that wraps to keep the node size in check`"]</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart LR
|
||||
A[A text that needs to be wrapped wraps to another line]
|
||||
B[A text that needs to be<br/>wrapped wraps to another line]
|
||||
C["`A text that needs to be wrapped to another line`"]</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart LR
|
||||
C["`A text
|
||||
that needs
|
||||
to be wrapped
|
||||
in another
|
||||
way`"]
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid">
|
||||
classDiagram-v2
|
||||
note "I love this diagram!\nDo you love it?"
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
stateDiagram-v2
|
||||
State1: The state with a note with minus - and plus + in it
|
||||
note left of State1
|
||||
Important information! You can write
|
||||
notes with . and in them.
|
||||
end note </pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
mindmap
|
||||
root
|
||||
Child3(A node with an icon and with a long text that wraps to keep the node size in check)
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: {"theme": "forest"} }%%
|
||||
mindmap
|
||||
id1[**Start2**<br/>end]
|
||||
id2[**Start2**<br />end]
|
||||
%% Another comment
|
||||
id3[**Start2**<br>end] %% Comment
|
||||
id4[**Start2**<br >end<br >the very end]
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
mindmap
|
||||
id1["`**Start2**
|
||||
second line 😎 with long text that is wrapping to the next line`"]
|
||||
id2["`Child **with bold** text`"]
|
||||
id3["`Children of which some
|
||||
is using *italic type of* text`"]
|
||||
id4[Child]
|
||||
id5["`Child
|
||||
Row
|
||||
and another
|
||||
`"]
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
mindmap
|
||||
id1("`**Root**`"]
|
||||
id2["`A formatted text... with **bold** and *italics*`"]
|
||||
id3[Regular labels works as usual]
|
||||
id4["`Emojis and unicode works too: 🤓
|
||||
शान्तिः سلام 和平 `"]
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd["`**AMD** Latte GPU`"]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk", "htmlLabels": false}} }%%
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd["`**AMD** Latte GPU`"]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
|
||||
<br />
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd[AMD Latte GPU]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
<br />
|
||||
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart LR
|
||||
B1 --be be--x B2
|
||||
B1 --bo bo--o B3
|
||||
subgraph Ugge
|
||||
B2
|
||||
B3
|
||||
subgraph inner
|
||||
B4
|
||||
B5
|
||||
end
|
||||
subgraph inner2
|
||||
subgraph deeper
|
||||
C4
|
||||
C5
|
||||
end
|
||||
C6
|
||||
end
|
||||
|
||||
B4 --> C4
|
||||
|
||||
B3 -- X --> B4
|
||||
B2 --> inner
|
||||
|
||||
C4 --> C5
|
||||
end
|
||||
|
||||
subgraph outer
|
||||
B6
|
||||
end
|
||||
B6 --> B5
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
sequenceDiagram
|
||||
Customer->>+Stripe: Makes a payment request
|
||||
Stripe->>+Bank: Forwards the payment request to the bank
|
||||
Bank->>+Customer: Asks for authorization
|
||||
Customer->>+Bank: Provides authorization
|
||||
Bank->>+Stripe: Sends a response with payment details
|
||||
Stripe->>+Merchant: Sends a notification of payment receipt
|
||||
Merchant->>+Stripe: Confirms the payment
|
||||
Stripe->>+Customer: Sends a confirmation of payment
|
||||
Customer->>+Merchant: Receives goods or services
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
Long history
|
||||
::icon(fa fa-book)
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
Strategic planning
|
||||
Argument mapping
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
</pre>
|
||||
<br />
|
||||
<pre id="diagram" class="mermaid2">
|
||||
example-diagram
|
||||
</pre>
|
||||
|
||||
<!-- <div id="cy"></div> -->
|
||||
<!-- <script src="http://localhost:9000/packages/mermaid-mindmap/dist/mermaid-mindmap-detector.js"></script> -->
|
||||
<!-- <script src="./mermaid-example-diagram-detector.js"></script> -->
|
||||
<!-- <script src="//cdn.jsdelivr.net/npm/mermaid@9.1.7/dist/mermaid.min.js"></script> -->
|
||||
<!-- <script src="./mermaid.js"></script> -->
|
||||
|
||||
<scrpt>
|
||||
// import mindmap from '../../packages/mermaid-mindmap/src/detector'; // import example from
|
||||
'../../packages/mermaid-example-diagram/src/mermaid-example-diagram.core.mjs'; import mermaid
|
||||
from './mermaid.esm.mjs'; // await mermaid.registerExternalDiagrams([example]);
|
||||
mermaid.parseError = function (err, hash) { // console.error('Mermaid error: ', err); };
|
||||
mermaid.initialize({ // theme: 'forest', startOnLoad: true, logLevel: 0, flowchart: { //
|
||||
defaultRenderer: 'elk', useMaxWidth: false, // htmlLabels: false, htmlLabels: true, }, //
|
||||
htmlLabels: false, gantt: { useMaxWidth: false, }, useMaxWidth: false, }); function callback()
|
||||
{ alert('It worked'); } mermaid.parseError = function (err, hash) { console.error('In parse
|
||||
error:'); console.error(err); }; // mermaid.test1('first_slow', 1200).then((r) =>
|
||||
console.info(r)); // mermaid.test1('second_fast', 200).then((r) => console.info(r)); //
|
||||
mermaid.test1('third_fast', 200).then((r) => console.info(r)); // mermaid.test1('forth_slow',
|
||||
1200).then((r) => console.info(r));
|
||||
</scrpt>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.jsdelivr.net/npm/mermaid@10.2.0/dist/mermaid.min.js"
|
||||
></script>
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10.2.0/dist/mermaid.min.js';
|
||||
(function () {
|
||||
mermaid.initialize({ startOnLoad: false });
|
||||
const elements = document.getElementsByClassName('mermaid');
|
||||
console.log(elements);
|
||||
let id = 0;
|
||||
[...elements].forEach((elem) => {
|
||||
const insertSvg = function (svgCode) {
|
||||
elem.innerHTML = svgCode;
|
||||
};
|
||||
|
||||
console.log(atob(elem.innerText));
|
||||
|
||||
mermaid.render(`graphDiv-${id++}`, atob(elem.innerText), insertSvg);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -4,7 +4,7 @@
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
||||
<link
|
||||
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
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
|
||||
@ -14,34 +14,50 @@
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
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>
|
||||
body {
|
||||
/* background: rgb(221, 208, 208); */
|
||||
background: #333;
|
||||
/* background: #333; */
|
||||
font-family: 'Arial';
|
||||
/* font-size: 18px !important; */
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
.mermaid {
|
||||
border: 1px solid #ddd;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mermaid svg {
|
||||
/* font-size: 18px !important; */
|
||||
/* background-color: #efefef; */
|
||||
background-color: #333;
|
||||
background-image: radial-gradient(#333 51%, transparent 91%),
|
||||
radial-gradient(#333 51%, transparent 91%);
|
||||
|
||||
/* 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;
|
||||
border: 2px solid rgb(131, 142, 205);
|
||||
background-position:
|
||||
0 0,
|
||||
10px 10px;
|
||||
background-repeat: repeat; */
|
||||
}
|
||||
|
||||
.malware {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@ -57,546 +73,121 @@
|
||||
font-family: monospace;
|
||||
font-size: 72px;
|
||||
}
|
||||
|
||||
/* tspan {
|
||||
font-size: 6px !important;
|
||||
} */
|
||||
font-size: 6px !important;
|
||||
} */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
blockArrowId<["Label"]>(right)
|
||||
blockArrowId2<["Label"]>(left)
|
||||
blockArrowId3<["Label"]>(up)
|
||||
blockArrowId4<["Label"]>(down)
|
||||
blockArrowId5<["Label"]>(x)
|
||||
blockArrowId6<["Label"]>(y)
|
||||
blockArrowId6<["Label"]>(x, down)
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block:e:4
|
||||
columns 2
|
||||
f
|
||||
g
|
||||
end
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
block:e:4
|
||||
columns 2
|
||||
f
|
||||
g
|
||||
h
|
||||
end
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
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="mermaid">
|
||||
block-beta
|
||||
columns 3
|
||||
a b c
|
||||
e:3
|
||||
f g h
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
columns 1
|
||||
db(("DB"))
|
||||
blockArrowId6<[" "]>(down)
|
||||
block:ID
|
||||
A
|
||||
B["A wide one in the middle"]
|
||||
C
|
||||
end
|
||||
space
|
||||
D
|
||||
ID --> D
|
||||
C --> D
|
||||
style B fill:#f9F,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
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">
|
||||
<div class="flex">
|
||||
<pre id="diagram" class="mermaid">
|
||||
---
|
||||
config:
|
||||
look: handDrawn
|
||||
flowchart:
|
||||
htmlLabels: false
|
||||
---
|
||||
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">
|
||||
A[I am a long text, where do I go??? handdrawn - false]
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid">
|
||||
---
|
||||
config:
|
||||
look: handdrawn
|
||||
flowchart:
|
||||
htmlLabels: true
|
||||
---
|
||||
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">
|
||||
A[I am a long text, where do I go??? handdrawn - true]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<pre id="diagram" class="mermaid">
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
htmlLabels: false
|
||||
---
|
||||
flowchart
|
||||
A[I am a long text, where do I go??? classic - false]
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid">
|
||||
---
|
||||
config:
|
||||
flowchart:
|
||||
htmlLabels: true
|
||||
---
|
||||
flowchart
|
||||
A[I am a long text, where do I go??? classic - true]
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
<pre id="diagram2" 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="mermaid2">
|
||||
classDiagram-v2
|
||||
note "I love this diagram!\nDo you love it?"
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
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: 🤓
|
||||
शान्तिः سلام 和平 `"]
|
||||
id1(Start)-->id2(Stop)
|
||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
||||
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd["`**AMD** Latte GPU`"]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
%%{init: {"flowchart": {"defaultRenderer": "elk", "htmlLabels": false}} }%%
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd["`**AMD** Latte GPU`"]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
|
||||
<br />
|
||||
<pre id="diagram" class="mermaid2">
|
||||
flowchart TB
|
||||
%% I could not figure out how to use double quotes in labels in Mermaid
|
||||
subgraph ibm[IBM Espresso CPU]
|
||||
core0[IBM PowerPC Broadway Core 0]
|
||||
core1[IBM PowerPC Broadway Core 1]
|
||||
core2[IBM PowerPC Broadway Core 2]
|
||||
|
||||
rom[16 KB ROM]
|
||||
|
||||
core0 --- core2
|
||||
|
||||
rom --> core2
|
||||
end
|
||||
|
||||
subgraph amd[AMD Latte GPU]
|
||||
mem[Memory & I/O Bridge]
|
||||
dram[DRAM Controller]
|
||||
edram[32 MB EDRAM MEM1]
|
||||
rom[512 B SEEPROM]
|
||||
|
||||
sata[SATA IF]
|
||||
exi[EXI]
|
||||
|
||||
subgraph gx[GX]
|
||||
sram[3 MB 1T-SRAM]
|
||||
end
|
||||
|
||||
radeon[AMD Radeon R7xx GX2]
|
||||
|
||||
mem --- gx
|
||||
mem --- radeon
|
||||
|
||||
rom --- mem
|
||||
|
||||
mem --- sata
|
||||
mem --- exi
|
||||
|
||||
dram --- sata
|
||||
dram --- exi
|
||||
end
|
||||
|
||||
ddr3[2 GB DDR3 RAM MEM2]
|
||||
|
||||
mem --- ddr3
|
||||
dram --- ddr3
|
||||
edram --- ddr3
|
||||
|
||||
core1 --- mem
|
||||
|
||||
exi --- rtc
|
||||
rtc{{rtc}}
|
||||
</pre
|
||||
>
|
||||
<br />
|
||||
|
||||
<pre id="diagram" class="mermaid2">
|
||||
<pre id="diagram3" 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
|
||||
A:::foo & B:::bar --> C:::foobar
|
||||
classDef foo stroke:#f00
|
||||
classDef bar stroke:#0f0
|
||||
classDef ash color:red
|
||||
class C ash
|
||||
style C stroke:#00f, fill:black
|
||||
|
||||
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> -->
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
stateDiagram
|
||||
A:::foo
|
||||
B:::bar --> C:::foobar
|
||||
classDef foo stroke:#f00
|
||||
classDef bar stroke:#0f0
|
||||
style C stroke:#00f, fill:black, color:white
|
||||
|
||||
</pre>
|
||||
|
||||
<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';
|
||||
// await mermaid.registerExternalDiagrams([example]);
|
||||
// import { layouts } from './mermaid-layout-elk.esm.mjs';
|
||||
// mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.parseError = function (err, hash) {
|
||||
// console.error('Mermaid error: ', err);
|
||||
console.error('Mermaid error: ', err);
|
||||
};
|
||||
window.callback = function () {
|
||||
alert('A callback was triggered');
|
||||
};
|
||||
// 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({
|
||||
theme: 'dark',
|
||||
startOnLoad: true,
|
||||
// theme: 'base',
|
||||
// handDrawnSeed: 12,
|
||||
// look: 'handDrawn',
|
||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||
// layout: 'dagre',
|
||||
// layout: 'elk',
|
||||
// layout: 'fixed',
|
||||
// htmlLabels: false,
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
// fontFamily: 'Caveat',
|
||||
// fontFamily: 'Kalam',
|
||||
// fontFamily: 'courier',
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
messageFontFamily: 'courier',
|
||||
},
|
||||
fontSize: 12,
|
||||
logLevel: 0,
|
||||
securityLevel: 'loose',
|
||||
});
|
||||
function callback() {
|
||||
alert('It worked');
|
||||
@ -605,10 +196,6 @@ mindmap
|
||||
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));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
1271
cypress/platform/state-refactor.html
Normal file
1271
cypress/platform/state-refactor.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
import flowchartELK from './mermaid-flowchart-elk.esm.mjs';
|
||||
import { layouts } from './mermaid-layout-elk.esm.mjs';
|
||||
import externalExample from './mermaid-example-diagram.esm.mjs';
|
||||
import zenUml from './mermaid-zenuml.esm.mjs';
|
||||
|
||||
@ -46,7 +46,9 @@ const contentLoaded = async function () {
|
||||
document.getElementsByTagName('body')[0].appendChild(div);
|
||||
}
|
||||
|
||||
await mermaid.registerExternalDiagrams([externalExample, zenUml, flowchartELK]);
|
||||
await mermaid.registerExternalDiagrams([externalExample, zenUml]);
|
||||
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.initialize(graphObj.mermaid);
|
||||
await mermaid.run();
|
||||
}
|
||||
|
43
docs/config/setup/interfaces/mermaid.LayoutData.md
Normal file
43
docs/config/setup/interfaces/mermaid.LayoutData.md
Normal file
@ -0,0 +1,43 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/interfaces/mermaid.LayoutData.md](../../../../packages/mermaid/src/docs/config/setup/interfaces/mermaid.LayoutData.md).
|
||||
|
||||
# Interface: LayoutData
|
||||
|
||||
[mermaid](../modules/mermaid.md).LayoutData
|
||||
|
||||
## Indexable
|
||||
|
||||
▪ \[key: `string`]: `any`
|
||||
|
||||
## Properties
|
||||
|
||||
### config
|
||||
|
||||
• **config**: `MermaidConfig`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/types.d.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.d.ts#L118)
|
||||
|
||||
---
|
||||
|
||||
### edges
|
||||
|
||||
• **edges**: `Edge`\[]
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/types.d.ts:117](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.d.ts#L117)
|
||||
|
||||
---
|
||||
|
||||
### nodes
|
||||
|
||||
• **nodes**: `Node`\[]
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/types.d.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.d.ts#L116)
|
@ -0,0 +1,39 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md](../../../../packages/mermaid/src/docs/config/setup/interfaces/mermaid.LayoutLoaderDefinition.md).
|
||||
|
||||
# Interface: LayoutLoaderDefinition
|
||||
|
||||
[mermaid](../modules/mermaid.md).LayoutLoaderDefinition
|
||||
|
||||
## Properties
|
||||
|
||||
### algorithm
|
||||
|
||||
• `Optional` **algorithm**: `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/render.ts:9](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L9)
|
||||
|
||||
---
|
||||
|
||||
### loader
|
||||
|
||||
• **loader**: `LayoutLoader`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8)
|
||||
|
||||
---
|
||||
|
||||
### name
|
||||
|
||||
• **name**: `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7)
|
@ -28,7 +28,7 @@ page.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:425](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L425)
|
||||
[packages/mermaid/src/mermaid.ts:432](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L432)
|
||||
|
||||
---
|
||||
|
||||
@ -59,7 +59,7 @@ A graph definition key
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:427](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L427)
|
||||
[packages/mermaid/src/mermaid.ts:434](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L434)
|
||||
|
||||
---
|
||||
|
||||
@ -89,7 +89,7 @@ Use [initialize](mermaid.Mermaid.md#initialize) and [run](mermaid.Mermaid.md#run
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:421](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L421)
|
||||
[packages/mermaid/src/mermaid.ts:427](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L427)
|
||||
|
||||
---
|
||||
|
||||
@ -116,7 +116,50 @@ This function should be called before the run function.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:424](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L424)
|
||||
[packages/mermaid/src/mermaid.ts:431](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L431)
|
||||
|
||||
---
|
||||
|
||||
### internalHelpers
|
||||
|
||||
• **internalHelpers**: `Object`
|
||||
|
||||
Internal helpers for mermaid
|
||||
|
||||
**`Deprecated`**
|
||||
|
||||
- This should not be used by external packages, as the definitions will change without notice.
|
||||
|
||||
#### Type declaration
|
||||
|
||||
| Name | Type |
|
||||
| :--------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `common` | { `evaluate`: (`val?`: `string` \| `boolean`) => `boolean` ; `getMax`: (...`values`: `number`\[]) => `number` ; `getMin`: (...`values`: `number`\[]) => `number` ; `getRows`: (`s?`: `string`) => `string`\[] ; `getUrl`: (`useAbsolute`: `boolean`) => `string` ; `hasBreaks`: (`text`: `string`) => `boolean` ; `lineBreakRegex`: `RegExp` ; `removeScript`: (`txt`: `string`) => `string` ; `sanitizeText`: (`text`: `string`, `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` ; `sanitizeTextOrArray`: (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] ; `splitBreaks`: (`text`: `string`) => `string`\[] } |
|
||||
| `common.evaluate` | (`val?`: `string` \| `boolean`) => `boolean` |
|
||||
| `common.getMax` | (...`values`: `number`\[]) => `number` |
|
||||
| `common.getMin` | (...`values`: `number`\[]) => `number` |
|
||||
| `common.getRows` | (`s?`: `string`) => `string`\[] |
|
||||
| `common.getUrl` | (`useAbsolute`: `boolean`) => `string` |
|
||||
| `common.hasBreaks` | (`text`: `string`) => `boolean` |
|
||||
| `common.lineBreakRegex` | `RegExp` |
|
||||
| `common.removeScript` | (`txt`: `string`) => `string` |
|
||||
| `common.sanitizeText` | (`text`: `string`, `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` |
|
||||
| `common.sanitizeTextOrArray` | (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] |
|
||||
| `common.splitBreaks` | (`text`: `string`) => `string`\[] |
|
||||
| `getConfig` | () => [`MermaidConfig`](mermaid.MermaidConfig.md) |
|
||||
| `insertCluster` | (`elem`: `any`, `node`: `any`) => `any` |
|
||||
| `insertEdge` | (`elem`: `any`, `edge`: `any`, `clusterDb`: `any`, `diagramType`: `any`, `startNode`: `any`, `endNode`: `any`, `id`: `any`) => { `originalPath`: `any` ; `updatedPath`: `any` } |
|
||||
| `insertEdgeLabel` | (`elem`: `any`, `edge`: `any`) => `Promise`<`any`> |
|
||||
| `insertMarkers` | (`elem`: `any`, `markerArray`: `any`, `type`: `any`, `id`: `any`) => `void` |
|
||||
| `insertNode` | (`elem`: `any`, `node`: `any`, `dir`: `any`) => `Promise`<`any`> |
|
||||
| `interpolateToCurve` | (`interpolate`: `undefined` \| `string`, `defaultCurve`: `CurveFactory`) => `CurveFactory` |
|
||||
| `labelHelper` | (`parent`: `any`, `node`: `any`, `_classes`: `any`) => `Promise`<{ `bbox`: `any` ; `halfPadding`: `number` ; `label`: `any` = labelEl; `shapeSvg`: `any` }> |
|
||||
| `log` | `Record`<`LogLevel`, (...`data`: `any`\[]) => `void`(`message?`: `any`, ...`optionalParams`: `any`\[]) => `void`> |
|
||||
| `positionEdgeLabel` | (`edge`: `any`, `paths`: `any`) => `void` |
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:439](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L439)
|
||||
|
||||
---
|
||||
|
||||
@ -130,7 +173,7 @@ Use [parse](mermaid.Mermaid.md#parse) and [render](mermaid.Mermaid.md#render) in
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:415](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L415)
|
||||
[packages/mermaid/src/mermaid.ts:421](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L421)
|
||||
|
||||
---
|
||||
|
||||
@ -180,7 +223,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:416](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L416)
|
||||
[packages/mermaid/src/mermaid.ts:422](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L422)
|
||||
|
||||
---
|
||||
|
||||
@ -190,7 +233,7 @@ Error if the diagram is invalid and parseOptions.suppressErrors is false or not
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:410](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L410)
|
||||
[packages/mermaid/src/mermaid.ts:416](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L416)
|
||||
|
||||
---
|
||||
|
||||
@ -218,7 +261,31 @@ Used to register external diagram types.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:423](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L423)
|
||||
[packages/mermaid/src/mermaid.ts:430](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L430)
|
||||
|
||||
---
|
||||
|
||||
### registerLayoutLoaders
|
||||
|
||||
• **registerLayoutLoaders**: (`loaders`: [`LayoutLoaderDefinition`](mermaid.LayoutLoaderDefinition.md)\[]) => `void`
|
||||
|
||||
#### Type declaration
|
||||
|
||||
▸ (`loaders`): `void`
|
||||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :-------- | :--------------------------------------------------------------- |
|
||||
| `loaders` | [`LayoutLoaderDefinition`](mermaid.LayoutLoaderDefinition.md)\[] |
|
||||
|
||||
##### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:429](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L429)
|
||||
|
||||
---
|
||||
|
||||
@ -244,7 +311,7 @@ Used to register external diagram types.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:417](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L417)
|
||||
[packages/mermaid/src/mermaid.ts:423](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L423)
|
||||
|
||||
---
|
||||
|
||||
@ -292,7 +359,7 @@ Renders the mermaid diagrams
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:422](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L422)
|
||||
[packages/mermaid/src/mermaid.ts:428](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L428)
|
||||
|
||||
---
|
||||
|
||||
@ -327,7 +394,7 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:426](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L426)
|
||||
[packages/mermaid/src/mermaid.ts:433](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L433)
|
||||
|
||||
---
|
||||
|
||||
@ -337,4 +404,4 @@ to it (eg. dart interop wrapper). (Initially there is no parseError member of me
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:409](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L409)
|
||||
[packages/mermaid/src/mermaid.ts:415](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L415)
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L85)
|
||||
[packages/mermaid/src/config.type.ts:112](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L112)
|
||||
|
||||
---
|
||||
|
||||
@ -29,7 +29,7 @@ This matters if you are using base tag settings.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L104)
|
||||
[packages/mermaid/src/config.type.ts:131](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L131)
|
||||
|
||||
---
|
||||
|
||||
@ -39,7 +39,7 @@ This matters if you are using base tag settings.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:162](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L162)
|
||||
[packages/mermaid/src/config.type.ts:189](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L189)
|
||||
|
||||
---
|
||||
|
||||
@ -49,7 +49,7 @@ This matters if you are using base tag settings.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:159](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L159)
|
||||
[packages/mermaid/src/config.type.ts:186](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L186)
|
||||
|
||||
---
|
||||
|
||||
@ -59,7 +59,7 @@ This matters if you are using base tag settings.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:150](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L150)
|
||||
[packages/mermaid/src/config.type.ts:177](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L177)
|
||||
|
||||
---
|
||||
|
||||
@ -69,7 +69,7 @@ This matters if you are using base tag settings.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L76)
|
||||
[packages/mermaid/src/config.type.ts:103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L103)
|
||||
|
||||
---
|
||||
|
||||
@ -83,7 +83,7 @@ You can set this attribute to base the seed on a static string.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:144](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L144)
|
||||
[packages/mermaid/src/config.type.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L171)
|
||||
|
||||
---
|
||||
|
||||
@ -101,7 +101,7 @@ should not change unless content is changed.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:137](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L137)
|
||||
[packages/mermaid/src/config.type.ts:164](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L164)
|
||||
|
||||
---
|
||||
|
||||
@ -111,7 +111,24 @@ should not change unless content is changed.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:163](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L163)
|
||||
[packages/mermaid/src/config.type.ts:190](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L190)
|
||||
|
||||
---
|
||||
|
||||
### elk
|
||||
|
||||
• `Optional` **elk**: `Object`
|
||||
|
||||
#### Type declaration
|
||||
|
||||
| Name | Type | Description |
|
||||
| :----------------------- | :---------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mergeEdges?` | `boolean` | Elk specific option that allows edges to share path where it convenient. It can make for pretty diagrams but can also make it harder to read the diagram. |
|
||||
| `nodePlacementStrategy?` | `"SIMPLE"` \| `"NETWORK_SIMPLEX"` \| `"LINEAR_SEGMENTS"` \| `"BRANDES_KOEPF"` | Elk specific option affecting how nodes are placed. |
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:91](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L91)
|
||||
|
||||
---
|
||||
|
||||
@ -121,7 +138,7 @@ should not change unless content is changed.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:152](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L152)
|
||||
[packages/mermaid/src/config.type.ts:179](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L179)
|
||||
|
||||
---
|
||||
|
||||
@ -131,7 +148,7 @@ should not change unless content is changed.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L145)
|
||||
[packages/mermaid/src/config.type.ts:172](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L172)
|
||||
|
||||
---
|
||||
|
||||
@ -145,7 +162,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L84)
|
||||
[packages/mermaid/src/config.type.ts:111](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L111)
|
||||
|
||||
---
|
||||
|
||||
@ -155,7 +172,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:165](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L165)
|
||||
[packages/mermaid/src/config.type.ts:192](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L192)
|
||||
|
||||
---
|
||||
|
||||
@ -169,7 +186,7 @@ If set to true, ignores legacyMathML.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L126)
|
||||
[packages/mermaid/src/config.type.ts:153](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L153)
|
||||
|
||||
---
|
||||
|
||||
@ -179,7 +196,7 @@ If set to true, ignores legacyMathML.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L147)
|
||||
[packages/mermaid/src/config.type.ts:174](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L174)
|
||||
|
||||
---
|
||||
|
||||
@ -189,7 +206,19 @@ If set to true, ignores legacyMathML.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:158](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L158)
|
||||
[packages/mermaid/src/config.type.ts:185](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L185)
|
||||
|
||||
---
|
||||
|
||||
### handDrawnSeed
|
||||
|
||||
• `Optional` **handDrawnSeed**: `number`
|
||||
|
||||
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.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L76)
|
||||
|
||||
---
|
||||
|
||||
@ -199,7 +228,7 @@ If set to true, ignores legacyMathML.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:77](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L77)
|
||||
[packages/mermaid/src/config.type.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L104)
|
||||
|
||||
---
|
||||
|
||||
@ -209,7 +238,19 @@ If set to true, ignores legacyMathML.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L148)
|
||||
[packages/mermaid/src/config.type.ts:175](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L175)
|
||||
|
||||
---
|
||||
|
||||
### layout
|
||||
|
||||
• `Optional` **layout**: `string`
|
||||
|
||||
Defines which layout algorithm to use for rendering the diagram.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L81)
|
||||
|
||||
---
|
||||
|
||||
@ -224,7 +265,7 @@ fall back to legacy rendering for KaTeX.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:119](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L119)
|
||||
[packages/mermaid/src/config.type.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L146)
|
||||
|
||||
---
|
||||
|
||||
@ -236,7 +277,19 @@ This option decides the amount of logging to be used by mermaid.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:90](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L90)
|
||||
[packages/mermaid/src/config.type.ts:117](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L117)
|
||||
|
||||
---
|
||||
|
||||
### look
|
||||
|
||||
• `Optional` **look**: `"classic"` | `"handDrawn"`
|
||||
|
||||
Defines which main look to use for the diagram.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L71)
|
||||
|
||||
---
|
||||
|
||||
@ -246,7 +299,7 @@ This option decides the amount of logging to be used by mermaid.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:166](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L166)
|
||||
[packages/mermaid/src/config.type.ts:193](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L193)
|
||||
|
||||
---
|
||||
|
||||
@ -258,7 +311,7 @@ Defines the maximum number of edges that can be drawn in a graph.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:75](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L75)
|
||||
[packages/mermaid/src/config.type.ts:90](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L90)
|
||||
|
||||
---
|
||||
|
||||
@ -270,7 +323,7 @@ The maximum allowed size of the users text diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:70](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L70)
|
||||
[packages/mermaid/src/config.type.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L85)
|
||||
|
||||
---
|
||||
|
||||
@ -280,7 +333,7 @@ The maximum allowed size of the users text diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:157](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L157)
|
||||
[packages/mermaid/src/config.type.ts:184](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L184)
|
||||
|
||||
---
|
||||
|
||||
@ -290,7 +343,7 @@ The maximum allowed size of the users text diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:161](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L161)
|
||||
[packages/mermaid/src/config.type.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L188)
|
||||
|
||||
---
|
||||
|
||||
@ -300,7 +353,7 @@ The maximum allowed size of the users text diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:153](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L153)
|
||||
[packages/mermaid/src/config.type.ts:180](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L180)
|
||||
|
||||
---
|
||||
|
||||
@ -310,7 +363,7 @@ The maximum allowed size of the users text diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:154](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L154)
|
||||
[packages/mermaid/src/config.type.ts:181](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L181)
|
||||
|
||||
---
|
||||
|
||||
@ -320,7 +373,7 @@ The maximum allowed size of the users text diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:156](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L156)
|
||||
[packages/mermaid/src/config.type.ts:183](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L183)
|
||||
|
||||
---
|
||||
|
||||
@ -330,7 +383,7 @@ The maximum allowed size of the users text diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:160](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L160)
|
||||
[packages/mermaid/src/config.type.ts:187](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L187)
|
||||
|
||||
---
|
||||
|
||||
@ -344,7 +397,7 @@ This prevents malicious graph directives from overriding a site's default securi
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:111](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L111)
|
||||
[packages/mermaid/src/config.type.ts:138](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L138)
|
||||
|
||||
---
|
||||
|
||||
@ -356,7 +409,7 @@ Level of trust for parsed diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L94)
|
||||
[packages/mermaid/src/config.type.ts:121](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L121)
|
||||
|
||||
---
|
||||
|
||||
@ -366,7 +419,7 @@ Level of trust for parsed diagram
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L146)
|
||||
[packages/mermaid/src/config.type.ts:173](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L173)
|
||||
|
||||
---
|
||||
|
||||
@ -378,7 +431,7 @@ Dictates whether mermaid starts on Page load
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L98)
|
||||
[packages/mermaid/src/config.type.ts:125](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L125)
|
||||
|
||||
---
|
||||
|
||||
@ -388,7 +441,7 @@ Dictates whether mermaid starts on Page load
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:151](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L151)
|
||||
[packages/mermaid/src/config.type.ts:178](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L178)
|
||||
|
||||
---
|
||||
|
||||
@ -401,13 +454,13 @@ This is useful when you want to control how to handle syntax errors in your appl
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:172](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L172)
|
||||
[packages/mermaid/src/config.type.ts:199](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L199)
|
||||
|
||||
---
|
||||
|
||||
### theme
|
||||
|
||||
• `Optional` **theme**: `"default"` | `"forest"` | `"dark"` | `"neutral"` | `"null"`
|
||||
• `Optional` **theme**: `"default"` | `"base"` | `"dark"` | `"forest"` | `"neutral"` | `"null"`
|
||||
|
||||
Theme, the CSS style sheet.
|
||||
You may also use `themeCSS` to override this value.
|
||||
@ -444,7 +497,7 @@ You may also use `themeCSS` to override this value.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:149](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L149)
|
||||
[packages/mermaid/src/config.type.ts:176](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L176)
|
||||
|
||||
---
|
||||
|
||||
@ -454,7 +507,7 @@ You may also use `themeCSS` to override this value.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:164](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L164)
|
||||
[packages/mermaid/src/config.type.ts:191](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L191)
|
||||
|
||||
---
|
||||
|
||||
@ -464,4 +517,4 @@ You may also use `themeCSS` to override this value.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.type.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L155)
|
||||
[packages/mermaid/src/config.type.ts:182](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L182)
|
||||
|
@ -18,7 +18,7 @@ The nodes to render. If this is set, `querySelector` will be ignored.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:39](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L39)
|
||||
[packages/mermaid/src/mermaid.ts:45](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L45)
|
||||
|
||||
---
|
||||
|
||||
@ -44,7 +44,7 @@ A callback to call after each diagram is rendered.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:43](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L43)
|
||||
[packages/mermaid/src/mermaid.ts:49](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L49)
|
||||
|
||||
---
|
||||
|
||||
@ -56,7 +56,7 @@ The query selector to use when finding elements to render. Default: `".mermaid"`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:35](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L35)
|
||||
[packages/mermaid/src/mermaid.ts:41](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L41)
|
||||
|
||||
---
|
||||
|
||||
@ -68,4 +68,4 @@ If `true`, errors will be logged to the console, but not thrown. Default: `false
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:47](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L47)
|
||||
[packages/mermaid/src/mermaid.ts:53](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L53)
|
||||
|
@ -118,7 +118,7 @@ The siteConfig
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/config.ts:218](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L218)
|
||||
[packages/mermaid/src/config.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L221)
|
||||
|
||||
---
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/defaultConfig.ts:275](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L275)
|
||||
[packages/mermaid/src/defaultConfig.ts:279](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L279)
|
||||
|
||||
---
|
||||
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
- [DetailedError](../interfaces/mermaid.DetailedError.md)
|
||||
- [ExternalDiagramDefinition](../interfaces/mermaid.ExternalDiagramDefinition.md)
|
||||
- [LayoutData](../interfaces/mermaid.LayoutData.md)
|
||||
- [LayoutLoaderDefinition](../interfaces/mermaid.LayoutLoaderDefinition.md)
|
||||
- [Mermaid](../interfaces/mermaid.Mermaid.md)
|
||||
- [MermaidConfig](../interfaces/mermaid.MermaidConfig.md)
|
||||
- [ParseOptions](../interfaces/mermaid.ParseOptions.md)
|
||||
@ -54,4 +56,4 @@
|
||||
|
||||
#### Defined in
|
||||
|
||||
[packages/mermaid/src/mermaid.ts:430](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L430)
|
||||
[packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
|
||||
|
@ -109,6 +109,7 @@ export default tseslint.config(
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/only-throw-error': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
|
||||
// END
|
||||
'json/*': ['error', 'allowComments'],
|
||||
|
@ -61,7 +61,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-cypress": "^3.42.3",
|
||||
"@applitools/eyes-cypress": "^3.44.4",
|
||||
"@argos-ci/cypress": "^2.1.0",
|
||||
"@cspell/eslint-plugin": "^8.8.4",
|
||||
"@cypress/code-coverage": "^3.12.30",
|
||||
@ -84,7 +84,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"cspell": "^8.6.0",
|
||||
"cypress": "^13.7.1",
|
||||
"cypress": "^13.11.0",
|
||||
"cypress-image-snapshot": "^4.0.1",
|
||||
"esbuild": "^0.21.5",
|
||||
"eslint": "^9.4.0",
|
||||
|
@ -663,7 +663,6 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, i
|
||||
/**
|
||||
* Recursive function that iterates over an array of nodes and inserts the children of each node.
|
||||
* It also recursively populates the inserts the children of the children and so on.
|
||||
* @param {*} graph
|
||||
* @param nodeArray
|
||||
* @param parentLookupDb
|
||||
*/
|
||||
|
43
packages/mermaid-layout-elk/package.json
Normal file
43
packages/mermaid-layout-elk/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@mermaid-js/layout-elk",
|
||||
"version": "0.0.1",
|
||||
"description": "ELK layout engine for mermaid",
|
||||
"module": "dist/mermaid-layout-elk.core.mjs",
|
||||
"types": "dist/packages/mermaid-layout-elk/src/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-layout-elk.core.mjs",
|
||||
"types": "./dist/packages/mermaid-layout-elk/src/index.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"elk",
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"contributors": [
|
||||
"Knut Sveidqvist",
|
||||
"Sidharth Vinod"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"elkjs": "^0.9.3",
|
||||
"d3": "^7.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
32
packages/mermaid-layout-elk/src/find-common-ancestor.ts
Normal file
32
packages/mermaid-layout-elk/src/find-common-ancestor.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export interface TreeData {
|
||||
parentById: Record<string, string>;
|
||||
childrenById: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export const findCommonAncestor = (id1: string, id2: string, { parentById }: TreeData) => {
|
||||
const visited = new Set();
|
||||
let currentId = id1;
|
||||
|
||||
// Edge case with self edges
|
||||
if (id1 === id2) {
|
||||
return parentById[id1] || 'root';
|
||||
}
|
||||
|
||||
while (currentId) {
|
||||
visited.add(currentId);
|
||||
if (currentId === id2) {
|
||||
return currentId;
|
||||
}
|
||||
currentId = parentById[currentId];
|
||||
}
|
||||
|
||||
currentId = id2;
|
||||
while (currentId) {
|
||||
if (visited.has(currentId)) {
|
||||
return currentId;
|
||||
}
|
||||
currentId = parentById[currentId];
|
||||
}
|
||||
|
||||
return 'root';
|
||||
};
|
17
packages/mermaid-layout-elk/src/layouts.ts
Normal file
17
packages/mermaid-layout-elk/src/layouts.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { LayoutLoaderDefinition } from 'mermaid';
|
||||
|
||||
const loader = async () => await import(`./render.js`);
|
||||
const algos = ['elk.stress', 'elk.force', 'elk.mrtree', 'elk.sporeOverlap'];
|
||||
|
||||
export const layouts: LayoutLoaderDefinition[] = [
|
||||
{
|
||||
name: 'elk',
|
||||
loader,
|
||||
algorithm: 'elk.layered',
|
||||
},
|
||||
...algos.map((algo) => ({
|
||||
name: algo,
|
||||
loader,
|
||||
algorithm: algo,
|
||||
})),
|
||||
];
|
997
packages/mermaid-layout-elk/src/render.ts
Normal file
997
packages/mermaid-layout-elk/src/render.ts
Normal file
@ -0,0 +1,997 @@
|
||||
// @ts-nocheck File not ready to check types
|
||||
import { curveLinear } from 'd3';
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
import mermaid, { type LayoutData } from 'mermaid';
|
||||
import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
|
||||
|
||||
const {
|
||||
common,
|
||||
getConfig,
|
||||
insertCluster,
|
||||
insertEdge,
|
||||
insertEdgeLabel,
|
||||
insertMarkers,
|
||||
insertNode,
|
||||
interpolateToCurve,
|
||||
labelHelper,
|
||||
log,
|
||||
positionEdgeLabel,
|
||||
} = mermaid.internalHelpers;
|
||||
// import { insertEdge } from '../../mermaid/src/rendering-util/rendering-elements/edges.js';
|
||||
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 {
|
||||
// A subgraph
|
||||
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;
|
||||
// Give some padding for elk
|
||||
labelData.height = bbox.height - 2;
|
||||
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 APA12', 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 = async (relX, relY, nodeArray, svg, subgraphsEl, depth) => {
|
||||
await Promise.all(
|
||||
nodeArray.map(async 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: Math.max(node.width, node.labels ? node.labels[0]?.width || 0 : 0),
|
||||
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;
|
||||
await insertCluster(subgraphEl, clusterNode);
|
||||
|
||||
log.debug('Id (UIO)= ', node.id, node.width, 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})`
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
nodeArray.map(async function (node) {
|
||||
if (node?.isGroup) {
|
||||
await 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 = (nodeArr): TreeData => {
|
||||
const parentLookupDb: TreeData = { parentById: {}, childrenById: {} };
|
||||
const subgraphs = nodeArr.filter((node) => node.isGroup);
|
||||
log.info('Subgraphs - ', subgraphs);
|
||||
subgraphs.forEach((subgraph) => {
|
||||
const children = nodeArr.filter((node) => node.parentId === subgraph.id);
|
||||
children.forEach((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: string, dest: string, parentLookupDb: TreeData) {
|
||||
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 = async 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;
|
||||
|
||||
await Promise.all(
|
||||
edges.map(async 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;
|
||||
}
|
||||
|
||||
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 = await 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: LayoutData, 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.nodePlacementStrategy,
|
||||
'elk.layered.mergeEdges': data4Layout.config.elk.mergeEdges,
|
||||
'elk.direction': 'DOWN',
|
||||
'spacing.baseValue': 30,
|
||||
// 'spacing.nodeNode': 40,
|
||||
// 'spacing.nodeNodeBetweenLayers': 45,
|
||||
// 'spacing.edgeNode': 40,
|
||||
// 'spacing.edgeNodeBetweenLayers': 30,
|
||||
// 'spacing.edgeEdge': 30,
|
||||
// 'spacing.edgeEdgeBetweenLayers': 40,
|
||||
// 'spacing.nodeSelfLoop': 50,
|
||||
},
|
||||
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 edgePaths');
|
||||
|
||||
// Add the edges to the elk graph, this will entail creating the actual edges
|
||||
elkGraph = await 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) {
|
||||
node.labels = [
|
||||
{
|
||||
text: node.label,
|
||||
width: node?.labelData?.width || 50,
|
||||
height: node?.labelData?.height || 50,
|
||||
},
|
||||
(node.width = node.width + 2 * node.padding),
|
||||
log.debug('UIO node label', node?.labelData?.width, node.padding),
|
||||
];
|
||||
node.layoutOptions = {
|
||||
'spacing.baseValue': 30,
|
||||
'nodeLabels.placement': '[H_CENTER V_TOP, INSIDE]',
|
||||
};
|
||||
if (node.dir) {
|
||||
node.layoutOptions = {
|
||||
...node.layoutOptions,
|
||||
'elk.algorithm': algorithm,
|
||||
'elk.direction': dir2ElkDirection(node.dir),
|
||||
'nodePlacement.strategy': data4Layout.config['elk.nodePlacement.strategy'],
|
||||
'elk.layered.mergeEdges': data4Layout.config['elk.mergeEdges'],
|
||||
'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);
|
||||
}
|
||||
});
|
||||
|
||||
const g = await elk.layout(elkGraph);
|
||||
|
||||
// debugger;
|
||||
await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
|
||||
g.edges?.map((edge) => {
|
||||
// (elem, edge, clusterDb, diagramType, graph, id)
|
||||
const startNode = nodeDb[edge.sources[0]];
|
||||
const startCluster = parentLookupDb[edge.sources[0]];
|
||||
const endNode = nodeDb[edge.targets[0]];
|
||||
const sourceId = edge.start;
|
||||
const targetId = edge.end;
|
||||
|
||||
const offset = calcOffset(sourceId, targetId, parentLookupDb);
|
||||
log.debug(
|
||||
'offset',
|
||||
offset,
|
||||
sourceId,
|
||||
' ==> ',
|
||||
targetId,
|
||||
'edge:',
|
||||
edge,
|
||||
'cluster:',
|
||||
startCluster,
|
||||
startNode
|
||||
);
|
||||
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 },
|
||||
];
|
||||
|
||||
let sw = startNode.width;
|
||||
let ew = endNode.width;
|
||||
if (startNode.isGroup) {
|
||||
const bbox = startNode.domId.node().getBBox();
|
||||
// sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width);
|
||||
sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding);
|
||||
// sw = startNode.width;
|
||||
log.debug(
|
||||
'UIO width',
|
||||
startNode.id,
|
||||
startNode.with,
|
||||
'bbox.width=',
|
||||
bbox.width,
|
||||
'lw=',
|
||||
startNode.labels[0].width,
|
||||
'node:',
|
||||
startNode.width,
|
||||
'SW = ',
|
||||
sw
|
||||
// 'HTML:',
|
||||
// startNode.domId.node().innerHTML
|
||||
);
|
||||
}
|
||||
if (endNode.isGroup) {
|
||||
const bbox = endNode.domId.node().getBBox();
|
||||
ew = Math.max(endNode.width, endNode.labels[0].width + endNode.padding);
|
||||
|
||||
log.debug(
|
||||
'UIO width',
|
||||
startNode.id,
|
||||
startNode.with,
|
||||
bbox.width,
|
||||
'EW = ',
|
||||
ew,
|
||||
'HTML:',
|
||||
startNode.innerHTML
|
||||
);
|
||||
}
|
||||
if (startNode.shape === 'diamond') {
|
||||
edge.points.unshift({
|
||||
x: startNode.x + startNode.width / 2 + offset.x,
|
||||
y: startNode.y + startNode.height / 2 + offset.y,
|
||||
});
|
||||
}
|
||||
if (endNode.shape === 'diamond') {
|
||||
edge.points.push({
|
||||
x: endNode.x + endNode.width / 2 + offset.x,
|
||||
y: endNode.y + endNode.height / 2 + offset.y,
|
||||
});
|
||||
}
|
||||
|
||||
edge.points = cutPathAtIntersect(
|
||||
edge.points.reverse(),
|
||||
{
|
||||
x: startNode.x + startNode.width / 2 + offset.x,
|
||||
y: startNode.y + startNode.height / 2 + offset.y,
|
||||
width: sw,
|
||||
height: startNode.height,
|
||||
padding: startNode.padding,
|
||||
},
|
||||
startNode.shape === 'diamond'
|
||||
).reverse();
|
||||
|
||||
edge.points = cutPathAtIntersect(
|
||||
edge.points,
|
||||
{
|
||||
x: endNode.x + ew / 2 + endNode.offset.x,
|
||||
y: endNode.y + endNode.height / 2 + endNode.offset.y,
|
||||
width: ew,
|
||||
height: endNode.height,
|
||||
padding: endNode.padding,
|
||||
},
|
||||
endNode.shape === 'diamond'
|
||||
);
|
||||
|
||||
const paths = insertEdge(
|
||||
edgesEl,
|
||||
edge,
|
||||
clusterDb,
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
log.info('APA12 edge points after insert', JSON.stringify(edge.points));
|
||||
|
||||
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);
|
||||
}
|
||||
// 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,
|
||||
// startNode,
|
||||
// endNode,
|
||||
// data4Layout.diagramId
|
||||
// );
|
||||
// log.info('APA12 edge points after insert', JSON.stringify(edge.points));
|
||||
|
||||
// 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);
|
||||
});
|
||||
};
|
||||
|
||||
function intersectLine(p1, p2, q1, q2) {
|
||||
log.debug('UIO intersectLine', p1, p2, q1, q2);
|
||||
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,
|
||||
// p7 and p473.
|
||||
|
||||
// let a1, a2, b1, b2, c1, c2;
|
||||
// let r1, r2, r3, r4;
|
||||
// let denom, offset, num;
|
||||
// let x, y;
|
||||
|
||||
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +
|
||||
// b1 y + c1 = 0.
|
||||
const a1 = p2.y - p1.y;
|
||||
const b1 = p1.x - p2.x;
|
||||
const c1 = p2.x * p1.y - p1.x * p2.y;
|
||||
|
||||
// Compute r3 and r4.
|
||||
const r3 = a1 * q1.x + b1 * q1.y + c1;
|
||||
const 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
|
||||
const a2 = q2.y - q1.y;
|
||||
const b2 = q1.x - q2.x;
|
||||
const c2 = q2.x * q1.y - q1.x * q2.y;
|
||||
|
||||
// Compute r1 and r2
|
||||
const r1 = a2 * p1.x + b2 * p1.y + c2;
|
||||
const 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.
|
||||
const denom = a1 * b2 - a2 * b1;
|
||||
if (denom === 0) {
|
||||
return /*COLLINEAR*/;
|
||||
}
|
||||
|
||||
const 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.
|
||||
let num = b1 * c2 - b2 * c1;
|
||||
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
num = a2 * c1 - a1 * c2;
|
||||
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
|
||||
|
||||
return { x: x, y: y };
|
||||
}
|
||||
|
||||
function sameSign(r1, r2) {
|
||||
return r1 * r2 > 0;
|
||||
}
|
||||
const diamondIntersection = (bounds, outsidePoint, insidePoint) => {
|
||||
const x1 = bounds.x;
|
||||
const y1 = bounds.y;
|
||||
|
||||
const w = bounds.width; //+ bounds.padding;
|
||||
const h = bounds.height; // + bounds.padding;
|
||||
|
||||
const polyPoints = [
|
||||
{ x: x1, y: y1 - h / 2 },
|
||||
{ x: x1 + w / 2, y: y1 },
|
||||
{ x: x1, y: y1 + h / 2 },
|
||||
{ x: x1 - w / 2, y: y1 },
|
||||
];
|
||||
log.debug(
|
||||
`UIO diamondIntersection calc abc89:
|
||||
outsidePoint: ${JSON.stringify(outsidePoint)}
|
||||
insidePoint : ${JSON.stringify(insidePoint)}
|
||||
node : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`,
|
||||
polyPoints
|
||||
);
|
||||
|
||||
const intersections = [];
|
||||
|
||||
let minX = Number.POSITIVE_INFINITY;
|
||||
let 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);
|
||||
}
|
||||
|
||||
// const left = x1 - w / 2;
|
||||
// const top = y1 + h / 2;
|
||||
|
||||
for (let i = 0; i < polyPoints.length; i++) {
|
||||
const p1 = polyPoints[i];
|
||||
const p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
|
||||
const intersect = intersectLine(
|
||||
bounds,
|
||||
outsidePoint,
|
||||
{ x: p1.x, y: p1.y },
|
||||
{ x: p2.x, y: p2.y }
|
||||
);
|
||||
|
||||
if (intersect) {
|
||||
intersections.push(intersect);
|
||||
}
|
||||
}
|
||||
|
||||
if (!intersections.length) {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
log.debug('UIO intersections', intersections);
|
||||
|
||||
if (intersections.length > 1) {
|
||||
// More intersections, find the one nearest to edge end point
|
||||
intersections.sort(function (p, q) {
|
||||
const pdx = p.x - outsidePoint.x;
|
||||
const pdy = p.y - outsidePoint.y;
|
||||
const distp = Math.sqrt(pdx * pdx + pdy * pdy);
|
||||
|
||||
const qdx = q.x - outsidePoint.x;
|
||||
const qdy = q.y - outsidePoint.y;
|
||||
const distq = Math.sqrt(qdx * qdx + qdy * qdy);
|
||||
|
||||
return distp < distq ? -1 : distp === distq ? 0 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
return intersections[0];
|
||||
};
|
||||
|
||||
export const intersection = (node, outsidePoint, insidePoint) => {
|
||||
log.debug(`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 Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||
|
||||
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
||||
// Intersection is top or bottom of rect.
|
||||
const 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.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line
|
||||
|
||||
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;
|
||||
}
|
||||
const 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.debug(`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 };
|
||||
}
|
||||
};
|
||||
const outsideNode = (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;
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const cutPathAtIntersect = (_points, bounds, isDiamond: boolean) => {
|
||||
log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
|
||||
const points = [];
|
||||
let lastPointOutside = _points[0];
|
||||
let isInside = false;
|
||||
_points.forEach((point) => {
|
||||
// const node = clusterDb[edge.toCluster].node;
|
||||
log.debug(' checking point', point, bounds);
|
||||
|
||||
// check if point is inside the boundary rect
|
||||
if (!outsideNode(bounds, point) && !isInside) {
|
||||
// First point inside the rect found
|
||||
// Calc the intersection coord between the point anf the last point outside the rect
|
||||
let inter;
|
||||
|
||||
if (isDiamond) {
|
||||
const inter2 = diamondIntersection(bounds, lastPointOutside, point);
|
||||
const distance = Math.sqrt(
|
||||
(lastPointOutside.x - inter2.x) ** 2 + (lastPointOutside.y - inter2.y) ** 2
|
||||
);
|
||||
if (distance > 1) {
|
||||
inter = inter2;
|
||||
}
|
||||
}
|
||||
if (!inter) {
|
||||
inter = intersection(bounds, lastPointOutside, point);
|
||||
}
|
||||
|
||||
// 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.debug('abc88 no intersect', inter, points);
|
||||
}
|
||||
// points.push(inter);
|
||||
isInside = true;
|
||||
} else {
|
||||
// Outside
|
||||
log.debug('abc88 outside', point, lastPointOutside, points);
|
||||
lastPointOutside = point;
|
||||
// points.push(point);
|
||||
if (!isInside) {
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
});
|
||||
log.debug('returning points', points);
|
||||
return points;
|
||||
};
|
10
packages/mermaid-layout-elk/tsconfig.json
Normal file
10
packages/mermaid-layout-elk/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
@ -81,6 +81,7 @@
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^13.0.2",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.1",
|
||||
"ts-dedent": "^2.2.0",
|
||||
"uuid": "^9.0.1"
|
||||
@ -133,7 +134,6 @@
|
||||
"dist/",
|
||||
"README.md"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
|
@ -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 (directive.fontFamily && !directive.themeVariables?.fontFamily) {
|
||||
directive.themeVariables = { fontFamily: directive.fontFamily };
|
||||
directive.themeVariables = {
|
||||
...directive.themeVariables,
|
||||
fontFamily: directive.fontFamily,
|
||||
};
|
||||
}
|
||||
|
||||
directives.push(directive);
|
||||
|
@ -61,9 +61,24 @@ export interface MermaidConfig {
|
||||
* You may also use `themeCSS` to override this value.
|
||||
*
|
||||
*/
|
||||
theme?: 'default' | 'forest' | 'dark' | 'neutral' | 'null';
|
||||
theme?: 'default' | 'base' | 'dark' | 'forest' | 'neutral' | 'null';
|
||||
themeVariables?: any;
|
||||
themeCSS?: string;
|
||||
/**
|
||||
* Defines which main look to use for the diagram.
|
||||
*
|
||||
*/
|
||||
look?: 'classic' | 'handDrawn';
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@ -73,6 +88,18 @@ export interface MermaidConfig {
|
||||
*
|
||||
*/
|
||||
maxEdges?: number;
|
||||
elk?: {
|
||||
/**
|
||||
* Elk specific option that allows edges to share path where it convenient. It can make for pretty diagrams but can also make it harder to read the diagram.
|
||||
*
|
||||
*/
|
||||
mergeEdges?: boolean;
|
||||
/**
|
||||
* Elk specific option affecting how nodes are placed.
|
||||
*
|
||||
*/
|
||||
nodePlacementStrategy?: 'SIMPLE' | 'NETWORK_SIMPLEX' | 'LINEAR_SEGMENTS' | 'BRANDES_KOEPF';
|
||||
};
|
||||
darkMode?: boolean;
|
||||
htmlLabels?: boolean;
|
||||
/**
|
||||
@ -697,6 +724,8 @@ export interface StateDiagramConfig extends BaseDiagramConfig {
|
||||
textHeight?: number;
|
||||
titleShift?: number;
|
||||
noteMargin?: number;
|
||||
nodeSpacing?: number;
|
||||
rankSpacing?: number;
|
||||
forkWidth?: number;
|
||||
forkHeight?: number;
|
||||
miniPadding?: number;
|
||||
|
@ -97,25 +97,26 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
|
||||
// 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) {
|
||||
graph.edges().forEach(async 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);
|
||||
await insertEdgeLabel(edgeLabels, edge);
|
||||
});
|
||||
|
||||
graph.edges().forEach(function (e) {
|
||||
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
|
||||
});
|
||||
log.info('Graph before layout:', JSON.stringify(graphlibJson.write(graph)));
|
||||
log.info('#############################################');
|
||||
log.info('### Layout ###');
|
||||
log.info('#############################################');
|
||||
log.info(graph);
|
||||
dagreLayout(graph);
|
||||
log.info('Graph after layout:', graphlibJson.write(graph));
|
||||
log.info('Graph after layout:', JSON.stringify(graphlibJson.write(graph)));
|
||||
// Move the nodes to the correct place
|
||||
let diff = 0;
|
||||
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
|
@ -1,12 +1,12 @@
|
||||
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 intersect from './intersect/index.js';
|
||||
import createLabel from './createLabel.js';
|
||||
import note from './shapes/note.js';
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
import { log } from '../logger.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) => {
|
||||
if (str) {
|
||||
@ -395,6 +395,7 @@ const rect = async (parent, node) => {
|
||||
// add the rect
|
||||
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 totalHeight = bbox.height + node.padding * 2;
|
||||
const totalWidth = node.positioned ? node.width : bbox.width + node.padding;
|
||||
@ -1154,9 +1155,6 @@ export const insertNode = async (elem, node, dir) => {
|
||||
if (node.class) {
|
||||
el.attr('class', 'node default ' + node.class);
|
||||
}
|
||||
// MC Special
|
||||
newEl.attr('data-node', 'true');
|
||||
newEl.attr('data-id', node.id);
|
||||
|
||||
nodeElems[node.id] = newEl;
|
||||
|
||||
|
@ -20,6 +20,10 @@ const config: RequiredDeep<MermaidConfig> = {
|
||||
// Set, even though they're `undefined` so that `configKeys` finds these keys
|
||||
// TODO: Should we replace these with `null` so that they can go in the JSON Schema?
|
||||
deterministicIDSeed: undefined,
|
||||
elk: {
|
||||
mergeEdges: false,
|
||||
nodePlacementStrategy: 'SIMPLE',
|
||||
},
|
||||
themeCSS: undefined,
|
||||
|
||||
// add non-JSON default config values
|
||||
|
@ -8,13 +8,6 @@ import type { BlockDB } from './blockDB.js';
|
||||
import { layout } from './layout.js';
|
||||
import { calculateBlockSizes, insertBlocks, insertEdges } from './renderHelpers.js';
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
*
|
||||
* @param text - The text with the classes
|
||||
* @param diagObj - The diagram object
|
||||
* @returns ClassDef - The styles
|
||||
*/
|
||||
export const getClasses = function (text: any, diagObj: any) {
|
||||
return diagObj.db.getClasses();
|
||||
};
|
||||
@ -45,8 +38,6 @@ export const draw = async function (
|
||||
const markers = ['point', 'circle', 'cross'];
|
||||
|
||||
// Add the marker definitions to the svg as marker tags
|
||||
// insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute);
|
||||
// insertMarkers(svg, markers, diagObj.type, true);
|
||||
insertMarkers(svg, markers, diagObj.type, id);
|
||||
|
||||
const bl = db.getBlocks();
|
||||
@ -59,11 +50,7 @@ export const draw = async function (
|
||||
await insertBlocks(nodes, bl, db);
|
||||
await insertEdges(nodes, edges, blArr, db, id);
|
||||
|
||||
// log.debug('Here', bl);
|
||||
|
||||
// Establish svg dimensions and get width and height
|
||||
//
|
||||
// const bounds2 = nodes.node().getBoundingClientRect();
|
||||
// Why, oh why ????
|
||||
if (bounds) {
|
||||
const bounds2 = bounds;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { select } from 'd3';
|
||||
import utils from '../../utils.js';
|
||||
import utils, { getEdgeId } from '../../utils.js';
|
||||
import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import common from '../common/common.js';
|
||||
import type { Node, Edge } from '../../rendering-util/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
@ -200,10 +201,17 @@ export const updateLink = function (positions: ('default' | number)[], style: st
|
||||
if (pos === 'default') {
|
||||
edges.defaultStyle = style;
|
||||
} else {
|
||||
if (utils.isSubstringInArray('fill', style) === -1) {
|
||||
style.push('fill:none');
|
||||
}
|
||||
// if (utils.isSubstringInArray('fill', style) === -1) {
|
||||
// style.push('fill:none');
|
||||
// }
|
||||
edges[pos].style = style;
|
||||
// if edges[pos].style does have fill not set, set it to none
|
||||
if (
|
||||
(edges[pos]?.style?.length ?? 0) > 0 &&
|
||||
!edges[pos]?.style?.some((s) => s?.startsWith('fill'))
|
||||
) {
|
||||
edges[pos]?.style?.push('fill:none');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -219,7 +227,7 @@ export const addClass = function (ids: string, style: string[]) {
|
||||
if (style !== undefined && style !== null) {
|
||||
style.forEach(function (s) {
|
||||
if (/color/.exec(s)) {
|
||||
const newStyle = s.replace('fill', 'bgFill').replace('color', 'fill');
|
||||
const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
|
||||
classNode.textStyles.push(newStyle);
|
||||
}
|
||||
classNode.styles.push(s);
|
||||
@ -728,14 +736,12 @@ export const destructLink = (_str: string, _startStr: string) => {
|
||||
|
||||
// Todo optimizer this by caching existing nodes
|
||||
const exists = (allSgs: FlowSubGraph[], _id: string) => {
|
||||
let res = false;
|
||||
allSgs.forEach((sg) => {
|
||||
const pos = sg.nodes.indexOf(_id);
|
||||
if (pos >= 0) {
|
||||
res = true;
|
||||
for (const sg of allSgs) {
|
||||
if (sg.nodes.includes(_id)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* Deletes an id from all subgraphs
|
||||
@ -755,11 +761,174 @@ export const lex = {
|
||||
firstGraph,
|
||||
};
|
||||
|
||||
const getTypeFromVertex = (vertex: FlowVertex) => {
|
||||
if (vertex.type === 'square') {
|
||||
return 'squareRect';
|
||||
}
|
||||
if (vertex.type === 'round') {
|
||||
return 'roundedRect';
|
||||
}
|
||||
|
||||
return vertex.type ?? 'squareRect';
|
||||
};
|
||||
|
||||
const findNode = (nodes: Node[], id: string) => nodes.find((node) => node.id === id);
|
||||
const destructEdgeType = (type: string | undefined) => {
|
||||
let arrowTypeStart = 'none';
|
||||
let arrowTypeEnd = 'arrow_point';
|
||||
switch (type) {
|
||||
case 'arrow_point':
|
||||
case 'arrow_circle':
|
||||
case 'arrow_cross':
|
||||
arrowTypeEnd = type;
|
||||
break;
|
||||
|
||||
case 'double_arrow_point':
|
||||
case 'double_arrow_circle':
|
||||
case 'double_arrow_cross':
|
||||
arrowTypeStart = type.replace('double_', '');
|
||||
arrowTypeEnd = arrowTypeStart;
|
||||
break;
|
||||
}
|
||||
return { arrowTypeStart, arrowTypeEnd };
|
||||
};
|
||||
|
||||
const addNodeFromVertex = (
|
||||
vertex: FlowVertex,
|
||||
nodes: Node[],
|
||||
parentDB: Map<string, string>,
|
||||
subGraphDB: Map<string, boolean>,
|
||||
config: any,
|
||||
look: string
|
||||
) => {
|
||||
const parentId = parentDB.get(vertex.id);
|
||||
const isGroup = subGraphDB.get(vertex.id) ?? false;
|
||||
|
||||
const node = findNode(nodes, vertex.id);
|
||||
if (node) {
|
||||
node.cssStyles = vertex.styles;
|
||||
node.cssCompiledStyles = getCompiledStyles(vertex.classes);
|
||||
node.cssClasses = vertex.classes.join(' ');
|
||||
} else {
|
||||
nodes.push({
|
||||
id: vertex.id,
|
||||
label: vertex.text,
|
||||
labelStyle: '',
|
||||
parentId,
|
||||
padding: config.flowchart?.padding || 8,
|
||||
cssStyles: vertex.styles,
|
||||
cssCompiledStyles: getCompiledStyles(['default', 'node', ...vertex.classes]),
|
||||
cssClasses: 'default ' + vertex.classes.join(' '),
|
||||
shape: getTypeFromVertex(vertex),
|
||||
dir: vertex.dir,
|
||||
domId: vertex.domId,
|
||||
isGroup,
|
||||
look,
|
||||
link: vertex.link,
|
||||
linkTarget: vertex.linkTarget,
|
||||
tooltip: getTooltip(vertex.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getCompiledStyles(classDefs: string[]) {
|
||||
let compiledStyles: string[] = [];
|
||||
for (const customClass of classDefs) {
|
||||
const cssClass = classes.get(customClass);
|
||||
if (cssClass?.styles) {
|
||||
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
|
||||
}
|
||||
if (cssClass?.textStyles) {
|
||||
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim());
|
||||
}
|
||||
}
|
||||
return compiledStyles;
|
||||
}
|
||||
|
||||
export const getData = () => {
|
||||
const config = getConfig();
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
const subGraphs = getSubGraphs();
|
||||
const parentDB = new Map<string, string>();
|
||||
const subGraphDB = new Map<string, boolean>();
|
||||
|
||||
// Setup the subgraph data for adding nodes
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
const subGraph = subGraphs[i];
|
||||
if (subGraph.nodes.length > 0) {
|
||||
subGraphDB.set(subGraph.id, true);
|
||||
}
|
||||
for (const id of subGraph.nodes) {
|
||||
parentDB.set(id, subGraph.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Data is setup, add the nodes
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
const subGraph = subGraphs[i];
|
||||
nodes.push({
|
||||
id: subGraph.id,
|
||||
label: subGraph.title,
|
||||
labelStyle: '',
|
||||
parentId: parentDB.get(subGraph.id),
|
||||
padding: 8,
|
||||
cssCompiledStyles: getCompiledStyles(subGraph.classes),
|
||||
cssClasses: subGraph.classes.join(' '),
|
||||
shape: 'rect',
|
||||
dir: subGraph.dir,
|
||||
isGroup: true,
|
||||
look: config.look,
|
||||
});
|
||||
}
|
||||
|
||||
const n = getVertices();
|
||||
n.forEach((vertex) => {
|
||||
addNodeFromVertex(vertex, nodes, parentDB, subGraphDB, config, config.look || 'classic');
|
||||
});
|
||||
|
||||
const e = getEdges();
|
||||
e.forEach((rawEdge, index) => {
|
||||
const { arrowTypeStart, arrowTypeEnd } = destructEdgeType(rawEdge.type);
|
||||
const styles = [...(e.defaultStyle ?? [])];
|
||||
|
||||
if (rawEdge.style) {
|
||||
styles.push(...rawEdge.style);
|
||||
}
|
||||
const edge: Edge = {
|
||||
id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }),
|
||||
start: rawEdge.start,
|
||||
end: rawEdge.end,
|
||||
type: rawEdge.type ?? 'normal',
|
||||
label: rawEdge.text,
|
||||
labelpos: 'c',
|
||||
thickness: rawEdge.stroke,
|
||||
minlen: rawEdge.length,
|
||||
classes:
|
||||
rawEdge?.stroke === 'invisible'
|
||||
? ''
|
||||
: 'edge-thickness-normal edge-pattern-solid flowchart-link',
|
||||
arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart,
|
||||
arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd,
|
||||
arrowheadStyle: 'fill: #333',
|
||||
labelStyle: styles,
|
||||
style: styles,
|
||||
pattern: rawEdge.stroke,
|
||||
look: config.look,
|
||||
};
|
||||
edges.push(edge);
|
||||
});
|
||||
|
||||
return { nodes, edges, other: {}, config };
|
||||
};
|
||||
|
||||
export default {
|
||||
defaultConfig: () => defaultConfig.flowchart,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
getData,
|
||||
setAccDescription,
|
||||
addVertex,
|
||||
lookUpDomId,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import flowParser from './parser/flow.jison';
|
||||
import flowDb from './flowDb.js';
|
||||
import flowRendererV2 from './flowRenderer-v2.js';
|
||||
import renderer from './flowRenderer-v3-unified.js';
|
||||
import flowStyles from './styles.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { setConfig } from '../../diagram-api/diagramAPI.js';
|
||||
@ -9,16 +9,14 @@ import { setConfig } from '../../diagram-api/diagramAPI.js';
|
||||
export const diagram = {
|
||||
parser: flowParser,
|
||||
db: flowDb,
|
||||
renderer: flowRendererV2,
|
||||
renderer,
|
||||
styles: flowStyles,
|
||||
init: (cnf: MermaidConfig) => {
|
||||
if (!cnf.flowchart) {
|
||||
cnf.flowchart = {};
|
||||
}
|
||||
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
// flowchart-v2 uses dagre-wrapper, which doesn't have access to flowchart cnf
|
||||
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
|
||||
flowRendererV2.setConf(cnf.flowchart);
|
||||
flowDb.clear();
|
||||
flowDb.setGen('gen-2');
|
||||
},
|
||||
|
@ -1,24 +1,23 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import flowParser from './parser/flow.jison';
|
||||
import flowDb from './flowDb.js';
|
||||
import flowRenderer from './flowRenderer.js';
|
||||
import flowRendererV2 from './flowRenderer-v2.js';
|
||||
import renderer from './flowRenderer-v3-unified.js';
|
||||
import flowStyles from './styles.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { setConfig } from '../../diagram-api/diagramAPI.js';
|
||||
|
||||
export const diagram = {
|
||||
parser: flowParser,
|
||||
db: flowDb,
|
||||
renderer: flowRendererV2,
|
||||
renderer,
|
||||
styles: flowStyles,
|
||||
init: (cnf: MermaidConfig) => {
|
||||
if (!cnf.flowchart) {
|
||||
cnf.flowchart = {};
|
||||
}
|
||||
// TODO, broken as of 2022-09-14 (13809b50251845475e6dca65cc395761be38fbd2)
|
||||
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
flowRenderer.setConf(cnf.flowchart);
|
||||
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
|
||||
flowDb.clear();
|
||||
flowDb.setGen('gen-1');
|
||||
flowDb.setGen('gen-2');
|
||||
},
|
||||
};
|
||||
|
@ -1,515 +0,0 @@
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { select, curveLinear, selectAll } from 'd3';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import utils, { getEdgeId } from '../../utils.js';
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
|
||||
import { log } from '../../logger.js';
|
||||
import common, { evaluate, renderKatex } from '../common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
|
||||
const conf = {};
|
||||
export const setConf = function (cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (const key of keys) {
|
||||
conf[key] = cnf[key];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the vertices found during parsing to the graph to be rendered.
|
||||
*
|
||||
* @param vert Object containing the vertices.
|
||||
* @param g The graph that is to be drawn.
|
||||
* @param svgId
|
||||
* @param root
|
||||
* @param doc
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addVertices = async function (vert, g, svgId, root, doc, diagObj) {
|
||||
const svg = root.select(`[id="${svgId}"]`);
|
||||
const keys = vert.keys();
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
for (const id of keys) {
|
||||
const vertex = vert.get(id);
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
let classStr = 'default';
|
||||
if (vertex.classes.length > 0) {
|
||||
classStr = vertex.classes.join(' ');
|
||||
}
|
||||
classStr = classStr + ' flowchart-label';
|
||||
const styles = getStylesFromArray(vertex.styles);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
let vertexNode;
|
||||
log.info('vertex', vertex, vertex.labelType);
|
||||
if (vertex.labelType === 'markdown') {
|
||||
log.info('vertex', vertex, vertex.labelType);
|
||||
} else {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
const node = {
|
||||
label: vertexText,
|
||||
};
|
||||
vertexNode = addHtmlLabel(svg, node).node();
|
||||
vertexNode.parentNode.removeChild(vertexNode);
|
||||
} else {
|
||||
const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||
|
||||
const rows = vertexText.split(common.lineBreakRegex);
|
||||
|
||||
for (const row of rows) {
|
||||
const tspan = doc.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', '1');
|
||||
tspan.textContent = row;
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
vertexNode = svgLabel;
|
||||
}
|
||||
}
|
||||
|
||||
let radius = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radius = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'diamond':
|
||||
_shape = 'question';
|
||||
break;
|
||||
case 'hexagon':
|
||||
_shape = 'hexagon';
|
||||
break;
|
||||
case 'odd':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'lean_right':
|
||||
_shape = 'lean_right';
|
||||
break;
|
||||
case 'lean_left':
|
||||
_shape = 'lean_left';
|
||||
break;
|
||||
case 'trapezoid':
|
||||
_shape = 'trapezoid';
|
||||
break;
|
||||
case 'inv_trapezoid':
|
||||
_shape = 'inv_trapezoid';
|
||||
break;
|
||||
case 'odd_right':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'circle':
|
||||
_shape = 'circle';
|
||||
break;
|
||||
case 'ellipse':
|
||||
_shape = 'ellipse';
|
||||
break;
|
||||
case 'stadium':
|
||||
_shape = 'stadium';
|
||||
break;
|
||||
case 'subroutine':
|
||||
_shape = 'subroutine';
|
||||
break;
|
||||
case 'cylinder':
|
||||
_shape = 'cylinder';
|
||||
break;
|
||||
case 'group':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'doublecircle':
|
||||
_shape = 'doublecircle';
|
||||
break;
|
||||
default:
|
||||
_shape = 'rect';
|
||||
}
|
||||
const labelText = await renderKatex(vertexText, getConfig());
|
||||
|
||||
// Add the node
|
||||
g.setNode(vertex.id, {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText,
|
||||
labelType: vertex.labelType,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
link: vertex.link,
|
||||
linkTarget: vertex.linkTarget,
|
||||
tooltip: diagObj.db.getTooltip(vertex.id) || '',
|
||||
domId: diagObj.db.lookUpDomId(vertex.id),
|
||||
haveCallback: vertex.haveCallback,
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
dir: vertex.dir,
|
||||
type: vertex.type,
|
||||
props: vertex.props,
|
||||
padding: getConfig().flowchart.padding,
|
||||
});
|
||||
|
||||
log.info('setNode', {
|
||||
labelStyle: styles.labelStyle,
|
||||
labelType: vertex.labelType,
|
||||
shape: _shape,
|
||||
labelText,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
domId: diagObj.db.lookUpDomId(vertex.id),
|
||||
width: vertex.type === 'group' ? 500 : undefined,
|
||||
type: vertex.type,
|
||||
dir: vertex.dir,
|
||||
props: vertex.props,
|
||||
padding: getConfig().flowchart.padding,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add edges to graph based on parsed graph definition
|
||||
*
|
||||
* @param {object} edges The edges to add to the graph
|
||||
* @param {object} g The graph object
|
||||
* @param _diagObj
|
||||
*/
|
||||
export const addEdges = async function (edges, g, _diagObj) {
|
||||
log.info('abc78 edges = ', edges);
|
||||
let cnt = 0;
|
||||
let linkIdCnt = {};
|
||||
|
||||
let defaultStyle;
|
||||
let defaultLabelStyle;
|
||||
|
||||
if (edges.defaultStyle !== undefined) {
|
||||
const defaultStyles = getStylesFromArray(edges.defaultStyle);
|
||||
defaultStyle = defaultStyles.style;
|
||||
defaultLabelStyle = defaultStyles.labelStyle;
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
cnt++;
|
||||
|
||||
// Identify Link
|
||||
const linkIdBase = getEdgeId(edge.start, edge.end, {
|
||||
counter: cnt,
|
||||
prefix: 'L',
|
||||
});
|
||||
|
||||
// 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]);
|
||||
}
|
||||
let linkId = `${linkIdBase}_${linkIdCnt[linkIdBase]}`;
|
||||
|
||||
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;
|
||||
//edgeData.id = 'id' + cnt;
|
||||
|
||||
// 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;
|
||||
case 'invisible':
|
||||
edgeData.thickness = 'invisible';
|
||||
edgeData.pattern = 'solid';
|
||||
edgeData.style = 'stroke-width: 0;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;
|
||||
|
||||
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 = await renderKatex(edge.text.replace(common.lineBreakRegex, '\n'), getConfig());
|
||||
|
||||
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;
|
||||
|
||||
// Add the edge to the graph
|
||||
g.setEdge(edge.start, edge.end, edgeData, cnt);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
*
|
||||
* @param text
|
||||
* @param diagObj
|
||||
* @returns {Map<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
||||
*/
|
||||
export const getClasses = function (text, diagObj) {
|
||||
return diagObj.db.getClasses();
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param text
|
||||
* @param id
|
||||
* @param _version
|
||||
* @param diagObj
|
||||
*/
|
||||
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
log.info('Drawing flowchart');
|
||||
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = diagObj.db.getDirection();
|
||||
if (dir === undefined) {
|
||||
dir = 'TD';
|
||||
}
|
||||
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
const nodeSpacing = conf.nodeSpacing ?? 50;
|
||||
const rankSpacing = conf.rankSpacing ?? 50;
|
||||
|
||||
// Handle root and document for when rendering in sandbox mode
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
}
|
||||
const root =
|
||||
securityLevel === 'sandbox'
|
||||
? select(sandboxElement.nodes()[0].contentDocument.body)
|
||||
: select('body');
|
||||
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: dir,
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 0,
|
||||
marginy: 0,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
let subG;
|
||||
const subGraphs = diagObj.db.getSubGraphs();
|
||||
log.info('Subgraphs - ', subGraphs);
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
log.info('Subgraph - ', subG);
|
||||
diagObj.db.addVertex(
|
||||
subG.id,
|
||||
{ text: subG.title, type: subG.labelType },
|
||||
'group',
|
||||
undefined,
|
||||
subG.classes,
|
||||
subG.dir
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const vert = diagObj.db.getVertices();
|
||||
|
||||
const edges = diagObj.db.getEdges();
|
||||
|
||||
log.info('Edges', edges);
|
||||
let i = 0;
|
||||
for (i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
|
||||
selectAll('cluster').append('text');
|
||||
|
||||
for (const node of subG.nodes) {
|
||||
log.info('Setting up subgraphs', node, subG.id);
|
||||
g.setParent(node, subG.id);
|
||||
}
|
||||
}
|
||||
await addVertices(vert, g, id, root, doc, diagObj);
|
||||
await addEdges(edges, g, diagObj);
|
||||
|
||||
// Add custom shapes
|
||||
// flowChartShapes.addToRenderV2(addShape);
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
|
||||
// Run the renderer. This is what draws the final graph.
|
||||
const element = root.select('#' + id + ' g');
|
||||
await render(element, g, ['point', 'circle', 'cross'], 'flowchart', id);
|
||||
|
||||
utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());
|
||||
|
||||
setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth);
|
||||
|
||||
// Index nodes
|
||||
diagObj.db.indexNodes('subGraph' + i);
|
||||
|
||||
// Add label rects for non html labels
|
||||
if (!conf.htmlLabels) {
|
||||
const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (const label of labels) {
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// If node has a link, wrap it in an anchor SVG object.
|
||||
const keys = [...vert.keys()];
|
||||
keys.forEach((key) => {
|
||||
const vertex = vert.get(key);
|
||||
|
||||
if (vertex.link) {
|
||||
const node = select('#' + id + ' [id="' + key + '"]');
|
||||
if (node) {
|
||||
const link = doc.createElementNS('http://www.w3.org/2000/svg', 'a');
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' '));
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
|
||||
if (securityLevel === 'sandbox') {
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'target', '_top');
|
||||
} else if (vertex.linkTarget) {
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'target', vertex.linkTarget);
|
||||
}
|
||||
|
||||
const linkNode = node.insert(function () {
|
||||
return link;
|
||||
}, ':first-child');
|
||||
|
||||
const shape = node.select('.label-container');
|
||||
if (shape) {
|
||||
linkNode.append(function () {
|
||||
return shape.node();
|
||||
});
|
||||
}
|
||||
|
||||
const label = node.select('.label');
|
||||
if (label) {
|
||||
linkNode.append(function () {
|
||||
return label.node();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
addVertices,
|
||||
addEdges,
|
||||
getClasses,
|
||||
draw,
|
||||
};
|
@ -0,0 +1,99 @@
|
||||
import { select } from 'd3';
|
||||
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 { getDirection } from './flowDb.js';
|
||||
|
||||
export const getClasses = function (
|
||||
text: string,
|
||||
diagramObj: any
|
||||
): Map<string, DiagramStyleClassDef> {
|
||||
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, flowchart: conf, layout } = getConfig();
|
||||
|
||||
// Handle root and document for when rendering in sandbox mode
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
}
|
||||
|
||||
// @ts-ignore - document is always available
|
||||
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
|
||||
|
||||
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||
// into the Layout data format
|
||||
log.debug('Before getData: ');
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
log.debug('Data: ', data4Layout);
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const { element, svg } = getDiagramElements(id, securityLevel);
|
||||
const direction = getDirection();
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = layout;
|
||||
data4Layout.direction = direction;
|
||||
data4Layout.nodeSpacing = conf?.nodeSpacing || 50;
|
||||
data4Layout.rankSpacing = conf?.rankSpacing || 50;
|
||||
data4Layout.markers = ['point', 'circle', 'cross'];
|
||||
|
||||
data4Layout.diagramId = id;
|
||||
log.debug('REF1:', data4Layout);
|
||||
await render(data4Layout, svg, element);
|
||||
const padding = data4Layout.config.flowchart?.padding ?? 8;
|
||||
utils.insertTitle(
|
||||
svg,
|
||||
'flowchartTitleText',
|
||||
conf?.titleTopMargin || 0,
|
||||
diag.db.getDiagramTitle()
|
||||
);
|
||||
setupViewPortForSVG(svg, padding, 'flowchart', conf?.useMaxWidth || false);
|
||||
|
||||
// If node has a link, wrap it in an anchor SVG object.
|
||||
for (const vertex of data4Layout.nodes) {
|
||||
const node = select(`#${id} [id="${vertex.id}"]`);
|
||||
if (!node || !vertex.link) {
|
||||
continue;
|
||||
}
|
||||
const link = doc.createElementNS('http://www.w3.org/2000/svg', 'a');
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.cssClasses);
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
|
||||
if (securityLevel === 'sandbox') {
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'target', '_top');
|
||||
} else if (vertex.linkTarget) {
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'target', vertex.linkTarget);
|
||||
}
|
||||
|
||||
const linkNode = node.insert(function () {
|
||||
return link;
|
||||
}, ':first-child');
|
||||
|
||||
const shape = node.select('.label-container');
|
||||
if (shape) {
|
||||
linkNode.append(function () {
|
||||
return shape.node();
|
||||
});
|
||||
}
|
||||
|
||||
const label = node.select('.label');
|
||||
if (label) {
|
||||
linkNode.append(function () {
|
||||
return label.node();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
getClasses,
|
||||
draw,
|
||||
};
|
@ -1,150 +0,0 @@
|
||||
import flowDb from './flowDb.js';
|
||||
import { parser } from './parser/flow.jison';
|
||||
import flowRenderer from './flowRenderer.js';
|
||||
import { addDiagrams } from '../../diagram-api/diagram-orchestration.js';
|
||||
|
||||
const diag = {
|
||||
db: flowDb,
|
||||
};
|
||||
addDiagrams();
|
||||
|
||||
describe('when using mermaid and ', function () {
|
||||
describe('when calling addEdges ', function () {
|
||||
beforeEach(function () {
|
||||
parser.yy = flowDb;
|
||||
flowDb.clear();
|
||||
flowDb.setGen('gen-2');
|
||||
});
|
||||
it('should handle edges with text', async () => {
|
||||
parser.parse('graph TD;A-->|text ex|B;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B-');
|
||||
expect(options.arrowhead).toBe('normal');
|
||||
expect(options.label.match('text ex')).toBeTruthy();
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should handle edges without text', async function () {
|
||||
parser.parse('graph TD;A-->B;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B-');
|
||||
expect(options.arrowhead).toBe('normal');
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should handle open-ended edges', async () => {
|
||||
parser.parse('graph TD;A---B;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B-');
|
||||
expect(options.arrowhead).toBe('none');
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should handle edges with styles defined', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B-');
|
||||
expect(options.arrowhead).toBe('none');
|
||||
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;');
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
it('should handle edges with interpolation defined', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 interpolate basis');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B-');
|
||||
expect(options.arrowhead).toBe('none');
|
||||
expect(options.curve).toBe('basis'); // mocked as string
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
it('should handle edges with text and styles defined', async () => {
|
||||
parser.parse('graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B-');
|
||||
expect(options.arrowhead).toBe('none');
|
||||
expect(options.label.match('the text')).toBeTruthy();
|
||||
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;');
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should set fill to "none" by default when handling edges', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B');
|
||||
expect(options.arrowhead).toBe('none');
|
||||
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;');
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should not set fill to none if fill is set in linkStyle', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
const mockG = {
|
||||
setEdge: function (start, end, options) {
|
||||
expect(start).toContain('flowchart-A-');
|
||||
expect(end).toContain('flowchart-B-');
|
||||
expect(options.arrowhead).toBe('none');
|
||||
expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:blue;');
|
||||
},
|
||||
};
|
||||
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,503 +0,0 @@
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { select, curveLinear, selectAll } from 'd3';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { render as Render } from 'dagre-d3-es';
|
||||
import { applyStyle } from 'dagre-d3-es/src/dagre-js/util.js';
|
||||
import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
|
||||
import { log } from '../../logger.js';
|
||||
import common, { evaluate, renderKatex } from '../common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray, getEdgeId } from '../../utils.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import flowChartShapes from './flowChartShapes.js';
|
||||
import { replaceIconSubstring } from '../../rendering-util/createText.js';
|
||||
|
||||
const conf = {};
|
||||
export const setConf = function (cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (const key of keys) {
|
||||
conf[key] = cnf[key];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that adds the vertices found in the graph definition to the graph to be rendered.
|
||||
*
|
||||
* @param vert Object containing the vertices.
|
||||
* @param g The graph that is to be drawn.
|
||||
* @param svgId
|
||||
* @param root
|
||||
* @param _doc
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addVertices = async function (vert, g, svgId, root, _doc, diagObj) {
|
||||
const svg = !root ? select(`[id="${svgId}"]`) : root.select(`[id="${svgId}"]`);
|
||||
const doc = !_doc ? document : _doc;
|
||||
const keys = Object.keys(vert);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
for (const id of keys) {
|
||||
const vertex = vert[id];
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
let classStr = 'default';
|
||||
if (vertex.classes.length > 0) {
|
||||
classStr = vertex.classes.join(' ');
|
||||
}
|
||||
|
||||
const styles = getStylesFromArray(vertex.styles);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
let vertexNode;
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
const replacedVertexText = replaceIconSubstring(vertexText);
|
||||
const node = {
|
||||
label: await renderKatex(replacedVertexText, getConfig()),
|
||||
};
|
||||
vertexNode = addHtmlLabel(svg, node).node();
|
||||
vertexNode.parentNode.removeChild(vertexNode);
|
||||
} else {
|
||||
const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
|
||||
|
||||
const rows = vertexText.split(common.lineBreakRegex);
|
||||
|
||||
for (const row of rows) {
|
||||
const tspan = doc.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', '1');
|
||||
tspan.textContent = row;
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
vertexNode = svgLabel;
|
||||
}
|
||||
|
||||
let radius = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radius = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'diamond':
|
||||
_shape = 'question';
|
||||
break;
|
||||
case 'hexagon':
|
||||
_shape = 'hexagon';
|
||||
break;
|
||||
case 'odd':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'lean_right':
|
||||
_shape = 'lean_right';
|
||||
break;
|
||||
case 'lean_left':
|
||||
_shape = 'lean_left';
|
||||
break;
|
||||
case 'trapezoid':
|
||||
_shape = 'trapezoid';
|
||||
break;
|
||||
case 'inv_trapezoid':
|
||||
_shape = 'inv_trapezoid';
|
||||
break;
|
||||
case 'odd_right':
|
||||
_shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'circle':
|
||||
_shape = 'circle';
|
||||
break;
|
||||
case 'ellipse':
|
||||
_shape = 'ellipse';
|
||||
break;
|
||||
case 'stadium':
|
||||
_shape = 'stadium';
|
||||
break;
|
||||
case 'subroutine':
|
||||
_shape = 'subroutine';
|
||||
break;
|
||||
case 'cylinder':
|
||||
_shape = 'cylinder';
|
||||
break;
|
||||
case 'group':
|
||||
_shape = 'rect';
|
||||
break;
|
||||
default:
|
||||
_shape = 'rect';
|
||||
}
|
||||
// Add the node
|
||||
log.warn('Adding node', vertex.id, vertex.domId);
|
||||
g.setNode(diagObj.db.lookUpDomId(vertex.id), {
|
||||
labelType: 'svg',
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
label: vertexNode,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: diagObj.db.lookUpDomId(vertex.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add edges to graph based on parsed graph definition
|
||||
*
|
||||
* @param {object} edges The edges to add to the graph
|
||||
* @param {object} g The graph object
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addEdges = async function (edges, g, diagObj) {
|
||||
let cnt = 0;
|
||||
|
||||
let defaultStyle;
|
||||
let defaultLabelStyle;
|
||||
|
||||
if (edges.defaultStyle !== undefined) {
|
||||
const defaultStyles = getStylesFromArray(edges.defaultStyle);
|
||||
defaultStyle = defaultStyles.style;
|
||||
defaultLabelStyle = defaultStyles.labelStyle;
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
cnt++;
|
||||
|
||||
// Identify Link
|
||||
const linkId = getEdgeId(edge.start, edge.end, {
|
||||
counter: cnt,
|
||||
prefix: 'L',
|
||||
});
|
||||
const linkNameStart = 'LS-' + edge.start;
|
||||
const linkNameEnd = 'LE-' + edge.end;
|
||||
|
||||
const edgeData = {};
|
||||
|
||||
// Set link type for rendering
|
||||
if (edge.type === 'arrow_open') {
|
||||
edgeData.arrowhead = 'none';
|
||||
} else {
|
||||
edgeData.arrowhead = 'normal';
|
||||
}
|
||||
|
||||
let style = '';
|
||||
let labelStyle = '';
|
||||
|
||||
if (edge.style !== undefined) {
|
||||
const styles = getStylesFromArray(edge.style);
|
||||
style = styles.style;
|
||||
labelStyle = styles.labelStyle;
|
||||
} else {
|
||||
switch (edge.stroke) {
|
||||
case 'normal':
|
||||
style = 'fill:none';
|
||||
if (defaultStyle !== undefined) {
|
||||
style = defaultStyle;
|
||||
}
|
||||
if (defaultLabelStyle !== undefined) {
|
||||
labelStyle = defaultLabelStyle;
|
||||
}
|
||||
break;
|
||||
case 'dotted':
|
||||
style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
|
||||
break;
|
||||
case 'thick':
|
||||
style = ' stroke-width: 3.5px;fill:none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
edgeData.style = style;
|
||||
edgeData.labelStyle = labelStyle;
|
||||
|
||||
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';
|
||||
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
edgeData.labelType = 'html';
|
||||
edgeData.label = `<span id="L-${linkId}" class="edgeLabel L-${linkNameStart}' L-${linkNameEnd}" style="${
|
||||
edgeData.labelStyle
|
||||
}">${await renderKatex(replaceIconSubstring(edge.text), getConfig())}</span>`;
|
||||
} else {
|
||||
edgeData.labelType = 'text';
|
||||
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.class = linkNameStart + ' ' + linkNameEnd;
|
||||
edgeData.minlen = edge.length || 1;
|
||||
|
||||
// Add the edge to the graph
|
||||
g.setEdge(diagObj.db.lookUpDomId(edge.start), diagObj.db.lookUpDomId(edge.end), edgeData, cnt);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
*
|
||||
* @param text
|
||||
* @param diagObj
|
||||
* @returns {Map<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
||||
*/
|
||||
export const getClasses = function (text, diagObj) {
|
||||
log.info('Extracting classes');
|
||||
return diagObj.db.getClasses();
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param text
|
||||
* @param id
|
||||
* @param _version
|
||||
* @param diagObj
|
||||
*/
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
log.info('Drawing flowchart');
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
}
|
||||
const root =
|
||||
securityLevel === 'sandbox'
|
||||
? select(sandboxElement.nodes()[0].contentDocument.body)
|
||||
: select('body');
|
||||
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
|
||||
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = diagObj.db.getDirection();
|
||||
if (dir === undefined) {
|
||||
dir = 'TD';
|
||||
}
|
||||
const nodeSpacing = conf.nodeSpacing ?? 50;
|
||||
const rankSpacing = conf.rankSpacing ?? 50;
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: dir,
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
let subG;
|
||||
const subGraphs = diagObj.db.getSubGraphs();
|
||||
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
diagObj.db.addVertex(subG.id, subG.title, 'group', undefined, subG.classes);
|
||||
}
|
||||
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const vert = diagObj.db.getVertices();
|
||||
log.warn('Get vertices', vert);
|
||||
|
||||
const edges = diagObj.db.getEdges();
|
||||
|
||||
let i = 0;
|
||||
for (i = subGraphs.length - 1; i >= 0; i--) {
|
||||
subG = subGraphs[i];
|
||||
|
||||
selectAll('cluster').append('text');
|
||||
|
||||
for (const node of subG.nodes) {
|
||||
log.warn(
|
||||
'Setting subgraph',
|
||||
node,
|
||||
diagObj.db.lookUpDomId(node),
|
||||
diagObj.db.lookUpDomId(subG.id)
|
||||
);
|
||||
g.setParent(diagObj.db.lookUpDomId(node), diagObj.db.lookUpDomId(subG.id));
|
||||
}
|
||||
}
|
||||
await addVertices(vert, g, id, root, doc, diagObj);
|
||||
await addEdges(edges, g, diagObj);
|
||||
|
||||
// Create the renderer
|
||||
const render = new Render();
|
||||
|
||||
// Add custom shapes
|
||||
flowChartShapes.addToRender(render);
|
||||
|
||||
// Add our custom arrow - an empty arrowhead
|
||||
render.arrows().none = function normal(parent, id, edge, type) {
|
||||
const marker = parent
|
||||
.append('marker')
|
||||
.attr('id', id)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 8)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto');
|
||||
|
||||
const path = marker.append('path').attr('d', 'M 0 0 L 0 0 L 0 0 z');
|
||||
applyStyle(path, edge[type + 'Style']);
|
||||
};
|
||||
|
||||
// Override normal arrowhead defined in d3. Remove style & add class to allow css styling.
|
||||
render.arrows().normal = function normal(parent, id) {
|
||||
const marker = parent
|
||||
.append('marker')
|
||||
.attr('id', id)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 9)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 8)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto');
|
||||
|
||||
marker
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
|
||||
.attr('class', 'arrowheadPath')
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
|
||||
// Run the renderer. This is what draws the final graph.
|
||||
const element = root.select('#' + id + ' g');
|
||||
render(element, g);
|
||||
|
||||
element.selectAll('g.node').attr('title', function () {
|
||||
return diagObj.db.getTooltip(this.id);
|
||||
});
|
||||
|
||||
// Index nodes
|
||||
diagObj.db.indexNodes('subGraph' + i);
|
||||
|
||||
// reposition labels
|
||||
for (i = 0; i < subGraphs.length; i++) {
|
||||
subG = subGraphs[i];
|
||||
if (subG.title !== 'undefined') {
|
||||
const clusterRects = doc.querySelectorAll(
|
||||
'#' + id + ' [id="' + diagObj.db.lookUpDomId(subG.id) + '"] rect'
|
||||
);
|
||||
const clusterEl = doc.querySelectorAll(
|
||||
'#' + id + ' [id="' + diagObj.db.lookUpDomId(subG.id) + '"]'
|
||||
);
|
||||
|
||||
const xPos = clusterRects[0].x.baseVal.value;
|
||||
const yPos = clusterRects[0].y.baseVal.value;
|
||||
const _width = clusterRects[0].width.baseVal.value;
|
||||
const cluster = select(clusterEl[0]);
|
||||
const te = cluster.select('.label');
|
||||
te.attr('transform', `translate(${xPos + _width / 2}, ${yPos + 14})`);
|
||||
te.attr('id', id + 'Text');
|
||||
|
||||
for (const className of subG.classes) {
|
||||
clusterEl[0].classList.add(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add label rects for non html labels
|
||||
if (!conf.htmlLabels) {
|
||||
const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (const label of labels) {
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
// rect.setAttribute('style', 'fill:#e8e8e8;');
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
}
|
||||
}
|
||||
setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth);
|
||||
|
||||
// If node has a link, wrap it in an anchor SVG object.
|
||||
const keys = [...vert.keys()];
|
||||
keys.forEach(function (key) {
|
||||
const vertex = vert.get(key);
|
||||
|
||||
if (vertex.link) {
|
||||
const node = root.select('#' + id + ' [id="' + diagObj.db.lookUpDomId(key) + '"]');
|
||||
if (node) {
|
||||
const link = doc.createElementNS('http://www.w3.org/2000/svg', 'a');
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' '));
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
|
||||
if (securityLevel === 'sandbox') {
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'target', '_top');
|
||||
} else if (vertex.linkTarget) {
|
||||
link.setAttributeNS('http://www.w3.org/2000/svg', 'target', vertex.linkTarget);
|
||||
}
|
||||
|
||||
const linkNode = node.insert(function () {
|
||||
return link;
|
||||
}, ':first-child');
|
||||
|
||||
const shape = node.select('.label-container');
|
||||
if (shape) {
|
||||
linkNode.append(function () {
|
||||
return shape.node();
|
||||
});
|
||||
}
|
||||
|
||||
const label = node.select('.label');
|
||||
if (label) {
|
||||
linkNode.append(function () {
|
||||
return label.node();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
addVertices,
|
||||
addEdges,
|
||||
getClasses,
|
||||
draw,
|
||||
};
|
@ -1,277 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { addVertices, addEdges } from './flowRenderer.js';
|
||||
import { setConfig } from '../../diagram-api/diagramAPI.js';
|
||||
|
||||
setConfig({
|
||||
flowchart: {
|
||||
htmlLabels: false,
|
||||
},
|
||||
});
|
||||
|
||||
describe('the flowchart renderer', function () {
|
||||
describe('when adding vertices to a graph', function () {
|
||||
[
|
||||
['round', 'rect', 5],
|
||||
['square', 'rect'],
|
||||
['diamond', 'question'],
|
||||
['hexagon', 'hexagon'],
|
||||
['odd', 'rect_left_inv_arrow'],
|
||||
['lean_right', 'lean_right'],
|
||||
['lean_left', 'lean_left'],
|
||||
['trapezoid', 'trapezoid'],
|
||||
['inv_trapezoid', 'inv_trapezoid'],
|
||||
['odd_right', 'rect_left_inv_arrow'],
|
||||
['circle', 'circle'],
|
||||
['ellipse', 'ellipse'],
|
||||
['stadium', 'stadium'],
|
||||
['subroutine', 'subroutine'],
|
||||
['cylinder', 'cylinder'],
|
||||
['group', 'rect'],
|
||||
].forEach(function ([type, expectedShape, expectedRadios = 0]) {
|
||||
it(`should add the correct shaped node to the graph for vertex type ${type}`, async function () {
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
lookUpDomId: () => {
|
||||
return 'my-node-id';
|
||||
},
|
||||
},
|
||||
};
|
||||
const addedNodes = [];
|
||||
const mockG = {
|
||||
setNode: function (id, object) {
|
||||
addedNodes.push([id, object]);
|
||||
},
|
||||
};
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type,
|
||||
id: 'my-node-id',
|
||||
classes: [],
|
||||
styles: [],
|
||||
text: 'my vertex text',
|
||||
},
|
||||
},
|
||||
mockG,
|
||||
'svg-id',
|
||||
undefined,
|
||||
undefined,
|
||||
fakeDiag
|
||||
);
|
||||
expect(addedNodes).toHaveLength(1);
|
||||
expect(addedNodes[0][0]).toEqual('my-node-id');
|
||||
expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id');
|
||||
expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg');
|
||||
expect(addedNodes[0][1]).toHaveProperty('shape', expectedShape);
|
||||
expect(addedNodes[0][1]).toHaveProperty('rx', expectedRadios);
|
||||
expect(addedNodes[0][1]).toHaveProperty('ry', expectedRadios);
|
||||
});
|
||||
});
|
||||
|
||||
['Multi<br>Line', 'Multi<br/>Line', 'Multi<br />Line', 'Multi<br\t/>Line'].forEach(
|
||||
function (labelText) {
|
||||
it('should handle multiline texts with different line breaks', async function () {
|
||||
const addedNodes = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
lookUpDomId: () => {
|
||||
return 'my-node-id';
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockG = {
|
||||
setNode: function (id, object) {
|
||||
addedNodes.push([id, object]);
|
||||
},
|
||||
};
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type: 'rect',
|
||||
id: 'my-node-id',
|
||||
classes: [],
|
||||
styles: [],
|
||||
text: 'Multi<br>Line',
|
||||
},
|
||||
},
|
||||
mockG,
|
||||
'svg-id',
|
||||
false,
|
||||
document,
|
||||
fakeDiag
|
||||
);
|
||||
expect(addedNodes).toHaveLength(1);
|
||||
expect(addedNodes[0][0]).toEqual('my-node-id');
|
||||
expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id');
|
||||
expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg');
|
||||
expect(addedNodes[0][1].label).toBeDefined();
|
||||
expect(addedNodes[0][1].label).toBeDefined(); // <text> node
|
||||
expect(addedNodes[0][1].label.firstChild.innerHTML).toEqual('Multi'); // <tspan> node, line 1
|
||||
expect(addedNodes[0][1].label.lastChild.innerHTML).toEqual('Line'); // <tspan> node, line 2
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
[
|
||||
[['fill:#fff'], 'fill:#fff;', ''],
|
||||
[['color:#ccc'], '', 'color:#ccc;'],
|
||||
[['fill:#fff', 'color:#ccc'], 'fill:#fff;', 'color:#ccc;'],
|
||||
[
|
||||
['fill:#fff', 'color:#ccc', 'text-align:center'],
|
||||
'fill:#fff;',
|
||||
'color:#ccc;text-align:center;',
|
||||
],
|
||||
].forEach(function ([style, expectedStyle, expectedLabelStyle]) {
|
||||
it(`should add the styles to style and/or labelStyle for style ${style}`, async function () {
|
||||
const addedNodes = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
lookUpDomId: () => {
|
||||
return 'my-node-id';
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockG = {
|
||||
setNode: function (id, object) {
|
||||
addedNodes.push([id, object]);
|
||||
},
|
||||
};
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type: 'rect',
|
||||
id: 'my-node-id',
|
||||
classes: [],
|
||||
styles: style,
|
||||
text: 'my vertex text',
|
||||
},
|
||||
},
|
||||
mockG,
|
||||
'svg-id',
|
||||
undefined,
|
||||
undefined,
|
||||
fakeDiag
|
||||
);
|
||||
expect(addedNodes).toHaveLength(1);
|
||||
expect(addedNodes[0][0]).toEqual('my-node-id');
|
||||
expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id');
|
||||
expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg');
|
||||
expect(addedNodes[0][1]).toHaveProperty('style', expectedStyle);
|
||||
expect(addedNodes[0][1]).toHaveProperty('labelStyle', expectedLabelStyle);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should add default class to all nodes which do not have another class assigned`, async function () {
|
||||
const addedNodes = [];
|
||||
const mockG = {
|
||||
setNode: function (id, object) {
|
||||
addedNodes.push([id, object]);
|
||||
},
|
||||
};
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
lookUpDomId: () => {
|
||||
return 'my-node-id';
|
||||
},
|
||||
},
|
||||
};
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type: 'rect',
|
||||
id: 'my-node-id',
|
||||
classes: [],
|
||||
styles: [],
|
||||
text: 'my vertex text',
|
||||
},
|
||||
v2: {
|
||||
type: 'rect',
|
||||
id: 'myNode',
|
||||
classes: ['myClass'],
|
||||
styles: [],
|
||||
text: 'my vertex text',
|
||||
},
|
||||
},
|
||||
mockG,
|
||||
'svg-id',
|
||||
undefined,
|
||||
undefined,
|
||||
fakeDiag
|
||||
);
|
||||
expect(addedNodes).toHaveLength(2);
|
||||
expect(addedNodes[0][0]).toEqual('my-node-id');
|
||||
expect(addedNodes[0][1]).toHaveProperty('class', 'default');
|
||||
expect(addedNodes[1][0]).toEqual('my-node-id');
|
||||
expect(addedNodes[1][1]).toHaveProperty('class', 'myClass');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adding edges to a graph', function () {
|
||||
it('should handle multiline texts and set centered label position', async function () {
|
||||
const addedEdges = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
lookUpDomId: () => {
|
||||
return 'my-node-id';
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockG = {
|
||||
setEdge: function (s, e, data, c) {
|
||||
addedEdges.push(data);
|
||||
},
|
||||
};
|
||||
await addEdges(
|
||||
[
|
||||
{ text: 'Multi<br>Line' },
|
||||
{ text: 'Multi<br/>Line' },
|
||||
{ text: 'Multi<br />Line' },
|
||||
{ text: 'Multi<br\t/>Line' },
|
||||
{ style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi<br>Line' },
|
||||
{ style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi<br/>Line' },
|
||||
{ style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi<br />Line' },
|
||||
{ style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi<br\t/>Line' },
|
||||
],
|
||||
mockG,
|
||||
fakeDiag
|
||||
);
|
||||
|
||||
addedEdges.forEach(function (edge) {
|
||||
expect(edge).toHaveProperty('label', 'Multi\nLine');
|
||||
expect(edge).toHaveProperty('labelpos', 'c');
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
[['stroke:DarkGray'], 'stroke:DarkGray;', ''],
|
||||
[['color:red'], '', 'fill:red;'],
|
||||
[['stroke:DarkGray', 'color:red'], 'stroke:DarkGray;', 'fill:red;'],
|
||||
[
|
||||
['stroke:DarkGray', 'color:red', 'stroke-width:2px'],
|
||||
'stroke:DarkGray;stroke-width:2px;',
|
||||
'fill:red;',
|
||||
],
|
||||
].forEach(function ([style, expectedStyle, expectedLabelStyle]) {
|
||||
it(`should add the styles to style and/or labelStyle for style ${style}`, async function () {
|
||||
const addedEdges = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
lookUpDomId: () => {
|
||||
return 'my-node-id';
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockG = {
|
||||
setEdge: function (s, e, data, c) {
|
||||
addedEdges.push(data);
|
||||
},
|
||||
};
|
||||
await addEdges([{ style: style, text: 'styling' }], mockG, fakeDiag);
|
||||
|
||||
expect(addedEdges).toHaveLength(1);
|
||||
expect(addedEdges[0]).toHaveProperty('style', expectedStyle);
|
||||
expect(addedEdges[0]).toHaveProperty('labelStyle', expectedLabelStyle);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -38,11 +38,14 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
.cluster-label text {
|
||||
fill: ${options.titleColor};
|
||||
}
|
||||
.cluster-label span,p {
|
||||
.cluster-label span {
|
||||
color: ${options.titleColor};
|
||||
}
|
||||
.cluster-label span p {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.label text,span,p {
|
||||
.label text,span {
|
||||
fill: ${options.nodeTextColor || options.textColor};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
@ -56,7 +59,7 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
stroke: ${options.nodeBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.flowchart-label text {
|
||||
.rough-node .label text , .node .label text {
|
||||
text-anchor: middle;
|
||||
}
|
||||
// .flowchart-label .text-outer-tspan {
|
||||
@ -95,6 +98,9 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
|
||||
.edgeLabel {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
p {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
}
|
||||
rect {
|
||||
opacity: 0.5;
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
@ -106,7 +112,7 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
/* For html labels only */
|
||||
.labelBkg {
|
||||
background-color: ${fade(options.edgeLabelBackground, 0.5)};
|
||||
// background-color:
|
||||
// background-color:
|
||||
}
|
||||
|
||||
.cluster rect {
|
||||
@ -119,7 +125,7 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
fill: ${options.titleColor};
|
||||
}
|
||||
|
||||
.cluster span,p {
|
||||
.cluster span {
|
||||
color: ${options.titleColor};
|
||||
}
|
||||
/* .cluster div {
|
||||
|
@ -23,7 +23,7 @@ export interface FlowEdge {
|
||||
end: string;
|
||||
interpolate?: string;
|
||||
type?: string;
|
||||
stroke?: string;
|
||||
stroke?: 'normal' | 'thick' | 'invisible' | 'dotted';
|
||||
style?: string[];
|
||||
length?: number;
|
||||
text: string;
|
||||
|
@ -16,18 +16,20 @@ import defaultConfig from '../../defaultConfig.js';
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
function drawNodes(
|
||||
async function drawNodes(
|
||||
db: MindmapDB,
|
||||
svg: D3Element,
|
||||
mindmap: FilledMindMapNode,
|
||||
section: number,
|
||||
conf: MermaidConfig
|
||||
) {
|
||||
drawNode(db, svg, mindmap, section, conf);
|
||||
await drawNode(db, svg, mindmap, section, conf);
|
||||
if (mindmap.children) {
|
||||
mindmap.children.forEach((child, index) => {
|
||||
drawNodes(db, svg, child, section < 0 ? index : section, conf);
|
||||
});
|
||||
await Promise.all(
|
||||
mindmap.children.map((child, index) =>
|
||||
drawNodes(db, svg, child, section < 0 ? index : section, conf)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,7 +179,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
||||
edgesElem.attr('class', 'mindmap-edges');
|
||||
const nodesElem = svg.append('g');
|
||||
nodesElem.attr('class', 'mindmap-nodes');
|
||||
drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
||||
await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
||||
|
||||
// Next step is to layout the mindmap, giving each node a position
|
||||
|
||||
|
@ -174,13 +174,13 @@ const roundedRectBkg: ShapeFunction = function (db, elem, node) {
|
||||
* @param conf - The configuration object
|
||||
* @returns The height nodes dom element
|
||||
*/
|
||||
export const drawNode = function (
|
||||
export const drawNode = async function (
|
||||
db: MindmapDB,
|
||||
elem: D3Element,
|
||||
node: FilledMindMapNode,
|
||||
fullSection: number,
|
||||
conf: MermaidConfig
|
||||
): number {
|
||||
): Promise<number> {
|
||||
const htmlLabels = conf.htmlLabels;
|
||||
const section = fullSection % (MAX_SECTIONS - 1);
|
||||
const nodeElem = elem.append('g');
|
||||
@ -195,7 +195,7 @@ export const drawNode = function (
|
||||
// Create the wrapped text element
|
||||
const textElem = nodeElem.append('g');
|
||||
const description = node.descr.replace(/(<br\/*>)/g, '\n');
|
||||
createText(
|
||||
await createText(
|
||||
textElem,
|
||||
description,
|
||||
{
|
||||
|
@ -1088,6 +1088,19 @@ export const draw = async function (_text: string, id: string, _version: string,
|
||||
|
||||
const { bounds: box } = bounds.getBounds();
|
||||
|
||||
if (box.startx === undefined) {
|
||||
box.startx = 0;
|
||||
}
|
||||
if (box.starty === undefined) {
|
||||
box.starty = 0;
|
||||
}
|
||||
if (box.stopx === undefined) {
|
||||
box.stopx = 0;
|
||||
}
|
||||
if (box.stopy === undefined) {
|
||||
box.stopy = 0;
|
||||
}
|
||||
|
||||
// Make sure the height of the diagram supports long menus.
|
||||
let boxHeight = box.stopy - box.starty;
|
||||
if (boxHeight < requiredBoxSize.maxHeight) {
|
||||
|
411
packages/mermaid/src/diagrams/state/dataFetcher.js
Normal file
411
packages/mermaid/src/diagrams/state/dataFetcher.js
Normal file
@ -0,0 +1,411 @@
|
||||
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 = new Map();
|
||||
|
||||
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, look, classes) => {
|
||||
// graphItemCount = 0;
|
||||
log.trace('items', doc);
|
||||
doc.forEach((item) => {
|
||||
switch (item.stmt) {
|
||||
case STMT_STATE:
|
||||
dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, look, classes);
|
||||
break;
|
||||
case DEFAULT_STATE_TYPE:
|
||||
dataFetcher(parentParsedItem, item, diagramStates, nodes, edges, altFlag, look, classes);
|
||||
break;
|
||||
case STMT_RELATION:
|
||||
{
|
||||
dataFetcher(
|
||||
parentParsedItem,
|
||||
item.state1,
|
||||
diagramStates,
|
||||
nodes,
|
||||
edges,
|
||||
altFlag,
|
||||
look,
|
||||
classes
|
||||
);
|
||||
dataFetcher(
|
||||
parentParsedItem,
|
||||
item.state2,
|
||||
diagramStates,
|
||||
nodes,
|
||||
edges,
|
||||
altFlag,
|
||||
look,
|
||||
classes
|
||||
);
|
||||
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,
|
||||
look,
|
||||
};
|
||||
edges.push(edgeData);
|
||||
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 (const parsedItemDoc of parsedItem.doc) {
|
||||
if (parsedItemDoc.stmt === 'dir') {
|
||||
dir = parsedItemDoc.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
};
|
||||
|
||||
function insertOrUpdateNode(nodes, nodeData, classes) {
|
||||
if (!nodeData.id || nodeData.id === '</join></fork>' || nodeData.id === '</choice>') {
|
||||
return;
|
||||
}
|
||||
|
||||
//Populate node style attributes if nodeData has classes defined
|
||||
if (nodeData.cssClasses) {
|
||||
if (!Array.isArray(nodeData.cssCompiledStyles)) {
|
||||
nodeData.cssCompiledStyles = [];
|
||||
}
|
||||
|
||||
nodeData.cssClasses.split(' ').forEach((cssClass) => {
|
||||
if (classes.get(cssClass)) {
|
||||
const classDef = classes.get(cssClass);
|
||||
nodeData.cssCompiledStyles = [...nodeData.cssCompiledStyles, ...classDef.styles];
|
||||
}
|
||||
});
|
||||
}
|
||||
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.classes) {
|
||||
let classStr = '';
|
||||
// for each class in classes, add it to the string as comma separated
|
||||
for (let i = 0; i < dbInfoItem.classes.length; i++) {
|
||||
//do not add comma for the last class
|
||||
if (i === dbInfoItem.classes.length - 1) {
|
||||
classStr += dbInfoItem.classes[i];
|
||||
}
|
||||
//add comma for all other classes
|
||||
else {
|
||||
classStr += dbInfoItem.classes[i] + ' ';
|
||||
}
|
||||
}
|
||||
return classStr;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function getStylesFromDbInfo(dbInfoItem) {
|
||||
if (dbInfoItem === undefined || dbInfoItem === null) {
|
||||
return;
|
||||
} else {
|
||||
if (dbInfoItem.styles) {
|
||||
return dbInfoItem.styles;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const dataFetcher = (
|
||||
parent,
|
||||
parsedItem,
|
||||
diagramStates,
|
||||
nodes,
|
||||
edges,
|
||||
altFlag,
|
||||
look,
|
||||
classes
|
||||
) => {
|
||||
const itemId = parsedItem.id;
|
||||
const dbState = diagramStates.get(itemId);
|
||||
const classStr = getClassesFromDbInfo(dbState);
|
||||
const style = getStylesFromDbInfo(dbState);
|
||||
|
||||
log.info('dataFetcher parsedItem', parsedItem, dbState, style);
|
||||
|
||||
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.get(itemId)) {
|
||||
nodeDb.set(itemId, {
|
||||
id: itemId,
|
||||
shape,
|
||||
description: common.sanitizeText(itemId, getConfig()),
|
||||
cssClasses: `${classStr} ${CSS_DIAGRAM_STATE}`,
|
||||
cssStyles: style,
|
||||
});
|
||||
}
|
||||
|
||||
const newNode = nodeDb.get(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) {
|
||||
if (newNode.type === 'group') {
|
||||
newNode.shape = SHAPE_GROUP;
|
||||
} else {
|
||||
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,
|
||||
cssCompiledStyles: [],
|
||||
cssStyles: newNode.cssStyles,
|
||||
id: itemId,
|
||||
dir: newNode.dir,
|
||||
domId: stateDomId(itemId, graphItemCount),
|
||||
type: newNode.type,
|
||||
isGroup: newNode.type === 'group',
|
||||
padding: 8,
|
||||
rx: 10,
|
||||
ry: 10,
|
||||
look,
|
||||
};
|
||||
|
||||
// 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: [],
|
||||
cssCompilesStyles: [],
|
||||
id: itemId + NOTE_ID + '-' + graphItemCount,
|
||||
domId: stateDomId(itemId, graphItemCount, NOTE),
|
||||
type: newNode.type,
|
||||
isGroup: newNode.type === 'group',
|
||||
padding: getConfig().flowchart.padding,
|
||||
look,
|
||||
position: parsedItem.note.position,
|
||||
};
|
||||
const parentNodeId = itemId + PARENT_ID;
|
||||
const groupData = {
|
||||
labelStyle: '',
|
||||
shape: SHAPE_NOTEGROUP,
|
||||
label: parsedItem.note.text,
|
||||
cssClasses: newNode.cssClasses,
|
||||
cssStyles: [],
|
||||
id: itemId + PARENT_ID,
|
||||
domId: stateDomId(itemId, graphItemCount, PARENT),
|
||||
type: 'group',
|
||||
isGroup: true,
|
||||
padding: 16, //getConfig().flowchart.padding
|
||||
look,
|
||||
position: parsedItem.note.position,
|
||||
};
|
||||
graphItemCount++;
|
||||
|
||||
//add parent id to groupData
|
||||
groupData.id = parentNodeId;
|
||||
//add parent id to noteData
|
||||
noteData.parentId = parentNodeId;
|
||||
//nodeData.parentId = parentNodeId;
|
||||
|
||||
//insert groupData
|
||||
insertOrUpdateNode(nodes, groupData, classes);
|
||||
//insert noteData
|
||||
insertOrUpdateNode(nodes, noteData, classes);
|
||||
//insert nodeData
|
||||
insertOrUpdateNode(nodes, nodeData, classes);
|
||||
|
||||
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,
|
||||
look,
|
||||
});
|
||||
} else {
|
||||
insertOrUpdateNode(nodes, nodeData, classes);
|
||||
}
|
||||
}
|
||||
if (parsedItem.doc) {
|
||||
log.trace('Adding nodes children ');
|
||||
setupDoc(parsedItem, parsedItem.doc, diagramStates, nodes, edges, !altFlag, look, classes);
|
||||
}
|
||||
};
|
||||
|
||||
export const reset = () => {
|
||||
nodeDb.clear();
|
||||
graphItemCount = 0;
|
||||
};
|
@ -217,4 +217,50 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('style statement for a state (style)', () => {
|
||||
describe('defining (style)', () => {
|
||||
it('has "style" as a keyword, an id, and can set a css style attribute', function () {
|
||||
stateDiagram.parser.parse(`stateDiagram-v2
|
||||
id1
|
||||
style id1 background:#bbb`);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const data4Layout = stateDiagram.parser.yy.getData();
|
||||
|
||||
expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']);
|
||||
});
|
||||
it('has "style" as a keyword, an id, and can set a css style attribute', function () {
|
||||
stateDiagram.parser.parse(`stateDiagram-v2
|
||||
id1
|
||||
id2
|
||||
style id1,id2 background:#bbb`);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const data4Layout = stateDiagram.parser.yy.getData();
|
||||
|
||||
expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']);
|
||||
expect(data4Layout.nodes[1].cssStyles).toEqual(['background:#bbb']);
|
||||
});
|
||||
|
||||
it('can define multiple attributes separated by commas', function () {
|
||||
stateDiagram.parser.parse(`stateDiagram-v2
|
||||
id1
|
||||
id2
|
||||
style id1,id2 background:#bbb, font-weight:bold, font-style:italic;`);
|
||||
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const data4Layout = stateDiagram.parser.yy.getData();
|
||||
|
||||
expect(data4Layout.nodes[0].cssStyles).toEqual([
|
||||
'background:#bbb',
|
||||
'font-weight:bold',
|
||||
'font-style:italic',
|
||||
]);
|
||||
expect(data4Layout.nodes[1].cssStyles).toEqual([
|
||||
'background:#bbb',
|
||||
'font-weight:bold',
|
||||
'font-style:italic',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -27,6 +27,13 @@
|
||||
%x CLASSDEFID
|
||||
%x CLASS
|
||||
%x CLASS_STYLE
|
||||
|
||||
// Style statement states
|
||||
%x STYLE
|
||||
%x STYLE_IDS
|
||||
%x STYLEDEF_STYLES
|
||||
%x STYLEDEF_STYLEOPTS
|
||||
|
||||
%x NOTE
|
||||
%x NOTE_ID
|
||||
%x NOTE_TEXT
|
||||
@ -75,6 +82,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
<CLASS>(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' }
|
||||
<CLASS_STYLE>[^\n]* { this.popState(); return 'STYLECLASS' }
|
||||
|
||||
<INITIAL,struct>"style"\s+ { this.pushState('STYLE'); return 'style'; }
|
||||
<STYLE>[\w,]+\s+ { this.popState(); this.pushState('STYLEDEF_STYLES'); return 'STYLE_IDS' }
|
||||
<STYLEDEF_STYLES>[^\n]* { this.popState(); return 'STYLEDEF_STYLEOPTS' }
|
||||
|
||||
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
|
||||
<SCALE>\d+ return 'WIDTH';
|
||||
<SCALE>\s+"width" {this.popState();}
|
||||
@ -168,6 +179,7 @@ line
|
||||
|
||||
statement
|
||||
: classDefStatement
|
||||
| styleStatement
|
||||
| cssClassStatement
|
||||
| idStatement { /* console.log('got id', $1); */
|
||||
$$=$1;
|
||||
@ -246,6 +258,12 @@ classDefStatement
|
||||
}
|
||||
;
|
||||
|
||||
styleStatement
|
||||
: style STYLE_IDS STYLEDEF_STYLEOPTS {
|
||||
$$ = { stmt: 'style', id: $2.trim(), styleClass: $3.trim() };
|
||||
}
|
||||
;
|
||||
|
||||
cssClassStatement
|
||||
: class CLASSENTITY_IDS STYLECLASS {
|
||||
//console.log('apply class: id(s): ',$2, ' style class: ', $3);
|
||||
|
@ -14,19 +14,90 @@ export const STMT_STATE = 'state';
|
||||
export const STMT_RELATION = 'relation';
|
||||
// parsed statement type for a classDef
|
||||
export const STMT_CLASSDEF = 'classDef';
|
||||
export const STMT_STYLEDEF = 'style';
|
||||
// parsed statement type for applyClass
|
||||
export const STMT_APPLYCLASS = 'applyClass';
|
||||
|
||||
export const DEFAULT_STATE_TYPE = 'default';
|
||||
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 {
|
||||
DEFAULT_DIAGRAM_DIRECTION,
|
||||
DEFAULT_NESTED_DOC_DIR,
|
||||
STMT_STATE,
|
||||
STMT_RELATION,
|
||||
STMT_CLASSDEF,
|
||||
STMT_STYLEDEF,
|
||||
STMT_APPLYCLASS,
|
||||
DEFAULT_STATE_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
G_EDGE_STYLE,
|
||||
G_EDGE_ARROWHEADSTYLE,
|
||||
G_EDGE_LABELPOS,
|
||||
G_EDGE_LABELTYPE,
|
||||
G_EDGE_THICKNESS,
|
||||
CSS_EDGE,
|
||||
CSS_DIAGRAM,
|
||||
SHAPE_STATE,
|
||||
SHAPE_STATE_WITH_DESC,
|
||||
SHAPE_START,
|
||||
SHAPE_END,
|
||||
SHAPE_DIVIDER,
|
||||
SHAPE_GROUP,
|
||||
SHAPE_NOTE,
|
||||
SHAPE_NOTEGROUP,
|
||||
CSS_STATE,
|
||||
CSS_DIAGRAM_STATE,
|
||||
CSS_NOTE,
|
||||
CSS_NOTE_EDGE,
|
||||
CSS_EDGE_NOTE_EDGE,
|
||||
CSS_DIAGRAM_NOTE,
|
||||
CSS_CLUSTER,
|
||||
CSS_DIAGRAM_CLUSTER,
|
||||
CSS_CLUSTER_ALT,
|
||||
CSS_DIAGRAM_CLUSTER_ALT,
|
||||
PARENT,
|
||||
NOTE,
|
||||
DOMID_STATE,
|
||||
DOMID_TYPE_SPACER,
|
||||
NOTE_ID,
|
||||
PARENT_ID,
|
||||
};
|
||||
|
@ -11,12 +11,15 @@ import {
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import { dataFetcher, reset as resetDataFetching } from './dataFetcher.js';
|
||||
import { getDir } from './stateRenderer-v3-unified.js';
|
||||
|
||||
import {
|
||||
DEFAULT_DIAGRAM_DIRECTION,
|
||||
STMT_STATE,
|
||||
STMT_RELATION,
|
||||
STMT_CLASSDEF,
|
||||
STMT_STYLEDEF,
|
||||
STMT_APPLYCLASS,
|
||||
DEFAULT_STATE_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
@ -43,10 +46,15 @@ function newClassesList() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
let nodes = [];
|
||||
let edges = [];
|
||||
|
||||
let direction = DEFAULT_DIAGRAM_DIRECTION;
|
||||
let rootDoc = [];
|
||||
let classes = newClassesList(); // style classes defined by a classDef
|
||||
|
||||
// --------------------------------------
|
||||
|
||||
const newDoc = () => {
|
||||
return {
|
||||
/** @type {{ id1: string, id2: string, relationTitle: string }[]} */
|
||||
@ -165,9 +173,10 @@ const extract = (_doc) => {
|
||||
log.info(doc);
|
||||
clear(true);
|
||||
|
||||
log.info('Extract', doc);
|
||||
log.info('Extract initial document:', doc);
|
||||
|
||||
doc.forEach((item) => {
|
||||
log.warn('Statement', item.stmt);
|
||||
switch (item.stmt) {
|
||||
case STMT_STATE:
|
||||
addState(
|
||||
@ -187,11 +196,47 @@ const extract = (_doc) => {
|
||||
case STMT_CLASSDEF:
|
||||
addStyleClass(item.id.trim(), item.classes);
|
||||
break;
|
||||
case STMT_STYLEDEF:
|
||||
{
|
||||
const ids = item.id.trim().split(',');
|
||||
const styles = item.styleClass.split(',');
|
||||
ids.forEach((id) => {
|
||||
let foundState = getState(id);
|
||||
if (foundState === undefined) {
|
||||
const trimmedId = id.trim();
|
||||
addState(trimmedId);
|
||||
foundState = getState(trimmedId);
|
||||
}
|
||||
foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim());
|
||||
});
|
||||
}
|
||||
break;
|
||||
case STMT_APPLYCLASS:
|
||||
setCssClass(item.id.trim(), item.styleClass);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const diagramStates = getStates();
|
||||
const config = getConfig();
|
||||
const look = config.look;
|
||||
resetDataFetching();
|
||||
dataFetcher(undefined, getRootDocV2(), diagramStates, nodes, edges, true, look, classes);
|
||||
nodes.forEach((node) => {
|
||||
if (Array.isArray(node.label)) {
|
||||
// add the rest as description
|
||||
node.description = node.label.slice(1);
|
||||
if (node.isGroup && node.description.length > 0) {
|
||||
throw new Error(
|
||||
'Group nodes can only have label. Remove the additional description for node [' +
|
||||
node.id +
|
||||
']'
|
||||
);
|
||||
}
|
||||
// add first description as label
|
||||
node.label = node.label[0];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -276,6 +321,8 @@ export const addState = function (
|
||||
};
|
||||
|
||||
export const clear = function (saveCommon) {
|
||||
nodes = [];
|
||||
edges = [];
|
||||
documents = {
|
||||
root: newDoc(),
|
||||
};
|
||||
@ -516,7 +563,7 @@ export const setCssClass = function (itemIds, cssClassName) {
|
||||
export const setStyle = function (itemId, styleText) {
|
||||
const item = getState(itemId);
|
||||
if (item !== undefined) {
|
||||
item.textStyles.push(styleText);
|
||||
item.styles.push(styleText);
|
||||
}
|
||||
};
|
||||
|
||||
@ -540,8 +587,14 @@ const setDirection = (dir) => {
|
||||
|
||||
const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
|
||||
|
||||
export const getData = () => {
|
||||
const config = getConfig();
|
||||
return { nodes, edges, other: {}, config, direction: getDir(getRootDocV2()) };
|
||||
};
|
||||
|
||||
export default {
|
||||
getConfig: () => getConfig().state,
|
||||
getData,
|
||||
addState,
|
||||
clear,
|
||||
getState,
|
||||
|
@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import parser from './parser/stateDiagram.jison';
|
||||
import db from './stateDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './stateRenderer-v2.js';
|
||||
import renderer from './stateRenderer-v3-unified.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
|
@ -1,479 +0,0 @@
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { select } from 'd3';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import common from '../common/common.js';
|
||||
import utils, { getEdgeId } from '../../utils.js';
|
||||
|
||||
import {
|
||||
DEFAULT_DIAGRAM_DIRECTION,
|
||||
DEFAULT_NESTED_DOC_DIR,
|
||||
STMT_STATE,
|
||||
STMT_RELATION,
|
||||
DEFAULT_STATE_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
} from './stateCommon.js';
|
||||
|
||||
// --------------------------------------
|
||||
// Shapes
|
||||
const SHAPE_STATE = 'rect';
|
||||
const SHAPE_STATE_WITH_DESC = 'rectWithTitle';
|
||||
const SHAPE_START = 'start';
|
||||
const SHAPE_END = 'end';
|
||||
const SHAPE_DIVIDER = 'divider';
|
||||
const SHAPE_GROUP = 'roundedWithTitle';
|
||||
const SHAPE_NOTE = 'note';
|
||||
const SHAPE_NOTEGROUP = 'noteGroup';
|
||||
|
||||
// --------------------------------------
|
||||
// CSS classes
|
||||
const CSS_DIAGRAM = 'statediagram';
|
||||
const CSS_STATE = 'state';
|
||||
const CSS_DIAGRAM_STATE = `${CSS_DIAGRAM}-${CSS_STATE}`;
|
||||
const CSS_EDGE = 'transition';
|
||||
const CSS_NOTE = 'note';
|
||||
const CSS_NOTE_EDGE = 'note-edge';
|
||||
const CSS_EDGE_NOTE_EDGE = `${CSS_EDGE} ${CSS_NOTE_EDGE}`;
|
||||
const CSS_DIAGRAM_NOTE = `${CSS_DIAGRAM}-${CSS_NOTE}`;
|
||||
const CSS_CLUSTER = 'cluster';
|
||||
const CSS_DIAGRAM_CLUSTER = `${CSS_DIAGRAM}-${CSS_CLUSTER}`;
|
||||
const CSS_CLUSTER_ALT = 'cluster-alt';
|
||||
const CSS_DIAGRAM_CLUSTER_ALT = `${CSS_DIAGRAM}-${CSS_CLUSTER_ALT}`;
|
||||
|
||||
// --------------------------------------
|
||||
// DOM and element IDs
|
||||
const PARENT = 'parent';
|
||||
const NOTE = 'note';
|
||||
const DOMID_STATE = 'state';
|
||||
const DOMID_TYPE_SPACER = '----';
|
||||
const NOTE_ID = `${DOMID_TYPE_SPACER}${NOTE}`;
|
||||
const PARENT_ID = `${DOMID_TYPE_SPACER}${PARENT}`;
|
||||
// --------------------------------------
|
||||
// Graph edge settings
|
||||
const G_EDGE_STYLE = 'fill:none';
|
||||
const G_EDGE_ARROWHEADSTYLE = 'fill: #333';
|
||||
const G_EDGE_LABELPOS = 'c';
|
||||
const G_EDGE_LABELTYPE = 'text';
|
||||
const G_EDGE_THICKNESS = 'normal';
|
||||
|
||||
// --------------------------------------
|
||||
// List of nodes created from the parsed diagram statement items
|
||||
let nodeDb = {};
|
||||
|
||||
let graphItemCount = 0; // used to construct ids, etc.
|
||||
|
||||
// Configuration
|
||||
const conf = {};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
export const setConf = function (cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (const key of keys) {
|
||||
conf[key] = cnf[key];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the all the classdef styles (a.k.a. classes) from classDef statements in the graph definition.
|
||||
*
|
||||
* @param {string} text - the diagram text to be parsed
|
||||
* @param diagramObj
|
||||
* @returns {Map<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles (a Map with keys = strings, values = )
|
||||
*/
|
||||
export const getClasses = function (text, diagramObj) {
|
||||
diagramObj.db.extract(diagramObj.db.getRootDocV2());
|
||||
return diagramObj.db.getClasses();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.classes) {
|
||||
return dbInfoItem.classes.join(' ');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a graph node based on the statement information
|
||||
*
|
||||
* @param g - graph
|
||||
* @param {object} parent
|
||||
* @param {object} parsedItem - parsed statement item
|
||||
* @param {Map<string, object>} diagramStates - the list of all known states for the diagram
|
||||
* @param {object} diagramDb
|
||||
* @param {boolean} altFlag - for clusters, add the "statediagram-cluster-alt" CSS class
|
||||
*/
|
||||
const setupNode = (g, parent, parsedItem, diagramStates, diagramDb, altFlag) => {
|
||||
const itemId = parsedItem.id;
|
||||
const classStr = getClassesFromDbInfo(diagramStates.get(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()),
|
||||
classes: `${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 ', itemId, getDir(parsedItem));
|
||||
newNode.type = 'group';
|
||||
newNode.dir = getDir(parsedItem);
|
||||
newNode.shape = parsedItem.type === DIVIDER_TYPE ? SHAPE_DIVIDER : SHAPE_GROUP;
|
||||
newNode.classes =
|
||||
newNode.classes +
|
||||
' ' +
|
||||
CSS_DIAGRAM_CLUSTER +
|
||||
' ' +
|
||||
(altFlag ? CSS_DIAGRAM_CLUSTER_ALT : '');
|
||||
}
|
||||
|
||||
// This is what will be added to the graph
|
||||
const nodeData = {
|
||||
labelStyle: '',
|
||||
shape: newNode.shape,
|
||||
labelText: newNode.description,
|
||||
// typeof newNode.description === 'object'
|
||||
// ? newNode.description[0]
|
||||
// : newNode.description,
|
||||
classes: newNode.classes,
|
||||
style: '', //styles.style,
|
||||
id: itemId,
|
||||
dir: newNode.dir,
|
||||
domId: stateDomId(itemId, graphItemCount),
|
||||
type: newNode.type,
|
||||
padding: 15, //getConfig().flowchart.padding
|
||||
};
|
||||
// if (useHtmlLabels) {
|
||||
nodeData.centerLabel = true;
|
||||
// }
|
||||
|
||||
if (parsedItem.note) {
|
||||
// Todo: set random id
|
||||
const noteData = {
|
||||
labelStyle: '',
|
||||
shape: SHAPE_NOTE,
|
||||
labelText: parsedItem.note.text,
|
||||
classes: CSS_DIAGRAM_NOTE,
|
||||
// useHtmlLabels: false,
|
||||
style: '', // styles.style,
|
||||
id: itemId + NOTE_ID + '-' + graphItemCount,
|
||||
domId: stateDomId(itemId, graphItemCount, NOTE),
|
||||
type: newNode.type,
|
||||
padding: 15, //getConfig().flowchart.padding
|
||||
};
|
||||
const groupData = {
|
||||
labelStyle: '',
|
||||
shape: SHAPE_NOTEGROUP,
|
||||
labelText: parsedItem.note.text,
|
||||
classes: newNode.classes,
|
||||
style: '', // styles.style,
|
||||
id: itemId + PARENT_ID,
|
||||
domId: stateDomId(itemId, graphItemCount, PARENT),
|
||||
type: 'group',
|
||||
padding: 0, //getConfig().flowchart.padding
|
||||
};
|
||||
|
||||
const parentNodeId = itemId + PARENT_ID;
|
||||
g.setNode(parentNodeId, groupData);
|
||||
|
||||
g.setNode(noteData.id, noteData);
|
||||
g.setNode(itemId, nodeData);
|
||||
|
||||
g.setParent(itemId, parentNodeId);
|
||||
g.setParent(noteData.id, parentNodeId);
|
||||
|
||||
let from = itemId;
|
||||
let to = noteData.id;
|
||||
|
||||
if (parsedItem.note.position === 'left of') {
|
||||
from = noteData.id;
|
||||
to = itemId;
|
||||
}
|
||||
|
||||
g.setEdge(from, to, {
|
||||
arrowhead: 'none',
|
||||
arrowType: '',
|
||||
style: G_EDGE_STYLE,
|
||||
labelStyle: '',
|
||||
id: getEdgeId(from, to, {
|
||||
counter: graphItemCount,
|
||||
}),
|
||||
classes: CSS_EDGE_NOTE_EDGE,
|
||||
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
|
||||
labelpos: G_EDGE_LABELPOS,
|
||||
labelType: G_EDGE_LABELTYPE,
|
||||
thickness: G_EDGE_THICKNESS,
|
||||
});
|
||||
|
||||
graphItemCount++;
|
||||
} else {
|
||||
g.setNode(itemId, nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
if (parent && parent.id !== 'root') {
|
||||
log.trace('Setting node ', itemId, ' to be child of its parent ', parent.id);
|
||||
g.setParent(itemId, parent.id);
|
||||
}
|
||||
if (parsedItem.doc) {
|
||||
log.trace('Adding nodes children ');
|
||||
setupDoc(g, parsedItem, parsedItem.doc, diagramStates, diagramDb, !altFlag);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn parsed statements (item.stmt) into nodes, relationships, etc. for a document.
|
||||
* (A document may be nested within others.)
|
||||
*
|
||||
* @param g
|
||||
* @param parentParsedItem - parsed Item that is the parent of this document (doc)
|
||||
* @param doc - the document to set up; it is a list of parsed statements
|
||||
* @param {Map<string, object>} diagramStates - the list of all known states for the diagram
|
||||
* @param diagramDb
|
||||
* @param {boolean} altFlag
|
||||
* @todo This duplicates some of what is done in stateDb.js extract method
|
||||
*/
|
||||
const setupDoc = (g, parentParsedItem, doc, diagramStates, diagramDb, altFlag) => {
|
||||
// graphItemCount = 0;
|
||||
log.trace('items', doc);
|
||||
doc.forEach((item) => {
|
||||
switch (item.stmt) {
|
||||
case STMT_STATE:
|
||||
setupNode(g, parentParsedItem, item, diagramStates, diagramDb, altFlag);
|
||||
break;
|
||||
case DEFAULT_STATE_TYPE:
|
||||
setupNode(g, parentParsedItem, item, diagramStates, diagramDb, altFlag);
|
||||
break;
|
||||
case STMT_RELATION:
|
||||
{
|
||||
setupNode(g, parentParsedItem, item.state1, diagramStates, diagramDb, altFlag);
|
||||
setupNode(g, parentParsedItem, item.state2, diagramStates, diagramDb, altFlag);
|
||||
const edgeData = {
|
||||
id: getEdgeId(item.state1.id, item.state2.id, {
|
||||
counter: graphItemCount,
|
||||
}),
|
||||
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,
|
||||
};
|
||||
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 (const parsedItemDoc of parsedItem.doc) {
|
||||
if (parsedItemDoc.stmt === 'dir') {
|
||||
dir = parsedItemDoc.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a state diagram in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param {any} text
|
||||
* @param {any} id
|
||||
* @param _version
|
||||
* @param diag
|
||||
*/
|
||||
export const draw = async function (text, id, _version, diag) {
|
||||
log.info('Drawing state diagram (v2)', id);
|
||||
nodeDb = {};
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = diag.db.getDirection();
|
||||
if (dir === undefined) {
|
||||
dir = DEFAULT_DIAGRAM_DIRECTION;
|
||||
}
|
||||
|
||||
const { securityLevel, state: conf } = getConfig();
|
||||
const nodeSpacing = conf.nodeSpacing || 50;
|
||||
const rankSpacing = conf.rankSpacing || 50;
|
||||
|
||||
log.info(diag.db.getRootDocV2());
|
||||
|
||||
// This parses the diagram text and sets the classes, relations, styles, classDefs, etc.
|
||||
diag.db.extract(diag.db.getRootDocV2());
|
||||
log.info(diag.db.getRootDocV2());
|
||||
|
||||
const diagramStates = diag.db.getStates();
|
||||
|
||||
// Create the input mermaid.graph
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: getDir(diag.db.getRootDocV2()),
|
||||
nodesep: nodeSpacing,
|
||||
ranksep: rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
setupNode(g, undefined, diag.db.getRootDocV2(), diagramStates, diag.db, true);
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
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.
|
||||
|
||||
const element = root.select('#' + id + ' g');
|
||||
await render(element, g, ['barb'], CSS_DIAGRAM, id);
|
||||
|
||||
const padding = 8;
|
||||
|
||||
utils.insertTitle(svg, 'statediagramTitleText', conf.titleTopMargin, diag.db.getDiagramTitle());
|
||||
|
||||
const bounds = svg.node().getBBox();
|
||||
const width = bounds.width + padding * 2;
|
||||
const height = bounds.height + padding * 2;
|
||||
|
||||
// Zoom in a bit
|
||||
svg.attr('class', CSS_DIAGRAM);
|
||||
|
||||
const svgBounds = svg.node().getBBox();
|
||||
|
||||
configureSvgSize(svg, height, width, conf.useMaxWidth);
|
||||
|
||||
// Ensure the viewBox includes the whole svgBounds area with extra space for padding
|
||||
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`;
|
||||
log.debug(`viewBox ${vBox}`);
|
||||
svg.attr('viewBox', vBox);
|
||||
|
||||
// Add label rects for non html labels
|
||||
// if (!evaluate(conf.htmlLabels) || true) {
|
||||
const labels = document.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
|
||||
for (const label of labels) {
|
||||
// Get dimensions of label
|
||||
const dim = label.getBBox();
|
||||
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', SHAPE_STATE);
|
||||
rect.setAttribute('rx', 0);
|
||||
rect.setAttribute('ry', 0);
|
||||
rect.setAttribute('width', dim.width);
|
||||
rect.setAttribute('height', dim.height);
|
||||
|
||||
label.insertBefore(rect, label.firstChild);
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
getClasses,
|
||||
draw,
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { expectTypeOf } from 'vitest';
|
||||
|
||||
import { parser } from './parser/stateDiagram.jison';
|
||||
import stateDb from './stateDb.js';
|
||||
import stateRendererV2 from './stateRenderer-v2.js';
|
||||
|
||||
// Can use this instead of having to register diagrams and load/orchestrate them, etc.
|
||||
class FauxDiagramObj {
|
||||
db = stateDb;
|
||||
parser = parser;
|
||||
renderer = stateRendererV2;
|
||||
|
||||
constructor(options = { db: stateDb, parser: parser, renderer: stateRendererV2 }) {
|
||||
this.db = options.db;
|
||||
this.parser = options.parser;
|
||||
this.renderer = options.renderer;
|
||||
this.parser.yy = this.db;
|
||||
}
|
||||
}
|
||||
|
||||
describe('stateRenderer-v2', () => {
|
||||
describe('getClasses', () => {
|
||||
const diagramText = 'statediagram-v2\n';
|
||||
const fauxStateDiagram = new FauxDiagramObj();
|
||||
|
||||
it('returns a {}', () => {
|
||||
const result = stateRendererV2.getClasses(diagramText, fauxStateDiagram);
|
||||
expectTypeOf(result).toBeObject();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,85 @@
|
||||
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
|
||||
*/
|
||||
export const getDir = (parsedItem: any, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
|
||||
if (!parsedItem.doc) {
|
||||
return defaultDir;
|
||||
}
|
||||
|
||||
let dir = defaultDir;
|
||||
|
||||
for (const parsedItemDoc of parsedItem.doc) {
|
||||
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);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = layout;
|
||||
|
||||
// TODO: Should we move these two to baseConfig? These types are not there in StateConfig.
|
||||
|
||||
data4Layout.nodeSpacing = conf?.nodeSpacing || 50;
|
||||
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,
|
||||
getDir,
|
||||
};
|
@ -68,6 +68,18 @@ g.stateGroup line {
|
||||
fill: ${options.labelBackgroundColor};
|
||||
opacity: 0.5;
|
||||
}
|
||||
.edgeLabel {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
p {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
}
|
||||
rect {
|
||||
opacity: 0.5;
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
fill: ${options.edgeLabelBackground};
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
.edgeLabel .label text {
|
||||
fill: ${options.transitionLabelColor || options.tertiaryTextColor};
|
||||
}
|
||||
@ -124,6 +136,7 @@ g.stateGroup line {
|
||||
|
||||
.cluster-label, .nodeLabel {
|
||||
color: ${options.stateLabelColor};
|
||||
// line-height: 1;
|
||||
}
|
||||
|
||||
.statediagram-cluster rect.outer {
|
||||
|
31
packages/mermaid/src/internals.ts
Normal file
31
packages/mermaid/src/internals.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { getConfig } from './config.js';
|
||||
import common from './diagrams/common/common.js';
|
||||
import { log } from './logger.js';
|
||||
import { insertCluster } from './rendering-util/rendering-elements/clusters.js';
|
||||
import {
|
||||
insertEdge,
|
||||
insertEdgeLabel,
|
||||
positionEdgeLabel,
|
||||
} from './rendering-util/rendering-elements/edges.js';
|
||||
import insertMarkers from './rendering-util/rendering-elements/markers.js';
|
||||
import { insertNode } from './rendering-util/rendering-elements/nodes.js';
|
||||
import { labelHelper } from './rendering-util/rendering-elements/shapes/util.js';
|
||||
import { interpolateToCurve } from './utils.js';
|
||||
|
||||
/**
|
||||
* Internal helpers for mermaid
|
||||
* @deprecated - This should not be used by external packages, as the definitions will change without notice.
|
||||
*/
|
||||
export const internalHelpers = {
|
||||
common,
|
||||
getConfig,
|
||||
insertCluster,
|
||||
insertEdge,
|
||||
insertEdgeLabel,
|
||||
insertMarkers,
|
||||
insertNode,
|
||||
interpolateToCurve,
|
||||
labelHelper,
|
||||
log,
|
||||
positionEdgeLabel,
|
||||
};
|
@ -16,6 +16,10 @@ import type { DetailedError } from './utils.js';
|
||||
import type { ExternalDiagramDefinition } from './diagram-api/types.js';
|
||||
import type { UnknownDiagramError } from './errors.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';
|
||||
import type { LayoutData } from './rendering-util/types.js';
|
||||
|
||||
export type {
|
||||
MermaidConfig,
|
||||
@ -26,6 +30,8 @@ export type {
|
||||
ParseOptions,
|
||||
ParseResult,
|
||||
UnknownDiagramError,
|
||||
LayoutLoaderDefinition,
|
||||
LayoutData,
|
||||
};
|
||||
|
||||
export interface RunOptions {
|
||||
@ -420,11 +426,17 @@ export interface Mermaid {
|
||||
*/
|
||||
init: typeof init;
|
||||
run: typeof run;
|
||||
registerLayoutLoaders: typeof registerLayoutLoaders;
|
||||
registerExternalDiagrams: typeof registerExternalDiagrams;
|
||||
initialize: typeof initialize;
|
||||
contentLoaded: typeof contentLoaded;
|
||||
setParseErrorHandler: typeof setParseErrorHandler;
|
||||
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 = {
|
||||
@ -435,11 +447,13 @@ const mermaid: Mermaid = {
|
||||
init,
|
||||
run,
|
||||
registerExternalDiagrams,
|
||||
registerLayoutLoaders,
|
||||
initialize,
|
||||
parseError: undefined,
|
||||
contentLoaded,
|
||||
setParseErrorHandler,
|
||||
detectType,
|
||||
internalHelpers,
|
||||
};
|
||||
|
||||
export default mermaid;
|
||||
|
@ -30,7 +30,6 @@ vi.mock('./diagrams/packet/renderer.js');
|
||||
vi.mock('./diagrams/xychart/xychartRenderer.js');
|
||||
vi.mock('./diagrams/requirement/requirementRenderer.js');
|
||||
vi.mock('./diagrams/sequence/sequenceRenderer.js');
|
||||
vi.mock('./diagrams/state/stateRenderer-v2.js');
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
|
@ -145,7 +145,11 @@ export const createCssStyles = (
|
||||
}
|
||||
// create the css styles for the tspan element and the text styles (only if there are textStyles)
|
||||
if (!isEmpty(styleClassDef.textStyles)) {
|
||||
cssStyles += cssImportantStyles(styleClassDef.id, 'tspan', styleClassDef.textStyles);
|
||||
cssStyles += cssImportantStyles(
|
||||
styleClassDef.id,
|
||||
'tspan',
|
||||
(styleClassDef?.textStyles || []).map((s) => s.replace('color', 'fill'))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2,12 +2,15 @@
|
||||
// @ts-nocheck TODO: Fix types
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import type { Group } from '../diagram-api/types.js';
|
||||
import { select } from 'd3';
|
||||
import type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js';
|
||||
import { log } from '../logger.js';
|
||||
import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js';
|
||||
import { decodeEntities } from '../utils.js';
|
||||
import { splitLineToFitWidth } from './splitText.js';
|
||||
import type { MarkdownLine, MarkdownWord } from './types.js';
|
||||
import common, { hasKatex, renderKatex } from '$root/diagrams/common/common.js';
|
||||
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||
|
||||
function applyStyle(dom, styleFn) {
|
||||
if (styleFn) {
|
||||
@ -15,11 +18,13 @@ function applyStyle(dom, styleFn) {
|
||||
}
|
||||
}
|
||||
|
||||
function addHtmlSpan(element, node, width, classes, addBackground = false) {
|
||||
async function addHtmlSpan(element, node, width, classes, addBackground = false) {
|
||||
const fo = element.append('foreignObject');
|
||||
const div = fo.append('xhtml:div');
|
||||
|
||||
const label = node.label;
|
||||
let label = node.label;
|
||||
if (node.label && hasKatex(node.label)) {
|
||||
label = await renderKatex(node.label.replace(common.lineBreakRegex, '\n'), getConfig());
|
||||
}
|
||||
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
|
||||
const span = div.append('span');
|
||||
span.html(label);
|
||||
@ -29,6 +34,7 @@ function addHtmlSpan(element, node, width, classes, addBackground = false) {
|
||||
applyStyle(div, node.labelStyle);
|
||||
div.style('display', 'table-cell');
|
||||
div.style('white-space', 'nowrap');
|
||||
div.style('line-height', '1.5');
|
||||
div.style('max-width', width + 'px');
|
||||
div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
|
||||
if (addBackground) {
|
||||
@ -43,8 +49,8 @@ function addHtmlSpan(element, node, width, classes, addBackground = false) {
|
||||
bbox = div.node().getBoundingClientRect();
|
||||
}
|
||||
|
||||
fo.style('width', bbox.width);
|
||||
fo.style('height', bbox.height);
|
||||
// fo.style('width', bbox.width);
|
||||
// fo.style('height', bbox.height);
|
||||
|
||||
return fo.node();
|
||||
}
|
||||
@ -107,7 +113,7 @@ function createFormattedText(
|
||||
) {
|
||||
const lineHeight = 1.1;
|
||||
const labelGroup = g.append('g');
|
||||
const bkg = labelGroup.insert('rect').attr('class', 'background');
|
||||
const bkg = labelGroup.insert('rect').attr('class', 'background').attr('style', 'stroke: none');
|
||||
const textElement = labelGroup.append('text').attr('y', '-10.1');
|
||||
let lineIndex = 0;
|
||||
for (const line of structuredText) {
|
||||
@ -180,7 +186,7 @@ export function replaceIconSubstring(text: string) {
|
||||
|
||||
// Note when using from flowcharts converting the API isNode means classes should be set accordingly. When using htmlLabels => to sett classes to'nodeLabel' when isNode=true otherwise 'edgeLabel'
|
||||
// When not using htmlLabels => to set classes to 'title-row' when isTitle=true otherwise 'title-row'
|
||||
export const createText = (
|
||||
export const createText = async (
|
||||
el,
|
||||
text = '',
|
||||
{
|
||||
@ -194,22 +200,77 @@ export const createText = (
|
||||
} = {},
|
||||
config: MermaidConfig
|
||||
) => {
|
||||
log.info('createText', text, style, isTitle, classes, useHtmlLabels, isNode, addSvgBackground);
|
||||
log.info(
|
||||
'XYZ createText',
|
||||
text,
|
||||
style,
|
||||
isTitle,
|
||||
classes,
|
||||
useHtmlLabels,
|
||||
isNode,
|
||||
'addSvgBackground: ',
|
||||
addSvgBackground
|
||||
);
|
||||
if (useHtmlLabels) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
|
||||
const htmlText = markdownToHTML(text, config);
|
||||
const decodedReplacedText = replaceIconSubstring(decodeEntities(htmlText));
|
||||
|
||||
//for Katex the text could contain escaped characters, \\relax that should be transformed to \relax
|
||||
const inputForKatex = text.replace(/\\\\/g, '\\');
|
||||
|
||||
const node = {
|
||||
isNode,
|
||||
label: decodedReplacedText,
|
||||
label: hasKatex(text) ? inputForKatex : decodedReplacedText,
|
||||
labelStyle: style.replace('fill:', 'color:'),
|
||||
};
|
||||
const vertexNode = addHtmlSpan(el, node, width, classes, addSvgBackground);
|
||||
const vertexNode = await addHtmlSpan(el, node, width, classes, addSvgBackground);
|
||||
return vertexNode;
|
||||
} else {
|
||||
const structuredText = markdownToLines(text, config);
|
||||
const svgLabel = createFormattedText(width, el, structuredText, addSvgBackground);
|
||||
//sometimes the user might add br tags with 1 or more spaces in between, so we need to replace them with <br/>
|
||||
const sanitizeBR = text.replace(/<br\s*\/?>/g, '<br/>');
|
||||
const structuredText = markdownToLines(sanitizeBR.replace('<br>', '<br/>'), config);
|
||||
const svgLabel = createFormattedText(
|
||||
width,
|
||||
el,
|
||||
structuredText,
|
||||
text ? addSvgBackground : false
|
||||
);
|
||||
if (isNode) {
|
||||
if (/stroke:/.exec(style)) {
|
||||
style = style.replace('stroke:', 'lineColor:');
|
||||
}
|
||||
|
||||
const nodeLabelTextStyle = style
|
||||
.replace(/stroke:[^;]+;?/g, '')
|
||||
.replace(/stroke-width:[^;]+;?/g, '')
|
||||
.replace(/fill:[^;]+;?/g, '')
|
||||
.replace(/color:/g, 'fill:');
|
||||
select(svgLabel).attr('style', nodeLabelTextStyle);
|
||||
// svgLabel.setAttribute('style', style);
|
||||
} else {
|
||||
//On style, assume `stroke`, `stroke-width` are used for edge path, so remove them
|
||||
// remove `fill`
|
||||
// use `background` as `fill` for label rect,
|
||||
|
||||
const edgeLabelRectStyle = style
|
||||
.replace(/stroke:[^;]+;?/g, '')
|
||||
.replace(/stroke-width:[^;]+;?/g, '')
|
||||
.replace(/fill:[^;]+;?/g, '')
|
||||
.replace(/background:/g, 'fill:');
|
||||
select(svgLabel)
|
||||
.select('rect')
|
||||
.attr('style', edgeLabelRectStyle.replace(/background:/g, 'fill:'));
|
||||
|
||||
// for text, update fill color with `color`
|
||||
const edgeLabelTextStyle = style
|
||||
.replace(/stroke:[^;]+;?/g, '')
|
||||
.replace(/stroke-width:[^;]+;?/g, '')
|
||||
.replace(/fill:[^;]+;?/g, '')
|
||||
.replace(/color:/g, 'fill:');
|
||||
select(svgLabel).select('text').attr('style', edgeLabelTextStyle);
|
||||
}
|
||||
return svgLabel;
|
||||
}
|
||||
};
|
||||
|
@ -9,8 +9,10 @@ import type { MermaidConfig } from '../config.type.js';
|
||||
* @returns processed markdown
|
||||
*/
|
||||
function preprocessMarkdown(markdown: string, { markdownAutoWrap }: MermaidConfig): string {
|
||||
//Replace <br/>with \n
|
||||
const withoutBR = markdown.replace(/<br\/>/g, '\n');
|
||||
// Replace multiple newlines with a single newline
|
||||
const withoutMultipleNewlines = markdown.replace(/\n{2,}/g, '\n');
|
||||
const withoutMultipleNewlines = withoutBR.replace(/\n{2,}/g, '\n');
|
||||
// Remove extra spaces at the beginning of each line
|
||||
const withoutExtraSpaces = dedent(withoutMultipleNewlines);
|
||||
if (markdownAutoWrap === false) {
|
||||
@ -46,6 +48,8 @@ export function markdownToLines(markdown: string, config: MermaidConfig = {}): M
|
||||
node.tokens.forEach((contentNode) => {
|
||||
processNode(contentNode as MarkedToken, node.type);
|
||||
});
|
||||
} else if (node.type === 'html') {
|
||||
lines[currentLine].push({ content: node.text, type: 'normal' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +58,8 @@ export function markdownToLines(markdown: string, config: MermaidConfig = {}): M
|
||||
treeNode.tokens?.forEach((contentNode) => {
|
||||
processNode(contentNode as MarkedToken);
|
||||
});
|
||||
} else if (treeNode.type === 'html') {
|
||||
lines[currentLine].push({ content: treeNode.text, type: 'normal' });
|
||||
}
|
||||
});
|
||||
|
||||
@ -75,6 +81,10 @@ export function markdownToHTML(markdown: string, { markdownAutoWrap }: MermaidCo
|
||||
return `<em>${node.tokens?.map(output).join('')}</em>`;
|
||||
} else if (node.type === 'paragraph') {
|
||||
return `<p>${node.tokens?.map(output).join('')}</p>`;
|
||||
} else if (node.type === 'space') {
|
||||
return '';
|
||||
} else if (node.type === 'html') {
|
||||
return `${node.text}`;
|
||||
}
|
||||
return `Unsupported markdown: ${node.type}`;
|
||||
}
|
||||
|
54
packages/mermaid/src/rendering-util/insertElementsForSize.js
Normal file
54
packages/mermaid/src/rendering-util/insertElementsForSize.js
Normal file
@ -0,0 +1,54 @@
|
||||
import { select } from 'd3';
|
||||
import { insertNode } from '../dagre-wrapper/nodes.js';
|
||||
|
||||
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, data) {
|
||||
const nodesElem = el.insert('g').attr('class', 'nodes');
|
||||
el.insert('g').attr('class', 'edges');
|
||||
data.nodes.forEach(async (item) => {
|
||||
item.shape = 'rect';
|
||||
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);
|
||||
});
|
||||
}
|
@ -0,0 +1,323 @@
|
||||
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?.clusterNode) {
|
||||
// const children = graph.children(v);
|
||||
log.info('Cluster identified XBX', v, node.width, graph.node(v));
|
||||
|
||||
// `node.graph.setGraph` applies the graph configurations such as nodeSpacing to subgraphs as without this the default values would be used
|
||||
// We override only the `ranksep` and `nodesep` configurations to allow for setting subgraph spacing while avoiding overriding other properties
|
||||
const { ranksep, nodesep } = graph.graph();
|
||||
node.graph.setGraph({
|
||||
...node.graph.graph(),
|
||||
ranksep: ranksep + 25,
|
||||
nodesep,
|
||||
});
|
||||
|
||||
// "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.height = o.diff;
|
||||
node.diff = o.diff || 0;
|
||||
log.info(
|
||||
'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 XBX',
|
||||
v,
|
||||
node.id,
|
||||
node,
|
||||
node.width,
|
||||
'Graph:',
|
||||
graph
|
||||
);
|
||||
log.info(findNonClusterChild(node.id, graph));
|
||||
clusterDb.set(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);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const processEdges = async () => {
|
||||
const edgePromises = graph.edges().map(async 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.get(e.v),
|
||||
clusterDb.get(e.w)
|
||||
);
|
||||
await insertEdgeLabel(edgeLabels, edge);
|
||||
});
|
||||
|
||||
await Promise.all(edgePromises);
|
||||
};
|
||||
|
||||
await processEdges();
|
||||
|
||||
log.info('Graph before layout:', JSON.stringify(graphlibJson.write(graph)));
|
||||
|
||||
log.info('############################################# XXX');
|
||||
log.info('### Layout ### XXX');
|
||||
log.info('############################################# XXX');
|
||||
|
||||
dagreLayout(graph);
|
||||
|
||||
log.info('Graph after layout:', JSON.stringify(graphlibJson.write(graph)));
|
||||
// Move the nodes to the correct place
|
||||
let diff = 0;
|
||||
let { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
await Promise.all(
|
||||
sortNodesByHierarchy(graph).map(async function (v) {
|
||||
const node = graph.node(v);
|
||||
log.info(
|
||||
'Position XBX => ' + v + ': (' + node.x,
|
||||
',' + node.y,
|
||||
') width: ',
|
||||
node.width,
|
||||
' height: ',
|
||||
node.height
|
||||
);
|
||||
if (node?.clusterNode) {
|
||||
// Adjust for padding when on root level
|
||||
node.y += subGraphTitleTotalMargin;
|
||||
|
||||
log.info(
|
||||
'A tainted cluster node XBX1',
|
||||
v,
|
||||
node.id,
|
||||
node.width,
|
||||
node.height,
|
||||
node.x,
|
||||
node.y,
|
||||
graph.parent(v)
|
||||
);
|
||||
clusterDb.get(node.id).node = node;
|
||||
positionNode(node);
|
||||
} else {
|
||||
// A tainted cluster node
|
||||
if (graph.children(v).length > 0) {
|
||||
log.info(
|
||||
'A pure cluster node XBX1',
|
||||
v,
|
||||
node.id,
|
||||
node.x,
|
||||
node.y,
|
||||
node.width,
|
||||
node.height,
|
||||
graph.parent(v)
|
||||
);
|
||||
node.height += subGraphTitleTotalMargin;
|
||||
graph.node(node.parentId);
|
||||
const halfPadding = node?.padding / 2 || 0;
|
||||
const labelHeight = node?.labelBBox?.height || 0;
|
||||
const offsetY = labelHeight - halfPadding || 0;
|
||||
log.debug('OffsetY', offsetY, 'labelHeight', labelHeight, 'halfPadding', halfPadding);
|
||||
await insertCluster(clusters, node);
|
||||
|
||||
// A cluster in the non-recursive way
|
||||
clusterDb.get(node.id).node = node;
|
||||
} else {
|
||||
// Regular node
|
||||
const parent = graph.node(node.parentId);
|
||||
node.y += subGraphTitleTotalMargin / 2;
|
||||
log.info(
|
||||
'A regular node XBX1 - using the padding',
|
||||
node.id,
|
||||
'parent',
|
||||
node.parentId,
|
||||
node.width,
|
||||
node.height,
|
||||
node.x,
|
||||
node.y,
|
||||
'offsetY',
|
||||
node.offsetY,
|
||||
'parent',
|
||||
parent,
|
||||
parent?.offsetY,
|
||||
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 startNode = graph.node(e.v);
|
||||
var endNode = graph.node(e.w);
|
||||
const paths = insertEdge(edgePaths, edge, clusterDb, diagramType, startNode, endNode, 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.warn('Returning from recursive render XAX', elem, diff);
|
||||
return { elem, diff };
|
||||
};
|
||||
|
||||
export const render = async (data4Layout, svg, element) => {
|
||||
const graph = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: data4Layout.direction,
|
||||
nodesep:
|
||||
data4Layout.config?.nodeSpacing ||
|
||||
data4Layout.config?.flowchart?.nodeSpacing ||
|
||||
data4Layout.nodeSpacing,
|
||||
ranksep:
|
||||
data4Layout.config?.rankSpacing ||
|
||||
data4Layout.config?.flowchart?.rankSpacing ||
|
||||
data4Layout.rankSpacing,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||
clearNodes();
|
||||
clearEdges();
|
||||
clearClusters();
|
||||
clearGraphlib();
|
||||
|
||||
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 }, edge.id);
|
||||
});
|
||||
|
||||
log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph)));
|
||||
adjustClustersAndEdges(graph);
|
||||
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
|
||||
const siteConfig = getConfig();
|
||||
await recursiveRender(
|
||||
element,
|
||||
graph,
|
||||
data4Layout.type,
|
||||
data4Layout.diagramId,
|
||||
undefined,
|
||||
siteConfig
|
||||
);
|
||||
};
|
@ -0,0 +1,464 @@
|
||||
/** 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 = new Map();
|
||||
let descendants = new Map();
|
||||
let parents = new Map();
|
||||
|
||||
export const clear = () => {
|
||||
descendants.clear();
|
||||
parents.clear();
|
||||
clusterDb.clear();
|
||||
};
|
||||
|
||||
const isDescendant = (id, ancestorId) => {
|
||||
const ancestorDescendants = descendants.get(ancestorId) || [];
|
||||
log.trace('In isDescendant', ancestorId, ' ', id, ' = ', ancestorDescendants.includes(id));
|
||||
return ancestorDescendants.includes(id);
|
||||
};
|
||||
|
||||
const edgeInCluster = (edge, clusterId) => {
|
||||
const clusterDescendants = descendants.get(clusterId) || [];
|
||||
log.info('Descendants of ', clusterId, ' is ', clusterDescendants);
|
||||
log.info('Edge is ', edge);
|
||||
if (edge.v === clusterId || edge.w === clusterId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!clusterDescendants) {
|
||||
log.debug('Tilt, ', clusterId, ',not in descendants');
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
clusterDescendants.includes(edge.v) ||
|
||||
isDescendant(edge.v, clusterId) ||
|
||||
isDescendant(edge.w, clusterId) ||
|
||||
clusterDescendants.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) || [];
|
||||
|
||||
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);
|
||||
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 {
|
||||
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) => {
|
||||
const children = graph.children(id);
|
||||
let res = [...children];
|
||||
|
||||
for (const child of children) {
|
||||
parents.set(child, id);
|
||||
res = [...res, ...extractDescendants(child, graph)];
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const findCommonEdges = (graph, id1, id2) => {
|
||||
const edges1 = graph.edges().filter((edge) => edge.v === id1 || edge.w === id1);
|
||||
const edges2 = graph.edges().filter((edge) => edge.v === id2 || edge.w === id2);
|
||||
const edges1Prim = edges1.map((edge) => {
|
||||
return { v: edge.v === id1 ? id2 : edge.v, w: edge.w === id1 ? id1 : edge.w };
|
||||
});
|
||||
const edges2Prim = edges2.map((edge) => {
|
||||
return { v: edge.v, w: edge.w };
|
||||
});
|
||||
const result = edges1Prim.filter((edgeIn1) => {
|
||||
return edges2Prim.some((edge) => edgeIn1.v === edge.v && edgeIn1.w === edge.w);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const findNonClusterChild = (id, graph, clusterId) => {
|
||||
const children = graph.children(id);
|
||||
log.trace('Searching children of id ', id, children);
|
||||
if (children.length < 1) {
|
||||
return id;
|
||||
}
|
||||
let reserve;
|
||||
for (const child of children) {
|
||||
const _id = findNonClusterChild(child, graph, clusterId);
|
||||
|
||||
const commonEdges = findCommonEdges(graph, clusterId, _id);
|
||||
|
||||
if (_id) {
|
||||
if (commonEdges.length > 0) {
|
||||
reserve = _id;
|
||||
} else {
|
||||
return _id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return reserve;
|
||||
};
|
||||
|
||||
const getAnchorId = (id) => {
|
||||
if (!clusterDb.has(id)) {
|
||||
return id;
|
||||
}
|
||||
if (!clusterDb.get(id).externalConnections) {
|
||||
return id;
|
||||
}
|
||||
|
||||
if (clusterDb.has(id)) {
|
||||
return clusterDb.get(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 ');
|
||||
}
|
||||
|
||||
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, id)
|
||||
);
|
||||
descendants.set(id, extractDescendants(id, graph));
|
||||
clusterDb.set(id, { id: findNonClusterChild(id, graph, id), clusterData: graph.node(id) });
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
const d1 = isDescendant(edge.v, id);
|
||||
const d2 = isDescendant(edge.w, id);
|
||||
|
||||
if (d1 ^ d2) {
|
||||
log.warn('Edge: ', edge, ' leaves cluster ', id);
|
||||
log.warn('Descendants of XXX ', id, ': ', descendants.get(id));
|
||||
clusterDb.get(id).externalConnections = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.debug('Not a cluster ', id, descendants);
|
||||
}
|
||||
});
|
||||
|
||||
for (let id of clusterDb.keys()) {
|
||||
const nonClusterChild = clusterDb.get(id).id;
|
||||
const parent = graph.parent(nonClusterChild);
|
||||
|
||||
if (parent !== id && clusterDb.has(parent) && !clusterDb.get(parent).externalConnections) {
|
||||
clusterDb.get(id).id = parent;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
log.warn(
|
||||
'Fix XXX',
|
||||
clusterDb,
|
||||
'ids:',
|
||||
e.v,
|
||||
e.w,
|
||||
'Translating: ',
|
||||
clusterDb.get(e.v),
|
||||
' --- ',
|
||||
clusterDb.get(e.w)
|
||||
);
|
||||
if (clusterDb.get(e.v) && clusterDb.get(e.w) && clusterDb.get(e.v) === clusterDb.get(e.w)) {
|
||||
log.warn('Fixing and trying link to self - removing XXX', e.v, e.w, e.name);
|
||||
log.warn('Fixing and trying - 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 specialId1 = e.w + '---' + e.v + '---1';
|
||||
const specialId2 = e.w + '---' + e.v + '---2';
|
||||
graph.setNode(specialId1, {
|
||||
domId: specialId1,
|
||||
id: specialId1,
|
||||
labelStyle: '',
|
||||
label: '',
|
||||
padding: 0,
|
||||
shape: 'labelRect',
|
||||
style: '',
|
||||
width: 10,
|
||||
height: 10,
|
||||
});
|
||||
graph.setNode(specialId2, {
|
||||
domId: specialId2,
|
||||
id: specialId2,
|
||||
labelStyle: '',
|
||||
padding: 0,
|
||||
shape: 'labelRect',
|
||||
style: '',
|
||||
width: 10,
|
||||
height: 10,
|
||||
});
|
||||
const edge1 = structuredClone(edge);
|
||||
const edgeMid = structuredClone(edge);
|
||||
const edge2 = structuredClone(edge);
|
||||
edge1.label = '';
|
||||
edge1.arrowTypeEnd = 'none';
|
||||
edge1.id = e.name + '-cyclic-special-1';
|
||||
edgeMid.arrowTypeEnd = 'none';
|
||||
edgeMid.id = e.name + '-cyclic-special-mid';
|
||||
edge2.label = '';
|
||||
edge1.fromCluster = e.v;
|
||||
edge2.toCluster = e.v;
|
||||
edge2.id = e.name + '-cyclic-special-2';
|
||||
graph.setEdge(v, specialId1, edge1, e.name + '-cyclic-special-0');
|
||||
graph.setEdge(specialId1, specialId2, edgeMid, e.name + '-cyclic-special-1');
|
||||
graph.setEdge(specialId2, w, edge2, e.name + '-cyclic-special-2');
|
||||
} else if (clusterDb.get(e.v) || clusterDb.get(e.w)) {
|
||||
log.warn('Fixing and trying - 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.get(parent).externalConnections = true;
|
||||
edge.fromCluster = e.v;
|
||||
}
|
||||
if (w !== e.w) {
|
||||
const parent = graph.parent(w);
|
||||
clusterDb.get(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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
log.debug('Nodes = ', nodes, depth);
|
||||
for (const node of nodes) {
|
||||
log.debug(
|
||||
'Extracting node',
|
||||
node,
|
||||
clusterDb,
|
||||
clusterDb.has(node) && !clusterDb.get(node).externalConnections,
|
||||
!graph.parent(node),
|
||||
graph.node(node),
|
||||
graph.children('D'),
|
||||
' Depth ',
|
||||
depth
|
||||
);
|
||||
if (!clusterDb.has(node)) {
|
||||
log.debug('Not a cluster', node, depth);
|
||||
} else if (
|
||||
!clusterDb.get(node).externalConnections &&
|
||||
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.get(node)?.clusterData?.dir) {
|
||||
dir = clusterDb.get(node).clusterData.dir;
|
||||
log.warn('Fixing dir', clusterDb.get(node).clusterData.dir, dir);
|
||||
}
|
||||
|
||||
const clusterGraph = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: dir,
|
||||
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.get(node).clusterData,
|
||||
label: clusterDb.get(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.get(node).externalConnections,
|
||||
' no parent: ',
|
||||
!graph.parent(node),
|
||||
' children ',
|
||||
graph.children(node) && graph.children(node).length > 0,
|
||||
graph.children('D'),
|
||||
depth
|
||||
);
|
||||
log.debug(clusterDb);
|
||||
}
|
||||
}
|
||||
|
||||
nodes = graph.nodes();
|
||||
log.warn('New list of nodes', nodes);
|
||||
for (const node of nodes) {
|
||||
const data = graph.node(node);
|
||||
log.warn(' Now next level', node, data);
|
||||
if (data.clusterNode) {
|
||||
extractor(data.graph, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sorter = (graph, nodes) => {
|
||||
if (nodes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let result = Object.assign([], nodes);
|
||||
nodes.forEach((node) => {
|
||||
const children = graph.children(node);
|
||||
const sorted = sorter(graph, children);
|
||||
result = [...result, ...sorted];
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const sortNodesByHierarchy = (graph) => sorter(graph, graph.children());
|
@ -0,0 +1,508 @@
|
||||
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import {
|
||||
validate,
|
||||
adjustClustersAndEdges,
|
||||
extractDescendants,
|
||||
sortNodesByHierarchy,
|
||||
} from './mermaid-graphlib.js';
|
||||
import { setLogLevel, log } from '$root/logger.js';
|
||||
|
||||
describe('Graphlib decorations', () => {
|
||||
let g;
|
||||
beforeEach(function () {
|
||||
setLogLevel(1);
|
||||
g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
});
|
||||
g.setGraph({
|
||||
rankdir: 'TB',
|
||||
nodesep: 10,
|
||||
ranksep: 10,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
});
|
||||
g.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate', function () {
|
||||
it('Validate should detect edges between clusters', function () {
|
||||
/*
|
||||
subgraph C1
|
||||
a --> b
|
||||
end
|
||||
subgraph C2
|
||||
c
|
||||
end
|
||||
C1 --> C2
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setParent('a', 'C1');
|
||||
g.setParent('b', 'C1');
|
||||
g.setParent('c', 'C2');
|
||||
g.setEdge('a', 'b');
|
||||
g.setEdge('C1', 'C2');
|
||||
|
||||
expect(validate(g)).toBe(false);
|
||||
});
|
||||
it('Validate should not detect edges between clusters after adjustment', function () {
|
||||
/*
|
||||
subgraph C1
|
||||
a --> b
|
||||
end
|
||||
subgraph C2
|
||||
c
|
||||
end
|
||||
C1 --> C2
|
||||
*/
|
||||
g.setNode('a', {});
|
||||
g.setNode('b', {});
|
||||
g.setNode('c', {});
|
||||
g.setParent('a', 'C1');
|
||||
g.setParent('b', 'C1');
|
||||
g.setParent('c', 'C2');
|
||||
g.setEdge('a', 'b');
|
||||
g.setEdge('C1', 'C2');
|
||||
|
||||
adjustClustersAndEdges(g);
|
||||
log.info(g.edges());
|
||||
expect(validate(g)).toBe(true);
|
||||
});
|
||||
|
||||
it('Validate should detect edges between clusters and transform clusters GLB4', function () {
|
||||
/*
|
||||
a --> b
|
||||
subgraph C1
|
||||
subgraph C2
|
||||
a
|
||||
end
|
||||
b
|
||||
end
|
||||
C1 --> c
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setNode('C1', { data: 4 });
|
||||
g.setNode('C2', { data: 5 });
|
||||
g.setParent('a', 'C2');
|
||||
g.setParent('b', 'C1');
|
||||
g.setParent('C2', 'C1');
|
||||
g.setEdge('a', 'b', { name: 'C1-internal-link' });
|
||||
g.setEdge('C1', 'c', { name: 'C1-external-link' });
|
||||
|
||||
adjustClustersAndEdges(g);
|
||||
log.info(g.nodes());
|
||||
expect(g.nodes().length).toBe(2);
|
||||
expect(validate(g)).toBe(true);
|
||||
});
|
||||
it('Validate should detect edges between clusters and transform clusters GLB5', function () {
|
||||
/*
|
||||
a --> b
|
||||
subgraph C1
|
||||
a
|
||||
end
|
||||
subgraph C2
|
||||
b
|
||||
end
|
||||
C1 -->
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setParent('a', 'C1');
|
||||
g.setParent('b', 'C2');
|
||||
// g.setEdge('a', 'b', { name: 'C1-internal-link' });
|
||||
g.setEdge('C1', 'C2', { name: 'C1-external-link' });
|
||||
|
||||
log.info(g.nodes());
|
||||
adjustClustersAndEdges(g);
|
||||
log.info(g.nodes());
|
||||
expect(g.nodes().length).toBe(2);
|
||||
expect(validate(g)).toBe(true);
|
||||
});
|
||||
it('adjustClustersAndEdges GLB6', function () {
|
||||
/*
|
||||
subgraph C1
|
||||
a
|
||||
end
|
||||
C1 --> b
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('C1', { data: 3 });
|
||||
g.setParent('a', 'C1');
|
||||
g.setEdge('C1', 'b', { data: 'link1' }, '1');
|
||||
|
||||
// log.info(g.edges())
|
||||
adjustClustersAndEdges(g);
|
||||
log.info(g.edges());
|
||||
expect(g.nodes()).toEqual(['b', 'C1']);
|
||||
expect(g.edges().length).toBe(1);
|
||||
expect(validate(g)).toBe(true);
|
||||
expect(g.node('C1').clusterNode).toBe(true);
|
||||
|
||||
const C1Graph = g.node('C1').graph;
|
||||
expect(C1Graph.nodes()).toEqual(['a']);
|
||||
});
|
||||
it('adjustClustersAndEdges GLB7', function () {
|
||||
/*
|
||||
subgraph C1
|
||||
a
|
||||
end
|
||||
C1 --> b
|
||||
C1 --> c
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setParent('a', 'C1');
|
||||
g.setNode('C1', { data: 4 });
|
||||
g.setEdge('C1', 'b', { data: 'link1' }, '1');
|
||||
g.setEdge('C1', 'c', { data: 'link2' }, '2');
|
||||
|
||||
log.info(g.node('C1'));
|
||||
adjustClustersAndEdges(g);
|
||||
log.info(g.edges());
|
||||
expect(g.nodes()).toEqual(['b', 'c', 'C1']);
|
||||
expect(g.nodes().length).toBe(3);
|
||||
expect(g.edges().length).toBe(2);
|
||||
|
||||
expect(g.edges().length).toBe(2);
|
||||
const edgeData = g.edge(g.edges()[1]);
|
||||
expect(edgeData.data).toBe('link2');
|
||||
expect(validate(g)).toBe(true);
|
||||
|
||||
const C1Graph = g.node('C1').graph;
|
||||
expect(C1Graph.nodes()).toEqual(['a']);
|
||||
});
|
||||
it('adjustClustersAndEdges GLB8', function () {
|
||||
/*
|
||||
subgraph A
|
||||
a
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
subgraph C
|
||||
c
|
||||
end
|
||||
A --> B
|
||||
A --> C
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setParent('a', 'A');
|
||||
g.setParent('b', 'B');
|
||||
g.setParent('c', 'C');
|
||||
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||
|
||||
// log.info(g.edges())
|
||||
adjustClustersAndEdges(g);
|
||||
expect(g.nodes()).toEqual(['A', 'B', 'C']);
|
||||
expect(g.edges().length).toBe(2);
|
||||
|
||||
expect(g.edges().length).toBe(2);
|
||||
const edgeData = g.edge(g.edges()[1]);
|
||||
expect(edgeData.data).toBe('link2');
|
||||
expect(validate(g)).toBe(true);
|
||||
|
||||
const CGraph = g.node('C').graph;
|
||||
expect(CGraph.nodes()).toEqual(['c']);
|
||||
});
|
||||
|
||||
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB10', function () {
|
||||
/*
|
||||
subgraph C
|
||||
subgraph D
|
||||
d
|
||||
end
|
||||
end
|
||||
*/
|
||||
|
||||
g.setNode('C', { data: 1 });
|
||||
g.setNode('D', { data: 2 });
|
||||
g.setNode('d', { data: 3 });
|
||||
g.setParent('d', 'D');
|
||||
g.setParent('D', 'C');
|
||||
|
||||
// log.info('Graph before', g.node('D'))
|
||||
// log.info('Graph before', graphlibJson.write(g))
|
||||
adjustClustersAndEdges(g);
|
||||
// log.info('Graph after', graphlibJson.write(g), g.node('C').graph)
|
||||
|
||||
const CGraph = g.node('C').graph;
|
||||
const DGraph = CGraph.node('D').graph;
|
||||
|
||||
expect(CGraph.nodes()).toEqual(['D']);
|
||||
expect(DGraph.nodes()).toEqual(['d']);
|
||||
|
||||
expect(g.nodes()).toEqual(['C']);
|
||||
expect(g.nodes().length).toBe(1);
|
||||
});
|
||||
|
||||
it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB11', function () {
|
||||
/*
|
||||
subgraph A
|
||||
a
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
subgraph C
|
||||
subgraph D
|
||||
d
|
||||
end
|
||||
end
|
||||
A --> B
|
||||
A --> C
|
||||
*/
|
||||
|
||||
g.setNode('C', { data: 1 });
|
||||
g.setNode('D', { data: 2 });
|
||||
g.setNode('d', { data: 3 });
|
||||
g.setNode('B', { data: 4 });
|
||||
g.setNode('b', { data: 5 });
|
||||
g.setNode('A', { data: 6 });
|
||||
g.setNode('a', { data: 7 });
|
||||
g.setParent('a', 'A');
|
||||
g.setParent('b', 'B');
|
||||
g.setParent('d', 'D');
|
||||
g.setParent('D', 'C');
|
||||
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||
|
||||
log.info('Graph before', g.node('D'));
|
||||
log.info('Graph before', graphlibJson.write(g));
|
||||
adjustClustersAndEdges(g);
|
||||
log.trace('Graph after', graphlibJson.write(g));
|
||||
expect(g.nodes()).toEqual(['C', 'B', 'A']);
|
||||
expect(g.nodes().length).toBe(3);
|
||||
expect(g.edges().length).toBe(2);
|
||||
|
||||
const AGraph = g.node('A').graph;
|
||||
const BGraph = g.node('B').graph;
|
||||
const CGraph = g.node('C').graph;
|
||||
// log.info(CGraph.nodes());
|
||||
const DGraph = CGraph.node('D').graph;
|
||||
// log.info('DG', CGraph.children('D'));
|
||||
|
||||
log.info('A', AGraph.nodes());
|
||||
expect(AGraph.nodes().length).toBe(1);
|
||||
expect(AGraph.nodes()).toEqual(['a']);
|
||||
log.trace('Nodes', BGraph.nodes());
|
||||
expect(BGraph.nodes().length).toBe(1);
|
||||
expect(BGraph.nodes()).toEqual(['b']);
|
||||
expect(CGraph.nodes()).toEqual(['D']);
|
||||
expect(CGraph.nodes().length).toEqual(1);
|
||||
|
||||
expect(AGraph.edges().length).toBe(0);
|
||||
expect(BGraph.edges().length).toBe(0);
|
||||
expect(CGraph.edges().length).toBe(0);
|
||||
expect(DGraph.nodes()).toEqual(['d']);
|
||||
expect(DGraph.edges().length).toBe(0);
|
||||
// expect(CGraph.node('D')).toEqual({ data: 2 });
|
||||
expect(g.edges().length).toBe(2);
|
||||
|
||||
// expect(g.edges().length).toBe(2);
|
||||
// const edgeData = g.edge(g.edges()[1]);
|
||||
// expect(edgeData.data).toBe('link2');
|
||||
// expect(validate(g)).toBe(true);
|
||||
});
|
||||
it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB20', function () {
|
||||
/*
|
||||
a --> b
|
||||
subgraph b [Test]
|
||||
c --> d -->e
|
||||
end
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setNode('d', { data: 3 });
|
||||
g.setNode('e', { data: 3 });
|
||||
g.setParent('c', 'b');
|
||||
g.setParent('d', 'b');
|
||||
g.setParent('e', 'b');
|
||||
g.setEdge('a', 'b', { data: 'link1' }, '1');
|
||||
g.setEdge('c', 'd', { data: 'link2' }, '2');
|
||||
g.setEdge('d', 'e', { data: 'link2' }, '2');
|
||||
|
||||
log.info('Graph before', graphlibJson.write(g));
|
||||
adjustClustersAndEdges(g);
|
||||
const bGraph = g.node('b').graph;
|
||||
// log.trace('Graph after', graphlibJson.write(g))
|
||||
log.info('Graph after', graphlibJson.write(bGraph));
|
||||
expect(bGraph.nodes().length).toBe(3);
|
||||
expect(bGraph.edges().length).toBe(2);
|
||||
});
|
||||
it('adjustClustersAndEdges the extracted graphs shall contain the correct links GLB21', function () {
|
||||
/*
|
||||
state a {
|
||||
state b {
|
||||
state c {
|
||||
e
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setNode('e', { data: 3 });
|
||||
g.setParent('b', 'a');
|
||||
g.setParent('c', 'b');
|
||||
g.setParent('e', 'c');
|
||||
|
||||
log.info('Graph before', graphlibJson.write(g));
|
||||
adjustClustersAndEdges(g);
|
||||
const aGraph = g.node('a').graph;
|
||||
const bGraph = aGraph.node('b').graph;
|
||||
log.info('Graph after', graphlibJson.write(aGraph));
|
||||
const cGraph = bGraph.node('c').graph;
|
||||
// log.trace('Graph after', graphlibJson.write(g))
|
||||
expect(aGraph.nodes().length).toBe(1);
|
||||
expect(bGraph.nodes().length).toBe(1);
|
||||
expect(cGraph.nodes().length).toBe(1);
|
||||
expect(bGraph.edges().length).toBe(0);
|
||||
});
|
||||
});
|
||||
it('adjustClustersAndEdges should handle nesting GLB77', function () {
|
||||
/*
|
||||
flowchart TB
|
||||
subgraph A
|
||||
b-->B
|
||||
a-->c
|
||||
end
|
||||
subgraph B
|
||||
c
|
||||
end
|
||||
*/
|
||||
|
||||
const exportedGraph = JSON.parse(
|
||||
'{"options":{"directed":true,"multigraph":true,"compound":true},"nodes":[{"v":"A","value":{"labelStyle":"","shape":"rect","labelText":"A","rx":0,"ry":0,"cssClass":"default","style":"","id":"A","width":500,"type":"group","padding":15}},{"v":"B","value":{"labelStyle":"","shape":"rect","labelText":"B","rx":0,"ry":0,"class":"default","style":"","id":"B","width":500,"type":"group","padding":15},"parent":"A"},{"v":"b","value":{"labelStyle":"","shape":"rect","labelText":"b","rx":0,"ry":0,"class":"default","style":"","id":"b","padding":15},"parent":"A"},{"v":"c","value":{"labelStyle":"","shape":"rect","labelText":"c","rx":0,"ry":0,"class":"default","style":"","id":"c","padding":15},"parent":"B"},{"v":"a","value":{"labelStyle":"","shape":"rect","labelText":"a","rx":0,"ry":0,"class":"default","style":"","id":"a","padding":15},"parent":"A"}],"edges":[{"v":"b","w":"B","name":"1","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-b-B","cssClasses":"flowchart-link LS-b LE-B"}},{"v":"a","w":"c","name":"2","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-a-c","cssClasses":"flowchart-link LS-a LE-c"}}],"value":{"rankdir":"TB","nodesep":50,"ranksep":50,"marginx":8,"marginy":8}}'
|
||||
);
|
||||
const gr = graphlibJson.read(exportedGraph);
|
||||
|
||||
log.info('Graph before', graphlibJson.write(gr));
|
||||
adjustClustersAndEdges(gr);
|
||||
const aGraph = gr.node('A').graph;
|
||||
const bGraph = aGraph.node('B').graph;
|
||||
log.info('Graph after', graphlibJson.write(aGraph));
|
||||
// log.trace('Graph after', graphlibJson.write(g))
|
||||
expect(aGraph.parent('c')).toBe('B');
|
||||
expect(aGraph.parent('B')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
describe('extractDescendants', function () {
|
||||
let g;
|
||||
beforeEach(function () {
|
||||
setLogLevel(1);
|
||||
g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
});
|
||||
g.setGraph({
|
||||
rankdir: 'TB',
|
||||
nodesep: 10,
|
||||
ranksep: 10,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
});
|
||||
g.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
it('Simple case of one level descendants GLB9', function () {
|
||||
/*
|
||||
subgraph A
|
||||
a
|
||||
end
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
subgraph C
|
||||
c
|
||||
end
|
||||
A --> B
|
||||
A --> C
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setNode('c', { data: 3 });
|
||||
g.setParent('a', 'A');
|
||||
g.setParent('b', 'B');
|
||||
g.setParent('c', 'C');
|
||||
g.setEdge('A', 'B', { data: 'link1' }, '1');
|
||||
g.setEdge('A', 'C', { data: 'link2' }, '2');
|
||||
|
||||
// log.info(g.edges())
|
||||
const d1 = extractDescendants('A', g);
|
||||
const d2 = extractDescendants('B', g);
|
||||
const d3 = extractDescendants('C', g);
|
||||
|
||||
expect(d1).toEqual(['a']);
|
||||
expect(d2).toEqual(['b']);
|
||||
expect(d3).toEqual(['c']);
|
||||
});
|
||||
});
|
||||
describe('sortNodesByHierarchy', function () {
|
||||
let g;
|
||||
beforeEach(function () {
|
||||
setLogLevel(1);
|
||||
g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
});
|
||||
g.setGraph({
|
||||
rankdir: 'TB',
|
||||
nodesep: 10,
|
||||
ranksep: 10,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
});
|
||||
g.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
it('should sort proper en nodes are in reverse order', function () {
|
||||
/*
|
||||
a -->b
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
subgraph A
|
||||
B
|
||||
end
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setParent('b', 'B');
|
||||
g.setParent('B', 'A');
|
||||
g.setEdge('a', 'b', '1');
|
||||
expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
|
||||
});
|
||||
it('should sort proper en nodes are in correct order', function () {
|
||||
/*
|
||||
a -->b
|
||||
subgraph B
|
||||
b
|
||||
end
|
||||
subgraph A
|
||||
B
|
||||
end
|
||||
*/
|
||||
g.setNode('a', { data: 1 });
|
||||
g.setParent('B', 'A');
|
||||
g.setParent('b', 'B');
|
||||
g.setNode('b', { data: 2 });
|
||||
g.setEdge('a', 'b', '1');
|
||||
expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']);
|
||||
});
|
||||
});
|
44
packages/mermaid/src/rendering-util/render.ts
Normal file
44
packages/mermaid/src/rendering-util/render.ts
Normal file
@ -0,0 +1,44 @@
|
||||
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'),
|
||||
},
|
||||
// {
|
||||
// name: 'elk',
|
||||
// loader: async () => await import('../../../mermaid-layout-elk/src/render.js'),
|
||||
// },
|
||||
]);
|
||||
};
|
||||
|
||||
registerDefaultLayoutLoaders();
|
||||
|
||||
export const render = async (data4Layout: any, svg: any, element: any) => {
|
||||
if (!(data4Layout.layoutAlgorithm in layoutAlgorithms)) {
|
||||
throw new Error(`Unknown layout algorithm: ${data4Layout.layoutAlgorithm}`);
|
||||
}
|
||||
|
||||
const layoutDefinition = layoutAlgorithms[data4Layout.layoutAlgorithm];
|
||||
const layoutRenderer = await layoutDefinition.loader();
|
||||
return layoutRenderer.render(data4Layout, svg, element, layoutDefinition.algorithm);
|
||||
};
|
@ -0,0 +1,397 @@
|
||||
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';
|
||||
import {
|
||||
styles2String,
|
||||
userNodeOverrides,
|
||||
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
|
||||
|
||||
const rect = async (parent, node) => {
|
||||
log.info('Creating subgraph rect for ', node.id, node);
|
||||
const siteConfig = getConfig();
|
||||
const { themeVariables, handDrawnSeed } = siteConfig;
|
||||
const { clusterBkg, clusterBorder } = themeVariables;
|
||||
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', 'cluster ' + node.cssClasses)
|
||||
.attr('id', node.id)
|
||||
.attr('data-look', node.look);
|
||||
|
||||
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 = await createText(labelEl, node.label, {
|
||||
style: node.labelStyle,
|
||||
useHtmlLabels,
|
||||
isNode: 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 width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
|
||||
if (node.width <= bbox.width + node.padding) {
|
||||
node.diff = (width - node.width) / 2 - node.padding;
|
||||
} else {
|
||||
node.diff = -node.padding;
|
||||
}
|
||||
|
||||
const height = node.height;
|
||||
const x = node.x - width / 2;
|
||||
const y = node.y - height / 2;
|
||||
|
||||
log.trace('Data ', node, JSON.stringify(node));
|
||||
let rect;
|
||||
if (node.look === 'handDrawn') {
|
||||
// @ts-ignore TODO: Fix rough typings
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {
|
||||
roughness: 0.7,
|
||||
fill: clusterBkg,
|
||||
// fill: 'red',
|
||||
stroke: clusterBorder,
|
||||
fillWeight: 3,
|
||||
seed: handDrawnSeed,
|
||||
});
|
||||
const roughNode = rc.path(createRoundedRectPathD(x, y, width, height, 0), options);
|
||||
rect = shapeSvg.insert(() => {
|
||||
log.debug('Rough node insert CXC', roughNode);
|
||||
return roughNode;
|
||||
}, ':first-child');
|
||||
} else {
|
||||
// add the rect
|
||||
rect = shapeSvg.insert('rect', ':first-child');
|
||||
// center the rect around its coordinate
|
||||
rect
|
||||
.attr('style', nodeStyles)
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
}
|
||||
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
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})`
|
||||
);
|
||||
|
||||
if (labelStyles) {
|
||||
const span = labelEl.select('span');
|
||||
if (span) {
|
||||
span.attr('style', labelStyles);
|
||||
}
|
||||
}
|
||||
// Center the label
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
node.offsetX = 0;
|
||||
node.width = rectBox.width;
|
||||
node.height = rectBox.height;
|
||||
// Used by layout engine to position subgraph in parent
|
||||
node.offsetY = bbox.height - node.padding / 2;
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
return { cluster: shapeSvg, labelBBox: bbox };
|
||||
};
|
||||
|
||||
/**
|
||||
* Non visible cluster where the note is group with its
|
||||
*
|
||||
* @param {any} parent
|
||||
* @param {any} node
|
||||
* @returns {any} ShapeSvg
|
||||
*/
|
||||
const noteGroup = (parent, node) => {
|
||||
// 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 { cluster: shapeSvg, labelBBox: { width: 0, height: 0 } };
|
||||
};
|
||||
|
||||
const roundedWithTitle = async (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)
|
||||
.attr('data-id', node.id)
|
||||
.attr('data-look', node.look);
|
||||
|
||||
// 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(await 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);
|
||||
}
|
||||
|
||||
// Rounded With Title
|
||||
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 = (width - node.width) / 2 - node.padding;
|
||||
} else {
|
||||
node.diff = -node.padding;
|
||||
}
|
||||
|
||||
const height = node.height + padding;
|
||||
// const height = node.height + padding;
|
||||
const innerHeight = node.height + padding - bbox.height - 6;
|
||||
const x = node.x - width / 2;
|
||||
const y = node.y - height / 2;
|
||||
node.width = width;
|
||||
const innerY = node.y - node.height / 2 - halfPadding + bbox.height + 2;
|
||||
|
||||
// add the rect
|
||||
let rect;
|
||||
if (node.look === 'handDrawn') {
|
||||
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');
|
||||
const outerRectClass = 'outer';
|
||||
|
||||
// center the rect around its coordinate
|
||||
rect
|
||||
.attr('class', outerRectClass)
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('data-look', node.look);
|
||||
innerRect
|
||||
.attr('class', 'inner')
|
||||
.attr('x', x)
|
||||
.attr('y', innerY)
|
||||
.attr('width', width)
|
||||
.attr('height', innerHeight);
|
||||
}
|
||||
|
||||
label.attr(
|
||||
'transform',
|
||||
`translate(${node.x - bbox.width / 2}, ${y + 1 - (evaluate(siteConfig.flowchart.htmlLabels) ? 0 : 3)})`
|
||||
);
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
node.height = rectBox.height;
|
||||
node.offsetX = 0;
|
||||
// Used by layout engine to position subgraph in parent
|
||||
node.offsetY = bbox.height - node.padding / 2;
|
||||
node.labelBBox = bbox;
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
return { cluster: shapeSvg, labelBBox: bbox };
|
||||
};
|
||||
const divider = (parent, node) => {
|
||||
const siteConfig = getConfig();
|
||||
|
||||
const { themeVariables, handDrawnSeed } = siteConfig;
|
||||
const { nodeBorder } = themeVariables;
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', node.cssClasses)
|
||||
.attr('id', node.id)
|
||||
.attr('data-look', node.look);
|
||||
|
||||
// add the rect
|
||||
const outerRectG = shapeSvg.insert('g', ':first-child');
|
||||
|
||||
const padding = 0 * node.padding;
|
||||
|
||||
const width = node.width + padding;
|
||||
|
||||
node.diff = -node.padding;
|
||||
|
||||
const height = node.height + padding;
|
||||
// const height = node.height + padding;
|
||||
const x = node.x - width / 2;
|
||||
const y = node.y - height / 2;
|
||||
node.width = width;
|
||||
|
||||
// add the rect
|
||||
let rect;
|
||||
if (node.look === 'handDrawn') {
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const roughOuterNode = rc.rectangle(x, y, width, height, {
|
||||
fill: 'lightgrey',
|
||||
roughness: 0.5,
|
||||
strokeLineDash: [5],
|
||||
stroke: nodeBorder,
|
||||
seed: handDrawnSeed,
|
||||
});
|
||||
|
||||
rect = shapeSvg.insert(() => roughOuterNode, ':first-child');
|
||||
} else {
|
||||
rect = outerRectG.insert('rect', ':first-child');
|
||||
const outerRectClass = 'divider';
|
||||
|
||||
// center the rect around its coordinate
|
||||
rect
|
||||
.attr('class', outerRectClass)
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('data-look', node.look);
|
||||
}
|
||||
|
||||
const rectBox = rect.node().getBBox();
|
||||
node.height = rectBox.height;
|
||||
node.offsetX = 0;
|
||||
// Used by layout engine to position subgraph in parent
|
||||
node.offsetY = 0;
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersectRect(node, point);
|
||||
};
|
||||
|
||||
return { cluster: shapeSvg, labelBBox: {} };
|
||||
};
|
||||
|
||||
const squareRect = rect;
|
||||
const shapes = {
|
||||
rect,
|
||||
squareRect,
|
||||
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 +
|
||||
') (' +
|
||||
node?.width +
|
||||
', ' +
|
||||
node?.height +
|
||||
')',
|
||||
clusterElems[node.id]
|
||||
);
|
||||
const el = clusterElems[node.id];
|
||||
el.cluster.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||
};
|
@ -0,0 +1,105 @@
|
||||
import { select } from 'd3';
|
||||
import { log } from '$root/logger.js';
|
||||
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||
import common, { evaluate, renderKatex, hasKatex } 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 {Promise<SVGForeignObjectElement>} Node
|
||||
*/
|
||||
async function addHtmlLabel(node) {
|
||||
const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
|
||||
const div = fo.append('xhtml:div');
|
||||
|
||||
let label = node.label;
|
||||
if (node.label && hasKatex(node.label)) {
|
||||
label = await renderKatex(node.label.replace(common.lineBreakRegex, '\n'), getConfig());
|
||||
}
|
||||
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
|
||||
div.html(
|
||||
'<span class="' +
|
||||
labelClass +
|
||||
'" ' +
|
||||
(node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + // codeql [js/html-constructed-from-input] : false positive
|
||||
'>' +
|
||||
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 = async (_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 = await addHtmlLabel(node);
|
||||
// vertexNode.parentNode.removeChild(vertexNode);
|
||||
return vertexNode;
|
||||
} else {
|
||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
|
||||
let rows = [];
|
||||
if (typeof vertexText === 'string') {
|
||||
rows = vertexText.split(/\\n|\n|<br\s*\/?>/gi);
|
||||
} else if (Array.isArray(vertexText)) {
|
||||
rows = vertexText;
|
||||
} else {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
tspan.setAttribute('dy', '1em');
|
||||
tspan.setAttribute('x', '0');
|
||||
if (isTitle) {
|
||||
tspan.setAttribute('class', 'title-row');
|
||||
} else {
|
||||
tspan.setAttribute('class', 'row');
|
||||
}
|
||||
tspan.textContent = row.trim();
|
||||
svgLabel.appendChild(tspan);
|
||||
}
|
||||
return svgLabel;
|
||||
}
|
||||
};
|
||||
|
||||
export default createLabel;
|
@ -0,0 +1,80 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import type { SVG } from '$root/diagram-api/types.js';
|
||||
import type { Mocked } from 'vitest';
|
||||
import { addEdgeMarkers } from './edgeMarker.js';
|
||||
|
||||
describe('addEdgeMarker', () => {
|
||||
const svgPath = {
|
||||
attr: vitest.fn(),
|
||||
} as unknown as Mocked<SVG>;
|
||||
const url = 'http://example.com';
|
||||
const id = 'test';
|
||||
const diagramType = 'test';
|
||||
|
||||
beforeEach(() => {
|
||||
svgPath.attr.mockReset();
|
||||
});
|
||||
|
||||
it('should add markers for arrow_cross:arrow_point', () => {
|
||||
const arrowTypeStart = 'arrow_cross';
|
||||
const arrowTypeEnd = 'arrow_point';
|
||||
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-start',
|
||||
`url(${url}#${id}_${diagramType}-crossStart)`
|
||||
);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-end',
|
||||
`url(${url}#${id}_${diagramType}-pointEnd)`
|
||||
);
|
||||
});
|
||||
|
||||
it('should add markers for aggregation:arrow_point', () => {
|
||||
const arrowTypeStart = 'aggregation';
|
||||
const arrowTypeEnd = 'arrow_point';
|
||||
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-start',
|
||||
`url(${url}#${id}_${diagramType}-aggregationStart)`
|
||||
);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-end',
|
||||
`url(${url}#${id}_${diagramType}-pointEnd)`
|
||||
);
|
||||
});
|
||||
|
||||
it('should add markers for arrow_point:aggregation', () => {
|
||||
const arrowTypeStart = 'arrow_point';
|
||||
const arrowTypeEnd = 'aggregation';
|
||||
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-start',
|
||||
`url(${url}#${id}_${diagramType}-pointStart)`
|
||||
);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-end',
|
||||
`url(${url}#${id}_${diagramType}-aggregationEnd)`
|
||||
);
|
||||
});
|
||||
|
||||
it('should add markers for aggregation:composition', () => {
|
||||
const arrowTypeStart = 'aggregation';
|
||||
const arrowTypeEnd = 'composition';
|
||||
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-start',
|
||||
`url(${url}#${id}_${diagramType}-aggregationStart)`
|
||||
);
|
||||
expect(svgPath.attr).toHaveBeenCalledWith(
|
||||
'marker-end',
|
||||
`url(${url}#${id}_${diagramType}-compositionEnd)`
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add invalid markers', () => {
|
||||
const arrowTypeStart = 'this is an invalid marker';
|
||||
const arrowTypeEnd = ') url(https://my-malicious-site.example)';
|
||||
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
|
||||
expect(svgPath.attr).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
import type { SVG } from '$root/diagram-api/types.js';
|
||||
import { log } from '$root/logger.js';
|
||||
import type { EdgeData } from '$root/types.js';
|
||||
/**
|
||||
* Adds SVG markers to a path element based on the arrow types specified in the edge.
|
||||
*
|
||||
* @param svgPath - The SVG path element to add markers to.
|
||||
* @param edge - The edge data object containing the arrow types.
|
||||
* @param url - The URL of the SVG marker definitions.
|
||||
* @param id - The ID prefix for the SVG marker definitions.
|
||||
* @param diagramType - The type of diagram being rendered.
|
||||
*/
|
||||
export const addEdgeMarkers = (
|
||||
svgPath: SVG,
|
||||
edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>,
|
||||
url: string,
|
||||
id: string,
|
||||
diagramType: string
|
||||
) => {
|
||||
if (edge.arrowTypeStart) {
|
||||
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType);
|
||||
}
|
||||
if (edge.arrowTypeEnd) {
|
||||
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType);
|
||||
}
|
||||
};
|
||||
|
||||
const arrowTypesMap = {
|
||||
arrow_cross: 'cross',
|
||||
arrow_point: 'point',
|
||||
arrow_barb: 'barb',
|
||||
arrow_circle: 'circle',
|
||||
aggregation: 'aggregation',
|
||||
extension: 'extension',
|
||||
composition: 'composition',
|
||||
dependency: 'dependency',
|
||||
lollipop: 'lollipop',
|
||||
} as const;
|
||||
|
||||
const addEdgeMarker = (
|
||||
svgPath: SVG,
|
||||
position: 'start' | 'end',
|
||||
arrowType: string,
|
||||
url: string,
|
||||
id: string,
|
||||
diagramType: string
|
||||
) => {
|
||||
const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
|
||||
|
||||
if (!endMarkerType) {
|
||||
log.warn(`Unknown arrow type: ${arrowType}`);
|
||||
return; // unknown arrow type, ignore
|
||||
}
|
||||
|
||||
const suffix = position === 'start' ? 'Start' : 'End';
|
||||
svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`);
|
||||
};
|
562
packages/mermaid/src/rendering-util/rendering-elements/edges.js
Normal file
562
packages/mermaid/src/rendering-util/rendering-elements/edges.js
Normal file
@ -0,0 +1,562 @@
|
||||
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';
|
||||
|
||||
const edgeLabels = new Map();
|
||||
const terminalLabels = new Map();
|
||||
|
||||
export const clear = () => {
|
||||
edgeLabels.clear();
|
||||
terminalLabels.clear();
|
||||
};
|
||||
|
||||
export const getLabelStyles = (styleArray) => {
|
||||
let styles = styleArray ? styleArray.reduce((acc, style) => acc + ';' + style, '') : '';
|
||||
return styles;
|
||||
};
|
||||
|
||||
export const insertEdgeLabel = async (elem, edge) => {
|
||||
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||
|
||||
const labelElement = await createText(elem, edge.label, {
|
||||
style: getLabelStyles(edge.labelStyle),
|
||||
useHtmlLabels,
|
||||
addSvgBackground: true,
|
||||
isNode: false,
|
||||
});
|
||||
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.set(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 = await createLabel(
|
||||
edge.startLabelLeft,
|
||||
getLabelStyles(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.get(edge.id)) {
|
||||
terminalLabels.set(edge.id, {});
|
||||
}
|
||||
terminalLabels.get(edge.id).startLeft = startEdgeLabelLeft;
|
||||
setTerminalWidth(fo, edge.startLabelLeft);
|
||||
}
|
||||
if (edge.startLabelRight) {
|
||||
// Create the actual text element
|
||||
const startLabelElement = await createLabel(
|
||||
edge.startLabelRight,
|
||||
getLabelStyles(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.get(edge.id)) {
|
||||
terminalLabels.set(edge.id, {});
|
||||
}
|
||||
terminalLabels.get(edge.id).startRight = startEdgeLabelRight;
|
||||
setTerminalWidth(fo, edge.startLabelRight);
|
||||
}
|
||||
if (edge.endLabelLeft) {
|
||||
// Create the actual text element
|
||||
const endLabelElement = await createLabel(edge.endLabelLeft, getLabelStyles(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.get(edge.id)) {
|
||||
terminalLabels.set(edge.id, {});
|
||||
}
|
||||
terminalLabels.get(edge.id).endLeft = endEdgeLabelLeft;
|
||||
setTerminalWidth(fo, edge.endLabelLeft);
|
||||
}
|
||||
if (edge.endLabelRight) {
|
||||
// Create the actual text element
|
||||
const endLabelElement = await createLabel(edge.endLabelRight, getLabelStyles(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.get(edge.id)) {
|
||||
terminalLabels.set(edge.id, {});
|
||||
}
|
||||
terminalLabels.get(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.debug('Moving label abc88 ', edge.id, edge.label, edgeLabels.get(edge.id), paths);
|
||||
let path = paths.updatedPath ? paths.updatedPath : paths.originalPath;
|
||||
const siteConfig = getConfig();
|
||||
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
if (edge.label) {
|
||||
const el = edgeLabels.get(edge.id);
|
||||
let x = edge.x;
|
||||
let y = edge.y;
|
||||
if (path) {
|
||||
const pos = utils.calcLabelPosition(path);
|
||||
log.debug(
|
||||
'Moving label ' + edge.label + ' from (',
|
||||
x,
|
||||
',',
|
||||
y,
|
||||
') to (',
|
||||
pos.x,
|
||||
',',
|
||||
pos.y,
|
||||
') abc88'
|
||||
);
|
||||
if (paths.updatedPath) {
|
||||
x = pos.x;
|
||||
y = pos.y;
|
||||
}
|
||||
}
|
||||
el.attr('transform', `translate(${x}, ${y + subGraphTitleTotalMargin / 2})`);
|
||||
}
|
||||
|
||||
if (edge.startLabelLeft) {
|
||||
const el = terminalLabels.get(edge.id).startLeft;
|
||||
let x = edge.x;
|
||||
let y = edge.y;
|
||||
if (path) {
|
||||
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.get(edge.id).startRight;
|
||||
let x = edge.x;
|
||||
let y = edge.y;
|
||||
if (path) {
|
||||
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.get(edge.id).endLeft;
|
||||
let x = edge.x;
|
||||
let y = edge.y;
|
||||
if (path) {
|
||||
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.get(edge.id).endRight;
|
||||
let x = edge.x;
|
||||
let y = edge.y;
|
||||
if (path) {
|
||||
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) => {
|
||||
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;
|
||||
return dx >= w || dy >= h;
|
||||
};
|
||||
|
||||
export const intersection = (node, outsidePoint, insidePoint) => {
|
||||
log.debug(`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 w = node.width / 2;
|
||||
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||
const h = node.height / 2;
|
||||
|
||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||
|
||||
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;
|
||||
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.debug(`abc89 top/bottom calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res);
|
||||
|
||||
return res;
|
||||
} else {
|
||||
// Intersection on sides of rect
|
||||
if (insidePoint.x < outsidePoint.x) {
|
||||
r = outsidePoint.x - w - x;
|
||||
} else {
|
||||
r = x - w - outsidePoint.x;
|
||||
}
|
||||
let q = (Q * r) / R;
|
||||
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
||||
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
||||
log.debug(`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 };
|
||||
}
|
||||
};
|
||||
|
||||
const cutPathAtIntersect = (_points, boundaryNode) => {
|
||||
log.warn('abc88 cutPathAtIntersect', _points, boundaryNode);
|
||||
let points = [];
|
||||
let lastPointOutside = _points[0];
|
||||
let isInside = false;
|
||||
_points.forEach((point) => {
|
||||
log.info('abc88 checking point', point, boundaryNode);
|
||||
|
||||
if (!outsideNode(boundaryNode, point) && !isInside) {
|
||||
const inter = intersection(boundaryNode, lastPointOutside, point);
|
||||
log.debug('abc88 inside', point, lastPointOutside, inter);
|
||||
log.debug('abc88 intersection', inter, boundaryNode);
|
||||
|
||||
let pointPresent = false;
|
||||
points.forEach((p) => {
|
||||
pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
|
||||
});
|
||||
|
||||
if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
|
||||
points.push(inter);
|
||||
} else {
|
||||
log.warn('abc88 no intersect', inter, points);
|
||||
}
|
||||
isInside = true;
|
||||
} else {
|
||||
log.warn('abc88 outside', point, lastPointOutside);
|
||||
lastPointOutside = point;
|
||||
if (!isInside) {
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
});
|
||||
log.debug('returning points', points);
|
||||
return 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 };
|
||||
};
|
||||
|
||||
const fixCorners = function (lineData) {
|
||||
const { cornerPointPositions } = extractCornerPoints(lineData);
|
||||
const newLineData = [];
|
||||
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];
|
||||
|
||||
const newPrevPoint = findAdjacentPoint(prevPoint, cornerPoint, 5);
|
||||
const newNextPoint = findAdjacentPoint(nextPoint, cornerPoint, 5);
|
||||
|
||||
const xDiff = newNextPoint.x - newPrevPoint.x;
|
||||
const yDiff = newNextPoint.y - newPrevPoint.y;
|
||||
newLineData.push(newPrevPoint);
|
||||
|
||||
const a = Math.sqrt(2) * 2;
|
||||
let newCornerPoint = { x: cornerPoint.x, y: cornerPoint.y };
|
||||
if (Math.abs(nextPoint.x - prevPoint.x) > 10 && Math.abs(nextPoint.y - prevPoint.y) >= 10) {
|
||||
log.debug(
|
||||
'Corner point fixing',
|
||||
Math.abs(nextPoint.x - prevPoint.x),
|
||||
Math.abs(nextPoint.y - prevPoint.y)
|
||||
);
|
||||
const r = 5;
|
||||
if (cornerPoint.x === newPrevPoint.x) {
|
||||
newCornerPoint = {
|
||||
x: xDiff < 0 ? newPrevPoint.x - r + a : newPrevPoint.x + r - a,
|
||||
y: yDiff < 0 ? newPrevPoint.y - a : newPrevPoint.y + a,
|
||||
};
|
||||
} else {
|
||||
newCornerPoint = {
|
||||
x: xDiff < 0 ? newPrevPoint.x - a : newPrevPoint.x + a,
|
||||
y: yDiff < 0 ? newPrevPoint.y - r + a : newPrevPoint.y + r - a,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
log.debug(
|
||||
'Corner point skipping fixing',
|
||||
Math.abs(nextPoint.x - prevPoint.x),
|
||||
Math.abs(nextPoint.y - prevPoint.y)
|
||||
);
|
||||
}
|
||||
newLineData.push(newCornerPoint, newNextPoint);
|
||||
} else {
|
||||
newLineData.push(lineData[i]);
|
||||
}
|
||||
}
|
||||
return newLineData;
|
||||
};
|
||||
|
||||
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
|
||||
const { handDrawnSeed } = getConfig();
|
||||
let points = edge.points;
|
||||
let pointsHasChanged = false;
|
||||
const tail = startNode;
|
||||
var head = endNode;
|
||||
|
||||
if (head.intersect && tail.intersect) {
|
||||
points = points.slice(1, edge.points.length - 1);
|
||||
points.unshift(tail.intersect(points[0]));
|
||||
log.debug(
|
||||
'Last point APA12',
|
||||
edge.start,
|
||||
'-->',
|
||||
edge.end,
|
||||
points[points.length - 1],
|
||||
head,
|
||||
head.intersect(points[points.length - 1])
|
||||
);
|
||||
points.push(head.intersect(points[points.length - 1]));
|
||||
}
|
||||
if (edge.toCluster) {
|
||||
log.info('to cluster abc88', clusterDb.get(edge.toCluster));
|
||||
points = cutPathAtIntersect(edge.points, clusterDb.get(edge.toCluster).node);
|
||||
|
||||
pointsHasChanged = true;
|
||||
}
|
||||
|
||||
if (edge.fromCluster) {
|
||||
log.debug(
|
||||
'from cluster abc88',
|
||||
clusterDb.get(edge.fromCluster),
|
||||
JSON.stringify(points, null, 2)
|
||||
);
|
||||
points = cutPathAtIntersect(points.reverse(), clusterDb.get(edge.fromCluster).node).reverse();
|
||||
|
||||
pointsHasChanged = true;
|
||||
}
|
||||
|
||||
let lineData = points.filter((p) => !Number.isNaN(p.y));
|
||||
lineData = fixCorners(lineData);
|
||||
let lastPoint = lineData[lineData.length - 1];
|
||||
if (lineData.length > 1) {
|
||||
lastPoint = lineData[lineData.length - 1];
|
||||
const secondLastPoint = lineData[lineData.length - 2];
|
||||
const diffX = (lastPoint.x - secondLastPoint.x) / 2;
|
||||
const diffY = (lastPoint.y - secondLastPoint.y) / 2;
|
||||
const midPoint = { x: secondLastPoint.x + diffX, y: secondLastPoint.y + diffY };
|
||||
lineData.splice(-1, 0, midPoint);
|
||||
}
|
||||
let curve = curveBasis;
|
||||
if (edge.curve) {
|
||||
curve = edge.curve;
|
||||
}
|
||||
|
||||
const { x, y } = getLineFunctionsWithOffset(edge);
|
||||
const lineFunction = line().x(x).y(y).curve(curve);
|
||||
|
||||
let strokeClasses;
|
||||
switch (edge.thickness) {
|
||||
case 'normal':
|
||||
strokeClasses = 'edge-thickness-normal';
|
||||
break;
|
||||
case 'thick':
|
||||
strokeClasses = 'edge-thickness-thick';
|
||||
break;
|
||||
case 'invisible':
|
||||
strokeClasses = 'edge-thickness-invisible';
|
||||
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 svgPath;
|
||||
let linePath = lineFunction(lineData);
|
||||
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
|
||||
if (edge.look === 'handDrawn') {
|
||||
const rc = rough.svg(elem);
|
||||
Object.assign([], lineData);
|
||||
|
||||
const svgPathNode = rc.path(linePath, {
|
||||
roughness: 0.3,
|
||||
seed: handDrawnSeed,
|
||||
});
|
||||
|
||||
strokeClasses += ' transition';
|
||||
|
||||
svgPath = select(svgPathNode)
|
||||
.select('path')
|
||||
.attr('id', edge.id)
|
||||
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
|
||||
.attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
|
||||
let d = svgPath.attr('d');
|
||||
svgPath.attr('d', d);
|
||||
elem.node().appendChild(svgPath.node());
|
||||
} else {
|
||||
svgPath = elem
|
||||
.append('path')
|
||||
.attr('d', linePath)
|
||||
.attr('id', edge.id)
|
||||
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
|
||||
.attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
|
||||
}
|
||||
|
||||
let url = '';
|
||||
if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
|
||||
url =
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
window.location.search;
|
||||
url = url.replace(/\(/g, '\\(').replace(/\)/g, '\\)');
|
||||
}
|
||||
log.info('arrowTypeStart', edge.arrowTypeStart);
|
||||
log.info('arrowTypeEnd', edge.arrowTypeEnd);
|
||||
|
||||
addEdgeMarkers(svgPath, edge, url, id, diagramType);
|
||||
|
||||
let paths = {};
|
||||
if (pointsHasChanged) {
|
||||
paths.updatedPath = points;
|
||||
}
|
||||
paths.originalPath = edge.points;
|
||||
return paths;
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Borrowed with love from from dagre-d3. Many thanks to cpettitt!
|
||||
*/
|
||||
|
||||
import node from './intersect-node.js';
|
||||
import circle from './intersect-circle.js';
|
||||
import ellipse from './intersect-ellipse.js';
|
||||
import polygon from './intersect-polygon.js';
|
||||
import rect from './intersect-rect.js';
|
||||
|
||||
export default {
|
||||
node,
|
||||
circle,
|
||||
ellipse,
|
||||
polygon,
|
||||
rect,
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import intersectEllipse from './intersect-ellipse.js';
|
||||
|
||||
function intersectCircle(node, rx, point) {
|
||||
return intersectEllipse(node, rx, rx, point);
|
||||
}
|
||||
|
||||
export default intersectCircle;
|
@ -0,0 +1,24 @@
|
||||
function intersectEllipse(node, rx, ry, point) {
|
||||
// Formulae from: https://mathworld.wolfram.com/Ellipse-LineIntersection.html
|
||||
|
||||
var cx = node.x;
|
||||
var cy = node.y;
|
||||
|
||||
var px = cx - point.x;
|
||||
var py = cy - point.y;
|
||||
|
||||
var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px);
|
||||
|
||||
var dx = Math.abs((rx * ry * px) / det);
|
||||
if (point.x < cx) {
|
||||
dx = -dx;
|
||||
}
|
||||
var dy = Math.abs((rx * ry * py) / det);
|
||||
if (point.y < cy) {
|
||||
dy = -dy;
|
||||
}
|
||||
|
||||
return { x: cx + dx, y: cy + dy };
|
||||
}
|
||||
|
||||
export default intersectEllipse;
|
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
function sameSign(r1, r2) {
|
||||
return r1 * r2 > 0;
|
||||
}
|
||||
|
||||
export default intersectLine;
|
@ -0,0 +1,5 @@
|
||||
function intersectNode(node, point) {
|
||||
return node.intersect(point);
|
||||
}
|
||||
|
||||
export default intersectNode;
|
@ -0,0 +1,63 @@
|
||||
import intersectLine from './intersect-line.js';
|
||||
|
||||
/**
|
||||
* Returns the point ({x, y}) at which the point argument intersects with the node argument assuming
|
||||
* that it has the shape specified by polygon.
|
||||
*/
|
||||
function intersectPolygon(node, polyPoints, point) {
|
||||
let x1 = node.x;
|
||||
let y1 = node.y;
|
||||
|
||||
let intersections = [];
|
||||
|
||||
let minX = Number.POSITIVE_INFINITY;
|
||||
let 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);
|
||||
}
|
||||
|
||||
let left = x1 - node.width / 2 - minX;
|
||||
let top = y1 - node.height / 2 - minY;
|
||||
|
||||
for (let i = 0; i < polyPoints.length; i++) {
|
||||
let p1 = polyPoints[i];
|
||||
let p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
|
||||
let 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) {
|
||||
let pdx = p.x - point.x;
|
||||
let pdy = p.y - point.y;
|
||||
let distp = Math.sqrt(pdx * pdx + pdy * pdy);
|
||||
|
||||
let qdx = q.x - point.x;
|
||||
let qdy = q.y - point.y;
|
||||
let distq = Math.sqrt(qdx * qdx + qdy * qdy);
|
||||
|
||||
return distp < distq ? -1 : distp === distq ? 0 : 1;
|
||||
});
|
||||
}
|
||||
return intersections[0];
|
||||
}
|
||||
|
||||
export default intersectPolygon;
|
@ -0,0 +1,32 @@
|
||||
const intersectRect = (node, point) => {
|
||||
var x = node.x;
|
||||
var y = node.y;
|
||||
|
||||
// Rectangle intersection algorithm from:
|
||||
// https://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
||||
var dx = point.x - x;
|
||||
var dy = point.y - y;
|
||||
var w = node.width / 2;
|
||||
var h = node.height / 2;
|
||||
|
||||
var sx, sy;
|
||||
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
||||
// Intersection is top or bottom of rect.
|
||||
if (dy < 0) {
|
||||
h = -h;
|
||||
}
|
||||
sx = dy === 0 ? 0 : (h * dx) / dy;
|
||||
sy = h;
|
||||
} else {
|
||||
// Intersection is left or right of rect.
|
||||
if (dx < 0) {
|
||||
w = -w;
|
||||
}
|
||||
sx = w;
|
||||
sy = dx === 0 ? 0 : (w * dy) / dx;
|
||||
}
|
||||
|
||||
return { x: x + sx, y: y + sy };
|
||||
};
|
||||
|
||||
export default intersectRect;
|
@ -0,0 +1,293 @@
|
||||
/** Setup arrow head and define the marker. The result is appended to the svg. */
|
||||
import { log } from '$root/logger.js';
|
||||
|
||||
// Only add the number of markers that the diagram needs
|
||||
const insertMarkers = (elem, markerArray, type, id) => {
|
||||
markerArray.forEach((markerName) => {
|
||||
markers[markerName](elem, type, id);
|
||||
});
|
||||
};
|
||||
|
||||
const extension = (elem, type, id) => {
|
||||
log.trace('Making markers for ', id);
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-extensionStart')
|
||||
.attr('class', 'marker extension ' + type)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 1,7 L18,13 V 1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-extensionEnd')
|
||||
.attr('class', 'marker extension ' + type)
|
||||
.attr('refX', 1)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
const composition = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-compositionStart')
|
||||
.attr('class', 'marker composition ' + type)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-compositionEnd')
|
||||
.attr('class', 'marker composition ' + type)
|
||||
.attr('refX', 1)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
};
|
||||
const aggregation = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-aggregationStart')
|
||||
.attr('class', 'marker aggregation ' + type)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-aggregationEnd')
|
||||
.attr('class', 'marker aggregation ' + type)
|
||||
.attr('refX', 1)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
};
|
||||
const dependency = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-dependencyStart')
|
||||
.attr('class', 'marker dependency ' + type)
|
||||
.attr('refX', 6)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-dependencyEnd')
|
||||
.attr('class', 'marker dependency ' + type)
|
||||
.attr('refX', 13)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
|
||||
};
|
||||
const lollipop = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-lollipopStart')
|
||||
.attr('class', 'marker lollipop ' + type)
|
||||
.attr('refX', 13)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('circle')
|
||||
.attr('stroke', 'black')
|
||||
.attr('fill', 'transparent')
|
||||
.attr('cx', 7)
|
||||
.attr('cy', 7)
|
||||
.attr('r', 6);
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-lollipopEnd')
|
||||
.attr('class', 'marker lollipop ' + type)
|
||||
.attr('refX', 1)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
.attr('orient', 'auto')
|
||||
.append('circle')
|
||||
.attr('stroke', 'black')
|
||||
.attr('fill', 'transparent')
|
||||
.attr('cx', 7)
|
||||
.attr('cy', 7)
|
||||
.attr('r', 6);
|
||||
};
|
||||
const point = (elem, type, id) => {
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-pointEnd')
|
||||
.attr('class', 'marker ' + type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 5)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', 8)
|
||||
.attr('markerHeight', 8)
|
||||
.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', 8)
|
||||
.attr('markerHeight', 8)
|
||||
.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', 'userSpaceOnUse')
|
||||
.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;
|
122
packages/mermaid/src/rendering-util/rendering-elements/nodes.js
Normal file
122
packages/mermaid/src/rendering-util/rendering-elements/nodes.js
Normal file
@ -0,0 +1,122 @@
|
||||
import { log } from '$root/logger.js';
|
||||
import { state } from './shapes/state.ts';
|
||||
import { roundedRect } from './shapes/roundedRect.ts';
|
||||
import { squareRect } from './shapes/squareRect.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 { stadium } from './shapes/stadium.js';
|
||||
import { rectWithTitle } from './shapes/rectWithTitle.js';
|
||||
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||
import { subroutine } from './shapes/subroutine.js';
|
||||
import { cylinder } from './shapes/cylinder.js';
|
||||
import { circle } from './shapes/circle.js';
|
||||
import { doublecircle } from './shapes/doubleCircle.js';
|
||||
import { rect_left_inv_arrow } from './shapes/rectLeftInvArrow.js';
|
||||
import { question } from './shapes/question.js';
|
||||
import { hexagon } from './shapes/hexagon.js';
|
||||
import { lean_right } from './shapes/leanRight.js';
|
||||
import { lean_left } from './shapes/leanLeft.js';
|
||||
import { trapezoid } from './shapes/trapezoid.js';
|
||||
import { inv_trapezoid } from './shapes/invertedTrapezoid.js';
|
||||
import { labelRect } from './shapes/labelRect.js';
|
||||
|
||||
const shapes = {
|
||||
state,
|
||||
stateStart,
|
||||
stateEnd,
|
||||
fork: forkJoin,
|
||||
join: forkJoin,
|
||||
choice,
|
||||
note,
|
||||
roundedRect,
|
||||
rectWithTitle,
|
||||
squareRect,
|
||||
stadium,
|
||||
subroutine,
|
||||
cylinder,
|
||||
circle,
|
||||
doublecircle,
|
||||
odd: rect_left_inv_arrow,
|
||||
diamond: question,
|
||||
hexagon,
|
||||
lean_right,
|
||||
lean_left,
|
||||
trapezoid,
|
||||
inv_trapezoid,
|
||||
labelRect,
|
||||
};
|
||||
|
||||
const nodeElems = new Map();
|
||||
|
||||
export const insertNode = async (elem, node, dir) => {
|
||||
let newEl;
|
||||
let el;
|
||||
|
||||
//special check for rect shape (with or without rounded corners)
|
||||
if (node.shape === 'rect') {
|
||||
if (node.rx && node.ry) {
|
||||
node.shape = 'roundedRect';
|
||||
} else {
|
||||
node.shape = 'squareRect';
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
nodeElems.set(node.id, newEl);
|
||||
|
||||
if (node.haveCallback) {
|
||||
nodeElems.get(node.id).attr('class', nodeElems.get(node.id).attr('class') + ' clickable');
|
||||
}
|
||||
return newEl;
|
||||
};
|
||||
export const setNodeElem = (elem, node) => {
|
||||
nodeElems.set(node.id, elem);
|
||||
};
|
||||
export const clear = () => {
|
||||
nodeElems.clear();
|
||||
};
|
||||
|
||||
export const positionNode = (node) => {
|
||||
const el = nodeElems.get(node.id);
|
||||
log.trace(
|
||||
'Transforming node',
|
||||
node.diff,
|
||||
node,
|
||||
'translate(' + (node.x - node.width / 2 - 5) + ', ' + node.width / 2 + ')'
|
||||
);
|
||||
const padding = 8;
|
||||
const diff = node.diff || 0;
|
||||
if (node.clusterNode) {
|
||||
el.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(node.x + diff - node.width / 2) +
|
||||
', ' +
|
||||
(node.y - node.height / 2 - padding) +
|
||||
')'
|
||||
);
|
||||
} else {
|
||||
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');
|
||||
}
|
||||
return diff;
|
||||
};
|
@ -0,0 +1,64 @@
|
||||
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';
|
||||
// @ts-ignore TODO: Fix rough typings
|
||||
import rough from 'roughjs';
|
||||
import { solidStateFill, styles2String } from './handDrawnShapeStyles.js';
|
||||
import { getConfig } from '$root/diagram-api/diagramAPI.js';
|
||||
|
||||
export const choice = (parent: SVG, node: Node) => {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
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.look === 'handDrawn') {
|
||||
// @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
|
||||
choice
|
||||
.attr('class', 'state-start')
|
||||
// @ts-ignore TODO: Fix rough typings
|
||||
.attr('r', 7)
|
||||
.attr('width', 28)
|
||||
.attr('height', 28)
|
||||
.attr('style', nodeStyles);
|
||||
|
||||
node.width = 28;
|
||||
node.height = 28;
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.circle(node, 14, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user