mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-16 20:49:45 +02:00
Compare commits
10 Commits
antler_ng_
...
4743-timel
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ea8260aa51 | ||
![]() |
dabc220ed2 | ||
![]() |
d2c5cbd408 | ||
![]() |
20467bcbe6 | ||
![]() |
dd213fe86c | ||
![]() |
800f23fc01 | ||
![]() |
67aa1a4dc1 | ||
![]() |
c1bcdcfbad | ||
![]() |
f528e2daa4 | ||
![]() |
a867842f32 |
@@ -33,11 +33,6 @@ export const packageOptions = {
|
||||
packageName: 'mermaid-layout-elk',
|
||||
file: 'layouts.ts',
|
||||
},
|
||||
'mermaid-layout-tidy-tree': {
|
||||
name: 'mermaid-layout-tidy-tree',
|
||||
packageName: 'mermaid-layout-tidy-tree',
|
||||
file: 'index.ts',
|
||||
},
|
||||
examples: {
|
||||
name: 'mermaid-examples',
|
||||
packageName: 'examples',
|
||||
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Render newlines as spaces in class diagrams
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
Add IDs in architecture diagrams
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Ensure edge label color is applied when using classDef with edge IDs
|
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
'@mermaid-js/layout-tidy-tree': minor
|
||||
'@mermaid-js/layout-elk': minor
|
||||
---
|
||||
|
||||
feat: Update mindmap rendering to support multiple layouts, improved edge intersections, and new shapes
|
5
.changeset/ninety-adults-wink.md
Normal file
5
.changeset/ninety-adults-wink.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': major
|
||||
---
|
||||
|
||||
Currently, HTML tags such as <em>, <strong>, <sup>, <a>, <ul>, and <li> are supported in Flowchart and Class diagram labels but not in Timeline diagrams. This change introduces support for basic HTML formatting in Timeline labels, enabling richer text formatting and better usability for multi-line content like descriptions, footnotes, and styled annotations
|
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add IDs in architecture diagrams
|
@@ -1,9 +0,0 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: revert marked dependency from ^15.0.7 to ^16.0.0
|
||||
|
||||
- Reverted marked package version to ^16.0.0 for better compatibility
|
||||
- This is a dependency update that maintains API compatibility
|
||||
- All tests pass with the updated version
|
@@ -5,10 +5,8 @@ bmatrix
|
||||
braintree
|
||||
catmull
|
||||
compositTitleSize
|
||||
cose
|
||||
curv
|
||||
doublecircle
|
||||
elem
|
||||
elems
|
||||
gantt
|
||||
gitgraph
|
||||
|
@@ -1,5 +1,4 @@
|
||||
BRANDES
|
||||
Buzan
|
||||
circo
|
||||
handDrawn
|
||||
KOEPF
|
||||
|
3
.github/workflows/e2e-applitools.yml
vendored
3
.github/workflows/e2e-applitools.yml
vendored
@@ -23,6 +23,9 @@ env:
|
||||
jobs:
|
||||
e2e-applitools:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: cypress/browsers:node-20.11.0-chrome-121.0.6167.85-1-ff-120.0-edge-121.0.2277.83-1
|
||||
options: --user 1001
|
||||
steps:
|
||||
- if: ${{ ! env.USE_APPLI }}
|
||||
name: Warn if not using Applitools
|
||||
|
2
.github/workflows/e2e-timings.yml
vendored
2
.github/workflows/e2e-timings.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and create pull request
|
||||
uses: peter-evans/create-pull-request@18e469570b1cf0dfc11d60ec121099f8ff3e617a
|
||||
uses: peter-evans/create-pull-request@cb4d3bfce175d44325c6b7697f81e0afe8a79bdf
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
|
2
.github/workflows/validate-lockfile.yml
vendored
2
.github/workflows/validate-lockfile.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
# 2) No unwanted vitepress paths
|
||||
if grep -qF 'packages/mermaid/src/vitepress' pnpm-lock.yaml; then
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run \`rm -rf packages/mermaid/src/vitepress && pnpm install\` to regenerate.")
|
||||
issues+=("• Disallowed path 'packages/mermaid/src/vitepress' present. Run `rm -rf packages/mermaid/src/vitepress && pnpm install` to regenerate.")
|
||||
fi
|
||||
|
||||
# 3) Lockfile only changes when package.json changes
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@ node_modules/
|
||||
coverage/
|
||||
.idea/
|
||||
.pnpm-store/
|
||||
.instructions/
|
||||
|
||||
dist
|
||||
v8-compile-cache-0
|
||||
|
Binary file not shown.
Binary file not shown.
@@ -98,12 +98,12 @@ describe('Configuration', () => {
|
||||
it('should handle arrowMarkerAbsolute set to true', () => {
|
||||
renderGraph(
|
||||
`flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
{
|
||||
arrowMarkerAbsolute: true,
|
||||
}
|
||||
@@ -113,7 +113,8 @@ describe('Configuration', () => {
|
||||
cy.get('path')
|
||||
.first()
|
||||
.should('have.attr', 'marker-end')
|
||||
.and('include', 'url(http://localhost');
|
||||
.should('exist')
|
||||
.and('include', 'url(http\\:\\/\\/localhost');
|
||||
});
|
||||
});
|
||||
it('should not taint the initial configuration when using multiple directives', () => {
|
||||
|
@@ -524,18 +524,5 @@ describe('Class diagram', () => {
|
||||
`,
|
||||
{}
|
||||
);
|
||||
it('should handle an empty class body with empty braces', () => {
|
||||
imgSnapshotTest(
|
||||
` classDiagram
|
||||
class FooBase~T~ {}
|
||||
class Bar {
|
||||
+Zip
|
||||
+Zap()
|
||||
}
|
||||
FooBase <|-- Ba
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -109,7 +109,7 @@ describe('Flowchart ELK', () => {
|
||||
const style = svg.attr('style');
|
||||
expect(style).to.match(/^max-width: [\d.]+px;$/);
|
||||
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
|
||||
verifyNumber(maxWidthValue, 380, 15);
|
||||
verifyNumber(maxWidthValue, 380);
|
||||
});
|
||||
});
|
||||
it('8-elk: should render a flowchart when useMaxWidth is false', () => {
|
||||
@@ -128,7 +128,7 @@ describe('Flowchart ELK', () => {
|
||||
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);
|
||||
verifyNumber(width, 380, 15);
|
||||
verifyNumber(width, 380);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
|
@@ -1186,17 +1186,4 @@ end
|
||||
imgSnapshotTest(graph, { htmlLabels: false });
|
||||
});
|
||||
});
|
||||
|
||||
it('V2 - 17: should apply class def colour to edge label', () => {
|
||||
imgSnapshotTest(
|
||||
` graph LR
|
||||
id1(Start) link@-- "Label" -->id2(Stop)
|
||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
||||
|
||||
class id2 myClass
|
||||
classDef myClass fill:#bbf,stroke:#f66,stroke-width:2px,color:white,stroke-dasharray: 5 5
|
||||
class link myClass
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -1,79 +0,0 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util.ts';
|
||||
|
||||
describe('Mindmap Tidy Tree', () => {
|
||||
it('1-tidy-tree: should render a simple mindmap without children', () => {
|
||||
imgSnapshotTest(
|
||||
` ---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
`
|
||||
);
|
||||
});
|
||||
it('2-tidy-tree: should render a simple mindmap', () => {
|
||||
imgSnapshotTest(
|
||||
` ---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
`
|
||||
);
|
||||
});
|
||||
it('3-tidy-tree: should render a mindmap with different shapes', () => {
|
||||
imgSnapshotTest(
|
||||
` ---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
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
|
||||
id)I am a cloud(
|
||||
id))I am a bang((
|
||||
Tools
|
||||
`
|
||||
);
|
||||
});
|
||||
it('4-tidy-tree: should render a mindmap with children', () => {
|
||||
imgSnapshotTest(
|
||||
` ---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
((This is a mindmap))
|
||||
child1
|
||||
grandchild 1
|
||||
grandchild 2
|
||||
child2
|
||||
grandchild 3
|
||||
grandchild 4
|
||||
child3
|
||||
grandchild 5
|
||||
grandchild 6
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
@@ -159,10 +159,12 @@ root
|
||||
});
|
||||
it('square shape', () => {
|
||||
imgSnapshotTest(
|
||||
`mindmap
|
||||
`
|
||||
mindmap
|
||||
root[
|
||||
The root
|
||||
]`,
|
||||
]
|
||||
`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -170,10 +172,12 @@ root
|
||||
});
|
||||
it('rounded rect shape', () => {
|
||||
imgSnapshotTest(
|
||||
`mindmap
|
||||
`
|
||||
mindmap
|
||||
root((
|
||||
The root
|
||||
))`,
|
||||
))
|
||||
`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -181,10 +185,12 @@ root
|
||||
});
|
||||
it('circle shape', () => {
|
||||
imgSnapshotTest(
|
||||
`mindmap
|
||||
`
|
||||
mindmap
|
||||
root(
|
||||
The root
|
||||
)`,
|
||||
)
|
||||
`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -192,8 +198,10 @@ root
|
||||
});
|
||||
it('default shape', () => {
|
||||
imgSnapshotTest(
|
||||
`mindmap
|
||||
The root`,
|
||||
`
|
||||
mindmap
|
||||
The root
|
||||
`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -201,10 +209,12 @@ root
|
||||
});
|
||||
it('adding children', () => {
|
||||
imgSnapshotTest(
|
||||
`mindmap
|
||||
`
|
||||
mindmap
|
||||
The root
|
||||
child1
|
||||
child2`,
|
||||
child2
|
||||
`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -212,11 +222,13 @@ root
|
||||
});
|
||||
it('adding grand children', () => {
|
||||
imgSnapshotTest(
|
||||
`mindmap
|
||||
`
|
||||
mindmap
|
||||
The root
|
||||
child1
|
||||
child2
|
||||
child3`,
|
||||
child3
|
||||
`,
|
||||
{},
|
||||
undefined,
|
||||
shouldHaveRoot
|
||||
@@ -228,21 +240,25 @@ root
|
||||
`mindmap
|
||||
id1[\`**Start** with
|
||||
a second line 😎\`]
|
||||
id2[\`The dog in **the** hog... a *very long text* about it Word!\`]`
|
||||
id2[\`The dog in **the** hog... a *very long text* about it
|
||||
Word!\`]
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Include char sequence "graph" in text (#6795)', () => {
|
||||
it('has a label with char sequence "graph"', () => {
|
||||
imgSnapshotTest(
|
||||
` mindmap
|
||||
`
|
||||
mindmap
|
||||
root
|
||||
Photograph
|
||||
Waterfall
|
||||
Landscape
|
||||
Geography
|
||||
Mountains
|
||||
Rocks`,
|
||||
Rocks
|
||||
`,
|
||||
{ flowchart: { defaultRenderer: 'elk' } }
|
||||
);
|
||||
});
|
||||
|
@@ -225,4 +225,24 @@ timeline
|
||||
{}
|
||||
);
|
||||
});
|
||||
it('13: should render markdown htmlLabels', () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
config:
|
||||
theme: forest
|
||||
---
|
||||
|
||||
timeline
|
||||
title Timeline of Industrial Revolution
|
||||
section 17th-20th century
|
||||
Industry 1.0 : Machinery, Water power, Steam <br>power
|
||||
Industry 2.0 : Electricity, <strong>Internal combustion engine </strong>, Mass production
|
||||
Industry 3.0 : Electronics, Computers, Automation
|
||||
section 21st century
|
||||
Industry 4.0 : Internet, Robotics, Internet of Things
|
||||
Industry 5.0 : Artificial intelligence, Big data, 3D printing
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -32,8 +32,26 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&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=Recursive:wght@300..1000&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.recursive-mermaid {
|
||||
font-family: 'Recursive', sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
'slnt' 0,
|
||||
'CASL' 0,
|
||||
'CRSV' 0.5,
|
||||
'MONO' 0;
|
||||
}
|
||||
|
||||
body {
|
||||
/* background: rgb(221, 208, 208); */
|
||||
/* background: #333; */
|
||||
@@ -45,7 +63,9 @@
|
||||
h1 {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
border: 1px solid red;
|
||||
}
|
||||
.mermaid2 {
|
||||
display: none;
|
||||
}
|
||||
@@ -83,6 +103,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.class2 {
|
||||
fill: red;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
/* tspan {
|
||||
font-size: 6px !important;
|
||||
} */
|
||||
@@ -106,194 +131,6 @@
|
||||
|
||||
<body>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart-elk TB
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
one --> two
|
||||
three --> two
|
||||
two --> c2
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart TB
|
||||
|
||||
process_C
|
||||
subgraph container_Alpha
|
||||
subgraph process_B
|
||||
pppB
|
||||
end
|
||||
subgraph process_A
|
||||
pppA
|
||||
end
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
process_A-->|messages|container_Beta
|
||||
end
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart TB
|
||||
subgraph container_Beta
|
||||
process_C
|
||||
end
|
||||
subgraph container_Alpha
|
||||
subgraph process_B
|
||||
pppB
|
||||
end
|
||||
subgraph process_A
|
||||
pppA
|
||||
end
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
process_A-->|messages|container_Beta
|
||||
end
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart TB
|
||||
subgraph container_Beta
|
||||
process_C
|
||||
end
|
||||
|
||||
process_B-->|via_AWSBatch|container_Beta
|
||||
|
||||
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
classDiagram
|
||||
note "I love this diagram!\nDo you love it?"
|
||||
Class01 <|-- AveryLongClass : Cool
|
||||
<<interface>> Class01
|
||||
Class03 "1" *-- "*" Class04
|
||||
Class05 "1" o-- "many" Class06
|
||||
Class07 "1" .. "*" Class08
|
||||
Class09 "1" --> "*" C2 : Where am i?
|
||||
Class09 "*" --* "*" C3
|
||||
Class09 "1" --|> "1" Class07
|
||||
Class12 <|.. Class08
|
||||
Class11 ..>Class12
|
||||
Class07 : equals()
|
||||
Class07 : Object[] elementData
|
||||
Class01 : size()
|
||||
Class01 : int chimp
|
||||
Class01 : int gorilla
|
||||
Class01 : -int privateChimp
|
||||
Class01 : +int publicGorilla
|
||||
Class01 : #int protectedMarmoset
|
||||
Class08 <--> C2: Cool label
|
||||
class Class10 {
|
||||
<<service>>
|
||||
int id
|
||||
test()
|
||||
}
|
||||
note for Class10 "Cool class\nI said it's very cool class!"
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart-elk TB
|
||||
internet
|
||||
nat
|
||||
router
|
||||
compute1
|
||||
|
||||
subgraph project
|
||||
router
|
||||
nat
|
||||
subgraph subnet1
|
||||
compute1
|
||||
end
|
||||
end
|
||||
|
||||
%% router --> subnet1
|
||||
subnet1 --> nat
|
||||
%% nat --> internet
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
flowchart-elk TB
|
||||
internet
|
||||
nat
|
||||
router
|
||||
lb1
|
||||
lb2
|
||||
compute1
|
||||
compute2
|
||||
subgraph project
|
||||
router
|
||||
nat
|
||||
subgraph subnet1
|
||||
compute1
|
||||
lb1
|
||||
end
|
||||
subgraph subnet2
|
||||
compute2
|
||||
lb2
|
||||
end
|
||||
end
|
||||
internet --> router
|
||||
router --> subnet1 & subnet2
|
||||
subnet1 & subnet2 --> nat --> internet
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -320,149 +157,84 @@ treemap
|
||||
"Leaf 2.2": 25
|
||||
"Leaf 2.3": 12
|
||||
|
||||
</pre>
|
||||
<pre id="diagram5" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: rounded
|
||||
---
|
||||
flowchart LR
|
||||
I["fa:fa-code Text"] -- Mermaid js --> D["Use<br/>the<br/>editor!"]
|
||||
I --> D & D
|
||||
D@{ shape: question}
|
||||
I@{ shape: question}
|
||||
classDef class1 fill:red,color:blue,stroke:#FFD600;
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
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>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: linear
|
||||
---
|
||||
flowchart LR
|
||||
A[A] --> B[B]
|
||||
A[A] --- B([C])
|
||||
A@{ shape: diamond}
|
||||
%%B@{ shape: diamond}
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: linear
|
||||
---
|
||||
flowchart LR
|
||||
A[A] -- Mermaid js --> B[B]
|
||||
A[A] -- Mermaid js --- B[B]
|
||||
A@{ shape: diamond}
|
||||
B@{ shape: diamond}
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: rounded
|
||||
---
|
||||
flowchart LR
|
||||
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
|
||||
I --> D & D
|
||||
D@{ shape: question}
|
||||
I@{ shape: question}
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
curve: rounded
|
||||
elk:
|
||||
nodePlacementStrategy: NETWORK_SIMPLEX
|
||||
---
|
||||
flowchart LR
|
||||
D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"]
|
||||
D --> I & I
|
||||
a["a"]
|
||||
D@{ shape: trap-b}
|
||||
I@{ shape: lean-l}
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
|
||||
treemap:
|
||||
valueFormat: '$0,0'
|
||||
---
|
||||
flowchart LR
|
||||
%% subgraph s1["Untitled subgraph"]
|
||||
C["Evaluate"]
|
||||
%% end
|
||||
treemap
|
||||
"Budget"
|
||||
"Operations"
|
||||
"Salaries": 7000
|
||||
"Equipment": 2000
|
||||
"Supplies": 1000
|
||||
"Marketing"
|
||||
"Advertising": 4000
|
||||
"Events": 1000
|
||||
|
||||
B --> C
|
||||
</pre>
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
flowchart:
|
||||
//curve: linear
|
||||
---
|
||||
flowchart LR
|
||||
%% A ==> B
|
||||
%% A2 --> B2
|
||||
A{A} --> B((Bo boo)) & B & B & B
|
||||
|
||||
treemap
|
||||
title Accessible Treemap Title
|
||||
"Category A"
|
||||
"Item A1": 10
|
||||
"Item A2": 20
|
||||
"Category B"
|
||||
"Item B1": 15
|
||||
"Item B2": 25
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
theme: default
|
||||
look: classic
|
||||
---
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
subgraph s1["APA"]
|
||||
D{"Use the editor"}
|
||||
end
|
||||
subgraph S2["S2"]
|
||||
s1
|
||||
I>"fa:fa-code Text"]
|
||||
E["E"]
|
||||
end
|
||||
D -- Mermaid js --> I
|
||||
D --> I & E
|
||||
E --> I
|
||||
AB["apa@apa@"] --> B(("`apa@apa`"))
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart
|
||||
D(("for D"))
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
e1@{ animate: true}
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
|
||||
class e1 animate
|
||||
</pre>
|
||||
<h2>infinite</h2>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
|
||||
class e1 animate
|
||||
</pre>
|
||||
<h2>Mermaid - edge-animation-slow</h2>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
e1@{ animation: fast}
|
||||
</pre>
|
||||
<h2>Mermaid - edge-animation-fast</h2>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
A e1@--> B
|
||||
classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
|
||||
class e1 edge-animation-fast
|
||||
</pre>
|
||||
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
|
||||
info </pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -487,7 +259,7 @@ config:
|
||||
end
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -500,7 +272,7 @@ config:
|
||||
D-->I
|
||||
D-->I
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -539,7 +311,7 @@ flowchart LR
|
||||
n8@{ shape: rect}
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -555,7 +327,7 @@ flowchart LR
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -564,7 +336,7 @@ flowchart LR
|
||||
A{A} --> B & C
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -576,7 +348,7 @@ flowchart LR
|
||||
end
|
||||
</pre
|
||||
>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
@@ -594,7 +366,7 @@ flowchart LR
|
||||
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
kanban:
|
||||
@@ -613,81 +385,81 @@ kanban
|
||||
task3[💻 Develop login feature]@{ ticket: 103 }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||
A:::AClass
|
||||
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
flowchart LR
|
||||
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
|
||||
style A fill:#f9f,stroke:#333,stroke-width:4px
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
kanban
|
||||
id2[In progress]
|
||||
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
|
||||
</pre>
|
||||
<pre id="diagram4" class="mermaid">
|
||||
<pre id="diagram4" class="mermaid2">
|
||||
---
|
||||
config:
|
||||
kanban:
|
||||
@@ -751,22 +523,18 @@ kanban
|
||||
alert('It worked');
|
||||
}
|
||||
await mermaid.initialize({
|
||||
// theme: 'base',
|
||||
// theme: 'forest',
|
||||
// theme: 'default',
|
||||
// theme: 'forest',
|
||||
// handDrawnSeed: 12,
|
||||
// look: 'handDrawn',
|
||||
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
||||
// layout: 'dagre',
|
||||
layout: 'elk',
|
||||
// layout: 'elk',
|
||||
// layout: 'fixed',
|
||||
// htmlLabels: false,
|
||||
flowchart: { titleTopMargin: 10 },
|
||||
|
||||
// fontFamily: 'Caveat',
|
||||
// fontFamily: 'Kalam',
|
||||
// fontFamily: 'courier',
|
||||
fontFamily: 'arial',
|
||||
fontFamily: "'Recursive', sans-serif",
|
||||
sequence: {
|
||||
actorFontFamily: 'courier',
|
||||
noteFontFamily: 'courier',
|
||||
|
@@ -1,376 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Mermaid Quick Test Page</title>
|
||||
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
|
||||
<style>
|
||||
div.mermaid {
|
||||
font-family: 'Courier New', Courier, monospace !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
</pre>
|
||||
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
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
|
||||
id)I am a cloud(
|
||||
id))I am a bang((
|
||||
Tools
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
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
|
||||
id)I am a cloud(
|
||||
id))I am a bang((
|
||||
Tools
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
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
|
||||
id)I am a cloud(
|
||||
id))I am a bang((
|
||||
Tools
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
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
|
||||
id)I am a cloud(
|
||||
id))I am a bang((
|
||||
Tools
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
c
|
||||
d
|
||||
B
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
D
|
||||
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
c
|
||||
d
|
||||
B
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
D
|
||||
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
c
|
||||
d
|
||||
B
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
D
|
||||
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
|
||||
</pre>
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
a
|
||||
apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
b
|
||||
c
|
||||
d
|
||||
B
|
||||
apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
D
|
||||
apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on]
|
||||
|
||||
</pre>
|
||||
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
((This is a mindmap))
|
||||
child1
|
||||
grandchild 1
|
||||
grandchild 2
|
||||
child2
|
||||
grandchild 3
|
||||
grandchild 4
|
||||
child3
|
||||
grandchild 5
|
||||
grandchild 6
|
||||
|
||||
</pre>
|
||||
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: dagre
|
||||
---
|
||||
mindmap
|
||||
((This is a mindmap))
|
||||
child1
|
||||
grandchild 1
|
||||
grandchild 2
|
||||
child2
|
||||
grandchild 3
|
||||
grandchild 4
|
||||
child3
|
||||
grandchild 5
|
||||
grandchild 6
|
||||
|
||||
</pre>
|
||||
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
mindmap
|
||||
((This is a mindmap))
|
||||
child1
|
||||
grandchild 1
|
||||
grandchild 2
|
||||
child2
|
||||
grandchild 3
|
||||
grandchild 4
|
||||
child3
|
||||
grandchild 5
|
||||
grandchild 6
|
||||
|
||||
</pre>
|
||||
|
||||
<pre class="mermaid">
|
||||
---
|
||||
config:
|
||||
layout: cose-bilkent
|
||||
---
|
||||
mindmap
|
||||
((This is a mindmap))
|
||||
child1
|
||||
grandchild 1
|
||||
grandchild 2
|
||||
child2
|
||||
grandchild 3
|
||||
grandchild 4
|
||||
child3
|
||||
grandchild 5
|
||||
grandchild 6
|
||||
|
||||
</pre>
|
||||
|
||||
<hr />
|
||||
<script type="module">
|
||||
import mermaid from '/mermaid.esm.mjs';
|
||||
import tidytree from '/mermaid-layout-tidy-tree.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.registerLayoutLoaders(tidytree);
|
||||
mermaid.initialize({
|
||||
theme: 'default',
|
||||
logLevel: 3,
|
||||
securityLevel: 'loose',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,6 +1,5 @@
|
||||
import externalExample from './mermaid-example-diagram.esm.mjs';
|
||||
import layouts from './mermaid-layout-elk.esm.mjs';
|
||||
import tidyTree from './mermaid-layout-tidy-tree.esm.mjs';
|
||||
import zenUml from './mermaid-zenuml.esm.mjs';
|
||||
import mermaid from './mermaid.esm.mjs';
|
||||
|
||||
@@ -66,7 +65,6 @@ const contentLoaded = async function () {
|
||||
await mermaid.registerExternalDiagrams([externalExample, zenUml]);
|
||||
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.registerLayoutLoaders(tidyTree);
|
||||
mermaid.initialize(graphObj.mermaid);
|
||||
/**
|
||||
* CC-BY-4.0
|
||||
|
@@ -2,227 +2,219 @@
|
||||
"durations": [
|
||||
{
|
||||
"spec": "cypress/integration/other/configuration.spec.js",
|
||||
"duration": 5841
|
||||
"duration": 6297
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/external-diagrams.spec.js",
|
||||
"duration": 2138
|
||||
"duration": 2187
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/ghsa.spec.js",
|
||||
"duration": 3370
|
||||
"duration": 3509
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/iife.spec.js",
|
||||
"duration": 2052
|
||||
"duration": 2218
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/interaction.spec.js",
|
||||
"duration": 12243
|
||||
"duration": 12104
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/rerender.spec.js",
|
||||
"duration": 2065
|
||||
"duration": 2151
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/xss.spec.js",
|
||||
"duration": 31288
|
||||
"duration": 33064
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/appli.spec.js",
|
||||
"duration": 3421
|
||||
"duration": 3488
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/architecture.spec.ts",
|
||||
"duration": 97
|
||||
"duration": 106
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/block.spec.js",
|
||||
"duration": 18500
|
||||
"duration": 18317
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/c4.spec.js",
|
||||
"duration": 5793
|
||||
"duration": 5592
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
|
||||
"duration": 40966
|
||||
"duration": 39358
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
|
||||
"duration": 39176
|
||||
"duration": 37160
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
|
||||
"duration": 23468
|
||||
"duration": 23660
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
|
||||
"duration": 38291
|
||||
"duration": 36866
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram.spec.js",
|
||||
"duration": 16949
|
||||
"duration": 17334
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
|
||||
"duration": 9480
|
||||
"duration": 9871
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/current.spec.js",
|
||||
"duration": 2753
|
||||
"duration": 2833
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
|
||||
"duration": 88028
|
||||
"duration": 85321
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram.spec.js",
|
||||
"duration": 15615
|
||||
"duration": 15673
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
|
||||
"duration": 3706
|
||||
"duration": 3724
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
|
||||
"duration": 43905
|
||||
"duration": 41178
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
|
||||
"duration": 31217
|
||||
"duration": 29966
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-icon.spec.js",
|
||||
"duration": 7531
|
||||
"duration": 7689
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
|
||||
"duration": 25423
|
||||
"duration": 24709
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
|
||||
"duration": 49664
|
||||
"duration": 45565
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart.spec.js",
|
||||
"duration": 32525
|
||||
"duration": 31144
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gantt.spec.js",
|
||||
"duration": 20915
|
||||
"duration": 20808
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gitGraph.spec.js",
|
||||
"duration": 53556
|
||||
"duration": 49985
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/iconShape.spec.ts",
|
||||
"duration": 283038
|
||||
"duration": 273272
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/imageShape.spec.ts",
|
||||
"duration": 59434
|
||||
"duration": 55880
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/info.spec.ts",
|
||||
"duration": 3101
|
||||
"duration": 3271
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/journey.spec.js",
|
||||
"duration": 7099
|
||||
"duration": 7293
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/kanban.spec.ts",
|
||||
"duration": 7567
|
||||
"duration": 7861
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/katex.spec.js",
|
||||
"duration": 3817
|
||||
"duration": 3922
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
|
||||
"duration": 2624
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js",
|
||||
"duration": 4246
|
||||
"duration": 2726
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap.spec.ts",
|
||||
"duration": 11967
|
||||
"duration": 11670
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/newShapes.spec.ts",
|
||||
"duration": 151914
|
||||
"duration": 146020
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
|
||||
"duration": 116698
|
||||
"duration": 114244
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/packet.spec.ts",
|
||||
"duration": 4967
|
||||
"duration": 5036
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/pie.spec.ts",
|
||||
"duration": 6700
|
||||
"duration": 6545
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
|
||||
"duration": 8963
|
||||
"duration": 9097
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/radar.spec.js",
|
||||
"duration": 5540
|
||||
"duration": 5676
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirement.spec.js",
|
||||
"duration": 2782
|
||||
"duration": 2795
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
|
||||
"duration": 54797
|
||||
"duration": 51660
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sankey.spec.ts",
|
||||
"duration": 6914
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram-v2.spec.js",
|
||||
"duration": 20481
|
||||
"duration": 6957
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
|
||||
"duration": 38490
|
||||
"duration": 36026
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
|
||||
"duration": 30766
|
||||
"duration": 29551
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
|
||||
"duration": 16705
|
||||
"duration": 17364
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/theme.spec.js",
|
||||
"duration": 30928
|
||||
"duration": 30209
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/timeline.spec.ts",
|
||||
"duration": 8424
|
||||
"duration": 8699
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/treemap.spec.ts",
|
||||
"duration": 12533
|
||||
"duration": 12168
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/xyChart.spec.js",
|
||||
"duration": 21197
|
||||
"duration": 21453
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/zenuml.spec.js",
|
||||
"duration": 3455
|
||||
"duration": 3577
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/1433#issuecomment-1991554712)
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
|
||||
2. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
|
||||
3. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
|
||||
4. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||
|
@@ -1,40 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/layouts.md](../../packages/mermaid/src/docs/config/layouts.md).
|
||||
|
||||
# Layouts
|
||||
|
||||
This page lists the available layout algorithms supported in Mermaid diagrams.
|
||||
|
||||
## Supported Layouts
|
||||
|
||||
- **elk**: [ELK (Eclipse Layout Kernel)](https://www.eclipse.org/elk/)
|
||||
- **tidy-tree**: Tidy tree layout for hierarchical diagrams [Tidy Tree Configuration](/config/tidy-tree)
|
||||
- **cose-bilkent**: Cose Bilkent layout for force-directed graphs
|
||||
- **dagre**: Dagre layout for layered graphs
|
||||
|
||||
## How to Use
|
||||
|
||||
You can specify the layout in your diagram's YAML config or initialization options. For example:
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
graph TD;
|
||||
A-->B;
|
||||
B-->C;
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
graph TD;
|
||||
A-->B;
|
||||
B-->C;
|
||||
```
|
@@ -19,7 +19,6 @@
|
||||
- [addDirective](functions/addDirective.md)
|
||||
- [getConfig](functions/getConfig.md)
|
||||
- [getSiteConfig](functions/getSiteConfig.md)
|
||||
- [getUserDefinedConfig](functions/getUserDefinedConfig.md)
|
||||
- [reset](functions/reset.md)
|
||||
- [sanitize](functions/sanitize.md)
|
||||
- [saveConfigFromInitialize](functions/saveConfigFromInitialize.md)
|
||||
|
@@ -1,19 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md](../../../../../packages/mermaid/src/docs/config/setup/config/functions/getUserDefinedConfig.md).
|
||||
|
||||
[**mermaid**](../../README.md)
|
||||
|
||||
---
|
||||
|
||||
# Function: getUserDefinedConfig()
|
||||
|
||||
> **getUserDefinedConfig**(): [`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/config.ts:252](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L252)
|
||||
|
||||
## Returns
|
||||
|
||||
[`MermaidConfig`](../../mermaid/interfaces/MermaidConfig.md)
|
@@ -10,6 +10,10 @@
|
||||
|
||||
# mermaid
|
||||
|
||||
## Classes
|
||||
|
||||
- [UnknownDiagramError](classes/UnknownDiagramError.md)
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [DetailedError](interfaces/DetailedError.md)
|
||||
@@ -23,7 +27,6 @@
|
||||
- [RenderOptions](interfaces/RenderOptions.md)
|
||||
- [RenderResult](interfaces/RenderResult.md)
|
||||
- [RunOptions](interfaces/RunOptions.md)
|
||||
- [UnknownDiagramError](interfaces/UnknownDiagramError.md)
|
||||
|
||||
## Type Aliases
|
||||
|
||||
|
159
docs/config/setup/mermaid/classes/UnknownDiagramError.md
Normal file
159
docs/config/setup/mermaid/classes/UnknownDiagramError.md
Normal file
@@ -0,0 +1,159 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/classes/UnknownDiagramError.md).
|
||||
|
||||
[**mermaid**](../../README.md)
|
||||
|
||||
---
|
||||
|
||||
# Class: UnknownDiagramError
|
||||
|
||||
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
|
||||
|
||||
## Extends
|
||||
|
||||
- `Error`
|
||||
|
||||
## Constructors
|
||||
|
||||
### new UnknownDiagramError()
|
||||
|
||||
> **new UnknownDiagramError**(`message`): [`UnknownDiagramError`](UnknownDiagramError.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/errors.ts:2](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L2)
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### message
|
||||
|
||||
`string`
|
||||
|
||||
#### Returns
|
||||
|
||||
[`UnknownDiagramError`](UnknownDiagramError.md)
|
||||
|
||||
#### Overrides
|
||||
|
||||
`Error.constructor`
|
||||
|
||||
## Properties
|
||||
|
||||
### cause?
|
||||
|
||||
> `optional` **cause**: `unknown`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.cause`
|
||||
|
||||
---
|
||||
|
||||
### message
|
||||
|
||||
> **message**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.message`
|
||||
|
||||
---
|
||||
|
||||
### name
|
||||
|
||||
> **name**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.name`
|
||||
|
||||
---
|
||||
|
||||
### stack?
|
||||
|
||||
> `optional` **stack**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.stack`
|
||||
|
||||
---
|
||||
|
||||
### prepareStackTrace()?
|
||||
|
||||
> `static` `optional` **prepareStackTrace**: (`err`, `stackTraces`) => `any`
|
||||
|
||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:143
|
||||
|
||||
Optional override for formatting stack traces
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### err
|
||||
|
||||
`Error`
|
||||
|
||||
##### stackTraces
|
||||
|
||||
`CallSite`\[]
|
||||
|
||||
#### Returns
|
||||
|
||||
`any`
|
||||
|
||||
#### See
|
||||
|
||||
<https://v8.dev/docs/stack-trace-api#customizing-stack-traces>
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.prepareStackTrace`
|
||||
|
||||
---
|
||||
|
||||
### stackTraceLimit
|
||||
|
||||
> `static` **stackTraceLimit**: `number`
|
||||
|
||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:145
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.stackTraceLimit`
|
||||
|
||||
## Methods
|
||||
|
||||
### captureStackTrace()
|
||||
|
||||
> `static` **captureStackTrace**(`targetObject`, `constructorOpt`?): `void`
|
||||
|
||||
Defined in: node_modules/.pnpm/@types+node\@22.13.5/node_modules/@types/node/globals.d.ts:136
|
||||
|
||||
Create .stack property on a target object
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### targetObject
|
||||
|
||||
`object`
|
||||
|
||||
##### constructorOpt?
|
||||
|
||||
`Function`
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.captureStackTrace`
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ExternalDiagramDefinition
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L94)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/me
|
||||
|
||||
> **detector**: `DiagramDetector`
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L98)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L96)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:98](https://github.com/me
|
||||
|
||||
> **id**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L95)
|
||||
|
||||
---
|
||||
|
||||
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/me
|
||||
|
||||
> **loader**: `DiagramLoader`
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:99](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L99)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97)
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: LayoutData
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L168)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:145](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L145)
|
||||
|
||||
## Indexable
|
||||
|
||||
@@ -22,7 +22,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:168](https://github.co
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L171)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:148](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L148)
|
||||
|
||||
---
|
||||
|
||||
@@ -30,7 +30,7 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:171](https://github.co
|
||||
|
||||
> **edges**: `Edge`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L170)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L147)
|
||||
|
||||
---
|
||||
|
||||
@@ -38,4 +38,4 @@ Defined in: [packages/mermaid/src/rendering-util/types.ts:170](https://github.co
|
||||
|
||||
> **nodes**: `Node`\[]
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L169)
|
||||
Defined in: [packages/mermaid/src/rendering-util/types.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L146)
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: LayoutLoaderDefinition
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:21](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L21)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.co
|
||||
|
||||
> `optional` **algorithm**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:27](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L27)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:24](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L24)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:27](https://github.co
|
||||
|
||||
> **loader**: `LayoutLoader`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:26](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L26)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:23](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L23)
|
||||
|
||||
---
|
||||
|
||||
@@ -34,4 +34,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:26](https://github.co
|
||||
|
||||
> **name**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:25](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L25)
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:22](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L22)
|
||||
|
@@ -32,7 +32,7 @@ page.
|
||||
|
||||
### detectType()
|
||||
|
||||
> **detectType**: (`text`, `config?`) => `string`
|
||||
> **detectType**: (`text`, `config`?) => `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:449](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L449)
|
||||
|
||||
@@ -105,7 +105,7 @@ An array of objects with the id of the diagram.
|
||||
|
||||
### ~~init()~~
|
||||
|
||||
> **init**: (`config?`, `nodes?`, `callback?`) => `Promise`<`void`>
|
||||
> **init**: (`config`?, `nodes`?, `callback`?) => `Promise`<`void`>
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L442)
|
||||
|
||||
@@ -117,7 +117,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:442](https://github.com/mermaid-js/
|
||||
|
||||
[`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
**Deprecated**, please set configuration in [initialize](#initialize).
|
||||
**Deprecated**, please set configuration in [initialize](Mermaid.md#initialize).
|
||||
|
||||
##### nodes?
|
||||
|
||||
@@ -141,13 +141,13 @@ Called once for each rendered diagram's id.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
Use [initialize](#initialize) and [run](#run) instead.
|
||||
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
|
||||
|
||||
Renders the mermaid diagrams
|
||||
|
||||
#### Deprecated
|
||||
|
||||
Use [initialize](#initialize) and [run](#run) instead.
|
||||
Use [initialize](Mermaid.md#initialize) and [run](Mermaid.md#run) instead.
|
||||
|
||||
---
|
||||
|
||||
@@ -176,7 +176,7 @@ Configuration object for mermaid.
|
||||
|
||||
### ~~mermaidAPI~~
|
||||
|
||||
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }; `render`: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
|
||||
> **mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](MermaidConfig.md); `getConfig`: () => [`MermaidConfig`](MermaidConfig.md); `getDiagramFromText`: (`text`, `metadata`) => `Promise`<`Diagram`>; `getSiteConfig`: () => [`MermaidConfig`](MermaidConfig.md); `globalReset`: () => `void`; `initialize`: (`userOptions`) => `void`; `parse`: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>; `render`: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>; `reset`: () => `void`; `setConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); `updateSiteConfig`: (`conf`) => [`MermaidConfig`](MermaidConfig.md); }>
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L436)
|
||||
|
||||
@@ -184,81 +184,73 @@ Defined in: [packages/mermaid/src/mermaid.ts:436](https://github.com/mermaid-js/
|
||||
|
||||
#### Deprecated
|
||||
|
||||
Use [parse](#parse) and [render](#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
|
||||
Use [parse](Mermaid.md#parse) and [render](Mermaid.md#render) instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API.
|
||||
|
||||
---
|
||||
|
||||
### parse()
|
||||
|
||||
> **parse**: {(`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>; (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>; }
|
||||
> **parse**: (`text`, `parseOptions`) => `Promise`<`false` | [`ParseResult`](ParseResult.md)>(`text`, `parseOptions`?) => `Promise`<[`ParseResult`](ParseResult.md)>
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:437](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L437)
|
||||
|
||||
#### Call Signature
|
||||
|
||||
> (`text`, `parseOptions`): `Promise`<`false` | [`ParseResult`](ParseResult.md)>
|
||||
|
||||
Parse the text and validate the syntax.
|
||||
|
||||
##### Parameters
|
||||
#### Parameters
|
||||
|
||||
###### text
|
||||
##### text
|
||||
|
||||
`string`
|
||||
|
||||
The mermaid diagram definition.
|
||||
|
||||
###### parseOptions
|
||||
##### parseOptions
|
||||
|
||||
[`ParseOptions`](ParseOptions.md) & `object`
|
||||
|
||||
Options for parsing.
|
||||
|
||||
##### Returns
|
||||
#### Returns
|
||||
|
||||
`Promise`<`false` | [`ParseResult`](ParseResult.md)>
|
||||
|
||||
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
||||
|
||||
##### See
|
||||
#### See
|
||||
|
||||
[ParseOptions](ParseOptions.md)
|
||||
|
||||
##### Throws
|
||||
#### Throws
|
||||
|
||||
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
||||
|
||||
#### Call Signature
|
||||
|
||||
> (`text`, `parseOptions?`): `Promise`<[`ParseResult`](ParseResult.md)>
|
||||
|
||||
Parse the text and validate the syntax.
|
||||
|
||||
##### Parameters
|
||||
#### Parameters
|
||||
|
||||
###### text
|
||||
##### text
|
||||
|
||||
`string`
|
||||
|
||||
The mermaid diagram definition.
|
||||
|
||||
###### parseOptions?
|
||||
##### parseOptions?
|
||||
|
||||
[`ParseOptions`](ParseOptions.md)
|
||||
|
||||
Options for parsing.
|
||||
|
||||
##### Returns
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`ParseResult`](ParseResult.md)>
|
||||
|
||||
An object with the `diagramType` set to type of the diagram if valid. Otherwise `false` if parseOptions.suppressErrors is `true`.
|
||||
|
||||
##### See
|
||||
#### See
|
||||
|
||||
[ParseOptions](ParseOptions.md)
|
||||
|
||||
##### Throws
|
||||
#### Throws
|
||||
|
||||
Error if the diagram is invalid and parseOptions.suppressErrors is false or not set.
|
||||
|
||||
@@ -340,7 +332,7 @@ Defined in: [packages/mermaid/src/mermaid.ts:444](https://github.com/mermaid-js/
|
||||
|
||||
### render()
|
||||
|
||||
> **render**: (`id`, `text`, `svgContainingElement?`) => `Promise`<[`RenderResult`](RenderResult.md)>
|
||||
> **render**: (`id`, `text`, `svgContainingElement`?) => `Promise`<[`RenderResult`](RenderResult.md)>
|
||||
|
||||
Defined in: [packages/mermaid/src/mermaid.ts:438](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaid.ts#L438)
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L88)
|
||||
Defined in: [packages/mermaid/src/types.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L84)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:88](https://github.com/mermaid-js/mer
|
||||
|
||||
> `optional` **suppressErrors**: `boolean`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:93](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L93)
|
||||
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
|
||||
|
||||
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
|
||||
The `parseError` function will not be called.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: ParseResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||
Defined in: [packages/mermaid/src/types.ts:92](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L92)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mer
|
||||
|
||||
> **config**: [`MermaidConfig`](MermaidConfig.md)
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L104)
|
||||
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
||||
|
||||
The config passed as YAML frontmatter or directives
|
||||
|
||||
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L100)
|
||||
Defined in: [packages/mermaid/src/types.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L96)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderOptions
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L10)
|
||||
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)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,4 +18,4 @@ Defined in: [packages/mermaid/src/rendering-util/render.ts:10](https://github.co
|
||||
|
||||
> `optional` **algorithm**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/rendering-util/render.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L11)
|
||||
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)
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: RenderResult
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||
Defined in: [packages/mermaid/src/types.ts:110](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L110)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/me
|
||||
|
||||
> `optional` **bindFunctions**: (`element`) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:132](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L132)
|
||||
Defined in: [packages/mermaid/src/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L128)
|
||||
|
||||
Bind function to be called after the svg has been inserted into the DOM.
|
||||
This is necessary for adding event listeners to the elements in the svg.
|
||||
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
||||
|
||||
> **diagramType**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:122](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L122)
|
||||
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||
|
||||
The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
|
||||
|
||||
> **svg**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/types.ts:118](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L118)
|
||||
Defined in: [packages/mermaid/src/types.ts:114](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L114)
|
||||
|
||||
The svg code for the rendered graph.
|
||||
|
@@ -1,65 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md](../../../../../packages/mermaid/src/docs/config/setup/mermaid/interfaces/UnknownDiagramError.md).
|
||||
|
||||
[**mermaid**](../../README.md)
|
||||
|
||||
---
|
||||
|
||||
# Interface: UnknownDiagramError
|
||||
|
||||
Defined in: [packages/mermaid/src/errors.ts:1](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/errors.ts#L1)
|
||||
|
||||
## Extends
|
||||
|
||||
- `Error`
|
||||
|
||||
## Properties
|
||||
|
||||
### cause?
|
||||
|
||||
> `optional` **cause**: `unknown`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es2022.error.d.ts:26
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.cause`
|
||||
|
||||
---
|
||||
|
||||
### message
|
||||
|
||||
> **message**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1077
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.message`
|
||||
|
||||
---
|
||||
|
||||
### name
|
||||
|
||||
> **name**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1076
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.name`
|
||||
|
||||
---
|
||||
|
||||
### stack?
|
||||
|
||||
> `optional` **stack**: `string`
|
||||
|
||||
Defined in: node_modules/.pnpm/typescript\@5.7.3/node_modules/typescript/lib/lib.es5.d.ts:1078
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`Error.stack`
|
@@ -10,6 +10,6 @@
|
||||
|
||||
# Type Alias: InternalHelpers
|
||||
|
||||
> **InternalHelpers** = _typeof_ `internalHelpers`
|
||||
> **InternalHelpers**: _typeof_ `internalHelpers`
|
||||
|
||||
Defined in: [packages/mermaid/src/internals.ts:33](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/internals.ts#L33)
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Type Alias: ParseErrorFunction()
|
||||
|
||||
> **ParseErrorFunction** = (`err`, `hash?`) => `void`
|
||||
> **ParseErrorFunction**: (`err`, `hash`?) => `void`
|
||||
|
||||
Defined in: [packages/mermaid/src/Diagram.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/Diagram.ts#L10)
|
||||
|
||||
|
@@ -10,6 +10,6 @@
|
||||
|
||||
# Type Alias: SVG
|
||||
|
||||
> **SVG** = `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
> **SVG**: `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L126)
|
||||
|
@@ -10,6 +10,6 @@
|
||||
|
||||
# Type Alias: SVGGroup
|
||||
|
||||
> **SVGGroup** = `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
> **SVGGroup**: `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`>
|
||||
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:130](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L130)
|
||||
Defined in: [packages/mermaid/src/diagram-api/types.ts:128](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L128)
|
||||
|
@@ -1,89 +0,0 @@
|
||||
> **Warning**
|
||||
>
|
||||
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||
>
|
||||
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/config/tidy-tree.md](../../packages/mermaid/src/docs/config/tidy-tree.md).
|
||||
|
||||
# Tidy-tree Layout
|
||||
|
||||
The **tidy-tree** layout arranges nodes in a hierarchical, tree-like structure. It is especially useful for diagrams where parent-child relationships are important, such as mindmaps.
|
||||
|
||||
## Features
|
||||
|
||||
- Organizes nodes in a tidy, non-overlapping tree
|
||||
- Ideal for mindmaps and hierarchical data
|
||||
- Automatically adjusts spacing for readability
|
||||
|
||||
## Example Usage
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
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
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
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
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
- Currently, tidy-tree is primarily supported for mindmap diagrams.
|
@@ -326,9 +326,7 @@ Below is a comprehensive list of the newly introduced shapes and their correspon
|
||||
|
||||
| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| Bang | Bang | `bang` | Bang | `bang` |
|
||||
| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` |
|
||||
| Cloud | Cloud | `cloud` | cloud | `cloud` |
|
||||
| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` |
|
||||
| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` |
|
||||
| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` |
|
||||
|
@@ -314,22 +314,3 @@ You can also refer the [implementation in the live editor](https://github.com/me
|
||||
cspell:locale en,en-gb
|
||||
cspell:ignore Buzan
|
||||
--->
|
||||
|
||||
## Layouts
|
||||
|
||||
Mermaid also supports a Tidy Tree layout for mindmaps.
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap is a long thing))
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
```
|
||||
|
||||
Instructions to add and register tidy-tree layout are present in [Tidy Tree Configuration](/config/tidy-tree)
|
||||
|
@@ -138,7 +138,7 @@ xychart
|
||||
|
||||
## Chart Theme Variables
|
||||
|
||||
Themes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:
|
||||
Themes for xychart resides inside xychart attribute so to set the variables use this syntax:
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -163,52 +163,6 @@ config:
|
||||
| yAxisLineColor | Color of the y-axis line |
|
||||
| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |
|
||||
|
||||
### Setting Colors for Lines and Bars
|
||||
|
||||
To set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).
|
||||
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||
---
|
||||
xychart
|
||||
title "Different Colors in xyChart"
|
||||
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||
y-axis "valuesY" 0 --> 50
|
||||
%% Black line
|
||||
line [10,20,30,40]
|
||||
%% Blue bar
|
||||
bar [20,30,25,35]
|
||||
%% Green bar
|
||||
bar [15,25,20,30]
|
||||
%% Red line
|
||||
line [5,15,25,35]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
themeVariables:
|
||||
xyChart:
|
||||
plotColorPalette: '#000000, #0000FF, #00FF00, #FF0000'
|
||||
---
|
||||
xychart
|
||||
title "Different Colors in xyChart"
|
||||
x-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]
|
||||
y-axis "valuesY" 0 --> 50
|
||||
%% Black line
|
||||
line [10,20,30,40]
|
||||
%% Blue bar
|
||||
bar [20,30,25,35]
|
||||
%% Green bar
|
||||
bar [15,25,20,30]
|
||||
%% Red line
|
||||
line [5,15,25,35]
|
||||
```
|
||||
|
||||
## Example on config and theme
|
||||
|
||||
```mermaid-example
|
||||
|
@@ -17,7 +17,6 @@ export default tseslint.config(
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
ignores: [
|
||||
'**/*.d.ts',
|
||||
'**/dist/',
|
||||
'**/node_modules/',
|
||||
'.git/',
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine.
|
||||
|
||||
> [!NOTE]
|
||||
> [!NOTE]
|
||||
> The ELK Layout engine will not be available in all providers that support mermaid by default.
|
||||
> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine.
|
||||
|
||||
@@ -69,4 +69,4 @@ mermaid.registerLayoutLoaders(elkLayouts);
|
||||
- `elk.mrtree`: Multi-root tree layout
|
||||
- `elk.sporeOverlap`: Spore overlap layout
|
||||
|
||||
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively. -->
|
||||
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively -->
|
||||
|
@@ -1,67 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
intersection,
|
||||
ensureTrulyOutside,
|
||||
makeInsidePoint,
|
||||
tryNodeIntersect,
|
||||
replaceEndpoint,
|
||||
type RectLike,
|
||||
type P,
|
||||
} from '../geometry.js';
|
||||
|
||||
const approx = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
|
||||
|
||||
describe('geometry helpers', () => {
|
||||
it('intersection: vertical approach hits bottom border', () => {
|
||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||
const h = rect.height / 2; // 25
|
||||
const outside: P = { x: 0, y: 100 };
|
||||
const inside: P = { x: 0, y: 0 };
|
||||
const res = intersection(rect, outside, inside);
|
||||
expect(approx(res.x, 0)).toBe(true);
|
||||
expect(approx(res.y, h)).toBe(true);
|
||||
});
|
||||
|
||||
it('ensureTrulyOutside nudges near-boundary point outward', () => {
|
||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||
// near bottom boundary (y ~ h)
|
||||
const near: P = { x: 0, y: rect.height / 2 - 0.2 };
|
||||
const out = ensureTrulyOutside(rect, near, 10);
|
||||
expect(out.y).toBeGreaterThan(rect.height / 2);
|
||||
});
|
||||
|
||||
it('makeInsidePoint keeps x for vertical and y from center', () => {
|
||||
const rect: RectLike = { x: 10, y: 5, width: 100, height: 50 };
|
||||
const outside: P = { x: 10, y: 40 };
|
||||
const center: P = { x: 99, y: -123 }; // center y should be used
|
||||
const inside = makeInsidePoint(rect, outside, center);
|
||||
expect(inside.x).toBe(outside.x);
|
||||
expect(inside.y).toBe(center.y);
|
||||
});
|
||||
|
||||
it('tryNodeIntersect returns null for wrong-side intersections', () => {
|
||||
const rect: RectLike = { x: 0, y: 0, width: 100, height: 50 };
|
||||
const outside: P = { x: -50, y: 0 };
|
||||
const node = { intersect: () => ({ x: 10, y: 0 }) } as any; // right side of center
|
||||
const res = tryNodeIntersect(node, rect, outside);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
it('replaceEndpoint dedup removes end/start appropriately', () => {
|
||||
const pts: P[] = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
];
|
||||
// remove duplicate end
|
||||
replaceEndpoint(pts, 'end', { x: 1, y: 1 });
|
||||
expect(pts.length).toBe(1);
|
||||
|
||||
const pts2: P[] = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
];
|
||||
// remove duplicate start
|
||||
replaceEndpoint(pts2, 'start', { x: 0, y: 0 });
|
||||
expect(pts2.length).toBe(1);
|
||||
});
|
||||
});
|
@@ -1,9 +0,0 @@
|
||||
export interface TreeData {
|
||||
parentById: Record<string, string>;
|
||||
childrenById: Record<string, string[]>;
|
||||
}
|
||||
export declare const findCommonAncestor: (
|
||||
id1: string,
|
||||
id2: string,
|
||||
{ parentById }: TreeData
|
||||
) => string;
|
@@ -1,209 +0,0 @@
|
||||
/* Geometry utilities extracted from render.ts for reuse and testing */
|
||||
|
||||
export interface P {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface RectLike {
|
||||
x: number; // center x
|
||||
y: number; // center y
|
||||
width: number;
|
||||
height: number;
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
export interface NodeLike {
|
||||
intersect?: (p: P) => P | null;
|
||||
}
|
||||
|
||||
export const EPS = 1;
|
||||
export const PUSH_OUT = 10;
|
||||
|
||||
export const onBorder = (bounds: RectLike, p: P, tol = 0.5): boolean => {
|
||||
const halfW = bounds.width / 2;
|
||||
const halfH = bounds.height / 2;
|
||||
const left = bounds.x - halfW;
|
||||
const right = bounds.x + halfW;
|
||||
const top = bounds.y - halfH;
|
||||
const bottom = bounds.y + halfH;
|
||||
|
||||
const onLeft = Math.abs(p.x - left) <= tol && p.y >= top - tol && p.y <= bottom + tol;
|
||||
const onRight = Math.abs(p.x - right) <= tol && p.y >= top - tol && p.y <= bottom + tol;
|
||||
const onTop = Math.abs(p.y - top) <= tol && p.x >= left - tol && p.x <= right + tol;
|
||||
const onBottom = Math.abs(p.y - bottom) <= tol && p.x >= left - tol && p.x <= right + tol;
|
||||
return onLeft || onRight || onTop || onBottom;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute intersection between a rectangle (center x/y, width/height) and the line
|
||||
* segment from insidePoint -\> outsidePoint. Returns the point on the rectangle border.
|
||||
*
|
||||
* This version avoids snapping to outsidePoint when certain variables evaluate to 0
|
||||
* (previously caused vertical top/bottom cases to miss the border). It only enforces
|
||||
* axis-constant behavior for purely vertical/horizontal approaches.
|
||||
*/
|
||||
export const intersection = (node: RectLike, outsidePoint: P, insidePoint: P): P => {
|
||||
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.
|
||||
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,
|
||||
};
|
||||
|
||||
// Keep axis-constant special-cases only
|
||||
if (R === 0) {
|
||||
res.x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
res.y = outsidePoint.y;
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
// Intersection on sides of rect
|
||||
if (insidePoint.x < outsidePoint.x) {
|
||||
r = outsidePoint.x - w - x;
|
||||
} else {
|
||||
r = x - w - outsidePoint.x;
|
||||
}
|
||||
const 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;
|
||||
|
||||
// Only handle axis-constant cases
|
||||
if (R === 0) {
|
||||
_x = outsidePoint.x;
|
||||
}
|
||||
if (Q === 0) {
|
||||
_y = outsidePoint.y;
|
||||
}
|
||||
|
||||
return { x: _x, y: _y };
|
||||
}
|
||||
};
|
||||
|
||||
export const outsideNode = (node: RectLike, point: P): boolean => {
|
||||
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 ensureTrulyOutside = (bounds: RectLike, p: P, push = PUSH_OUT): P => {
|
||||
const dx = Math.abs(p.x - bounds.x);
|
||||
const dy = Math.abs(p.y - bounds.y);
|
||||
const w = bounds.width / 2;
|
||||
const h = bounds.height / 2;
|
||||
if (Math.abs(dx - w) < EPS || Math.abs(dy - h) < EPS) {
|
||||
const dirX = p.x - bounds.x;
|
||||
const dirY = p.y - bounds.y;
|
||||
const len = Math.sqrt(dirX * dirX + dirY * dirY);
|
||||
if (len > 0) {
|
||||
return {
|
||||
x: bounds.x + (dirX / len) * (len + push),
|
||||
y: bounds.y + (dirY / len) * (len + push),
|
||||
};
|
||||
}
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
export const makeInsidePoint = (bounds: RectLike, outside: P, center: P): P => {
|
||||
const isVertical = Math.abs(outside.x - bounds.x) < EPS;
|
||||
const isHorizontal = Math.abs(outside.y - bounds.y) < EPS;
|
||||
return {
|
||||
x: isVertical
|
||||
? outside.x
|
||||
: outside.x < bounds.x
|
||||
? bounds.x - bounds.width / 4
|
||||
: bounds.x + bounds.width / 4,
|
||||
y: isHorizontal ? outside.y : center.y,
|
||||
};
|
||||
};
|
||||
|
||||
export const tryNodeIntersect = (node: NodeLike, bounds: RectLike, outside: P): P | null => {
|
||||
if (!node?.intersect) {
|
||||
return null;
|
||||
}
|
||||
const res = node.intersect(outside);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
const wrongSide =
|
||||
(outside.x < bounds.x && res.x > bounds.x) || (outside.x > bounds.x && res.x < bounds.x);
|
||||
if (wrongSide) {
|
||||
return null;
|
||||
}
|
||||
const dist = Math.hypot(outside.x - res.x, outside.y - res.y);
|
||||
if (dist <= EPS) {
|
||||
return null;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export const fallbackIntersection = (bounds: RectLike, outside: P, center: P): P => {
|
||||
const inside = makeInsidePoint(bounds, outside, center);
|
||||
return intersection(bounds, outside, inside);
|
||||
};
|
||||
|
||||
export const computeNodeIntersection = (
|
||||
node: NodeLike,
|
||||
bounds: RectLike,
|
||||
outside: P,
|
||||
center: P
|
||||
): P => {
|
||||
const outside2 = ensureTrulyOutside(bounds, outside);
|
||||
return tryNodeIntersect(node, bounds, outside2) ?? fallbackIntersection(bounds, outside2, center);
|
||||
};
|
||||
|
||||
export const replaceEndpoint = (
|
||||
points: P[],
|
||||
which: 'start' | 'end',
|
||||
value: P | null | undefined,
|
||||
tol = 0.1
|
||||
) => {
|
||||
if (!value || points.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (which === 'start') {
|
||||
if (
|
||||
points.length > 0 &&
|
||||
Math.abs(points[0].x - value.x) < tol &&
|
||||
Math.abs(points[0].y - value.y) < tol
|
||||
) {
|
||||
// duplicate start remove it
|
||||
points.shift();
|
||||
} else {
|
||||
points[0] = value;
|
||||
}
|
||||
} else {
|
||||
const last = points.length - 1;
|
||||
if (
|
||||
points.length > 0 &&
|
||||
Math.abs(points[last].x - value.x) < tol &&
|
||||
Math.abs(points[last].y - value.y) < tol
|
||||
) {
|
||||
// duplicate end remove it
|
||||
points.pop();
|
||||
} else {
|
||||
points[last] = value;
|
||||
}
|
||||
}
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,6 @@
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
|
||||
"include": ["./src/**/*.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
||||
|
@@ -1,59 +0,0 @@
|
||||
# @mermaid-js/layout-tidy-tree
|
||||
|
||||
This package provides a bidirectional tidy tree layout engine for Mermaid based on the non-layered-tidy-tree-layout algorithm.
|
||||
|
||||
> [!NOTE]
|
||||
> The Tidy Tree Layout engine will not be available in all providers that support mermaid by default.
|
||||
> The websites will have to install the @mermaid-js/layout-tidy-tree package to use the Tidy Tree layout engine.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: tidy-tree
|
||||
---
|
||||
mindmap
|
||||
root((mindmap))
|
||||
A
|
||||
B
|
||||
```
|
||||
|
||||
### With bundlers
|
||||
|
||||
```sh
|
||||
npm install @mermaid-js/layout-tidy-tree
|
||||
```
|
||||
|
||||
```ts
|
||||
import mermaid from 'mermaid';
|
||||
import tidyTreeLayouts from '@mermaid-js/layout-tidy-tree';
|
||||
|
||||
mermaid.registerLayoutLoaders(tidyTreeLayouts);
|
||||
```
|
||||
|
||||
### With CDN
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||
import tidyTreeLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-tidy-tree@0/dist/mermaid-layout-tidy-tree.esm.min.mjs';
|
||||
|
||||
mermaid.registerLayoutLoaders(tidyTreeLayouts);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Tidy Tree Layout Overview
|
||||
|
||||
tidy-tree: The bidirectional tidy tree layout
|
||||
|
||||
The bidirectional tidy tree layout algorithm creates two separate trees that grow horizontally in opposite directions from a central root node:
|
||||
Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
|
||||
Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
|
||||
|
||||
This creates a balanced, symmetric layout that is ideal for mindmaps, organizational charts, and other tree-based diagrams.
|
||||
|
||||
Layout Structure:
|
||||
[Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
|
||||
↓ ↓ ↓ ↓
|
||||
[GrandChild] [GrandChild] [GrandChild] [GrandChild]
|
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@mermaid-js/layout-tidy-tree",
|
||||
"version": "0.1.0",
|
||||
"description": "Tidy-tree layout engine for mermaid",
|
||||
"module": "dist/mermaid-layout-tidy-tree.core.mjs",
|
||||
"types": "dist/layouts.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-layout-tidy-tree.core.mjs",
|
||||
"types": "./dist/layouts.d.ts"
|
||||
},
|
||||
"./": "./"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"tidy-tree",
|
||||
"mermaid",
|
||||
"layout"
|
||||
],
|
||||
"scripts": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"contributors": [
|
||||
"Knut Sveidqvist",
|
||||
"Sidharth Vinod"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.4.3",
|
||||
"mermaid": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mermaid": "^11.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Bidirectional Tidy-Tree Layout Algorithm for Generic Diagrams
|
||||
*
|
||||
* This module provides a layout algorithm implementation using the
|
||||
* non-layered-tidy-tree-layout algorithm for positioning nodes and edges
|
||||
* in tree structures with a bidirectional approach.
|
||||
*
|
||||
* The algorithm creates two separate trees that grow horizontally in opposite
|
||||
* directions from a central root node:
|
||||
* - Left tree: grows horizontally to the left (children alternate: 1st, 3rd, 5th...)
|
||||
* - Right tree: grows horizontally to the right (children alternate: 2nd, 4th, 6th...)
|
||||
*
|
||||
* This creates a balanced, symmetric layout that is ideal for mindmaps,
|
||||
* organizational charts, and other tree-based diagrams.
|
||||
*
|
||||
* The algorithm follows the unified rendering pattern and can be used
|
||||
* by any diagram type that provides compatible LayoutData.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render function for the bidirectional tidy-tree layout algorithm
|
||||
*
|
||||
* This function follows the unified rendering pattern used by all layout algorithms.
|
||||
* It takes LayoutData, inserts nodes into DOM, runs the bidirectional tidy-tree layout algorithm,
|
||||
* and renders the positioned elements to the SVG.
|
||||
*
|
||||
* Features:
|
||||
* - Alternates root children between left and right trees
|
||||
* - Left tree grows horizontally to the left (rotated 90° counterclockwise)
|
||||
* - Right tree grows horizontally to the right (rotated 90° clockwise)
|
||||
* - Uses tidy-tree algorithm for optimal spacing within each tree
|
||||
* - Creates symmetric, balanced layouts
|
||||
* - Maintains proper edge connections between all nodes
|
||||
*
|
||||
* Layout Structure:
|
||||
* ```
|
||||
* [Child 3] ← [Child 1] ← [Root] → [Child 2] → [Child 4]
|
||||
* ↓ ↓ ↓ ↓
|
||||
* [GrandChild] [GrandChild] [GrandChild] [GrandChild]
|
||||
* ```
|
||||
*
|
||||
* @param layoutData - Layout data containing nodes, edges, and configuration
|
||||
* @param svg - SVG element to render to
|
||||
* @param helpers - Internal helper functions for rendering
|
||||
* @param options - Rendering options
|
||||
*/
|
||||
export { default } from './layouts.js';
|
||||
export * from './types.js';
|
||||
export * from './layout.js';
|
||||
export { render } from './render.js';
|
@@ -1,409 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { executeTidyTreeLayout, validateLayoutData } from './layout.js';
|
||||
import type { LayoutResult } from './types.js';
|
||||
import type { LayoutData, MermaidConfig } from 'mermaid';
|
||||
|
||||
// Mock non-layered-tidy-tree-layout
|
||||
vi.mock('non-layered-tidy-tree-layout', () => ({
|
||||
BoundingBox: vi.fn().mockImplementation(() => ({})),
|
||||
Layout: vi.fn().mockImplementation(() => ({
|
||||
layout: vi.fn().mockImplementation((treeData) => {
|
||||
const result = { ...treeData };
|
||||
|
||||
if (result.id?.toString().startsWith('virtual-root')) {
|
||||
result.x = 0;
|
||||
result.y = 0;
|
||||
} else {
|
||||
result.x = 100;
|
||||
result.y = 50;
|
||||
}
|
||||
|
||||
if (result.children) {
|
||||
result.children.forEach((child: any, index: number) => {
|
||||
child.x = 50 + index * 100;
|
||||
child.y = 100;
|
||||
|
||||
if (child.children) {
|
||||
child.children.forEach((grandchild: any, gIndex: number) => {
|
||||
grandchild.x = 25 + gIndex * 50;
|
||||
grandchild.y = 200;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
result,
|
||||
boundingBox: {
|
||||
left: 0,
|
||||
right: 200,
|
||||
top: 0,
|
||||
bottom: 250,
|
||||
},
|
||||
};
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Tidy-Tree Layout Algorithm', () => {
|
||||
let mockConfig: MermaidConfig;
|
||||
let mockLayoutData: LayoutData;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
theme: 'default',
|
||||
} as MermaidConfig;
|
||||
|
||||
mockLayoutData = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'Root',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 100,
|
||||
height: 50,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child1',
|
||||
label: 'Child 1',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child2',
|
||||
label: 'Child 2',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child3',
|
||||
label: 'Child 3',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'child4',
|
||||
label: 'Child 4',
|
||||
isGroup: false,
|
||||
shape: 'rect',
|
||||
width: 80,
|
||||
height: 40,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'root_child1',
|
||||
start: 'root',
|
||||
end: 'child1',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_child2',
|
||||
start: 'root',
|
||||
end: 'child2',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_child3',
|
||||
start: 'root',
|
||||
end: 'child3',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_child4',
|
||||
start: 'root',
|
||||
end: 'child4',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
],
|
||||
config: mockConfig,
|
||||
direction: 'TB',
|
||||
type: 'test',
|
||||
diagramId: 'test-diagram',
|
||||
markers: [],
|
||||
};
|
||||
});
|
||||
|
||||
describe('validateLayoutData', () => {
|
||||
it('should validate correct layout data', () => {
|
||||
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for missing data', () => {
|
||||
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
|
||||
});
|
||||
|
||||
it('should throw error for missing config', () => {
|
||||
const invalidData = { ...mockLayoutData, config: null as any };
|
||||
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
|
||||
});
|
||||
|
||||
it('should throw error for invalid nodes array', () => {
|
||||
const invalidData = { ...mockLayoutData, nodes: null as any };
|
||||
expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required');
|
||||
});
|
||||
|
||||
it('should throw error for invalid edges array', () => {
|
||||
const invalidData = { ...mockLayoutData, edges: null as any };
|
||||
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeTidyTreeLayout function', () => {
|
||||
it('should execute layout algorithm successfully', async () => {
|
||||
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toBeDefined();
|
||||
expect(result.edges).toBeDefined();
|
||||
expect(Array.isArray(result.nodes)).toBe(true);
|
||||
expect(Array.isArray(result.edges)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return positioned nodes with coordinates', async () => {
|
||||
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result.nodes.length).toBeGreaterThan(0);
|
||||
result.nodes.forEach((node) => {
|
||||
expect(node.x).toBeDefined();
|
||||
expect(node.y).toBeDefined();
|
||||
expect(typeof node.x).toBe('number');
|
||||
expect(typeof node.y).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return positioned edges with coordinates', async () => {
|
||||
const result: LayoutResult = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result.edges.length).toBeGreaterThan(0);
|
||||
result.edges.forEach((edge) => {
|
||||
expect(edge.startX).toBeDefined();
|
||||
expect(edge.startY).toBeDefined();
|
||||
expect(edge.midX).toBeDefined();
|
||||
expect(edge.midY).toBeDefined();
|
||||
expect(edge.endX).toBeDefined();
|
||||
expect(edge.endY).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty layout data gracefully', async () => {
|
||||
const emptyData: LayoutData = {
|
||||
...mockLayoutData,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
await expect(executeTidyTreeLayout(emptyData)).rejects.toThrow(
|
||||
'No nodes found in layout data'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing nodes', async () => {
|
||||
const invalidData = { ...mockLayoutData, nodes: [] };
|
||||
|
||||
await expect(executeTidyTreeLayout(invalidData)).rejects.toThrow(
|
||||
'No nodes found in layout data'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty edges (single node tree)', async () => {
|
||||
const singleNodeData = {
|
||||
...mockLayoutData,
|
||||
edges: [],
|
||||
nodes: [mockLayoutData.nodes[0]],
|
||||
};
|
||||
|
||||
const result = await executeTidyTreeLayout(singleNodeData);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.edges).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should create bidirectional dual-tree layout with alternating left/right children', async () => {
|
||||
const result = await executeTidyTreeLayout(mockLayoutData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(5);
|
||||
|
||||
const rootNode = result.nodes.find((node) => node.id === 'root');
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(rootNode!.x).toBe(0);
|
||||
expect(rootNode!.y).toBe(20);
|
||||
|
||||
const child1 = result.nodes.find((node) => node.id === 'child1');
|
||||
const child2 = result.nodes.find((node) => node.id === 'child2');
|
||||
const child3 = result.nodes.find((node) => node.id === 'child3');
|
||||
const child4 = result.nodes.find((node) => node.id === 'child4');
|
||||
|
||||
expect(child1).toBeDefined();
|
||||
expect(child2).toBeDefined();
|
||||
expect(child3).toBeDefined();
|
||||
expect(child4).toBeDefined();
|
||||
|
||||
expect(child1!.x).toBeLessThan(rootNode!.x);
|
||||
expect(child2!.x).toBeGreaterThan(rootNode!.x);
|
||||
expect(child3!.x).toBeLessThan(rootNode!.x);
|
||||
expect(child4!.x).toBeGreaterThan(rootNode!.x);
|
||||
|
||||
expect(child1!.x).toBeLessThan(-100);
|
||||
expect(child3!.x).toBeLessThan(-100);
|
||||
|
||||
expect(child2!.x).toBeGreaterThan(100);
|
||||
expect(child4!.x).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it('should correctly transpose coordinates to prevent high nodes from covering nodes above them', async () => {
|
||||
const testData = {
|
||||
...mockLayoutData,
|
||||
nodes: [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'Root',
|
||||
isGroup: false,
|
||||
shape: 'rect' as const,
|
||||
width: 100,
|
||||
height: 50,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'tall-child',
|
||||
label: 'Tall Child',
|
||||
isGroup: false,
|
||||
shape: 'rect' as const,
|
||||
width: 80,
|
||||
height: 120,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
{
|
||||
id: 'short-child',
|
||||
label: 'Short Child',
|
||||
isGroup: false,
|
||||
shape: 'rect' as const,
|
||||
width: 80,
|
||||
height: 30,
|
||||
padding: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cssClasses: '',
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'root_tall',
|
||||
start: 'root',
|
||||
end: 'tall-child',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
{
|
||||
id: 'root_short',
|
||||
start: 'root',
|
||||
end: 'short-child',
|
||||
type: 'edge',
|
||||
classes: '',
|
||||
style: [],
|
||||
animate: false,
|
||||
arrowTypeEnd: 'arrow_point',
|
||||
arrowTypeStart: 'none',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await executeTidyTreeLayout(testData);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
|
||||
const rootNode = result.nodes.find((node) => node.id === 'root');
|
||||
const tallChild = result.nodes.find((node) => node.id === 'tall-child');
|
||||
const shortChild = result.nodes.find((node) => node.id === 'short-child');
|
||||
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(tallChild).toBeDefined();
|
||||
expect(shortChild).toBeDefined();
|
||||
|
||||
expect(tallChild!.x).not.toBe(shortChild!.x);
|
||||
|
||||
expect(tallChild!.width).toBe(80);
|
||||
expect(tallChild!.height).toBe(120);
|
||||
expect(shortChild!.width).toBe(80);
|
||||
expect(shortChild!.height).toBe(30);
|
||||
|
||||
const yDifference = Math.abs(tallChild!.y - shortChild!.y);
|
||||
expect(yDifference).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,629 +0,0 @@
|
||||
import type { LayoutData } from 'mermaid';
|
||||
import type { Bounds, Point } from 'mermaid/src/types.js';
|
||||
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout';
|
||||
import type {
|
||||
Edge,
|
||||
LayoutResult,
|
||||
Node,
|
||||
PositionedEdge,
|
||||
PositionedNode,
|
||||
TidyTreeNode,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Execute the tidy-tree layout algorithm on generic layout data
|
||||
*
|
||||
* This function takes layout data and uses the non-layered-tidy-tree-layout
|
||||
* algorithm to calculate optimal node positions for tree structures.
|
||||
*
|
||||
* @param data - The layout data containing nodes, edges, and configuration
|
||||
* @param config - Mermaid configuration object
|
||||
* @returns Promise resolving to layout result with positioned nodes and edges
|
||||
*/
|
||||
export function executeTidyTreeLayout(data: LayoutData): Promise<LayoutResult> {
|
||||
let intersectionShift = 50;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!data.nodes || !Array.isArray(data.nodes) || data.nodes.length === 0) {
|
||||
throw new Error('No nodes found in layout data');
|
||||
}
|
||||
|
||||
if (!data.edges || !Array.isArray(data.edges)) {
|
||||
data.edges = [];
|
||||
}
|
||||
|
||||
const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
|
||||
|
||||
const gap = 20;
|
||||
const bottomPadding = 40;
|
||||
intersectionShift = 30;
|
||||
|
||||
const bb = new BoundingBox(gap, bottomPadding);
|
||||
const layout = new Layout(bb);
|
||||
|
||||
let leftResult = null;
|
||||
let rightResult = null;
|
||||
|
||||
if (leftTree) {
|
||||
const leftLayoutResult = layout.layout(leftTree);
|
||||
leftResult = leftLayoutResult.result;
|
||||
}
|
||||
|
||||
if (rightTree) {
|
||||
const rightLayoutResult = layout.layout(rightTree);
|
||||
rightResult = rightLayoutResult.result;
|
||||
}
|
||||
|
||||
const positionedNodes = combineAndPositionTrees(rootNode, leftResult, rightResult);
|
||||
const positionedEdges = calculateEdgePositions(
|
||||
data.edges,
|
||||
positionedNodes,
|
||||
intersectionShift
|
||||
);
|
||||
resolve({
|
||||
nodes: positionedNodes,
|
||||
edges: positionedEdges,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LayoutData to dual-tree format (left and right trees)
|
||||
*
|
||||
* This function builds two separate tree structures from the nodes and edges,
|
||||
* alternating children between left and right trees.
|
||||
*/
|
||||
function convertToDualTreeFormat(data: LayoutData): {
|
||||
leftTree: TidyTreeNode | null;
|
||||
rightTree: TidyTreeNode | null;
|
||||
rootNode: TidyTreeNode;
|
||||
} {
|
||||
const { nodes, edges } = data;
|
||||
|
||||
const nodeMap = new Map<string, Node>();
|
||||
nodes.forEach((node) => nodeMap.set(node.id, node));
|
||||
|
||||
const children = new Map<string, string[]>();
|
||||
const parents = new Map<string, string>();
|
||||
|
||||
edges.forEach((edge) => {
|
||||
const parentId = edge.start;
|
||||
const childId = edge.end;
|
||||
|
||||
if (parentId && childId) {
|
||||
if (!children.has(parentId)) {
|
||||
children.set(parentId, []);
|
||||
}
|
||||
children.get(parentId)!.push(childId);
|
||||
parents.set(childId, parentId);
|
||||
}
|
||||
});
|
||||
|
||||
const rootNodeData = nodes.find((node) => !parents.has(node.id));
|
||||
if (!rootNodeData && nodes.length === 0) {
|
||||
throw new Error('No nodes available to create tree');
|
||||
}
|
||||
|
||||
const actualRoot = rootNodeData ?? nodes[0];
|
||||
|
||||
const rootNode: TidyTreeNode = {
|
||||
id: actualRoot.id,
|
||||
width: actualRoot.width ?? 100,
|
||||
height: actualRoot.height ?? 50,
|
||||
_originalNode: actualRoot,
|
||||
};
|
||||
|
||||
const rootChildren = children.get(actualRoot.id) ?? [];
|
||||
const leftChildren: string[] = [];
|
||||
const rightChildren: string[] = [];
|
||||
|
||||
rootChildren.forEach((childId, index) => {
|
||||
if (index % 2 === 0) {
|
||||
leftChildren.push(childId);
|
||||
} else {
|
||||
rightChildren.push(childId);
|
||||
}
|
||||
});
|
||||
|
||||
const leftTree = leftChildren.length > 0 ? buildSubTree(leftChildren, children, nodeMap) : null;
|
||||
|
||||
const rightTree =
|
||||
rightChildren.length > 0 ? buildSubTree(rightChildren, children, nodeMap) : null;
|
||||
|
||||
return { leftTree, rightTree, rootNode };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a subtree from a list of root children
|
||||
* For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
|
||||
*/
|
||||
function buildSubTree(
|
||||
rootChildren: string[],
|
||||
children: Map<string, string[]>,
|
||||
nodeMap: Map<string, Node>
|
||||
): TidyTreeNode {
|
||||
const virtualRoot: TidyTreeNode = {
|
||||
id: `virtual-root-${Math.random()}`,
|
||||
width: 1,
|
||||
height: 1,
|
||||
children: rootChildren
|
||||
.map((childId) => nodeMap.get(childId))
|
||||
.filter((child): child is Node => child !== undefined)
|
||||
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
|
||||
};
|
||||
|
||||
return virtualRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively convert a node and its children to tidy-tree format
|
||||
* This version transposes width/height for horizontal tree layout
|
||||
*/
|
||||
function convertNodeToTidyTreeTransposed(
|
||||
node: Node,
|
||||
children: Map<string, string[]>,
|
||||
nodeMap: Map<string, Node>
|
||||
): TidyTreeNode {
|
||||
const childIds = children.get(node.id) ?? [];
|
||||
const childNodes = childIds
|
||||
.map((childId) => nodeMap.get(childId))
|
||||
.filter((child): child is Node => child !== undefined)
|
||||
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
width: node.height ?? 50,
|
||||
height: node.width ?? 100,
|
||||
children: childNodes.length > 0 ? childNodes : undefined,
|
||||
_originalNode: node,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Combine and position the left and right trees around the root node
|
||||
* Creates a bidirectional layout where left tree grows left and right tree grows right
|
||||
*/
|
||||
function combineAndPositionTrees(
|
||||
rootNode: TidyTreeNode,
|
||||
leftResult: TidyTreeNode | null,
|
||||
rightResult: TidyTreeNode | null
|
||||
): PositionedNode[] {
|
||||
const positionedNodes: PositionedNode[] = [];
|
||||
|
||||
const rootX = 0;
|
||||
const rootY = 0;
|
||||
|
||||
const treeSpacing = rootNode.width / 2 + 30;
|
||||
const leftTreeNodes: PositionedNode[] = [];
|
||||
const rightTreeNodes: PositionedNode[] = [];
|
||||
|
||||
if (leftResult?.children) {
|
||||
positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
|
||||
}
|
||||
|
||||
if (rightResult?.children) {
|
||||
positionRightTreeBidirectional(
|
||||
rightResult.children,
|
||||
rightTreeNodes,
|
||||
rootX + treeSpacing,
|
||||
rootY
|
||||
);
|
||||
}
|
||||
|
||||
let leftTreeCenterY = 0;
|
||||
let rightTreeCenterY = 0;
|
||||
|
||||
if (leftTreeNodes.length > 0) {
|
||||
const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
|
||||
(a, b) => b - a
|
||||
);
|
||||
const firstLevelLeftX = leftTreeXPositions[0];
|
||||
const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
|
||||
|
||||
if (firstLevelLeftNodes.length > 0) {
|
||||
const leftMinY = Math.min(
|
||||
...firstLevelLeftNodes.map((node) => node.y - (node.height ?? 50) / 2)
|
||||
);
|
||||
const leftMaxY = Math.max(
|
||||
...firstLevelLeftNodes.map((node) => node.y + (node.height ?? 50) / 2)
|
||||
);
|
||||
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (rightTreeNodes.length > 0) {
|
||||
const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
|
||||
(a, b) => a - b
|
||||
);
|
||||
const firstLevelRightX = rightTreeXPositions[0];
|
||||
const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
|
||||
|
||||
if (firstLevelRightNodes.length > 0) {
|
||||
const rightMinY = Math.min(
|
||||
...firstLevelRightNodes.map((node) => node.y - (node.height ?? 50) / 2)
|
||||
);
|
||||
const rightMaxY = Math.max(
|
||||
...firstLevelRightNodes.map((node) => node.y + (node.height ?? 50) / 2)
|
||||
);
|
||||
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
const leftTreeOffset = -leftTreeCenterY;
|
||||
const rightTreeOffset = -rightTreeCenterY;
|
||||
|
||||
positionedNodes.push({
|
||||
id: String(rootNode.id),
|
||||
x: rootX,
|
||||
y: rootY + 20,
|
||||
section: 'root',
|
||||
width: rootNode._originalNode?.width ?? rootNode.width,
|
||||
height: rootNode._originalNode?.height ?? rootNode.height,
|
||||
originalNode: rootNode._originalNode,
|
||||
});
|
||||
|
||||
const leftTreeNodesWithOffset = leftTreeNodes.map((node) => ({
|
||||
id: node.id,
|
||||
x: node.x - (node.width ?? 0) / 2,
|
||||
y: node.y + leftTreeOffset + (node.height ?? 0) / 2,
|
||||
section: 'left' as const,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
originalNode: node.originalNode,
|
||||
}));
|
||||
|
||||
const rightTreeNodesWithOffset = rightTreeNodes.map((node) => ({
|
||||
id: node.id,
|
||||
x: node.x + (node.width ?? 0) / 2,
|
||||
y: node.y + rightTreeOffset + (node.height ?? 0) / 2,
|
||||
section: 'right' as const,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
originalNode: node.originalNode,
|
||||
}));
|
||||
|
||||
positionedNodes.push(...leftTreeNodesWithOffset);
|
||||
positionedNodes.push(...rightTreeNodesWithOffset);
|
||||
|
||||
return positionedNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position nodes from the left tree in a bidirectional layout (grows to the left)
|
||||
* Rotates the tree 90 degrees counterclockwise so it grows horizontally to the left
|
||||
*/
|
||||
function positionLeftTreeBidirectional(
|
||||
nodes: TidyTreeNode[],
|
||||
positionedNodes: PositionedNode[],
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
): void {
|
||||
nodes.forEach((node) => {
|
||||
const distanceFromRoot = node.y ?? 0;
|
||||
const verticalPosition = node.x ?? 0;
|
||||
|
||||
const originalWidth = node._originalNode?.width ?? 100;
|
||||
const originalHeight = node._originalNode?.height ?? 50;
|
||||
|
||||
const adjustedY = offsetY + verticalPosition;
|
||||
|
||||
positionedNodes.push({
|
||||
id: String(node.id),
|
||||
x: offsetX - distanceFromRoot,
|
||||
y: adjustedY,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
originalNode: node._originalNode,
|
||||
});
|
||||
|
||||
if (node.children) {
|
||||
positionLeftTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Position nodes from the right tree in a bidirectional layout (grows to the right)
|
||||
* Rotates the tree 90 degrees clockwise so it grows horizontally to the right
|
||||
*/
|
||||
function positionRightTreeBidirectional(
|
||||
nodes: TidyTreeNode[],
|
||||
positionedNodes: PositionedNode[],
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
): void {
|
||||
nodes.forEach((node) => {
|
||||
const distanceFromRoot = node.y ?? 0;
|
||||
const verticalPosition = node.x ?? 0;
|
||||
|
||||
const originalWidth = node._originalNode?.width ?? 100;
|
||||
const originalHeight = node._originalNode?.height ?? 50;
|
||||
|
||||
const adjustedY = offsetY + verticalPosition;
|
||||
|
||||
positionedNodes.push({
|
||||
id: String(node.id),
|
||||
x: offsetX + distanceFromRoot,
|
||||
y: adjustedY,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
originalNode: node._originalNode,
|
||||
});
|
||||
|
||||
if (node.children) {
|
||||
positionRightTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the intersection point of a line with a circle
|
||||
* @param circle - Circle coordinates and radius
|
||||
* @param lineStart - Starting point of the line
|
||||
* @param lineEnd - Ending point of the line
|
||||
* @returns The intersection point
|
||||
*/
|
||||
function computeCircleEdgeIntersection(circle: Bounds, lineStart: Point, lineEnd: Point): Point {
|
||||
const radius = Math.min(circle.width, circle.height) / 2;
|
||||
|
||||
const dx = lineEnd.x - lineStart.x;
|
||||
const dy = lineEnd.y - lineStart.y;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (length === 0) {
|
||||
return lineStart;
|
||||
}
|
||||
|
||||
const nx = dx / length;
|
||||
const ny = dy / length;
|
||||
|
||||
return {
|
||||
x: circle.x - nx * radius,
|
||||
y: circle.y - ny * radius,
|
||||
};
|
||||
}
|
||||
|
||||
function intersection(node: PositionedNode, outsidePoint: Point, insidePoint: Point): Point {
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
|
||||
if (!node.width || !node.height) {
|
||||
return { x: outsidePoint.x, y: outsidePoint.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.
|
||||
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;
|
||||
}
|
||||
|
||||
return res;
|
||||
} else {
|
||||
if (insidePoint.x < outsidePoint.x) {
|
||||
r = outsidePoint.x - w - x;
|
||||
} else {
|
||||
r = x - w - outsidePoint.x;
|
||||
}
|
||||
const 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;
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate edge positions based on positioned nodes
|
||||
* Now includes tree membership and node dimensions for precise edge calculations
|
||||
* Edges now stop at shape boundaries instead of extending to centers
|
||||
*/
|
||||
function calculateEdgePositions(
|
||||
edges: Edge[],
|
||||
positionedNodes: PositionedNode[],
|
||||
intersectionShift: number
|
||||
): PositionedEdge[] {
|
||||
const nodeInfo = new Map<string, PositionedNode>();
|
||||
positionedNodes.forEach((node) => {
|
||||
nodeInfo.set(node.id, node);
|
||||
});
|
||||
|
||||
return edges.map((edge) => {
|
||||
const sourceNode = nodeInfo.get(edge.start ?? '');
|
||||
const targetNode = nodeInfo.get(edge.end ?? '');
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return {
|
||||
id: edge.id,
|
||||
source: edge.start ?? '',
|
||||
target: edge.end ?? '',
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
midX: 0,
|
||||
midY: 0,
|
||||
endX: 0,
|
||||
endY: 0,
|
||||
points: [{ x: 0, y: 0 }],
|
||||
sourceSection: undefined,
|
||||
targetSection: undefined,
|
||||
sourceWidth: undefined,
|
||||
sourceHeight: undefined,
|
||||
targetWidth: undefined,
|
||||
targetHeight: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const sourceCenter = { x: sourceNode.x, y: sourceNode.y };
|
||||
const targetCenter = { x: targetNode.x, y: targetNode.y };
|
||||
|
||||
const isSourceRound = ['circle', 'cloud', 'bang'].includes(
|
||||
sourceNode.originalNode?.shape ?? ''
|
||||
);
|
||||
const isTargetRound = ['circle', 'cloud', 'bang'].includes(
|
||||
targetNode.originalNode?.shape ?? ''
|
||||
);
|
||||
|
||||
let startPos = isSourceRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: sourceNode.x,
|
||||
y: sourceNode.y,
|
||||
width: sourceNode.width ?? 100,
|
||||
height: sourceNode.height ?? 100,
|
||||
},
|
||||
targetCenter,
|
||||
sourceCenter
|
||||
)
|
||||
: intersection(sourceNode, sourceCenter, targetCenter);
|
||||
|
||||
let endPos = isTargetRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: targetNode.x,
|
||||
y: targetNode.y,
|
||||
width: targetNode.width ?? 100,
|
||||
height: targetNode.height ?? 100,
|
||||
},
|
||||
sourceCenter,
|
||||
targetCenter
|
||||
)
|
||||
: intersection(targetNode, targetCenter, sourceCenter);
|
||||
|
||||
const midX = (startPos.x + endPos.x) / 2;
|
||||
const midY = (startPos.y + endPos.y) / 2;
|
||||
|
||||
const points = [startPos];
|
||||
if (sourceNode.section === 'left') {
|
||||
points.push({
|
||||
x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
|
||||
y: sourceNode.y,
|
||||
});
|
||||
} else if (sourceNode.section === 'right') {
|
||||
points.push({
|
||||
x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
|
||||
y: sourceNode.y,
|
||||
});
|
||||
}
|
||||
if (targetNode.section === 'left') {
|
||||
points.push({
|
||||
x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
|
||||
y: targetNode.y,
|
||||
});
|
||||
} else if (targetNode.section === 'right') {
|
||||
points.push({
|
||||
x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
|
||||
y: targetNode.y,
|
||||
});
|
||||
}
|
||||
|
||||
points.push(endPos);
|
||||
|
||||
const secondPoint = points.length > 1 ? points[1] : targetCenter;
|
||||
startPos = isSourceRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: sourceNode.x,
|
||||
y: sourceNode.y,
|
||||
width: sourceNode.width ?? 100,
|
||||
height: sourceNode.height ?? 100,
|
||||
},
|
||||
secondPoint,
|
||||
sourceCenter
|
||||
)
|
||||
: intersection(sourceNode, secondPoint, sourceCenter);
|
||||
points[0] = startPos;
|
||||
|
||||
const secondLastPoint = points.length > 1 ? points[points.length - 2] : sourceCenter;
|
||||
endPos = isTargetRound
|
||||
? computeCircleEdgeIntersection(
|
||||
{
|
||||
x: targetNode.x,
|
||||
y: targetNode.y,
|
||||
width: targetNode.width ?? 100,
|
||||
height: targetNode.height ?? 100,
|
||||
},
|
||||
secondLastPoint,
|
||||
targetCenter
|
||||
)
|
||||
: intersection(targetNode, secondLastPoint, targetCenter);
|
||||
points[points.length - 1] = endPos;
|
||||
|
||||
return {
|
||||
id: edge.id,
|
||||
source: edge.start ?? '',
|
||||
target: edge.end ?? '',
|
||||
startX: startPos.x,
|
||||
startY: startPos.y,
|
||||
midX,
|
||||
midY,
|
||||
endX: endPos.x,
|
||||
endY: endPos.y,
|
||||
points,
|
||||
sourceSection: sourceNode?.section,
|
||||
targetSection: targetNode?.section,
|
||||
sourceWidth: sourceNode?.width,
|
||||
sourceHeight: sourceNode?.height,
|
||||
targetWidth: targetNode?.width,
|
||||
targetHeight: targetNode?.height,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate layout data structure
|
||||
* @param data - The data to validate
|
||||
* @returns True if data is valid, throws error otherwise
|
||||
*/
|
||||
export function validateLayoutData(data: LayoutData): boolean {
|
||||
if (!data) {
|
||||
throw new Error('Layout data is required');
|
||||
}
|
||||
|
||||
if (!data.config) {
|
||||
throw new Error('Configuration is required in layout data');
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.nodes)) {
|
||||
throw new Error('Nodes array is required in layout data');
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.edges)) {
|
||||
throw new Error('Edges array is required in layout data');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
import type { LayoutLoaderDefinition } from 'mermaid';
|
||||
|
||||
const loader = async () => await import(`./render.js`);
|
||||
|
||||
const tidyTreeLayout: LayoutLoaderDefinition[] = [
|
||||
{
|
||||
name: 'tidy-tree',
|
||||
loader,
|
||||
algorithm: 'tidy-tree',
|
||||
},
|
||||
];
|
||||
|
||||
export default tidyTreeLayout;
|
@@ -1,18 +0,0 @@
|
||||
declare module 'non-layered-tidy-tree-layout' {
|
||||
export class BoundingBox {
|
||||
constructor(gap: number, bottomPadding: number);
|
||||
}
|
||||
|
||||
export class Layout {
|
||||
constructor(boundingBox: BoundingBox);
|
||||
layout(data: any): {
|
||||
result: any;
|
||||
boundingBox: {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,180 +0,0 @@
|
||||
import type { InternalHelpers, LayoutData, RenderOptions, SVG } from 'mermaid';
|
||||
import { executeTidyTreeLayout } from './layout.js';
|
||||
|
||||
interface NodeWithPosition {
|
||||
id: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
domId?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render function for bidirectional tidy-tree layout algorithm
|
||||
*
|
||||
* This follows the same pattern as ELK and dagre renderers:
|
||||
* 1. Insert nodes into DOM to get their actual dimensions
|
||||
* 2. Run the bidirectional tidy-tree layout algorithm to calculate positions
|
||||
* 3. Position the nodes and edges based on layout results
|
||||
*
|
||||
* The bidirectional layout creates two trees that grow horizontally in opposite
|
||||
* directions from a central root node:
|
||||
* - Left tree: grows horizontally to the left (children: 1st, 3rd, 5th...)
|
||||
* - Right tree: grows horizontally to the right (children: 2nd, 4th, 6th...)
|
||||
*/
|
||||
export const render = async (
|
||||
data4Layout: LayoutData,
|
||||
svg: SVG,
|
||||
{
|
||||
insertCluster,
|
||||
insertEdge,
|
||||
insertEdgeLabel,
|
||||
insertMarkers,
|
||||
insertNode,
|
||||
log,
|
||||
positionEdgeLabel,
|
||||
}: InternalHelpers,
|
||||
{ algorithm: _algorithm }: RenderOptions
|
||||
) => {
|
||||
const nodeDb: Record<string, NodeWithPosition> = {};
|
||||
const clusterDb: Record<string, any> = {};
|
||||
|
||||
const element = svg.select('g');
|
||||
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
|
||||
|
||||
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
|
||||
const edgePaths = element.insert('g').attr('class', 'edgePaths');
|
||||
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
|
||||
const nodes = element.insert('g').attr('class', 'nodes');
|
||||
// Step 1: Insert nodes into DOM to get their actual dimensions
|
||||
log.debug('Inserting nodes into DOM for dimension calculation');
|
||||
|
||||
await Promise.all(
|
||||
data4Layout.nodes.map(async (node) => {
|
||||
if (node.isGroup) {
|
||||
const clusterNode: NodeWithPosition = {
|
||||
...node,
|
||||
id: node.id,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
};
|
||||
clusterDb[node.id] = clusterNode;
|
||||
nodeDb[node.id] = clusterNode;
|
||||
|
||||
await insertCluster(subGraphsEl, node);
|
||||
} else {
|
||||
const nodeWithPosition: NodeWithPosition = {
|
||||
...node,
|
||||
id: node.id,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
};
|
||||
nodeDb[node.id] = nodeWithPosition;
|
||||
|
||||
const nodeEl = await insertNode(nodes, node, {
|
||||
config: data4Layout.config,
|
||||
dir: data4Layout.direction || 'TB',
|
||||
});
|
||||
|
||||
const boundingBox = nodeEl.node()!.getBBox();
|
||||
nodeWithPosition.width = boundingBox.width;
|
||||
nodeWithPosition.height = boundingBox.height;
|
||||
nodeWithPosition.domId = nodeEl;
|
||||
|
||||
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
// Step 2: Run the bidirectional tidy-tree layout algorithm
|
||||
log.debug('Running bidirectional tidy-tree layout algorithm');
|
||||
|
||||
const updatedLayoutData = {
|
||||
...data4Layout,
|
||||
nodes: data4Layout.nodes.map((node) => {
|
||||
const nodeWithDimensions = nodeDb[node.id];
|
||||
return {
|
||||
...node,
|
||||
width: nodeWithDimensions.width ?? node.width ?? 100,
|
||||
height: nodeWithDimensions.height ?? node.height ?? 50,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const layoutResult = await executeTidyTreeLayout(updatedLayoutData);
|
||||
// Step 3: Position the nodes based on bidirectional layout results
|
||||
log.debug('Positioning nodes based on bidirectional layout results');
|
||||
|
||||
layoutResult.nodes.forEach((positionedNode) => {
|
||||
const node = nodeDb[positionedNode.id];
|
||||
if (node?.domId) {
|
||||
// Position the node at the calculated coordinates from bidirectional layout
|
||||
// The layout algorithm has already calculated positions for:
|
||||
// - Root node at center (0, 0)
|
||||
// - Left tree nodes with negative x coordinates (growing left)
|
||||
// - Right tree nodes with positive x coordinates (growing right)
|
||||
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
|
||||
// Store the final position
|
||||
node.x = positionedNode.x;
|
||||
node.y = positionedNode.y;
|
||||
// Step 3: Position the nodes based on bidirectional layout results
|
||||
log.debug(`Positioned node ${node.id} at (${positionedNode.x}, ${positionedNode.y})`);
|
||||
}
|
||||
});
|
||||
|
||||
log.debug('Inserting and positioning edges');
|
||||
|
||||
await Promise.all(
|
||||
data4Layout.edges.map(async (edge) => {
|
||||
await insertEdgeLabel(edgeLabels, edge);
|
||||
|
||||
const startNode = nodeDb[edge.start ?? ''];
|
||||
const endNode = nodeDb[edge.end ?? ''];
|
||||
|
||||
if (startNode && endNode) {
|
||||
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
|
||||
|
||||
if (positionedEdge) {
|
||||
log.debug('APA01 positionedEdge', positionedEdge);
|
||||
const edgeWithPath = {
|
||||
...edge,
|
||||
points: positionedEdge.points,
|
||||
};
|
||||
const paths = insertEdge(
|
||||
edgePaths,
|
||||
edgeWithPath,
|
||||
clusterDb,
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
|
||||
positionEdgeLabel(edgeWithPath, paths);
|
||||
} else {
|
||||
const edgeWithPath = {
|
||||
...edge,
|
||||
points: [
|
||||
{ x: startNode.x ?? 0, y: startNode.y ?? 0 },
|
||||
{ x: endNode.x ?? 0, y: endNode.y ?? 0 },
|
||||
],
|
||||
};
|
||||
|
||||
const paths = insertEdge(
|
||||
edgePaths,
|
||||
edgeWithPath,
|
||||
clusterDb,
|
||||
data4Layout.type,
|
||||
startNode,
|
||||
endNode,
|
||||
data4Layout.diagramId
|
||||
);
|
||||
positionEdgeLabel(edgeWithPath, paths);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
log.debug('Bidirectional tidy-tree rendering completed');
|
||||
};
|
@@ -1,69 +0,0 @@
|
||||
import type { LayoutData } from 'mermaid';
|
||||
|
||||
export type Node = LayoutData['nodes'][number];
|
||||
export type Edge = LayoutData['edges'][number];
|
||||
|
||||
/**
|
||||
* Positioned node after layout calculation
|
||||
*/
|
||||
export interface PositionedNode {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
section?: 'root' | 'left' | 'right';
|
||||
width?: number;
|
||||
height?: number;
|
||||
originalNode?: Node;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positioned edge after layout calculation
|
||||
*/
|
||||
export interface PositionedEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
sourceSection?: 'root' | 'left' | 'right';
|
||||
targetSection?: 'root' | 'left' | 'right';
|
||||
sourceWidth?: number;
|
||||
sourceHeight?: number;
|
||||
targetWidth?: number;
|
||||
targetHeight?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of layout algorithm execution
|
||||
*/
|
||||
export interface LayoutResult {
|
||||
nodes: PositionedNode[];
|
||||
edges: PositionedEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy-tree node structure compatible with non-layered-tidy-tree-layout
|
||||
*/
|
||||
export interface TidyTreeNode {
|
||||
id: string | number;
|
||||
width: number;
|
||||
height: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
children?: TidyTreeNode[];
|
||||
_originalNode?: Node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy-tree layout configuration
|
||||
*/
|
||||
export interface TidyTreeLayoutConfig {
|
||||
gap: number;
|
||||
bottomPadding: number;
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.d.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
@@ -229,6 +229,7 @@
|
||||
- [#5999](https://github.com/mermaid-js/mermaid/pull/5999) [`742ad7c`](https://github.com/mermaid-js/mermaid/commit/742ad7c130964df1fb5544e909d9556081285f68) Thanks [@knsv](https://github.com/knsv)! - Adding Kanban board, a new diagram type
|
||||
|
||||
- [#5880](https://github.com/mermaid-js/mermaid/pull/5880) [`bdf145f`](https://github.com/mermaid-js/mermaid/commit/bdf145ffe362462176d9c1e68d5f3ff5c9d962b0) Thanks [@yari-dewalt](https://github.com/yari-dewalt)! - Class diagram changes:
|
||||
|
||||
- Updates the class diagram to the new unified way of rendering.
|
||||
- Includes a new "classBox" shape to be used in diagrams
|
||||
- Other updates such as:
|
||||
|
@@ -48,10 +48,6 @@
|
||||
"types:build-config": "tsx scripts/create-types-from-json-schema.mts",
|
||||
"types:verify-config": "tsx scripts/create-types-from-json-schema.mts --verify",
|
||||
"checkCircle": "npx madge --circular ./src",
|
||||
"antlr:sequence:clean": "rimraf src/diagrams/sequence/parser/antlr/generated",
|
||||
"antlr:sequence": "pnpm run antlr:sequence:clean && antlr4ng -Dlanguage=TypeScript -Xexact-output-dir -o src/diagrams/sequence/parser/antlr/generated src/diagrams/sequence/parser/antlr/SequenceLexer.g4 src/diagrams/sequence/parser/antlr/SequenceParser.g4",
|
||||
"antlr:class:clean": "rimraf src/diagrams/class/parser/antlr/generated",
|
||||
"antlr:class": "pnpm run antlr:class:clean && antlr4ng -Dlanguage=TypeScript -Xexact-output-dir -o src/diagrams/class/parser/antlr/generated src/diagrams/class/parser/antlr/ClassLexer.g4 src/diagrams/class/parser/antlr/ClassParser.g4",
|
||||
"prepublishOnly": "pnpm docs:verify-version"
|
||||
},
|
||||
"repository": {
|
||||
@@ -72,11 +68,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
"@iconify/utils": "^3.0.1",
|
||||
"@iconify/utils": "^2.1.33",
|
||||
"@mermaid-js/parser": "workspace:^",
|
||||
"@types/d3": "^7.4.3",
|
||||
"antlr-ng": "^1.0.10",
|
||||
"antlr4ng": "^3.0.16",
|
||||
"cytoscape": "^3.29.3",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
@@ -129,14 +123,13 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"start-server-and-test": "^2.0.10",
|
||||
"type-fest": "^4.35.0",
|
||||
"typedoc": "^0.28.9",
|
||||
"typedoc-plugin-markdown": "^4.8.0",
|
||||
"typedoc": "^0.27.8",
|
||||
"typedoc-plugin-markdown": "^4.4.2",
|
||||
"typescript": "~5.7.3",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vitepress": "^1.0.2",
|
||||
"vitepress-plugin-search": "1.0.4-alpha.22",
|
||||
"antlr4ng-cli": "^2.0.0"
|
||||
"vitepress-plugin-search": "1.0.4-alpha.22"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
|
@@ -171,9 +171,7 @@ This Markdown should be kept.
|
||||
expect(buildShapeDoc()).toMatchInlineSnapshot(`
|
||||
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
|
||||
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| Bang | Bang | \`bang\` | Bang | \`bang\` |
|
||||
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
|
||||
| Cloud | Cloud | \`cloud\` | cloud | \`cloud\` |
|
||||
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
|
||||
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
|
||||
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
|
||||
|
@@ -78,187 +78,3 @@ describe('when working with site config', () => {
|
||||
expect(config_4.altFontFamily).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserDefinedConfig', () => {
|
||||
beforeEach(() => {
|
||||
configApi.reset();
|
||||
});
|
||||
|
||||
it('should return empty object when no user config is defined', () => {
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toEqual({});
|
||||
});
|
||||
|
||||
it('should return config from initialize only', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial' };
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toEqual(initConfig);
|
||||
});
|
||||
|
||||
it('should return config from directives only', () => {
|
||||
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
|
||||
const directive2: MermaidConfig = { theme: 'forest' };
|
||||
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontFamily": "Arial",
|
||||
"fontSize": 14,
|
||||
"layout": "elk",
|
||||
"theme": "forest",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should combine initialize config and directives', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', fontFamily: 'Arial', layout: 'dagre' };
|
||||
const directive1: MermaidConfig = { layout: 'elk', fontSize: 14 };
|
||||
const directive2: MermaidConfig = { theme: 'forest' };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontFamily": "Arial",
|
||||
"fontSize": 14,
|
||||
"layout": "elk",
|
||||
"theme": "forest",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle nested config objects properly', () => {
|
||||
const initConfig: MermaidConfig = {
|
||||
flowchart: { nodeSpacing: 50, rankSpacing: 100 },
|
||||
theme: 'default',
|
||||
};
|
||||
const directive: MermaidConfig = {
|
||||
flowchart: { nodeSpacing: 75, curve: 'basis' },
|
||||
mindmap: { padding: 20 },
|
||||
};
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"curve": "basis",
|
||||
"nodeSpacing": 75,
|
||||
"rankSpacing": 100,
|
||||
},
|
||||
"mindmap": {
|
||||
"padding": 20,
|
||||
},
|
||||
"theme": "default",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle complex nested overrides', () => {
|
||||
const initConfig: MermaidConfig = {
|
||||
flowchart: {
|
||||
nodeSpacing: 50,
|
||||
rankSpacing: 100,
|
||||
curve: 'linear',
|
||||
},
|
||||
theme: 'default',
|
||||
};
|
||||
const directive1: MermaidConfig = {
|
||||
flowchart: {
|
||||
nodeSpacing: 75,
|
||||
},
|
||||
fontSize: 12,
|
||||
};
|
||||
const directive2: MermaidConfig = {
|
||||
flowchart: {
|
||||
curve: 'basis',
|
||||
nodeSpacing: 100,
|
||||
},
|
||||
mindmap: {
|
||||
padding: 15,
|
||||
},
|
||||
};
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive1);
|
||||
configApi.addDirective(directive2);
|
||||
|
||||
const userConfig = configApi.getUserDefinedConfig();
|
||||
expect(userConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"curve": "basis",
|
||||
"nodeSpacing": 100,
|
||||
"rankSpacing": 100,
|
||||
},
|
||||
"fontSize": 12,
|
||||
"mindmap": {
|
||||
"padding": 15,
|
||||
},
|
||||
"theme": "default",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return independent copies (not references)', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', flowchart: { nodeSpacing: 50 } };
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
|
||||
const userConfig1 = configApi.getUserDefinedConfig();
|
||||
const userConfig2 = configApi.getUserDefinedConfig();
|
||||
|
||||
userConfig1.theme = 'neutral';
|
||||
userConfig1.flowchart!.nodeSpacing = 999;
|
||||
|
||||
expect(userConfig2).toMatchInlineSnapshot(`
|
||||
{
|
||||
"flowchart": {
|
||||
"nodeSpacing": 50,
|
||||
},
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle edge cases with undefined values', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark', layout: undefined };
|
||||
const directive: MermaidConfig = { fontSize: 14, fontFamily: undefined };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"fontSize": 14,
|
||||
"layout": undefined,
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should retain config from initialize after reset', () => {
|
||||
const initConfig: MermaidConfig = { theme: 'dark' };
|
||||
const directive: MermaidConfig = { layout: 'elk' };
|
||||
|
||||
configApi.saveConfigFromInitialize(initConfig);
|
||||
configApi.addDirective(directive);
|
||||
|
||||
expect(configApi.getUserDefinedConfig()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"layout": "elk",
|
||||
"theme": "dark",
|
||||
}
|
||||
`);
|
||||
|
||||
configApi.reset();
|
||||
});
|
||||
});
|
||||
|
@@ -248,17 +248,3 @@ const checkConfig = (config: MermaidConfig) => {
|
||||
issueWarning('LAZY_LOAD_DEPRECATED');
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserDefinedConfig = (): MermaidConfig => {
|
||||
let userConfig: MermaidConfig = {};
|
||||
|
||||
if (configFromInitialize) {
|
||||
userConfig = assignWithDepth(userConfig, configFromInitialize);
|
||||
}
|
||||
|
||||
for (const d of directives) {
|
||||
userConfig = assignWithDepth(userConfig, d);
|
||||
}
|
||||
|
||||
return userConfig;
|
||||
};
|
||||
|
@@ -1075,10 +1075,6 @@ export interface ArchitectureDiagramConfig extends BaseDiagramConfig {
|
||||
export interface MindmapDiagramConfig extends BaseDiagramConfig {
|
||||
padding?: number;
|
||||
maxNodeWidth?: number;
|
||||
/**
|
||||
* Layout algorithm to use for positioning mindmap nodes
|
||||
*/
|
||||
layoutAlgorithm?: string;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for kanban diagrams
|
||||
|
@@ -1,3 +1,5 @@
|
||||
// tests to check that comments are removed
|
||||
|
||||
import { cleanupComments } from './comments.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
@@ -8,12 +10,12 @@ describe('comments', () => {
|
||||
%% This is a comment
|
||||
%% This is another comment
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
%% This is a comment
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -27,9 +29,9 @@ graph TD
|
||||
%%{ init: {'theme': 'space before init'}}%%
|
||||
%%{init: {'theme': 'space after ending'}}%%
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
|
||||
B-->C
|
||||
B-->C
|
||||
%% This is a comment
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
@@ -37,9 +39,9 @@ graph TD
|
||||
%%{ init: {'theme': 'space before init'}}%%
|
||||
%%{init: {'theme': 'space after ending'}}%%
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
|
||||
B-->C
|
||||
B-->C
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -48,14 +50,14 @@ graph TD
|
||||
const text = `
|
||||
%% This is a comment
|
||||
graph TD
|
||||
A-->B
|
||||
%% This is a comment
|
||||
C-->D
|
||||
A-->B
|
||||
%% This is a comment
|
||||
C-->D
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
C-->D
|
||||
A-->B
|
||||
C-->D
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -68,11 +70,11 @@ graph TD
|
||||
|
||||
%% This is a comment
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
`;
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -80,12 +82,12 @@ graph TD
|
||||
it('should remove comments at end of text with no newline', () => {
|
||||
const text = `
|
||||
graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
%% This is a comment`;
|
||||
|
||||
expect(cleanupComments(text)).toMatchInlineSnapshot(`
|
||||
"graph TD
|
||||
A-->B
|
||||
A-->B
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
@@ -3,7 +3,6 @@ import type * as d3 from 'd3';
|
||||
import type { SetOptional, SetRequired } from 'type-fest';
|
||||
import type { Diagram } from '../Diagram.js';
|
||||
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
|
||||
import type { DiagramOrientation } from '../diagrams/git/gitGraphTypes.js';
|
||||
|
||||
export interface DiagramMetadata {
|
||||
title?: string;
|
||||
@@ -36,8 +35,7 @@ export interface DiagramDB {
|
||||
getAccTitle?: () => string;
|
||||
setAccDescription?: (description: string) => void;
|
||||
getAccDescription?: () => string;
|
||||
getDirection?: () => string | undefined;
|
||||
setDirection?: (dir: DiagramOrientation) => void;
|
||||
|
||||
setDisplayMode?: (title: string) => void;
|
||||
bindFunctions?: (element: Element) => void;
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { LayoutOptions, Position } from 'cytoscape';
|
||||
import type { Position } from 'cytoscape';
|
||||
import cytoscape from 'cytoscape';
|
||||
import type { FcoseLayoutOptions } from 'cytoscape-fcose';
|
||||
import fcose from 'cytoscape-fcose';
|
||||
import { select } from 'd3';
|
||||
import type { DrawDefinition, SVG } from '../../diagram-api/types.js';
|
||||
@@ -40,7 +41,7 @@ registerIconPacks([
|
||||
icons: architectureIcons,
|
||||
},
|
||||
]);
|
||||
cytoscape.use(fcose as any);
|
||||
cytoscape.use(fcose);
|
||||
|
||||
function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
|
||||
services.forEach((service) => {
|
||||
@@ -428,7 +429,7 @@ function layoutArchitecture(
|
||||
},
|
||||
alignmentConstraint,
|
||||
relativePlacementConstraint,
|
||||
} as LayoutOptions);
|
||||
} as FcoseLayoutOptions);
|
||||
|
||||
// Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend
|
||||
layout.one('layoutstop', () => {
|
||||
|
@@ -1,48 +0,0 @@
|
||||
import { describe } from 'vitest';
|
||||
import { draw } from './architectureRenderer.js';
|
||||
import { Diagram } from '../../Diagram.js';
|
||||
import { addDetector } from '../../diagram-api/detectType.js';
|
||||
import architectureDetector from './architectureDetector.js';
|
||||
import { ensureNodeFromSelector, jsdomIt } from '../../tests/util.js';
|
||||
|
||||
const { id, detector, loader } = architectureDetector;
|
||||
|
||||
addDetector(id, detector, loader); // Add architecture schemas to Mermaid
|
||||
|
||||
describe('architecture diagram SVGs', () => {
|
||||
jsdomIt('should add ids', async () => {
|
||||
const svgNode = await drawDiagram(`
|
||||
architecture-beta
|
||||
group api(cloud)[API]
|
||||
|
||||
service db(database)[Database] in api
|
||||
service disk1(disk)[Storage] in api
|
||||
service disk2(disk)[Storage] in api
|
||||
service server(server)[Server] in api
|
||||
|
||||
db:L -- R:server
|
||||
disk1:T -- B:server
|
||||
disk2:T -- B:db
|
||||
`);
|
||||
|
||||
const nodesForGroup = svgNode.querySelectorAll(`#group-api`);
|
||||
expect(nodesForGroup.length).toBe(1);
|
||||
|
||||
const serviceIds = [...svgNode.querySelectorAll(`[id^=service-]`)].map(({ id }) => id).sort();
|
||||
expect(serviceIds).toStrictEqual([
|
||||
'service-db',
|
||||
'service-disk1',
|
||||
'service-disk2',
|
||||
'service-server',
|
||||
]);
|
||||
|
||||
const edgeIds = [...svgNode.querySelectorAll(`.edge[id^=L_]`)].map(({ id }) => id).sort();
|
||||
expect(edgeIds).toStrictEqual(['L_db_server_0', 'L_disk1_server_0', 'L_disk2_db_0']);
|
||||
});
|
||||
});
|
||||
|
||||
async function drawDiagram(diagramText: string): Promise<Element> {
|
||||
const diagram = await Diagram.fromText(diagramText, {});
|
||||
await draw('NOT_USED', 'svg', '1.0.0', diagram);
|
||||
return ensureNodeFromSelector('#svg');
|
||||
}
|
@@ -20,7 +20,6 @@ import {
|
||||
type ArchitectureJunction,
|
||||
type ArchitectureService,
|
||||
} from './architectureTypes.js';
|
||||
import { getEdgeId } from '../../utils.js';
|
||||
|
||||
export const drawEdges = async function (
|
||||
edgesEl: D3Element,
|
||||
@@ -92,8 +91,7 @@ export const drawEdges = async function (
|
||||
|
||||
g.insert('path')
|
||||
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
|
||||
.attr('class', 'edge')
|
||||
.attr('id', getEdgeId(source, target, { prefix: 'L' }));
|
||||
.attr('class', 'edge');
|
||||
|
||||
if (sourceArrow) {
|
||||
const xShift = isArchitectureDirectionX(sourceDir)
|
||||
@@ -208,9 +206,8 @@ export const drawGroups = async function (
|
||||
if (data.type === 'group') {
|
||||
const { h, w, x1, y1 } = node.boundingBox();
|
||||
|
||||
const groupsNode = groupsEl.append('rect');
|
||||
groupsNode
|
||||
.attr('id', `group-${data.id}`)
|
||||
groupsEl
|
||||
.append('rect')
|
||||
.attr('x', x1 + halfIconSize)
|
||||
.attr('y', y1 + halfIconSize)
|
||||
.attr('width', w)
|
||||
@@ -265,7 +262,6 @@ export const drawGroups = async function (
|
||||
')'
|
||||
);
|
||||
}
|
||||
db.setElementForId(data.id, groupsNode);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -346,9 +342,9 @@ export const drawServices = async function (
|
||||
);
|
||||
}
|
||||
|
||||
serviceElem.attr('id', `service-${service.id}`).attr('class', 'architecture-service');
|
||||
serviceElem.attr('class', 'architecture-service');
|
||||
|
||||
const { width, height } = serviceElem.node().getBBox();
|
||||
const { width, height } = serviceElem._groups[0][0].getBBox();
|
||||
service.width = width;
|
||||
service.height = height;
|
||||
db.setElementForId(service.id, serviceElem);
|
||||
|
@@ -1,147 +0,0 @@
|
||||
## ANTLR migration plan for Class Diagrams (parity with Sequence)
|
||||
|
||||
This guide summarizes how to migrate the Class diagram parser from Jison to ANTLR (antlr4ng), following the approach used for Sequence diagrams. The goal is full feature parity and 100% test pass rate, while keeping the Jison implementation as the reference until the ANTLR path is green.
|
||||
|
||||
### Objectives
|
||||
|
||||
- Keep the existing Jison parser as the authoritative reference until parity is achieved
|
||||
- Add an ANTLR parser behind a runtime flag (`USE_ANTLR_PARSER=true`), mirroring Sequence
|
||||
- Achieve 100% test compatibility with the current Jison behavior, including error cases
|
||||
- Keep the public DB and rendering contracts unchanged
|
||||
|
||||
---
|
||||
|
||||
## 1) Prep and references
|
||||
|
||||
- Use the Sequence migration as a template for structure, scripts, and patterns:
|
||||
- antlr4ng grammar files: `SequenceLexer.g4`, `SequenceParser.g4`
|
||||
- wrapper: `antlr-parser.ts` providing a Jison-compatible `parse()` and `yy`
|
||||
- generation script: `pnpm --filter mermaid run antlr:sequence`
|
||||
- For Class diagrams, identify analogous files:
|
||||
- Jison grammar: `packages/mermaid/src/diagrams/class/parser/classDiagram.jison`
|
||||
- DB: `packages/mermaid/src/diagrams/class/classDb.ts`
|
||||
- Tests: `packages/mermaid/src/diagrams/class/classDiagram.spec.js`
|
||||
- Confirm Class diagram features in the Jison grammar and tests: classes, interfaces, enums, relationships (e.g., `--`, `*--`, `o--`, `<|--`, `--|>`), visibility markers (`+`, `-`, `#`, `~`), generics (`<T>`, nested), static/abstract indicators, fields/properties, methods (with parameters and return types), stereotypes (`<< >>`), notes, direction, style/config lines, and titles/accessibility lines if supported.
|
||||
|
||||
---
|
||||
|
||||
## 2) Create ANTLR grammars
|
||||
|
||||
- Create `ClassLexer.g4` and `ClassParser.g4` under `packages/mermaid/src/diagrams/class/parser/antlr/`
|
||||
- Lexer design guidelines (mirror Sequence approach):
|
||||
- Implement stateful lexing with modes to replicate Jison behavior (e.g., default, line/rest-of-line, config/title/acc modes if used)
|
||||
- Ensure token precedence resolves conflicts between relation arrows and generics (`<|--` vs `<T>`). Prefer longest-match arrow tokens and handle generics in parser context
|
||||
- Accept identifiers that include special characters that Jison allowed (quotes, underscores, digits, unicode as applicable)
|
||||
- Provide tokens for core keywords and symbols: `class`, `interface`, `enum`, relationship operators, visibility markers, `<< >>` stereotypes, `{ }` blocks, `:` type separators, `,` parameter separators, `[` `]` arrays, `<` `>` generics
|
||||
- Reuse common tokens shared across diagrams where appropriate (e.g., `TITLE`, `ACC_...`) if Class supports them
|
||||
- Parser design guidelines:
|
||||
- Follow the Jison grammar structure closely to minimize semantic drift
|
||||
- Allow the final statement in the file to omit a trailing newline (to avoid EOF vs NEWLINE mismatches)
|
||||
- Keep non-ambiguous rules for:
|
||||
- Class declarations and bodies (members split into fields/properties vs methods)
|
||||
- Modifiers (visibility, static, abstract)
|
||||
- Types (simple, namespaced, generic with nesting)
|
||||
- Relationships with labels (left->right/right->left forms) and multiplicities
|
||||
- Stereotypes and notes
|
||||
- Optional global lines (title, accTitle, accDescr) if supported by class diagrams
|
||||
|
||||
---
|
||||
|
||||
## 3) Add the wrapper and flag switch
|
||||
|
||||
- Add `packages/mermaid/src/diagrams/class/parser/antlr/antlr-parser.ts`:
|
||||
- Export an object `{ parse, parser, yy }` that mirrors the Jison parser shape
|
||||
- `parse(input)` should:
|
||||
- `this.yy.clear()` to reset DB (same as Sequence)
|
||||
- Build ANTLR's lexer/parser, set `BailErrorStrategy` to fail-fast on syntax errors
|
||||
- Walk the tree with a listener that calls classDb methods
|
||||
- Implement no-op bodies for `visitTerminal`, `visitErrorNode`, `enterEveryRule`, `exitEveryRule` (required by ParseTreeWalker)
|
||||
- Avoid `require()`; import from `antlr4ng`
|
||||
- Use minimal `any`; when casting is unavoidable, add clear comments
|
||||
- Add `packages/mermaid/src/diagrams/class/parser/classParser.ts` similar to Sequence `sequenceParser.ts`:
|
||||
- Import both the Jison parser and the ANTLR wrapper
|
||||
- Gate on `process.env.USE_ANTLR_PARSER === 'true'`
|
||||
- Normalize whitespace if Jison relies on specific newlines (keep parity with Sequence patterns)
|
||||
|
||||
---
|
||||
|
||||
## 4) Implement the listener (semantic actions)
|
||||
|
||||
Map parsed constructs to classDb calls. Typical handlers include:
|
||||
|
||||
- Class-like declarations
|
||||
- `db.addClass(id, { type: 'class'|'interface'|'enum', ... })`
|
||||
- `db.addClassMember(id, member)` for fields/properties/methods (capture visibility, static/abstract, types, params)
|
||||
- Stereotypes, annotations, notes: `db.addAnnotation(...)`, `db.addNote(...)` if applicable
|
||||
- Relationships
|
||||
- Parse arrow/operator to relation type; map to db constants (composition/aggregation/inheritance/realization/association)
|
||||
- `db.addRelation(lhs, rhs, { type, label, multiplicity })`
|
||||
- Title/Accessibility (if supported in Class diagrams)
|
||||
- `db.setDiagramTitle(...)`, `db.setAccTitle(...)`, `db.setAccDescription(...)`
|
||||
- Styles/Directives/Config lines as supported by the Jison grammar
|
||||
|
||||
Error handling:
|
||||
|
||||
- Use BailErrorStrategy; let invalid constructs throw where Jison tests expect failure
|
||||
- For robustness parity, only swallow exceptions in places where Jison tolerated malformed content without aborting
|
||||
|
||||
---
|
||||
|
||||
## 5) Scripts and generation
|
||||
|
||||
- Add package scripts similar to Sequence in `packages/mermaid/package.json`:
|
||||
- `antlr:class:clean`: remove generated TS
|
||||
- `antlr:class`: run antlr4ng to generate TS into `parser/antlr/generated`
|
||||
- Example command (once scripts exist):
|
||||
- `pnpm --filter mermaid run antlr:class`
|
||||
|
||||
---
|
||||
|
||||
## 6) Tests (Vitest)
|
||||
|
||||
- Run existing Class tests with the ANTLR parser enabled:
|
||||
- `USE_ANTLR_PARSER=true pnpm vitest packages/mermaid/src/diagrams/class/classDiagram.spec.js --run`
|
||||
- Start by making a small focused subset pass, then expand to the full suite
|
||||
- Add targeted tests for areas where the ANTLR grammar needs extra coverage (e.g., nested generics, tricky arrow/operator precedence, stereotypes, notes)
|
||||
- Keep test expectations identical to Jison’s behavior; only adjust if Jison’s behavior was explicitly flaky and already tolerated in the repo
|
||||
|
||||
---
|
||||
|
||||
## 7) Linting and quality
|
||||
|
||||
- Satisfy ESLint rules enforced in the repo:
|
||||
- Prefer imports over `require()`; no empty methods, avoid untyped `any` where reasonable
|
||||
- If `@ts-ignore` is necessary, include a descriptive reason (≥10 chars)
|
||||
- Provide minimal types for listener contexts where helpful; keep casts localized and commented
|
||||
- Prefix diagnostic debug logs with the project’s preferred prefix if temporary logging is needed (and clean up before commit)
|
||||
|
||||
---
|
||||
|
||||
## 8) Common pitfalls and tips
|
||||
|
||||
- NEWLINE vs EOF: allow the last statement without a trailing newline to prevent InputMismatch
|
||||
- Token conflicts: order matters; ensure relationship operators (e.g., `<|--`, `--|>`, `*--`, `o--`) win over generic `<`/`>` in the right contexts
|
||||
- Identifiers: match Jison’s permissiveness (quoted names, digits where allowed) and avoid over-greedy tokens that eat operators
|
||||
- Listener resilience: ensure classes and endpoints exist before adding relations (create implicitly if Jison did so)
|
||||
- Error parity: do not swallow exceptions for cases where tests expect failure
|
||||
|
||||
---
|
||||
|
||||
## 9) Rollout checklist
|
||||
|
||||
- [ ] Grammar compiles and generated files are committed
|
||||
- [ ] `USE_ANTLR_PARSER=true` passes all Class diagram tests
|
||||
- [ ] Sequence and other diagram suites remain green
|
||||
- [ ] No new ESLint errors; warnings minimized
|
||||
- [ ] PR includes notes on parity and how to run the ANTLR tests
|
||||
|
||||
---
|
||||
|
||||
## 10) Quick command reference
|
||||
|
||||
- Generate ANTLR targets (after adding scripts):
|
||||
- `pnpm --filter mermaid run antlr:class`
|
||||
- Run Class tests with ANTLR parser:
|
||||
- `USE_ANTLR_PARSER=true pnpm vitest packages/mermaid/src/diagrams/class/classDiagram.spec.js --run`
|
||||
- Run a single test:
|
||||
- `USE_ANTLR_PARSER=true pnpm vitest packages/mermaid/src/diagrams/class/classDiagram.spec.js -t "some test name" --run`
|
@@ -1,4 +1,4 @@
|
||||
import { parser } from './parser/classParser.ts';
|
||||
import { parser } from './parser/classDiagram.jison';
|
||||
import { ClassDB } from './classDb.js';
|
||||
|
||||
describe('class diagram, ', function () {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/classParser.ts';
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import { ClassDB } from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer-v3-unified.js';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method -- Broken for Vitest mocks, see https://github.com/vitest-dev/eslint-plugin-vitest/pull/286 */
|
||||
// @ts-expect-error Parser exposes mutable yy property without typings
|
||||
import { parser } from './parser/classParser.ts';
|
||||
// @ts-expect-error Jison doesn't export types
|
||||
import { parser } from './parser/classDiagram.jison';
|
||||
import { ClassDB } from './classDb.js';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import type { ClassMap, NamespaceNode } from './classTypes.js';
|
||||
@@ -1070,14 +1070,6 @@ describe('given a class diagram with members and methods ', function () {
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
it('should handle an empty class body with {}', function () {
|
||||
const str = 'classDiagram\nclass EmptyClass {}';
|
||||
parser.parse(str);
|
||||
const actual = parser.yy.getClass('EmptyClass');
|
||||
expect(actual.label).toBe('EmptyClass');
|
||||
expect(actual.members.length).toBe(0);
|
||||
expect(actual.methods.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/classParser.ts';
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import { ClassDB } from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer-v3-unified.js';
|
||||
|
@@ -1,229 +0,0 @@
|
||||
lexer grammar ClassLexer;
|
||||
|
||||
tokens {
|
||||
ACC_TITLE_VALUE,
|
||||
ACC_DESCR_VALUE,
|
||||
ACC_DESCR_MULTILINE_VALUE,
|
||||
ACC_DESCR_MULTI_END,
|
||||
OPEN_IN_STRUCT,
|
||||
MEMBER
|
||||
}
|
||||
|
||||
@members {
|
||||
private pendingClassBody = false;
|
||||
private pendingNamespaceBody = false;
|
||||
|
||||
private clearPendingScopes(): void {
|
||||
this.pendingClassBody = false;
|
||||
this.pendingNamespaceBody = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Common fragments
|
||||
fragment WS_INLINE: [ \t]+;
|
||||
fragment DIGIT: [0-9];
|
||||
fragment LETTER: [A-Za-z_];
|
||||
fragment IDENT_PART: [A-Za-z0-9_\-];
|
||||
fragment NOT_DQUOTE: ~[""];
|
||||
|
||||
|
||||
// Comments and whitespace
|
||||
COMMENT: '%%' ~[\r\n]* -> skip;
|
||||
NEWLINE: ('\r'? '\n')+ { this.clearPendingScopes(); };
|
||||
WS: [ \t]+ -> skip;
|
||||
|
||||
// Diagram title declaration
|
||||
CLASS_DIAGRAM_V2: 'classDiagram-v2' -> type(CLASS_DIAGRAM);
|
||||
CLASS_DIAGRAM: 'classDiagram';
|
||||
|
||||
// Directions
|
||||
DIRECTION_TB: 'direction' WS_INLINE+ 'TB';
|
||||
DIRECTION_BT: 'direction' WS_INLINE+ 'BT';
|
||||
DIRECTION_LR: 'direction' WS_INLINE+ 'LR';
|
||||
DIRECTION_RL: 'direction' WS_INLINE+ 'RL';
|
||||
|
||||
// Accessibility tokens
|
||||
ACC_TITLE: 'accTitle' WS_INLINE* ':' WS_INLINE* -> pushMode(ACC_TITLE_MODE);
|
||||
ACC_DESCR: 'accDescr' WS_INLINE* ':' WS_INLINE* -> pushMode(ACC_DESCR_MODE);
|
||||
ACC_DESCR_MULTI: 'accDescr' WS_INLINE* '{' -> pushMode(ACC_DESCR_MULTILINE_MODE);
|
||||
|
||||
// Statements captured as raw lines for semantic handling in listener
|
||||
STYLE_LINE: 'style' WS_INLINE+ ~[\r\n]*;
|
||||
CLASSDEF_LINE: 'classDef' ~[\r\n]*;
|
||||
CSSCLASS_LINE: 'cssClass' ~[\r\n]*;
|
||||
CALLBACK_LINE: 'callback' ~[\r\n]*;
|
||||
CLICK_LINE: 'click' ~[\r\n]*;
|
||||
LINK_LINE: 'link' ~[\r\n]*;
|
||||
CALL_LINE: 'call' ~[\r\n]*;
|
||||
|
||||
// Notes
|
||||
NOTE_FOR: 'note' WS_INLINE+ 'for';
|
||||
NOTE: 'note';
|
||||
|
||||
// Keywords that affect block handling
|
||||
CLASS: 'class' { this.pendingClassBody = true; };
|
||||
NAMESPACE: 'namespace' { this.pendingNamespaceBody = true; };
|
||||
|
||||
// Structural tokens
|
||||
STYLE_SEPARATOR: ':::';
|
||||
ANNOTATION_START: '<<';
|
||||
ANNOTATION_END: '>>';
|
||||
LBRACKET: '[';
|
||||
RBRACKET: ']';
|
||||
COMMA: ',';
|
||||
DOT: '.';
|
||||
EDGE_STATE: '[*]';
|
||||
GENERIC: '~' (~[~\r\n])+ '~';
|
||||
// Match strings without escape semantics to mirror Jison behavior
|
||||
// Allow any chars except an unescaped closing double-quote; permit newlines
|
||||
STRING: '"' NOT_DQUOTE* '"';
|
||||
BACKTICK_ID: '`' (~[`])* '`';
|
||||
LABEL: ':' (~[':\r\n;])*;
|
||||
|
||||
RELATION_ARROW
|
||||
: (LEFT_HEAD)? LINE_BODY (RIGHT_HEAD)?
|
||||
;
|
||||
fragment LEFT_HEAD
|
||||
: '<|'
|
||||
| '<'
|
||||
| 'o'
|
||||
| '*'
|
||||
| '()'
|
||||
;
|
||||
fragment RIGHT_HEAD
|
||||
: '|>'
|
||||
| '>'
|
||||
| 'o'
|
||||
| '*'
|
||||
| '()'
|
||||
;
|
||||
fragment LINE_BODY
|
||||
: '--'
|
||||
| '..'
|
||||
;
|
||||
|
||||
// Identifiers and numbers
|
||||
IDENTIFIER
|
||||
: (LETTER | DIGIT) IDENT_PART*
|
||||
;
|
||||
NUMBER: DIGIT+;
|
||||
PLUS: '+';
|
||||
MINUS: '-';
|
||||
HASH: '#';
|
||||
PERCENT: '%';
|
||||
STAR: '*';
|
||||
SLASH: '/';
|
||||
LPAREN: '(';
|
||||
RPAREN: ')';
|
||||
|
||||
// Structural braces with mode management
|
||||
STRUCT_START
|
||||
: '{'
|
||||
{
|
||||
if (this.pendingClassBody) {
|
||||
this.pendingClassBody = false;
|
||||
this.pushMode(ClassLexer.CLASS_BODY);
|
||||
} else {
|
||||
if (this.pendingNamespaceBody) {
|
||||
this.pendingNamespaceBody = false;
|
||||
}
|
||||
this.pushMode(ClassLexer.BLOCK);
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
STRUCT_END: '}' { /* default mode only */ };
|
||||
|
||||
// Default fallback (should not normally trigger)
|
||||
UNKNOWN: .;
|
||||
|
||||
// ===== Mode: ACC_TITLE =====
|
||||
mode ACC_TITLE_MODE;
|
||||
ACC_TITLE_MODE_WS: [ \t]+ -> skip;
|
||||
ACC_TITLE_VALUE: ~[\r\n;#]+ -> type(ACC_TITLE_VALUE), popMode;
|
||||
ACC_TITLE_MODE_NEWLINE: ('\r'? '\n')+ { this.popMode(); this.clearPendingScopes(); } -> type(NEWLINE);
|
||||
|
||||
// ===== Mode: ACC_DESCR =====
|
||||
mode ACC_DESCR_MODE;
|
||||
ACC_DESCR_MODE_WS: [ \t]+ -> skip;
|
||||
ACC_DESCR_VALUE: ~[\r\n;#]+ -> type(ACC_DESCR_VALUE), popMode;
|
||||
ACC_DESCR_MODE_NEWLINE: ('\r'? '\n')+ { this.popMode(); this.clearPendingScopes(); } -> type(NEWLINE);
|
||||
|
||||
// ===== Mode: ACC_DESCR_MULTILINE =====
|
||||
mode ACC_DESCR_MULTILINE_MODE;
|
||||
ACC_DESCR_MULTILINE_VALUE: (~[}])+ -> type(ACC_DESCR_MULTILINE_VALUE);
|
||||
ACC_DESCR_MULTI_END: '}' -> popMode, type(ACC_DESCR_MULTI_END);
|
||||
|
||||
// ===== Mode: CLASS_BODY =====
|
||||
mode CLASS_BODY;
|
||||
CLASS_BODY_WS: [ \t]+ -> skip;
|
||||
CLASS_BODY_COMMENT: '%%' ~[\r\n]* -> skip;
|
||||
CLASS_BODY_NEWLINE: ('\r'? '\n')+ -> type(NEWLINE);
|
||||
CLASS_BODY_STRUCT_END: '}' -> popMode, type(STRUCT_END);
|
||||
CLASS_BODY_OPEN_BRACE: '{' -> type(OPEN_IN_STRUCT);
|
||||
CLASS_BODY_EDGE_STATE: '[*]' -> type(EDGE_STATE);
|
||||
CLASS_BODY_MEMBER: ~[{}\r\n]+ -> type(MEMBER);
|
||||
|
||||
// ===== Mode: BLOCK =====
|
||||
mode BLOCK;
|
||||
BLOCK_WS: [ \t]+ -> skip;
|
||||
BLOCK_COMMENT: '%%' ~[\r\n]* -> skip;
|
||||
BLOCK_NEWLINE: ('\r'? '\n')+ -> type(NEWLINE);
|
||||
BLOCK_CLASS: 'class' { this.pendingClassBody = true; } -> type(CLASS);
|
||||
BLOCK_NAMESPACE: 'namespace' { this.pendingNamespaceBody = true; } -> type(NAMESPACE);
|
||||
BLOCK_STYLE_LINE: 'style' WS_INLINE+ ~[\r\n]* -> type(STYLE_LINE);
|
||||
BLOCK_CLASSDEF_LINE: 'classDef' ~[\r\n]* -> type(CLASSDEF_LINE);
|
||||
BLOCK_CSSCLASS_LINE: 'cssClass' ~[\r\n]* -> type(CSSCLASS_LINE);
|
||||
BLOCK_CALLBACK_LINE: 'callback' ~[\r\n]* -> type(CALLBACK_LINE);
|
||||
BLOCK_CLICK_LINE: 'click' ~[\r\n]* -> type(CLICK_LINE);
|
||||
BLOCK_LINK_LINE: 'link' ~[\r\n]* -> type(LINK_LINE);
|
||||
BLOCK_CALL_LINE: 'call' ~[\r\n]* -> type(CALL_LINE);
|
||||
BLOCK_NOTE_FOR: 'note' WS_INLINE+ 'for' -> type(NOTE_FOR);
|
||||
BLOCK_NOTE: 'note' -> type(NOTE);
|
||||
BLOCK_ACC_TITLE: 'accTitle' WS_INLINE* ':' WS_INLINE* -> type(ACC_TITLE), pushMode(ACC_TITLE_MODE);
|
||||
BLOCK_ACC_DESCR: 'accDescr' WS_INLINE* ':' WS_INLINE* -> type(ACC_DESCR), pushMode(ACC_DESCR_MODE);
|
||||
BLOCK_ACC_DESCR_MULTI: 'accDescr' WS_INLINE* '{' -> type(ACC_DESCR_MULTI), pushMode(ACC_DESCR_MULTILINE_MODE);
|
||||
BLOCK_STRUCT_START
|
||||
: '{'
|
||||
{
|
||||
if (this.pendingClassBody) {
|
||||
this.pendingClassBody = false;
|
||||
this.pushMode(ClassLexer.CLASS_BODY);
|
||||
} else {
|
||||
if (this.pendingNamespaceBody) {
|
||||
this.pendingNamespaceBody = false;
|
||||
}
|
||||
this.pushMode(ClassLexer.BLOCK);
|
||||
}
|
||||
}
|
||||
-> type(STRUCT_START)
|
||||
;
|
||||
BLOCK_STRUCT_END: '}' -> popMode, type(STRUCT_END);
|
||||
BLOCK_STYLE_SEPARATOR: ':::' -> type(STYLE_SEPARATOR);
|
||||
BLOCK_ANNOTATION_START: '<<' -> type(ANNOTATION_START);
|
||||
BLOCK_ANNOTATION_END: '>>' -> type(ANNOTATION_END);
|
||||
BLOCK_LBRACKET: '[' -> type(LBRACKET);
|
||||
BLOCK_RBRACKET: ']' -> type(RBRACKET);
|
||||
BLOCK_COMMA: ',' -> type(COMMA);
|
||||
BLOCK_DOT: '.' -> type(DOT);
|
||||
BLOCK_EDGE_STATE: '[*]' -> type(EDGE_STATE);
|
||||
BLOCK_GENERIC: '~' (~[~\r\n])+ '~' -> type(GENERIC);
|
||||
// Mirror Jison: no escape semantics inside strings in BLOCK mode as well
|
||||
BLOCK_STRING: '"' NOT_DQUOTE* '"' -> type(STRING);
|
||||
BLOCK_BACKTICK_ID: '`' (~[`])* '`' -> type(BACKTICK_ID);
|
||||
BLOCK_LABEL: ':' (~[':\r\n;])* -> type(LABEL);
|
||||
BLOCK_RELATION_ARROW
|
||||
: (LEFT_HEAD)? LINE_BODY (RIGHT_HEAD)?
|
||||
-> type(RELATION_ARROW)
|
||||
;
|
||||
BLOCK_IDENTIFIER: (LETTER | DIGIT) IDENT_PART* -> type(IDENTIFIER);
|
||||
BLOCK_NUMBER: DIGIT+ -> type(NUMBER);
|
||||
BLOCK_PLUS: '+' -> type(PLUS);
|
||||
BLOCK_MINUS: '-' -> type(MINUS);
|
||||
BLOCK_HASH: '#' -> type(HASH);
|
||||
BLOCK_PERCENT: '%' -> type(PERCENT);
|
||||
BLOCK_STAR: '*' -> type(STAR);
|
||||
BLOCK_SLASH: '/' -> type(SLASH);
|
||||
BLOCK_LPAREN: '(' -> type(LPAREN);
|
||||
BLOCK_RPAREN: ')' -> type(RPAREN);
|
||||
BLOCK_UNKNOWN: . -> type(UNKNOWN);
|
@@ -1,204 +0,0 @@
|
||||
parser grammar ClassParser;
|
||||
|
||||
options {
|
||||
tokenVocab = ClassLexer;
|
||||
}
|
||||
|
||||
start
|
||||
: (NEWLINE)* classDiagramSection EOF
|
||||
;
|
||||
|
||||
classDiagramSection
|
||||
: CLASS_DIAGRAM (NEWLINE)+ document
|
||||
;
|
||||
|
||||
document
|
||||
: (line)* statement?
|
||||
;
|
||||
|
||||
line
|
||||
: statement? NEWLINE
|
||||
;
|
||||
|
||||
statement
|
||||
: classStatement
|
||||
| namespaceStatement
|
||||
| relationStatement
|
||||
| noteStatement
|
||||
| annotationStatement
|
||||
| memberStatement
|
||||
| classDefStatement
|
||||
| styleStatement
|
||||
| cssClassStatement
|
||||
| directionStatement
|
||||
| accTitleStatement
|
||||
| accDescrStatement
|
||||
| accDescrMultilineStatement
|
||||
| callbackStatement
|
||||
| clickStatement
|
||||
| linkStatement
|
||||
| callStatement
|
||||
;
|
||||
|
||||
classStatement
|
||||
: classIdentifier classStatementTail?
|
||||
;
|
||||
|
||||
classStatementTail
|
||||
: STRUCT_START classMembers? STRUCT_END
|
||||
| STYLE_SEPARATOR cssClassRef classStatementCssTail?
|
||||
;
|
||||
|
||||
classStatementCssTail
|
||||
: STRUCT_START classMembers? STRUCT_END
|
||||
;
|
||||
|
||||
classIdentifier
|
||||
: CLASS className classLabel?
|
||||
;
|
||||
|
||||
classLabel
|
||||
: LBRACKET stringLiteral RBRACKET
|
||||
;
|
||||
|
||||
cssClassRef
|
||||
: className
|
||||
| IDENTIFIER
|
||||
;
|
||||
|
||||
classMembers
|
||||
: (NEWLINE | classMember)*
|
||||
;
|
||||
|
||||
classMember
|
||||
: MEMBER
|
||||
| EDGE_STATE
|
||||
;
|
||||
|
||||
namespaceStatement
|
||||
: namespaceIdentifier namespaceBlock
|
||||
;
|
||||
|
||||
namespaceIdentifier
|
||||
: NAMESPACE namespaceName
|
||||
;
|
||||
|
||||
namespaceName
|
||||
: className
|
||||
;
|
||||
|
||||
namespaceBlock
|
||||
: STRUCT_START (NEWLINE)* namespaceBody? STRUCT_END
|
||||
;
|
||||
|
||||
namespaceBody
|
||||
: namespaceLine+
|
||||
;
|
||||
|
||||
namespaceLine
|
||||
: (classStatement | namespaceStatement)? NEWLINE
|
||||
| classStatement
|
||||
| namespaceStatement
|
||||
;
|
||||
|
||||
relationStatement
|
||||
: className relation className relationLabel?
|
||||
| className stringLiteral relation className relationLabel?
|
||||
| className relation stringLiteral className relationLabel?
|
||||
| className stringLiteral relation stringLiteral className relationLabel?
|
||||
;
|
||||
|
||||
relation
|
||||
: RELATION_ARROW
|
||||
;
|
||||
|
||||
relationLabel
|
||||
: LABEL
|
||||
;
|
||||
|
||||
noteStatement
|
||||
: NOTE_FOR className noteBody
|
||||
| NOTE noteBody
|
||||
;
|
||||
|
||||
noteBody
|
||||
: stringLiteral
|
||||
;
|
||||
|
||||
annotationStatement
|
||||
: ANNOTATION_START annotationName ANNOTATION_END className
|
||||
;
|
||||
|
||||
annotationName
|
||||
: IDENTIFIER
|
||||
| stringLiteral
|
||||
;
|
||||
|
||||
memberStatement
|
||||
: className LABEL
|
||||
;
|
||||
|
||||
classDefStatement
|
||||
: CLASSDEF_LINE
|
||||
;
|
||||
|
||||
styleStatement
|
||||
: STYLE_LINE
|
||||
;
|
||||
|
||||
cssClassStatement
|
||||
: CSSCLASS_LINE
|
||||
;
|
||||
|
||||
directionStatement
|
||||
: DIRECTION_TB
|
||||
| DIRECTION_BT
|
||||
| DIRECTION_LR
|
||||
| DIRECTION_RL
|
||||
;
|
||||
|
||||
accTitleStatement
|
||||
: ACC_TITLE ACC_TITLE_VALUE
|
||||
;
|
||||
|
||||
accDescrStatement
|
||||
: ACC_DESCR ACC_DESCR_VALUE
|
||||
;
|
||||
|
||||
accDescrMultilineStatement
|
||||
: ACC_DESCR_MULTI ACC_DESCR_MULTILINE_VALUE ACC_DESCR_MULTI_END
|
||||
;
|
||||
|
||||
callbackStatement
|
||||
: CALLBACK_LINE
|
||||
;
|
||||
|
||||
clickStatement
|
||||
: CLICK_LINE
|
||||
;
|
||||
|
||||
linkStatement
|
||||
: LINK_LINE
|
||||
;
|
||||
|
||||
callStatement
|
||||
: CALL_LINE
|
||||
;
|
||||
|
||||
stringLiteral
|
||||
: STRING
|
||||
;
|
||||
|
||||
className
|
||||
: classNameSegment (DOT classNameSegment)*
|
||||
;
|
||||
|
||||
classNameSegment
|
||||
: IDENTIFIER genericSuffix?
|
||||
| BACKTICK_ID genericSuffix?
|
||||
| EDGE_STATE
|
||||
;
|
||||
|
||||
genericSuffix
|
||||
: GENERIC
|
||||
;
|
@@ -1,729 +0,0 @@
|
||||
import type { ParseTreeListener } from 'antlr4ng';
|
||||
import {
|
||||
BailErrorStrategy,
|
||||
CharStream,
|
||||
CommonTokenStream,
|
||||
ParseCancellationException,
|
||||
ParseTreeWalker,
|
||||
RecognitionException,
|
||||
type Token,
|
||||
} from 'antlr4ng';
|
||||
import {
|
||||
ClassParser,
|
||||
type ClassIdentifierContext,
|
||||
type ClassMembersContext,
|
||||
type ClassNameContext,
|
||||
type ClassNameSegmentContext,
|
||||
type ClassStatementContext,
|
||||
type NamespaceIdentifierContext,
|
||||
type RelationStatementContext,
|
||||
type NoteStatementContext,
|
||||
type AnnotationStatementContext,
|
||||
type MemberStatementContext,
|
||||
type ClassDefStatementContext,
|
||||
type StyleStatementContext,
|
||||
type CssClassStatementContext,
|
||||
type DirectionStatementContext,
|
||||
type AccTitleStatementContext,
|
||||
type AccDescrStatementContext,
|
||||
type AccDescrMultilineStatementContext,
|
||||
type CallbackStatementContext,
|
||||
type ClickStatementContext,
|
||||
type LinkStatementContext,
|
||||
type CallStatementContext,
|
||||
type CssClassRefContext,
|
||||
type StringLiteralContext,
|
||||
} from './generated/ClassParser.js';
|
||||
import { ClassParserListener } from './generated/ClassParserListener.js';
|
||||
import { ClassLexer } from './generated/ClassLexer.js';
|
||||
|
||||
type ClassDbLike = Record<string, any>;
|
||||
|
||||
const stripQuotes = (value: string): string => {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
||||
try {
|
||||
return JSON.parse(trimmed.replace(/\r?\n/g, '\\n')) as string;
|
||||
} catch {
|
||||
return trimmed.slice(1, -1).replace(/\\"/g, '"');
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const stripBackticks = (value: string): string => {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length >= 2 && trimmed.startsWith('`') && trimmed.endsWith('`')) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const splitCommaSeparated = (text: string): string[] =>
|
||||
text
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length > 0);
|
||||
|
||||
const getStringFromLiteral = (ctx: StringLiteralContext | undefined | null): string | undefined => {
|
||||
if (!ctx) {
|
||||
return undefined;
|
||||
}
|
||||
return stripQuotes(ctx.getText());
|
||||
};
|
||||
|
||||
const getClassNameText = (ctx: ClassNameContext): string => {
|
||||
const segments = ctx.classNameSegment();
|
||||
const parts: string[] = [];
|
||||
for (const segment of segments) {
|
||||
parts.push(getClassNameSegmentText(segment));
|
||||
}
|
||||
return parts.join('.');
|
||||
};
|
||||
|
||||
const getClassNameSegmentText = (ctx: ClassNameSegmentContext): string => {
|
||||
if (ctx.BACKTICK_ID()) {
|
||||
return stripBackticks(ctx.BACKTICK_ID()!.getText());
|
||||
}
|
||||
if (ctx.EDGE_STATE()) {
|
||||
return ctx.EDGE_STATE()!.getText();
|
||||
}
|
||||
return ctx.getText();
|
||||
};
|
||||
|
||||
const parseRelationArrow = (arrow: string, db: ClassDbLike) => {
|
||||
const relation = {
|
||||
type1: 'none',
|
||||
type2: 'none',
|
||||
lineType: db.lineType?.LINE ?? 0,
|
||||
};
|
||||
|
||||
const trimmed = arrow.trim();
|
||||
if (trimmed.includes('..')) {
|
||||
relation.lineType = db.lineType?.DOTTED_LINE ?? relation.lineType;
|
||||
}
|
||||
|
||||
const leftHeads: [string, keyof typeof db.relationType][] = [
|
||||
['<|', 'EXTENSION'],
|
||||
['()', 'LOLLIPOP'],
|
||||
['o', 'AGGREGATION'],
|
||||
['*', 'COMPOSITION'],
|
||||
['<', 'DEPENDENCY'],
|
||||
];
|
||||
|
||||
for (const [prefix, key] of leftHeads) {
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
relation.type1 = db.relationType?.[key] ?? relation.type1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const rightHeads: [string, keyof typeof db.relationType][] = [
|
||||
['|>', 'EXTENSION'],
|
||||
['()', 'LOLLIPOP'],
|
||||
['o', 'AGGREGATION'],
|
||||
['*', 'COMPOSITION'],
|
||||
['>', 'DEPENDENCY'],
|
||||
];
|
||||
|
||||
for (const [suffix, key] of rightHeads) {
|
||||
if (trimmed.endsWith(suffix)) {
|
||||
relation.type2 = db.relationType?.[key] ?? relation.type2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return relation;
|
||||
};
|
||||
|
||||
const parseStyleLine = (db: ClassDbLike, line: string) => {
|
||||
const trimmed = line.trim();
|
||||
const body = trimmed.slice('style'.length).trim();
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const match = /^(\S+)(\s+.+)?$/.exec(body);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const classId = match[1];
|
||||
const styleBody = match[2]?.trim() ?? '';
|
||||
if (!styleBody) {
|
||||
return;
|
||||
}
|
||||
const styles = splitCommaSeparated(styleBody);
|
||||
if (styles.length) {
|
||||
db.setCssStyle?.(classId, styles);
|
||||
}
|
||||
};
|
||||
|
||||
const parseClassDefLine = (db: ClassDbLike, line: string) => {
|
||||
const trimmed = line.trim();
|
||||
const body = trimmed.slice('classDef'.length).trim();
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const match = /^(\S+)(\s+.+)?$/.exec(body);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const idPart = match[1];
|
||||
const stylePart = match[2]?.trim() ?? '';
|
||||
const ids = splitCommaSeparated(idPart);
|
||||
const styles = stylePart ? splitCommaSeparated(stylePart) : [];
|
||||
db.defineClass?.(ids, styles);
|
||||
};
|
||||
|
||||
const parseCssClassLine = (db: ClassDbLike, line: string) => {
|
||||
const trimmed = line.trim();
|
||||
const body = trimmed.slice('cssClass'.length).trim();
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const match = /^("[^"]*"|\S+)\s+(\S+)/.exec(body);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const idsRaw = stripQuotes(match[1]);
|
||||
const className = match[2];
|
||||
db.setCssClass?.(idsRaw, className);
|
||||
};
|
||||
|
||||
const parseCallbackLine = (db: ClassDbLike, line: string) => {
|
||||
const trimmed = line.trim();
|
||||
const match = /^callback\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const target = match[1];
|
||||
const fn = stripQuotes(match[2]);
|
||||
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
|
||||
db.setClickEvent?.(target, fn);
|
||||
if (tooltip) {
|
||||
db.setTooltip?.(target, tooltip);
|
||||
}
|
||||
};
|
||||
|
||||
const parseClickLine = (db: ClassDbLike, line: string) => {
|
||||
const trimmed = line.trim();
|
||||
const callMatch = /^click\s+(\S+)\s+call\s+([^(]+)\(([^)]*)\)(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
|
||||
if (callMatch) {
|
||||
const target = callMatch[1];
|
||||
const fnName = callMatch[2].trim();
|
||||
const args = callMatch[3].trim();
|
||||
const tooltip = callMatch[4] ? stripQuotes(callMatch[4]) : undefined;
|
||||
if (args.length > 0) {
|
||||
db.setClickEvent?.(target, fnName, args);
|
||||
} else {
|
||||
db.setClickEvent?.(target, fnName);
|
||||
}
|
||||
if (tooltip) {
|
||||
db.setTooltip?.(target, tooltip);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
const hrefMatch = /^click\s+(\S+)\s+href\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(
|
||||
trimmed
|
||||
);
|
||||
if (hrefMatch) {
|
||||
const target = hrefMatch[1];
|
||||
const url = stripQuotes(hrefMatch[2]);
|
||||
const tooltip = hrefMatch[3] ? stripQuotes(hrefMatch[3]) : undefined;
|
||||
const targetWindow = hrefMatch[4];
|
||||
if (targetWindow) {
|
||||
db.setLink?.(target, url, targetWindow);
|
||||
} else {
|
||||
db.setLink?.(target, url);
|
||||
}
|
||||
if (tooltip) {
|
||||
db.setTooltip?.(target, tooltip);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
const genericMatch = /^click\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?\s*$/.exec(trimmed);
|
||||
if (genericMatch) {
|
||||
const target = genericMatch[1];
|
||||
const link = stripQuotes(genericMatch[2]);
|
||||
const tooltip = genericMatch[3] ? stripQuotes(genericMatch[3]) : undefined;
|
||||
db.setLink?.(target, link);
|
||||
if (tooltip) {
|
||||
db.setTooltip?.(target, tooltip);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const parseLinkLine = (db: ClassDbLike, line: string) => {
|
||||
const trimmed = line.trim();
|
||||
const match = /^link\s+(\S+)\s+("[^"]*")(?:\s+("[^"]*"))?(?:\s+(\S+))?\s*$/.exec(trimmed);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const target = match[1];
|
||||
const href = stripQuotes(match[2]);
|
||||
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
|
||||
const targetWindow = match[4];
|
||||
|
||||
if (targetWindow) {
|
||||
db.setLink?.(target, href, targetWindow);
|
||||
} else {
|
||||
db.setLink?.(target, href);
|
||||
}
|
||||
if (tooltip) {
|
||||
db.setTooltip?.(target, tooltip);
|
||||
}
|
||||
};
|
||||
|
||||
const parseCallLine = (db: ClassDbLike, lastTarget: string | undefined, line: string) => {
|
||||
if (!lastTarget) {
|
||||
return;
|
||||
}
|
||||
const trimmed = line.trim();
|
||||
const match = /^call\s+([^(]+)\(([^)]*)\)\s*("[^"]*")?\s*$/.exec(trimmed);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const fnName = match[1].trim();
|
||||
const args = match[2].trim();
|
||||
const tooltip = match[3] ? stripQuotes(match[3]) : undefined;
|
||||
if (args.length > 0) {
|
||||
db.setClickEvent?.(lastTarget, fnName, args);
|
||||
} else {
|
||||
db.setClickEvent?.(lastTarget, fnName);
|
||||
}
|
||||
if (tooltip) {
|
||||
db.setTooltip?.(lastTarget, tooltip);
|
||||
}
|
||||
};
|
||||
|
||||
interface NamespaceFrame {
|
||||
name?: string;
|
||||
classes: string[];
|
||||
}
|
||||
|
||||
class ClassDiagramParseListener extends ClassParserListener implements ParseTreeListener {
|
||||
private readonly classNames = new WeakMap<ClassIdentifierContext, string>();
|
||||
private readonly memberLists = new WeakMap<ClassMembersContext, string[]>();
|
||||
private readonly namespaceStack: NamespaceFrame[] = [];
|
||||
private lastClickTarget?: string;
|
||||
|
||||
constructor(private readonly db: ClassDbLike) {
|
||||
super();
|
||||
}
|
||||
|
||||
private recordClassInCurrentNamespace(name: string) {
|
||||
const current = this.namespaceStack[this.namespaceStack.length - 1];
|
||||
if (current?.name) {
|
||||
current.classes.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
override enterNamespaceStatement = (): void => {
|
||||
this.namespaceStack.push({ classes: [] });
|
||||
};
|
||||
|
||||
override exitNamespaceIdentifier = (ctx: NamespaceIdentifierContext): void => {
|
||||
const frame = this.namespaceStack[this.namespaceStack.length - 1];
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
const classNameCtx = ctx.namespaceName()?.className();
|
||||
if (!classNameCtx) {
|
||||
return;
|
||||
}
|
||||
const name = getClassNameText(classNameCtx);
|
||||
frame.name = name;
|
||||
this.db.addNamespace?.(name);
|
||||
};
|
||||
|
||||
override exitNamespaceStatement = (): void => {
|
||||
const frame = this.namespaceStack.pop();
|
||||
if (!frame?.name) {
|
||||
return;
|
||||
}
|
||||
if (frame.classes.length) {
|
||||
this.db.addClassesToNamespace?.(frame.name, frame.classes);
|
||||
}
|
||||
};
|
||||
|
||||
override exitClassIdentifier = (ctx: ClassIdentifierContext): void => {
|
||||
const id = getClassNameText(ctx.className());
|
||||
this.classNames.set(ctx, id);
|
||||
this.db.addClass?.(id);
|
||||
this.recordClassInCurrentNamespace(id);
|
||||
|
||||
const labelCtx = ctx.classLabel?.();
|
||||
if (labelCtx) {
|
||||
const label = getStringFromLiteral(labelCtx.stringLiteral());
|
||||
if (label !== undefined) {
|
||||
this.db.setClassLabel?.(id, label);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
override exitClassMembers = (ctx: ClassMembersContext): void => {
|
||||
const members: string[] = [];
|
||||
for (const memberCtx of ctx.classMember() ?? []) {
|
||||
if (memberCtx.MEMBER()) {
|
||||
members.push(memberCtx.MEMBER()!.getText());
|
||||
} else if (memberCtx.EDGE_STATE()) {
|
||||
members.push(memberCtx.EDGE_STATE()!.getText());
|
||||
}
|
||||
}
|
||||
members.reverse();
|
||||
this.memberLists.set(ctx, members);
|
||||
};
|
||||
|
||||
override exitClassStatement = (ctx: ClassStatementContext): void => {
|
||||
const identifierCtx = ctx.classIdentifier();
|
||||
if (!identifierCtx) {
|
||||
return;
|
||||
}
|
||||
const classId = this.classNames.get(identifierCtx);
|
||||
if (!classId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tailCtx = ctx.classStatementTail?.();
|
||||
const cssRefCtx = tailCtx?.cssClassRef?.();
|
||||
if (cssRefCtx) {
|
||||
const cssTarget = this.resolveCssClassRef(cssRefCtx);
|
||||
if (cssTarget) {
|
||||
this.db.setCssClass?.(classId, cssTarget);
|
||||
}
|
||||
}
|
||||
|
||||
const memberContexts: ClassMembersContext[] = [];
|
||||
const cm1 = tailCtx?.classMembers();
|
||||
if (cm1) {
|
||||
memberContexts.push(cm1);
|
||||
}
|
||||
const cssTailCtx = tailCtx?.classStatementCssTail?.();
|
||||
const cm2 = cssTailCtx?.classMembers();
|
||||
if (cm2) {
|
||||
memberContexts.push(cm2);
|
||||
}
|
||||
|
||||
for (const membersCtx of memberContexts) {
|
||||
const members = this.memberLists.get(membersCtx) ?? [];
|
||||
if (members.length) {
|
||||
this.db.addMembers?.(classId, members);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private resolveCssClassRef(ctx: CssClassRefContext): string | undefined {
|
||||
if (ctx.className()) {
|
||||
return getClassNameText(ctx.className()!);
|
||||
}
|
||||
if (ctx.IDENTIFIER()) {
|
||||
return ctx.IDENTIFIER()!.getText();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override exitRelationStatement = (ctx: RelationStatementContext): void => {
|
||||
const classNames = ctx.className();
|
||||
if (classNames.length < 2) {
|
||||
return;
|
||||
}
|
||||
const id1 = getClassNameText(classNames[0]);
|
||||
const id2 = getClassNameText(classNames[classNames.length - 1]);
|
||||
|
||||
const arrow = ctx.relation()?.getText() ?? '';
|
||||
const relation = parseRelationArrow(arrow, this.db);
|
||||
|
||||
let relationTitle1 = 'none';
|
||||
let relationTitle2 = 'none';
|
||||
const stringLiterals = ctx.stringLiteral();
|
||||
if (stringLiterals.length === 1 && ctx.children) {
|
||||
const stringCtx = stringLiterals[0];
|
||||
const children = ctx.children as unknown[];
|
||||
const stringIndex = children.indexOf(stringCtx);
|
||||
const relationCtx = ctx.relation();
|
||||
const relationIndex = relationCtx ? children.indexOf(relationCtx) : -1;
|
||||
if (relationIndex >= 0 && stringIndex >= 0 && stringIndex < relationIndex) {
|
||||
relationTitle1 = getStringFromLiteral(stringCtx) ?? 'none';
|
||||
} else {
|
||||
relationTitle2 = getStringFromLiteral(stringCtx) ?? 'none';
|
||||
}
|
||||
} else if (stringLiterals.length >= 2) {
|
||||
relationTitle1 = getStringFromLiteral(stringLiterals[0]) ?? 'none';
|
||||
relationTitle2 = getStringFromLiteral(stringLiterals[1]) ?? 'none';
|
||||
}
|
||||
|
||||
let title = 'none';
|
||||
const labelCtx = ctx.relationLabel?.();
|
||||
if (labelCtx?.LABEL()) {
|
||||
title = this.db.cleanupLabel?.(labelCtx.LABEL().getText()) ?? 'none';
|
||||
}
|
||||
|
||||
this.db.addRelation?.({
|
||||
id1,
|
||||
id2,
|
||||
relation,
|
||||
relationTitle1,
|
||||
relationTitle2,
|
||||
title,
|
||||
});
|
||||
};
|
||||
|
||||
override exitNoteStatement = (ctx: NoteStatementContext): void => {
|
||||
const noteCtx = ctx.noteBody();
|
||||
const literalText = noteCtx?.getText?.();
|
||||
const text = literalText !== undefined ? stripQuotes(literalText) : undefined;
|
||||
if (text === undefined) {
|
||||
return;
|
||||
}
|
||||
if (ctx.NOTE_FOR()) {
|
||||
const className = getClassNameText(ctx.className()!);
|
||||
this.db.addNote?.(text, className);
|
||||
} else {
|
||||
this.db.addNote?.(text);
|
||||
}
|
||||
};
|
||||
|
||||
override exitAnnotationStatement = (ctx: AnnotationStatementContext): void => {
|
||||
const className = getClassNameText(ctx.className());
|
||||
const nameCtx = ctx.annotationName();
|
||||
let annotation: string | undefined;
|
||||
if (nameCtx.IDENTIFIER()) {
|
||||
annotation = nameCtx.IDENTIFIER()!.getText();
|
||||
} else {
|
||||
annotation = getStringFromLiteral(nameCtx.stringLiteral());
|
||||
}
|
||||
if (annotation !== undefined) {
|
||||
this.db.addAnnotation?.(className, annotation);
|
||||
}
|
||||
};
|
||||
|
||||
override exitMemberStatement = (ctx: MemberStatementContext): void => {
|
||||
const className = getClassNameText(ctx.className());
|
||||
const labelToken = ctx.LABEL();
|
||||
if (!labelToken) {
|
||||
return;
|
||||
}
|
||||
const cleaned = this.db.cleanupLabel?.(labelToken.getText()) ?? labelToken.getText();
|
||||
this.db.addMember?.(className, cleaned);
|
||||
};
|
||||
|
||||
override exitClassDefStatement = (ctx: ClassDefStatementContext): void => {
|
||||
const token = ctx.CLASSDEF_LINE()?.getSymbol()?.text;
|
||||
if (token) {
|
||||
parseClassDefLine(this.db, token);
|
||||
}
|
||||
};
|
||||
|
||||
override exitStyleStatement = (ctx: StyleStatementContext): void => {
|
||||
const token = ctx.STYLE_LINE()?.getSymbol()?.text;
|
||||
if (token) {
|
||||
parseStyleLine(this.db, token);
|
||||
}
|
||||
};
|
||||
|
||||
override exitCssClassStatement = (ctx: CssClassStatementContext): void => {
|
||||
const token = ctx.CSSCLASS_LINE()?.getSymbol()?.text;
|
||||
if (token) {
|
||||
parseCssClassLine(this.db, token);
|
||||
}
|
||||
};
|
||||
|
||||
override exitDirectionStatement = (ctx: DirectionStatementContext): void => {
|
||||
if (ctx.DIRECTION_TB()) {
|
||||
this.db.setDirection?.('TB');
|
||||
} else if (ctx.DIRECTION_BT()) {
|
||||
this.db.setDirection?.('BT');
|
||||
} else if (ctx.DIRECTION_LR()) {
|
||||
this.db.setDirection?.('LR');
|
||||
} else if (ctx.DIRECTION_RL()) {
|
||||
this.db.setDirection?.('RL');
|
||||
}
|
||||
};
|
||||
|
||||
override exitAccTitleStatement = (ctx: AccTitleStatementContext): void => {
|
||||
const value = ctx.ACC_TITLE_VALUE()?.getText();
|
||||
if (value !== undefined) {
|
||||
this.db.setAccTitle?.(value.trim());
|
||||
}
|
||||
};
|
||||
|
||||
override exitAccDescrStatement = (ctx: AccDescrStatementContext): void => {
|
||||
const value = ctx.ACC_DESCR_VALUE()?.getText();
|
||||
if (value !== undefined) {
|
||||
this.db.setAccDescription?.(value.trim());
|
||||
}
|
||||
};
|
||||
|
||||
override exitAccDescrMultilineStatement = (ctx: AccDescrMultilineStatementContext): void => {
|
||||
const value = ctx.ACC_DESCR_MULTILINE_VALUE()?.getText();
|
||||
if (value !== undefined) {
|
||||
this.db.setAccDescription?.(value.trim());
|
||||
}
|
||||
};
|
||||
|
||||
override exitCallbackStatement = (ctx: CallbackStatementContext): void => {
|
||||
const token = ctx.CALLBACK_LINE()?.getSymbol()?.text;
|
||||
if (token) {
|
||||
parseCallbackLine(this.db, token);
|
||||
}
|
||||
};
|
||||
|
||||
override exitClickStatement = (ctx: ClickStatementContext): void => {
|
||||
const token = ctx.CLICK_LINE()?.getSymbol()?.text;
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const target = parseClickLine(this.db, token);
|
||||
if (target) {
|
||||
this.lastClickTarget = target;
|
||||
}
|
||||
};
|
||||
|
||||
override exitLinkStatement = (ctx: LinkStatementContext): void => {
|
||||
const token = ctx.LINK_LINE()?.getSymbol()?.text;
|
||||
if (token) {
|
||||
parseLinkLine(this.db, token);
|
||||
}
|
||||
};
|
||||
|
||||
override exitCallStatement = (ctx: CallStatementContext): void => {
|
||||
const token = ctx.CALL_LINE()?.getSymbol()?.text;
|
||||
if (token) {
|
||||
parseCallLine(this.db, this.lastClickTarget, token);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ANTLRClassParser {
|
||||
yy: ClassDbLike | null = null;
|
||||
|
||||
parse(input: string): unknown {
|
||||
if (!this.yy) {
|
||||
throw new Error('Class ANTLR parser missing yy (database).');
|
||||
}
|
||||
|
||||
this.yy.clear?.();
|
||||
|
||||
const inputStream = CharStream.fromString(input);
|
||||
const lexer = new ClassLexer(inputStream);
|
||||
const tokenStream = new CommonTokenStream(lexer);
|
||||
const parser = new ClassParser(tokenStream);
|
||||
|
||||
const anyParser = parser as unknown as {
|
||||
getErrorHandler?: () => unknown;
|
||||
setErrorHandler?: (handler: unknown) => void;
|
||||
errorHandler?: unknown;
|
||||
};
|
||||
const currentHandler = anyParser.getErrorHandler?.() ?? anyParser.errorHandler;
|
||||
const handlerName = (currentHandler as { constructor?: { name?: string } } | undefined)
|
||||
?.constructor?.name;
|
||||
if (!currentHandler || handlerName !== 'BailErrorStrategy') {
|
||||
if (typeof anyParser.setErrorHandler === 'function') {
|
||||
anyParser.setErrorHandler(new BailErrorStrategy());
|
||||
} else {
|
||||
(parser as unknown as { errorHandler: unknown }).errorHandler = new BailErrorStrategy();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const tree = parser.start();
|
||||
const listener = new ClassDiagramParseListener(this.yy);
|
||||
ParseTreeWalker.DEFAULT.walk(listener, tree);
|
||||
return tree;
|
||||
} catch (error) {
|
||||
throw this.transformParseError(error, parser);
|
||||
}
|
||||
}
|
||||
|
||||
private transformParseError(error: unknown, parser: ClassParser): Error {
|
||||
const recognitionError = this.unwrapRecognitionError(error);
|
||||
const offendingToken = this.resolveOffendingToken(recognitionError, parser);
|
||||
const line = offendingToken?.line ?? 0;
|
||||
const column = offendingToken?.column ?? 0;
|
||||
const message = `Parse error on line ${line}: Expecting 'STR'`;
|
||||
const cause = error instanceof Error ? error : undefined;
|
||||
const formatted = cause ? new Error(message, { cause }) : new Error(message);
|
||||
|
||||
Object.assign(formatted, {
|
||||
hash: {
|
||||
line,
|
||||
loc: {
|
||||
first_line: line,
|
||||
last_line: line,
|
||||
first_column: column,
|
||||
last_column: column,
|
||||
},
|
||||
text: offendingToken?.text ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
private unwrapRecognitionError(error: unknown): RecognitionException | undefined {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
}
|
||||
if (error instanceof RecognitionException) {
|
||||
return error;
|
||||
}
|
||||
if (error instanceof ParseCancellationException) {
|
||||
const cause = (error as { cause?: unknown }).cause;
|
||||
if (cause instanceof RecognitionException) {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
if (typeof error === 'object' && error !== null && 'cause' in error) {
|
||||
const cause = (error as { cause?: unknown }).cause;
|
||||
if (cause instanceof RecognitionException) {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private resolveOffendingToken(
|
||||
error: RecognitionException | undefined,
|
||||
parser: ClassParser
|
||||
): Token | undefined {
|
||||
const candidate = (error as { offendingToken?: Token })?.offendingToken;
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const current = (
|
||||
parser as unknown as { getCurrentToken?: () => Token | undefined }
|
||||
).getCurrentToken?.();
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const stream = (
|
||||
parser as unknown as { _input?: { LT?: (offset: number) => Token | undefined } }
|
||||
)._input;
|
||||
return stream?.LT?.(1);
|
||||
}
|
||||
}
|
||||
|
||||
const parserInstance = new ANTLRClassParser();
|
||||
|
||||
const exportedParser = {
|
||||
parse: (text: string) => parserInstance.parse(text),
|
||||
parser: parserInstance,
|
||||
yy: null as ClassDbLike | null,
|
||||
};
|
||||
|
||||
Object.defineProperty(exportedParser, 'yy', {
|
||||
get() {
|
||||
return parserInstance.yy;
|
||||
},
|
||||
set(value: ClassDbLike | null) {
|
||||
parserInstance.yy = value;
|
||||
},
|
||||
});
|
||||
|
||||
export default exportedParser;
|
@@ -293,7 +293,6 @@ classStatement
|
||||
: classIdentifier
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
|
||||
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
|
||||
| classIdentifier STRUCT_START STRUCT_STOP {}
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
|
||||
;
|
||||
|
||||
@@ -302,15 +301,8 @@ classIdentifier
|
||||
| CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
|
||||
;
|
||||
|
||||
|
||||
emptyBody
|
||||
:
|
||||
| SPACE emptyBody
|
||||
| NEWLINE emptyBody
|
||||
;
|
||||
|
||||
annotationStatement
|
||||
: ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
|
||||
:ANNOTATION_START alphaNumToken ANNOTATION_END className { yy.addAnnotation($4,$2); }
|
||||
;
|
||||
|
||||
members
|
||||
|
@@ -1,31 +0,0 @@
|
||||
// @ts-ignore: JISON parser lacks type definitions
|
||||
import jisonParser from './classDiagram.jison';
|
||||
import antlrParser from './antlr/antlr-parser.js';
|
||||
|
||||
const USE_ANTLR_PARSER = process.env.USE_ANTLR_PARSER === 'true';
|
||||
|
||||
const baseParser: any = USE_ANTLR_PARSER ? antlrParser : jisonParser;
|
||||
|
||||
const selectedParser: any = Object.create(baseParser);
|
||||
|
||||
selectedParser.parse = (source: string): unknown => {
|
||||
const normalized = source.replace(/\r\n/g, '\n');
|
||||
if (USE_ANTLR_PARSER) {
|
||||
return antlrParser.parse(normalized);
|
||||
}
|
||||
return jisonParser.parse(normalized);
|
||||
};
|
||||
|
||||
Object.defineProperty(selectedParser, 'yy', {
|
||||
get() {
|
||||
return baseParser.yy;
|
||||
},
|
||||
set(value) {
|
||||
baseParser.yy = value;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
export default selectedParser;
|
||||
export const parser = selectedParser;
|
@@ -1,251 +0,0 @@
|
||||
lexer grammar FlowLexer;
|
||||
|
||||
// Virtual tokens for parser
|
||||
tokens {
|
||||
NODIR, DIR, PIPE, PE, SQE, DIAMOND_STOP, STADIUMEND, SUBROUTINEEND, CYLINDEREND, DOUBLECIRCLEEND,
|
||||
ELLIPSE_END_TOKEN, TRAPEND, INVTRAPEND, PS, SQS, TEXT, CIRCLEEND, STR
|
||||
}
|
||||
|
||||
// Lexer modes to match Jison's state-based lexing
|
||||
// Based on Jison: %x string, md_string, acc_title, acc_descr, acc_descr_multiline, dir, vertex, text, etc.
|
||||
|
||||
// Shape data tokens - MUST be defined FIRST for absolute precedence over LINK_ID
|
||||
// Match exactly "@{" like Jison does (no whitespace allowed between @ and {)
|
||||
SHAPE_DATA_START: '@{' -> pushMode(SHAPE_DATA_MODE);
|
||||
|
||||
// Accessibility tokens
|
||||
ACC_TITLE: 'accTitle' WS* ':' WS* -> pushMode(ACC_TITLE_MODE);
|
||||
ACC_DESCR: 'accDescr' WS* ':' WS* -> pushMode(ACC_DESCR_MODE);
|
||||
ACC_DESCR_MULTI: 'accDescr' WS* '{' WS* -> pushMode(ACC_DESCR_MULTILINE_MODE);
|
||||
|
||||
// Interactivity tokens
|
||||
CALL: 'call' WS+ -> pushMode(CALLBACKNAME_MODE);
|
||||
HREF: 'href' WS;
|
||||
// CLICK token - matches 'click' + whitespace + node ID (like Jison)
|
||||
CLICK: 'click' WS+ [A-Za-z0-9_]+ -> pushMode(CLICK_MODE);
|
||||
|
||||
// Graph declaration tokens - these trigger direction mode
|
||||
GRAPH: ('flowchart-elk' | 'graph' | 'flowchart') -> pushMode(DIR_MODE);
|
||||
SUBGRAPH: 'subgraph';
|
||||
END: 'end';
|
||||
|
||||
// Link targets
|
||||
LINK_TARGET: ('_self' | '_blank' | '_parent' | '_top');
|
||||
|
||||
// Style and class tokens
|
||||
STYLE: 'style';
|
||||
DEFAULT: 'default';
|
||||
LINKSTYLE: 'linkStyle';
|
||||
INTERPOLATE: 'interpolate';
|
||||
CLASSDEF: 'classDef';
|
||||
CLASS: 'class';
|
||||
|
||||
// String tokens - must come early to avoid conflicts with QUOTE
|
||||
MD_STRING_START: '"`' -> pushMode(MD_STRING_MODE);
|
||||
|
||||
// Direction tokens - matches Jison's direction_tb, direction_bt, etc.
|
||||
// These handle "direction TB", "direction BT", etc. statements within subgraphs
|
||||
DIRECTION_TB: 'direction' WS+ 'TB' ~[\n]*;
|
||||
DIRECTION_BT: 'direction' WS+ 'BT' ~[\n]*;
|
||||
DIRECTION_RL: 'direction' WS+ 'RL' ~[\n]*;
|
||||
DIRECTION_LR: 'direction' WS+ 'LR' ~[\n]*;
|
||||
|
||||
// ELLIPSE_START must come very early to avoid conflicts with PAREN_START
|
||||
ELLIPSE_START: '(-' -> pushMode(ELLIPSE_TEXT_MODE);
|
||||
|
||||
// Link ID token - matches edge IDs like "e1@" when followed by link patterns
|
||||
// Uses a negative lookahead pattern to match the Jison lookahead (?=[^\{\"])
|
||||
// This prevents LINK_ID from matching "e1@{" and allows SHAPE_DATA_START to match "@{" correctly
|
||||
// The pattern matches any non-whitespace followed by @ but only when NOT followed by { or "
|
||||
LINK_ID: ~[ \t\r\n"]+ '@' {this.inputStream.LA(1) != '{'.charCodeAt(0) && this.inputStream.LA(1) != '"'.charCodeAt(0)}?;
|
||||
|
||||
NUM: [0-9]+;
|
||||
BRKT: '#';
|
||||
STYLE_SEPARATOR: ':::';
|
||||
COLON: ':';
|
||||
AMP: '&';
|
||||
SEMI: ';';
|
||||
COMMA: ',';
|
||||
MULT: '*';
|
||||
|
||||
// Edge patterns - these are complex in Jison, need careful translation
|
||||
// Normal edges without text: A-->B (matches Jison: \s*[xo<]?\-\-+[-xo>]\s*) - must come first to avoid conflicts
|
||||
LINK_NORMAL: WS* [xo<]? '--' '-'* [-xo>] WS*;
|
||||
// Normal edges with text: A-- text ---B (matches Jison: <INITIAL>\s*[xo<]?\-\-\s* -> START_LINK)
|
||||
START_LINK_NORMAL: WS* [xo<]? '--' WS+ -> pushMode(EDGE_TEXT_MODE);
|
||||
// Normal edges with text (no space): A--text---B - match -- followed by any non-dash character
|
||||
START_LINK_NORMAL_NOSPACE: WS* [xo<]? '--' -> pushMode(EDGE_TEXT_MODE);
|
||||
// Pipe-delimited edge text: A--x| (linkStatement for arrowText) - matches Jison linkStatement pattern
|
||||
LINK_STATEMENT_NORMAL: WS* [xo<]? '--' '-'* [xo<]?;
|
||||
|
||||
// Thick edges with text: A== text ===B (matches Jison: <INITIAL>\s*[xo<]?\=\=\s* -> START_LINK)
|
||||
START_LINK_THICK: WS* [xo<]? '==' WS+ -> pushMode(THICK_EDGE_TEXT_MODE);
|
||||
// Thick edges without text: A==>B (matches Jison: \s*[xo<]?\=\=+[=xo>]\s*)
|
||||
LINK_THICK: WS* [xo<]? '==' '='* [=xo>] WS*;
|
||||
LINK_STATEMENT_THICK: WS* [xo<]? '==' '='* [xo<]?;
|
||||
|
||||
// Dotted edges with text: A-. text .->B (matches Jison: <INITIAL>\s*[xo<]?\-\.\s* -> START_LINK)
|
||||
START_LINK_DOTTED: WS* [xo<]? '-.' WS* -> pushMode(DOTTED_EDGE_TEXT_MODE);
|
||||
// Dotted edges without text: A-.->B (matches Jison: \s*[xo<]?\-?\.+\-[xo>]?\s*)
|
||||
LINK_DOTTED: WS* [xo<]? '-' '.'+ '-' [xo>]? WS*;
|
||||
LINK_STATEMENT_DOTTED: WS* [xo<]? '-' '.'+ [xo<]?;
|
||||
|
||||
// Special link
|
||||
LINK_INVISIBLE: WS* '~~' '~'+ WS*;
|
||||
|
||||
// PIPE handling: push to TEXT_MODE to handle content between pipes
|
||||
// Put this AFTER link patterns to avoid interference with edge parsing
|
||||
PIPE: '|' -> pushMode(TEXT_MODE);
|
||||
|
||||
// Vertex shape tokens - MUST come first (longer patterns before shorter ones)
|
||||
DOUBLECIRCLE_START: '(((' -> pushMode(TEXT_MODE);
|
||||
CIRCLE_START: '((' -> pushMode(TEXT_MODE);
|
||||
// ELLIPSE_START moved to top of file for precedence
|
||||
|
||||
// Basic shape tokens - shorter patterns after longer ones
|
||||
SQUARE_START: '[' -> pushMode(TEXT_MODE), type(SQS);
|
||||
// PAREN_START must come AFTER ELLIPSE_START to avoid consuming '(' before '(-' can match
|
||||
PAREN_START: '(' -> pushMode(TEXT_MODE), type(PS);
|
||||
DIAMOND_START: '{' -> pushMode(TEXT_MODE);
|
||||
// PIPE_START removed - conflicts with PIPE token. Context-sensitive pipe handling in TEXT_MODE
|
||||
STADIUM_START: '([' -> pushMode(TEXT_MODE);
|
||||
SUBROUTINE_START: '[[' -> pushMode(TEXT_MODE);
|
||||
VERTEX_WITH_PROPS_START: '[|';
|
||||
CYLINDER_START: '[(' -> pushMode(TEXT_MODE);
|
||||
TRAP_START: '[/' -> pushMode(TRAP_TEXT_MODE);
|
||||
INVTRAP_START: '[\\' -> pushMode(TRAP_TEXT_MODE);
|
||||
|
||||
// Other basic shape tokens
|
||||
TAGSTART: '<';
|
||||
TAGEND: '>' -> pushMode(TEXT_MODE);
|
||||
UP: '^';
|
||||
DOWN: 'v';
|
||||
MINUS: '-';
|
||||
|
||||
// Node string - allow dashes with lookahead to prevent conflicts with links (matches Jison pattern)
|
||||
// Pattern: ([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+
|
||||
NODE_STRING: ([A-Za-z0-9!"#$%&'*+.`?\\/_] | '-' ~[>\-.] | '=' ~'=')+;
|
||||
|
||||
// Unicode text support (simplified from Jison's extensive Unicode ranges)
|
||||
UNICODE_TEXT: [\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE]+;
|
||||
|
||||
// String handling - matches Jison's <*>["] behavior (any mode can enter string mode)
|
||||
QUOTE: '"' -> pushMode(STRING_MODE), skip;
|
||||
|
||||
NEWLINE: ('\r'? '\n')+;
|
||||
WS: [ \t]+;
|
||||
|
||||
// Lexer modes
|
||||
mode ACC_TITLE_MODE;
|
||||
ACC_TITLE_VALUE: (~[\n;#])* -> popMode;
|
||||
|
||||
mode ACC_DESCR_MODE;
|
||||
ACC_DESCR_VALUE: (~[\n;#])* -> popMode;
|
||||
|
||||
mode ACC_DESCR_MULTILINE_MODE;
|
||||
ACC_DESCR_MULTILINE_END: '}' -> popMode;
|
||||
ACC_DESCR_MULTILINE_VALUE: (~[}])*;
|
||||
|
||||
mode SHAPE_DATA_MODE;
|
||||
SHAPE_DATA_STRING_START: '"' -> pushMode(SHAPE_DATA_STRING_MODE);
|
||||
SHAPE_DATA_CONTENT: (~[}"]+);
|
||||
SHAPE_DATA_END: '}' -> popMode;
|
||||
|
||||
mode SHAPE_DATA_STRING_MODE;
|
||||
SHAPE_DATA_STRING_END: '"' -> popMode;
|
||||
SHAPE_DATA_STRING_CONTENT: (~["]+);
|
||||
|
||||
mode CALLBACKNAME_MODE;
|
||||
CALLBACKNAME_PAREN_EMPTY: '(' WS* ')' -> popMode, type(CALLBACKARGS);
|
||||
CALLBACKNAME_PAREN_START: '(' -> popMode, pushMode(CALLBACKARGS_MODE);
|
||||
CALLBACKNAME: (~[(])*;
|
||||
|
||||
mode CALLBACKARGS_MODE;
|
||||
CALLBACKARGS_END: ')' -> popMode;
|
||||
CALLBACKARGS: (~[)])*;
|
||||
|
||||
mode CLICK_MODE;
|
||||
CLICK_NEWLINE: ('\r'? '\n')+ -> popMode, type(NEWLINE);
|
||||
CLICK_WS: WS -> skip;
|
||||
CLICK_CALL: 'call' WS+ -> type(CALL), pushMode(CALLBACKNAME_MODE);
|
||||
CLICK_HREF: 'href' -> type(HREF);
|
||||
CLICK_STR: '"' (~["])* '"' -> type(STR);
|
||||
CLICK_LINK_TARGET: ('_self' | '_blank' | '_parent' | '_top') -> type(LINK_TARGET);
|
||||
CLICK_CALLBACKNAME: [A-Za-z0-9_]+ -> type(CALLBACKNAME);
|
||||
|
||||
|
||||
|
||||
mode DIR_MODE;
|
||||
DIR_NEWLINE: ('\r'? '\n')* WS* '\n' -> popMode, type(NODIR);
|
||||
DIR_LR: WS* 'LR' -> popMode, type(DIR);
|
||||
DIR_RL: WS* 'RL' -> popMode, type(DIR);
|
||||
DIR_TB: WS* 'TB' -> popMode, type(DIR);
|
||||
DIR_BT: WS* 'BT' -> popMode, type(DIR);
|
||||
DIR_TD: WS* 'TD' -> popMode, type(DIR);
|
||||
DIR_BR: WS* 'BR' -> popMode, type(DIR);
|
||||
DIR_LEFT: WS* '<' -> popMode, type(DIR);
|
||||
DIR_RIGHT: WS* '>' -> popMode, type(DIR);
|
||||
DIR_UP: WS* '^' -> popMode, type(DIR);
|
||||
DIR_DOWN: WS* 'v' -> popMode, type(DIR);
|
||||
|
||||
mode STRING_MODE;
|
||||
STRING_END: '"' -> popMode, skip;
|
||||
STR: (~["]+);
|
||||
|
||||
mode MD_STRING_MODE;
|
||||
MD_STRING_END: '`"' -> popMode;
|
||||
MD_STR: (~[`"])+;
|
||||
|
||||
mode TEXT_MODE;
|
||||
// Allow nested diamond starts (for hexagon nodes)
|
||||
TEXT_DIAMOND_START: '{' -> pushMode(TEXT_MODE), type(DIAMOND_START);
|
||||
|
||||
// Handle nested parentheses and brackets like Jison
|
||||
TEXT_PAREN_START: '(' -> pushMode(TEXT_MODE), type(PS);
|
||||
TEXT_SQUARE_START: '[' -> pushMode(TEXT_MODE), type(SQS);
|
||||
|
||||
// Handle quoted strings in text mode - matches Jison's <*>["] behavior
|
||||
// Skip the opening quote token, just push to STRING_MODE like Jison does
|
||||
TEXT_STRING_START: '"' -> pushMode(STRING_MODE), skip;
|
||||
|
||||
// Handle closing pipe in text mode - pop back to default mode
|
||||
TEXT_PIPE_END: '|' -> popMode, type(PIPE);
|
||||
|
||||
TEXT_PAREN_END: ')' -> popMode, type(PE);
|
||||
TEXT_SQUARE_END: ']' -> popMode, type(SQE);
|
||||
TEXT_DIAMOND_END: '}' -> popMode, type(DIAMOND_STOP);
|
||||
TEXT_STADIUM_END: '])' -> popMode, type(STADIUMEND);
|
||||
TEXT_SUBROUTINE_END: ']]' -> popMode, type(SUBROUTINEEND);
|
||||
TEXT_CYLINDER_END: ')]' -> popMode, type(CYLINDEREND);
|
||||
TEXT_DOUBLECIRCLE_END: ')))' -> popMode, type(DOUBLECIRCLEEND);
|
||||
TEXT_CIRCLE_END: '))' -> popMode, type(CIRCLEEND);
|
||||
// Now allow all characters except the specific end tokens for this mode
|
||||
TEXT_CONTENT: (~[(){}|\]"])+;
|
||||
|
||||
mode ELLIPSE_TEXT_MODE;
|
||||
ELLIPSE_END: '-)' -> popMode, type(ELLIPSE_END_TOKEN);
|
||||
ELLIPSE_TEXT: (~[-)])+;
|
||||
|
||||
mode TRAP_TEXT_MODE;
|
||||
TRAP_END_BRACKET: '\\]' -> popMode, type(TRAPEND);
|
||||
INVTRAP_END_BRACKET: '/]' -> popMode, type(INVTRAPEND);
|
||||
TRAP_TEXT: (~[\\/\]])+;
|
||||
|
||||
mode EDGE_TEXT_MODE;
|
||||
// Handle space-delimited pattern: A-- text ----B or A-- text -->B (matches Jison: [^-]|\-(?!\-)+)
|
||||
// Must handle both cases: extra dashes without arrow (----) and dashes with arrow (-->)
|
||||
EDGE_TEXT_LINK_END: WS* '--' '-'* [-xo>]? WS* -> popMode, type(LINK_NORMAL);
|
||||
// Match any character including spaces and single dashes, but not double dashes
|
||||
EDGE_TEXT: (~[-] | '-' ~[-])+;
|
||||
|
||||
mode THICK_EDGE_TEXT_MODE;
|
||||
// Handle thick edge patterns: A== text ====B or A== text ==>B
|
||||
THICK_EDGE_TEXT_LINK_END: WS* '==' '='* [=xo>]? WS* -> popMode, type(LINK_THICK);
|
||||
THICK_EDGE_TEXT: (~[=] | '=' ~[=])+;
|
||||
|
||||
mode DOTTED_EDGE_TEXT_MODE;
|
||||
// Handle dotted edge patterns: A-. text ...-B or A-. text .->B
|
||||
DOTTED_EDGE_TEXT_LINK_END: WS* '.'+ '-' [xo>]? WS* -> popMode, type(LINK_DOTTED);
|
||||
DOTTED_EDGE_TEXT: ~[.]+;
|
||||
|
||||
|
@@ -1,286 +0,0 @@
|
||||
parser grammar FlowParser;
|
||||
|
||||
options {
|
||||
tokenVocab = FlowLexer;
|
||||
}
|
||||
|
||||
// Entry point - matches Jison's "start: graphConfig document"
|
||||
start: graphConfig document;
|
||||
|
||||
// Document structure - matches Jison's document rule
|
||||
document:
|
||||
line*
|
||||
;
|
||||
|
||||
// Line structure - matches Jison's line rule
|
||||
line:
|
||||
statement
|
||||
| SEMI
|
||||
| NEWLINE
|
||||
| WS
|
||||
;
|
||||
|
||||
// Graph configuration - matches Jison's graphConfig rule
|
||||
graphConfig:
|
||||
WS graphConfig
|
||||
| NEWLINE graphConfig
|
||||
| GRAPH NODIR // Default TB direction
|
||||
| GRAPH DIR firstStmtSeparator // Explicit direction
|
||||
;
|
||||
|
||||
// Statement types - matches Jison's statement rule
|
||||
statement:
|
||||
vertexStatement separator
|
||||
| standaloneVertex separator // For edge property statements like e1@{curve: basis}
|
||||
| styleStatement separator
|
||||
| linkStyleStatement separator
|
||||
| classDefStatement separator
|
||||
| classStatement separator
|
||||
| clickStatement separator
|
||||
| subgraphStatement separator
|
||||
| direction
|
||||
| accTitle
|
||||
| accDescr
|
||||
;
|
||||
|
||||
// Separators
|
||||
separator: NEWLINE | SEMI | EOF;
|
||||
firstStmtSeparator: SEMI | NEWLINE | spaceList NEWLINE;
|
||||
spaceList: WS spaceList | WS;
|
||||
|
||||
// Vertex statement - matches Jison's vertexStatement rule
|
||||
vertexStatement:
|
||||
vertexStatement link node shapeData // Chain with shape data
|
||||
| vertexStatement link node // Chain without shape data
|
||||
| vertexStatement link node spaceList // Chain with trailing space
|
||||
| node spaceList // Single node with space
|
||||
| node shapeData // Single node with shape data
|
||||
| node // Single node
|
||||
;
|
||||
|
||||
// Standalone vertex - for edge property statements like e1@{curve: basis}
|
||||
standaloneVertex:
|
||||
NODE_STRING shapeData
|
||||
| LINK_ID shapeData // For edge IDs like e1@{curve: basis}
|
||||
;
|
||||
|
||||
// Node definition - matches Jison's node rule
|
||||
node:
|
||||
styledVertex
|
||||
| node shapeData spaceList AMP spaceList styledVertex
|
||||
| node spaceList AMP spaceList styledVertex
|
||||
;
|
||||
|
||||
// Styled vertex - matches Jison's styledVertex rule
|
||||
styledVertex:
|
||||
vertex
|
||||
| vertex STYLE_SEPARATOR idString
|
||||
;
|
||||
|
||||
// Vertex shapes - matches Jison's vertex rule
|
||||
vertex:
|
||||
idString SQS text SQE // Square: [text]
|
||||
| idString DOUBLECIRCLE_START text DOUBLECIRCLEEND // Double circle: (((text)))
|
||||
| idString CIRCLE_START text CIRCLEEND // Circle: ((text))
|
||||
| idString ELLIPSE_START text ELLIPSE_END_TOKEN // Ellipse: (-text-)
|
||||
| idString STADIUM_START text STADIUMEND // Stadium: ([text])
|
||||
| idString SUBROUTINE_START text SUBROUTINEEND // Subroutine: [[text]]
|
||||
| idString VERTEX_WITH_PROPS_START NODE_STRING COLON NODE_STRING PIPE text SQE // Props: [|field:value|text]
|
||||
| idString CYLINDER_START text CYLINDEREND // Cylinder: [(text)]
|
||||
| idString PS text PE // Round: (text)
|
||||
| idString DIAMOND_START text DIAMOND_STOP // Diamond: {text}
|
||||
| idString DIAMOND_START DIAMOND_START text DIAMOND_STOP DIAMOND_STOP // Hexagon: {{text}}
|
||||
| idString TAGEND text SQE // Odd: >text]
|
||||
| idString TRAP_START text TRAPEND // Trapezoid: [/text\]
|
||||
| idString INVTRAP_START text INVTRAPEND // Inv trapezoid: [\text/]
|
||||
| idString TRAP_START text INVTRAPEND // Lean right: [/text/]
|
||||
| idString INVTRAP_START text TRAPEND // Lean left: [\text\]
|
||||
| idString // Plain node
|
||||
;
|
||||
|
||||
// Link definition - matches Jison's link rule
|
||||
link:
|
||||
linkStatement arrowText spaceList?
|
||||
| linkStatement
|
||||
| START_LINK_NORMAL edgeText LINK_NORMAL
|
||||
| START_LINK_NORMAL_NOSPACE edgeText LINK_NORMAL
|
||||
| START_LINK_THICK edgeText LINK_THICK
|
||||
| START_LINK_DOTTED edgeText LINK_DOTTED
|
||||
| LINK_ID START_LINK_NORMAL edgeText LINK_NORMAL
|
||||
| LINK_ID START_LINK_NORMAL_NOSPACE edgeText LINK_NORMAL
|
||||
| LINK_ID START_LINK_THICK edgeText LINK_THICK
|
||||
| LINK_ID START_LINK_DOTTED edgeText LINK_DOTTED
|
||||
;
|
||||
|
||||
// Link statement - matches Jison's linkStatement rule
|
||||
linkStatement:
|
||||
LINK_NORMAL
|
||||
| LINK_THICK
|
||||
| LINK_DOTTED
|
||||
| LINK_INVISIBLE
|
||||
| LINK_STATEMENT_NORMAL
|
||||
| LINK_STATEMENT_DOTTED
|
||||
| LINK_ID LINK_NORMAL
|
||||
| LINK_ID LINK_THICK
|
||||
| LINK_ID LINK_DOTTED
|
||||
| LINK_ID LINK_INVISIBLE
|
||||
| LINK_ID LINK_STATEMENT_NORMAL
|
||||
| LINK_ID LINK_STATEMENT_THICK
|
||||
;
|
||||
|
||||
// Edge text - matches Jison's edgeText rule
|
||||
edgeText:
|
||||
edgeTextToken
|
||||
| edgeText edgeTextToken
|
||||
| stringLiteral
|
||||
| MD_STR
|
||||
;
|
||||
|
||||
// Arrow text - matches Jison's arrowText rule
|
||||
arrowText:
|
||||
PIPE text PIPE
|
||||
;
|
||||
|
||||
// Text definition - matches Jison's text rule
|
||||
text:
|
||||
textToken
|
||||
| text textToken
|
||||
| stringLiteral
|
||||
| MD_STR
|
||||
| NODE_STRING
|
||||
| TEXT_CONTENT
|
||||
| ELLIPSE_TEXT
|
||||
| TRAP_TEXT
|
||||
;
|
||||
|
||||
// Shape data - matches Jison's shapeData rule
|
||||
shapeData:
|
||||
SHAPE_DATA_START shapeDataContent SHAPE_DATA_END
|
||||
;
|
||||
|
||||
shapeDataContent:
|
||||
shapeDataContent SHAPE_DATA_CONTENT
|
||||
| shapeDataContent SHAPE_DATA_STRING_START SHAPE_DATA_STRING_CONTENT SHAPE_DATA_STRING_END
|
||||
| SHAPE_DATA_CONTENT
|
||||
| SHAPE_DATA_STRING_START SHAPE_DATA_STRING_CONTENT SHAPE_DATA_STRING_END
|
||||
|
|
||||
;
|
||||
|
||||
// Style statement - matches Jison's styleStatement rule
|
||||
styleStatement:
|
||||
STYLE WS idString WS stylesOpt
|
||||
;
|
||||
|
||||
// Link style statement - matches Jison's linkStyleStatement rule
|
||||
linkStyleStatement:
|
||||
LINKSTYLE WS DEFAULT WS stylesOpt
|
||||
| LINKSTYLE WS numList WS stylesOpt
|
||||
| LINKSTYLE WS DEFAULT WS INTERPOLATE WS alphaNum WS stylesOpt
|
||||
| LINKSTYLE WS numList WS INTERPOLATE WS alphaNum WS stylesOpt
|
||||
| LINKSTYLE WS DEFAULT WS INTERPOLATE WS alphaNum
|
||||
| LINKSTYLE WS numList WS INTERPOLATE WS alphaNum
|
||||
;
|
||||
|
||||
// Class definition statement - matches Jison's classDefStatement rule
|
||||
classDefStatement:
|
||||
CLASSDEF WS idString WS stylesOpt
|
||||
;
|
||||
|
||||
// Class statement - matches Jison's classStatement rule
|
||||
classStatement:
|
||||
CLASS WS idString WS idString
|
||||
;
|
||||
|
||||
// String rule to handle STR patterns
|
||||
stringLiteral:
|
||||
STR
|
||||
;
|
||||
|
||||
// Click statement - matches Jison's clickStatement rule
|
||||
// CLICK token now contains both 'click' and node ID (like Jison)
|
||||
clickStatement:
|
||||
CLICK CALLBACKNAME
|
||||
| CLICK CALLBACKNAME stringLiteral
|
||||
| CLICK CALLBACKNAME CALLBACKARGS
|
||||
| CLICK CALLBACKNAME CALLBACKARGS stringLiteral
|
||||
| CLICK CALL CALLBACKNAME
|
||||
| CLICK CALL CALLBACKNAME stringLiteral
|
||||
| CLICK CALL CALLBACKNAME CALLBACKARGS
|
||||
| CLICK CALL CALLBACKNAME CALLBACKARGS stringLiteral
|
||||
| CLICK HREF stringLiteral
|
||||
| CLICK HREF stringLiteral stringLiteral
|
||||
| CLICK HREF stringLiteral LINK_TARGET
|
||||
| CLICK HREF stringLiteral stringLiteral LINK_TARGET
|
||||
| CLICK stringLiteral // CLICK STR - direct click with URL
|
||||
| CLICK stringLiteral stringLiteral // CLICK STR STR - click with URL and tooltip
|
||||
| CLICK stringLiteral LINK_TARGET // CLICK STR LINK_TARGET - click with URL and target
|
||||
| CLICK stringLiteral stringLiteral LINK_TARGET // CLICK STR STR LINK_TARGET - click with URL, tooltip, and target
|
||||
;
|
||||
|
||||
// Subgraph statement - matches Jison's subgraph rules
|
||||
subgraphStatement:
|
||||
SUBGRAPH WS textNoTags SQS text SQE separator document END
|
||||
| SUBGRAPH WS textNoTags separator document END
|
||||
| SUBGRAPH separator document END
|
||||
;
|
||||
|
||||
// Direction statement - matches Jison's direction rule
|
||||
direction:
|
||||
DIRECTION_TB
|
||||
| DIRECTION_BT
|
||||
| DIRECTION_RL
|
||||
| DIRECTION_LR
|
||||
;
|
||||
|
||||
// Accessibility statements
|
||||
accTitle: ACC_TITLE ACC_TITLE_VALUE;
|
||||
accDescr: ACC_DESCR ACC_DESCR_VALUE | ACC_DESCR_MULTI ACC_DESCR_MULTILINE_VALUE ACC_DESCR_MULTILINE_END;
|
||||
|
||||
// Number list - matches Jison's numList rule
|
||||
numList:
|
||||
NUM
|
||||
| numList COMMA NUM
|
||||
;
|
||||
|
||||
// Styles - matches Jison's stylesOpt rule
|
||||
stylesOpt:
|
||||
style
|
||||
| stylesOpt COMMA style
|
||||
;
|
||||
|
||||
// Style components - matches Jison's style rule
|
||||
style:
|
||||
styleComponent
|
||||
| style styleComponent
|
||||
;
|
||||
|
||||
// Style component - matches Jison's styleComponent rule
|
||||
styleComponent: NUM | NODE_STRING | COLON | WS | BRKT | STYLE | MULT | MINUS;
|
||||
|
||||
// Token definitions - matches Jison's token lists
|
||||
idString:
|
||||
idStringToken
|
||||
| idString idStringToken
|
||||
;
|
||||
|
||||
alphaNum:
|
||||
alphaNumToken
|
||||
| alphaNum alphaNumToken
|
||||
;
|
||||
|
||||
textNoTags:
|
||||
textNoTagsToken
|
||||
| textNoTags textNoTagsToken
|
||||
| stringLiteral
|
||||
| MD_STR
|
||||
;
|
||||
|
||||
// Token types - matches Jison's token definitions
|
||||
idStringToken: NUM | NODE_STRING | DOWN | MINUS | DEFAULT | COMMA | COLON | AMP | BRKT | MULT | UNICODE_TEXT;
|
||||
textToken: TEXT_CONTENT | TAGSTART | TAGEND | UNICODE_TEXT | NODE_STRING | WS;
|
||||
textNoTagsToken: NUM | NODE_STRING | WS | MINUS | AMP | UNICODE_TEXT | COLON | MULT | BRKT | keywords | START_LINK_NORMAL;
|
||||
edgeTextToken: EDGE_TEXT | THICK_EDGE_TEXT | DOTTED_EDGE_TEXT | UNICODE_TEXT;
|
||||
alphaNumToken: NUM | UNICODE_TEXT | NODE_STRING | DIR | DOWN | MINUS | COMMA | COLON | AMP | BRKT | MULT;
|
||||
|
||||
// Keywords - matches Jison's keywords rule
|
||||
keywords: STYLE | LINKSTYLE | CLASSDEF | CLASS | CLICK | GRAPH | DIR | SUBGRAPH | END | DOWN | UP;
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
const { CharStream } = require('antlr4ng');
|
||||
const { FlowLexer } = require('./generated/FlowLexer.ts');
|
||||
|
||||
const input = 'D@{ shape: rounded }';
|
||||
console.log('Input:', input);
|
||||
|
||||
const chars = CharStream.fromString(input);
|
||||
const lexer = new FlowLexer(chars);
|
||||
const tokens = lexer.getAllTokens();
|
||||
|
||||
console.log('Tokens:');
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
console.log(` [${i}] Type: ${token.type}, Text: '${token.text}', Channel: ${token.channel}`);
|
||||
}
|
@@ -1,22 +1,12 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import flowJisonParser from './flow.jison';
|
||||
import antlrParser from './antlr/antlr-parser.js';
|
||||
|
||||
// Configuration flag to switch between parsers
|
||||
// Set to true to test ANTLR parser, false to use original Jison parser
|
||||
const USE_ANTLR_PARSER = process.env.USE_ANTLR_PARSER === 'true';
|
||||
|
||||
const newParser = Object.assign({}, USE_ANTLR_PARSER ? antlrParser : flowJisonParser);
|
||||
const newParser = Object.assign({}, flowJisonParser);
|
||||
|
||||
newParser.parse = (src: string): unknown => {
|
||||
// remove the trailing whitespace after closing curly braces when ending a line break
|
||||
const newSrc = src.replace(/}\s*\n/g, '}\n');
|
||||
|
||||
if (USE_ANTLR_PARSER) {
|
||||
return antlrParser.parse(newSrc);
|
||||
} else {
|
||||
return flowJisonParser.parse(newSrc);
|
||||
}
|
||||
return flowJisonParser.parse(newSrc);
|
||||
};
|
||||
|
||||
export default newParser;
|
||||
|
@@ -1,297 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { MindmapDB } from './mindmapDb.js';
|
||||
import type { MindmapLayoutNode, MindmapLayoutEdge } from './mindmapDb.js';
|
||||
import type { Edge } from '../../rendering-util/types.js';
|
||||
|
||||
// Mock the getConfig function
|
||||
vi.mock('../../diagram-api/diagramAPI.js', () => ({
|
||||
getConfig: vi.fn(() => ({
|
||||
mindmap: {
|
||||
layoutAlgorithm: 'cose-bilkent',
|
||||
padding: 10,
|
||||
maxNodeWidth: 200,
|
||||
useMaxWidth: true,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('MindmapDb getData function', () => {
|
||||
let db: MindmapDB;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new MindmapDB();
|
||||
// Clear the database before each test
|
||||
db.clear();
|
||||
});
|
||||
|
||||
describe('getData', () => {
|
||||
it('should return empty data when no mindmap is set', () => {
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toEqual([]);
|
||||
expect(result.edges).toEqual([]);
|
||||
expect(result.config).toBeDefined();
|
||||
expect(result.rootNode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return structured data for simple mindmap', () => {
|
||||
// Create a simple mindmap structure
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child1', 'Child 1', 0);
|
||||
db.addNode(1, 'child2', 'Child 2', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
expect(result.edges).toHaveLength(2);
|
||||
expect(result.config).toBeDefined();
|
||||
expect(result.rootNode).toBeDefined();
|
||||
|
||||
// Check root node
|
||||
const rootNode = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '0');
|
||||
expect(rootNode).toBeDefined();
|
||||
expect(rootNode?.label).toBe('Root Node');
|
||||
expect(rootNode?.level).toBe(0);
|
||||
|
||||
// Check child nodes
|
||||
const child1 = (result.nodes as MindmapLayoutNode[]).find((n) => n.id === '1');
|
||||
expect(child1).toBeDefined();
|
||||
expect(child1?.label).toBe('Child 1');
|
||||
expect(child1?.level).toBe(1);
|
||||
|
||||
// Check edges
|
||||
expect(result.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
start: '0',
|
||||
end: '1',
|
||||
depth: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return structured data for hierarchical mindmap', () => {
|
||||
// Create a hierarchical mindmap structure
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child1', 'Child 1', 0);
|
||||
db.addNode(2, 'grandchild1', 'Grandchild 1', 0);
|
||||
db.addNode(2, 'grandchild2', 'Grandchild 2', 0);
|
||||
db.addNode(1, 'child2', 'Child 2', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toHaveLength(5);
|
||||
expect(result.edges).toHaveLength(4);
|
||||
|
||||
// Check that all levels are represented
|
||||
const levels = result.nodes.map((n) => (n as MindmapLayoutNode).level);
|
||||
expect(levels).toContain(0); // root
|
||||
expect(levels).toContain(1); // children
|
||||
expect(levels).toContain(2); // grandchildren
|
||||
|
||||
// Check edge relationships
|
||||
const edgeRelations = result.edges.map(
|
||||
(e) => `${(e as MindmapLayoutEdge).start}->${(e as MindmapLayoutEdge).end}`
|
||||
);
|
||||
expect(edgeRelations).toContain('0->1'); // root to child1
|
||||
expect(edgeRelations).toContain('1->2'); // child1 to grandchild1
|
||||
expect(edgeRelations).toContain('1->3'); // child1 to grandchild2
|
||||
expect(edgeRelations).toContain('0->4'); // root to child2
|
||||
});
|
||||
|
||||
it('should preserve node properties in processed data', () => {
|
||||
// Add a node with specific properties
|
||||
db.addNode(0, 'root', 'Root Node', 2); // type 2 = rectangle
|
||||
|
||||
// Set additional properties
|
||||
const mindmap = db.getMindmap();
|
||||
if (mindmap) {
|
||||
mindmap.width = 150;
|
||||
mindmap.height = 75;
|
||||
mindmap.padding = 15;
|
||||
mindmap.section = 1;
|
||||
mindmap.class = 'custom-class';
|
||||
mindmap.icon = 'star';
|
||||
}
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
const node = result.nodes[0] as MindmapLayoutNode;
|
||||
|
||||
expect(node.type).toBe(2);
|
||||
expect(node.width).toBe(150);
|
||||
expect(node.height).toBe(75);
|
||||
expect(node.padding).toBe(15);
|
||||
expect(node.section).toBeUndefined(); // Root node has undefined section
|
||||
expect(node.cssClasses).toBe('mindmap-node section-root section--1 custom-class');
|
||||
expect(node.icon).toBe('star');
|
||||
});
|
||||
|
||||
it('should generate unique edge IDs', () => {
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child1', 'Child 1', 0);
|
||||
db.addNode(1, 'child2', 'Child 2', 0);
|
||||
db.addNode(1, 'child3', 'Child 3', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
const edgeIds = result.edges.map((e: Edge) => e.id);
|
||||
const uniqueIds = new Set(edgeIds);
|
||||
|
||||
expect(edgeIds).toHaveLength(3);
|
||||
expect(uniqueIds.size).toBe(3); // All IDs should be unique
|
||||
});
|
||||
|
||||
it('should handle nodes with missing optional properties', () => {
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
|
||||
const result = db.getData();
|
||||
const node = result.nodes[0] as MindmapLayoutNode;
|
||||
|
||||
// Should handle undefined/missing properties gracefully
|
||||
expect(node.section).toBeUndefined(); // Root node has undefined section
|
||||
expect(node.cssClasses).toBe('mindmap-node section-root section--1'); // Root node gets special classes
|
||||
expect(node.icon).toBeUndefined();
|
||||
expect(node.x).toBeUndefined();
|
||||
expect(node.y).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should assign correct section classes based on sibling position', () => {
|
||||
// Create the example mindmap structure:
|
||||
// A
|
||||
// a0
|
||||
// aa0
|
||||
// a1
|
||||
// aaa
|
||||
// a2
|
||||
db.addNode(0, 'A', 'A', 0); // Root
|
||||
db.addNode(1, 'a0', 'a0', 0); // First child of root
|
||||
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
|
||||
db.addNode(1, 'a1', 'a1', 0); // Second child of root
|
||||
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
|
||||
db.addNode(1, 'a2', 'a2', 0); // Third child of root
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
// Find nodes by their labels
|
||||
const nodeA = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
|
||||
const nodeA0 = result.nodes.find((n) => n.label === 'a0') as MindmapLayoutNode;
|
||||
const nodeAa0 = result.nodes.find((n) => n.label === 'aa0') as MindmapLayoutNode;
|
||||
const nodeA1 = result.nodes.find((n) => n.label === 'a1') as MindmapLayoutNode;
|
||||
const nodeAaa = result.nodes.find((n) => n.label === 'aaa') as MindmapLayoutNode;
|
||||
const nodeA2 = result.nodes.find((n) => n.label === 'a2') as MindmapLayoutNode;
|
||||
|
||||
// Check section assignments
|
||||
expect(nodeA.section).toBeUndefined(); // Root has undefined section
|
||||
expect(nodeA0.section).toBe(0); // First child of root
|
||||
expect(nodeAa0.section).toBe(0); // Inherits from parent a0
|
||||
expect(nodeA1.section).toBe(1); // Second child of root
|
||||
expect(nodeAaa.section).toBe(1); // Inherits from parent a1
|
||||
expect(nodeA2.section).toBe(2); // Third child of root
|
||||
|
||||
// Check CSS classes
|
||||
expect(nodeA.cssClasses).toBe('mindmap-node section-root section--1');
|
||||
expect(nodeA0.cssClasses).toBe('mindmap-node section-0');
|
||||
expect(nodeAa0.cssClasses).toBe('mindmap-node section-0');
|
||||
expect(nodeA1.cssClasses).toBe('mindmap-node section-1');
|
||||
expect(nodeAaa.cssClasses).toBe('mindmap-node section-1');
|
||||
expect(nodeA2.cssClasses).toBe('mindmap-node section-2');
|
||||
});
|
||||
|
||||
it('should preserve custom classes while adding section classes', () => {
|
||||
db.addNode(0, 'root', 'Root Node', 0);
|
||||
db.addNode(1, 'child', 'Child Node', 0);
|
||||
|
||||
// Add custom classes to nodes
|
||||
const mindmap = db.getMindmap();
|
||||
if (mindmap) {
|
||||
mindmap.class = 'custom-root-class';
|
||||
if (mindmap.children?.[0]) {
|
||||
mindmap.children[0].class = 'custom-child-class';
|
||||
}
|
||||
}
|
||||
|
||||
const result = db.getData();
|
||||
const rootNode = result.nodes.find((n) => n.label === 'Root Node') as MindmapLayoutNode;
|
||||
const childNode = result.nodes.find((n) => n.label === 'Child Node') as MindmapLayoutNode;
|
||||
|
||||
// Should include both section classes and custom classes
|
||||
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1 custom-root-class');
|
||||
expect(childNode.cssClasses).toBe('mindmap-node section-0 custom-child-class');
|
||||
});
|
||||
|
||||
it('should not create any fake root nodes', () => {
|
||||
// Create a simple mindmap
|
||||
db.addNode(0, 'A', 'A', 0);
|
||||
db.addNode(1, 'a0', 'a0', 0);
|
||||
db.addNode(1, 'a1', 'a1', 0);
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
// Check that we only have the expected nodes
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
expect(result.nodes.map((n) => n.label)).toEqual(['A', 'a0', 'a1']);
|
||||
|
||||
// Check that there's no node with label "mindmap" or any other fake root
|
||||
const mindmapNode = result.nodes.find((n) => n.label === 'mindmap');
|
||||
expect(mindmapNode).toBeUndefined();
|
||||
|
||||
// Verify the root node has the correct classes
|
||||
const rootNode = result.nodes.find((n) => n.label === 'A') as MindmapLayoutNode;
|
||||
expect(rootNode.cssClasses).toBe('mindmap-node section-root section--1');
|
||||
expect(rootNode.level).toBe(0);
|
||||
});
|
||||
|
||||
it('should assign correct section classes to edges', () => {
|
||||
// Create the example mindmap structure:
|
||||
// A
|
||||
// a0
|
||||
// aa0
|
||||
// a1
|
||||
// aaa
|
||||
// a2
|
||||
db.addNode(0, 'A', 'A', 0); // Root
|
||||
db.addNode(1, 'a0', 'a0', 0); // First child of root
|
||||
db.addNode(2, 'aa0', 'aa0', 0); // Child of a0
|
||||
db.addNode(1, 'a1', 'a1', 0); // Second child of root
|
||||
db.addNode(2, 'aaa', 'aaa', 0); // Child of a1
|
||||
db.addNode(1, 'a2', 'a2', 0); // Third child of root
|
||||
|
||||
const result = db.getData();
|
||||
|
||||
// Should have 5 edges: A->a0, a0->aa0, A->a1, a1->aaa, A->a2
|
||||
expect(result.edges).toHaveLength(5);
|
||||
|
||||
// Find edges by their start and end nodes
|
||||
const edgeA_a0 = result.edges.find(
|
||||
(e) => e.start === '0' && e.end === '1'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA0_aa0 = result.edges.find(
|
||||
(e) => e.start === '1' && e.end === '2'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA_a1 = result.edges.find(
|
||||
(e) => e.start === '0' && e.end === '3'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA1_aaa = result.edges.find(
|
||||
(e) => e.start === '3' && e.end === '4'
|
||||
) as MindmapLayoutEdge;
|
||||
const edgeA_a2 = result.edges.find(
|
||||
(e) => e.start === '0' && e.end === '5'
|
||||
) as MindmapLayoutEdge;
|
||||
|
||||
// Check edge classes
|
||||
expect(edgeA_a0.classes).toBe('edge section-edge-0 edge-depth-1'); // A->a0: section-0, depth-1
|
||||
expect(edgeA0_aa0.classes).toBe('edge section-edge-0 edge-depth-2'); // a0->aa0: section-0, depth-2
|
||||
expect(edgeA_a1.classes).toBe('edge section-edge-1 edge-depth-1'); // A->a1: section-1, depth-1
|
||||
expect(edgeA1_aaa.classes).toBe('edge section-edge-1 edge-depth-2'); // a1->aaa: section-1, depth-2
|
||||
expect(edgeA_a2.classes).toBe('edge section-edge-2 edge-depth-1'); // A->a2: section-2, depth-1
|
||||
|
||||
// Check section assignments match the child nodes
|
||||
expect(edgeA_a0.section).toBe(0);
|
||||
expect(edgeA0_aa0.section).toBe(0);
|
||||
expect(edgeA_a1.section).toBe(1);
|
||||
expect(edgeA1_aaa.section).toBe(1);
|
||||
expect(edgeA_a2.section).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,26 +1,9 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { v4 } from 'uuid';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { sanitizeText } from '../../diagrams/common/common.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { MindmapNode } from './mindmapTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
import type { LayoutData, Node, Edge } from '../../rendering-util/types.js';
|
||||
import { getUserDefinedConfig } from '../../config.js';
|
||||
|
||||
// Extend Node type for mindmap-specific properties
|
||||
export type MindmapLayoutNode = Node & {
|
||||
level: number;
|
||||
nodeId: string;
|
||||
type: number;
|
||||
section?: number;
|
||||
};
|
||||
|
||||
// Extend Edge type for mindmap-specific properties
|
||||
export type MindmapLayoutEdge = Edge & {
|
||||
depth: number;
|
||||
section?: number;
|
||||
};
|
||||
|
||||
const nodeType = {
|
||||
DEFAULT: 0,
|
||||
@@ -44,6 +27,7 @@ export class MindmapDB {
|
||||
this.nodeType = nodeType;
|
||||
this.clear();
|
||||
this.getType = this.getType.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
this.getElementById = this.getElementById.bind(this);
|
||||
this.getParent = this.getParent.bind(this);
|
||||
this.getMindmap = this.getMindmap.bind(this);
|
||||
@@ -172,223 +156,6 @@ export class MindmapDB {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign section numbers to nodes based on their position relative to root
|
||||
* @param node - The mindmap node to process
|
||||
* @param sectionNumber - The section number to assign (undefined for root)
|
||||
*/
|
||||
public assignSections(node: MindmapNode, sectionNumber?: number): void {
|
||||
// For root node, section should be undefined (not -1)
|
||||
if (node.level === 0) {
|
||||
node.section = undefined;
|
||||
} else {
|
||||
// For non-root nodes, assign the section number
|
||||
node.section = sectionNumber;
|
||||
}
|
||||
// For root node's children, assign section numbers based on their index
|
||||
// For other nodes, inherit parent's section number
|
||||
if (node.children) {
|
||||
for (const [index, child] of node.children.entries()) {
|
||||
const childSectionNumber = node.level === 0 ? index : sectionNumber;
|
||||
this.assignSections(child, childSectionNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert mindmap tree structure to flat array of nodes
|
||||
* @param node - The mindmap node to process
|
||||
* @param processedNodes - Array to collect processed nodes
|
||||
*/
|
||||
public flattenNodes(node: MindmapNode, processedNodes: MindmapLayoutNode[]): void {
|
||||
// Build CSS classes for the node
|
||||
const cssClasses = ['mindmap-node'];
|
||||
|
||||
// Add section-specific classes
|
||||
if (node.level === 0) {
|
||||
// Root node gets special classes
|
||||
cssClasses.push('section-root', 'section--1');
|
||||
} else if (node.section !== undefined) {
|
||||
// Child nodes get section class based on their section number
|
||||
cssClasses.push(`section-${node.section}`);
|
||||
}
|
||||
|
||||
// Add any custom classes from the node
|
||||
if (node.class) {
|
||||
cssClasses.push(node.class);
|
||||
}
|
||||
|
||||
const classes = cssClasses.join(' ');
|
||||
|
||||
// Map mindmap node type to valid shape name
|
||||
const getShapeFromType = (type: number) => {
|
||||
switch (type) {
|
||||
case nodeType.CIRCLE:
|
||||
return 'mindmapCircle';
|
||||
case nodeType.RECT:
|
||||
return 'rect';
|
||||
case nodeType.ROUNDED_RECT:
|
||||
return 'rounded';
|
||||
case nodeType.CLOUD:
|
||||
return 'cloud';
|
||||
case nodeType.BANG:
|
||||
return 'bang';
|
||||
case nodeType.HEXAGON:
|
||||
return 'hexagon';
|
||||
case nodeType.DEFAULT:
|
||||
return 'defaultMindmapNode';
|
||||
case nodeType.NO_BORDER:
|
||||
default:
|
||||
return 'rect';
|
||||
}
|
||||
};
|
||||
|
||||
const processedNode: MindmapLayoutNode = {
|
||||
id: node.id.toString(),
|
||||
domId: 'node_' + node.id.toString(),
|
||||
label: node.descr,
|
||||
isGroup: false,
|
||||
shape: getShapeFromType(node.type),
|
||||
width: node.width,
|
||||
height: node.height ?? 0,
|
||||
padding: node.padding,
|
||||
cssClasses: classes,
|
||||
cssStyles: [],
|
||||
look: 'default',
|
||||
icon: node.icon,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
// Mindmap-specific properties
|
||||
level: node.level,
|
||||
nodeId: node.nodeId,
|
||||
type: node.type,
|
||||
section: node.section,
|
||||
};
|
||||
|
||||
processedNodes.push(processedNode);
|
||||
|
||||
// Recursively process children
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
this.flattenNodes(child, processedNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate edges from parent-child relationships in mindmap tree
|
||||
* @param node - The mindmap node to process
|
||||
* @param edges - Array to collect edges
|
||||
*/
|
||||
public generateEdges(node: MindmapNode, edges: MindmapLayoutEdge[]): void {
|
||||
if (!node.children) {
|
||||
return;
|
||||
}
|
||||
for (const child of node.children) {
|
||||
// Build CSS classes for the edge
|
||||
let edgeClasses = 'edge';
|
||||
|
||||
// Add section-specific classes based on the child's section
|
||||
if (child.section !== undefined) {
|
||||
edgeClasses += ` section-edge-${child.section}`;
|
||||
}
|
||||
|
||||
// Add depth class based on the parent's level + 1 (depth of the edge)
|
||||
const edgeDepth = node.level + 1;
|
||||
edgeClasses += ` edge-depth-${edgeDepth}`;
|
||||
|
||||
const edge: MindmapLayoutEdge = {
|
||||
id: `edge_${node.id}_${child.id}`,
|
||||
start: node.id.toString(),
|
||||
end: child.id.toString(),
|
||||
type: 'normal',
|
||||
curve: 'basis',
|
||||
thickness: 'normal',
|
||||
look: 'default',
|
||||
classes: edgeClasses,
|
||||
// Store mindmap-specific data
|
||||
depth: node.level,
|
||||
section: child.section,
|
||||
};
|
||||
|
||||
edges.push(edge);
|
||||
|
||||
// Recursively process child edges
|
||||
this.generateEdges(child, edges);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get structured data for layout algorithms
|
||||
* Following the pattern established by ER diagrams
|
||||
* @returns Structured data containing nodes, edges, and config
|
||||
*/
|
||||
public getData(): LayoutData {
|
||||
const mindmapRoot = this.getMindmap();
|
||||
const config = getConfig();
|
||||
|
||||
const userDefinedConfig = getUserDefinedConfig();
|
||||
const hasUserDefinedLayout = userDefinedConfig.layout !== undefined;
|
||||
|
||||
const finalConfig = config;
|
||||
if (!hasUserDefinedLayout) {
|
||||
finalConfig.layout = 'cose-bilkent';
|
||||
}
|
||||
|
||||
if (!mindmapRoot) {
|
||||
return {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
config: finalConfig,
|
||||
};
|
||||
}
|
||||
log.debug('getData: mindmapRoot', mindmapRoot, config);
|
||||
|
||||
// Assign section numbers to all nodes based on their position relative to root
|
||||
this.assignSections(mindmapRoot);
|
||||
|
||||
// Convert tree structure to flat arrays
|
||||
const processedNodes: MindmapLayoutNode[] = [];
|
||||
const processedEdges: MindmapLayoutEdge[] = [];
|
||||
|
||||
this.flattenNodes(mindmapRoot, processedNodes);
|
||||
this.generateEdges(mindmapRoot, processedEdges);
|
||||
|
||||
log.debug(
|
||||
`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`
|
||||
);
|
||||
|
||||
// Create shapes map for ELK compatibility
|
||||
const shapes = new Map<string, any>();
|
||||
for (const node of processedNodes) {
|
||||
shapes.set(node.id, {
|
||||
shape: node.shape,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
padding: node.padding,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: processedNodes,
|
||||
edges: processedEdges,
|
||||
config: finalConfig,
|
||||
// Store the root node for mindmap-specific layout algorithms
|
||||
rootNode: mindmapRoot,
|
||||
// Properties required by dagre layout algorithm
|
||||
markers: ['point'], // Mindmaps don't use markers
|
||||
direction: 'TB', // Top-to-bottom direction for mindmaps
|
||||
nodeSpacing: 50, // Default spacing between nodes
|
||||
rankSpacing: 50, // Default spacing between ranks
|
||||
// Add shapes for ELK compatibility
|
||||
shapes: Object.fromEntries(shapes),
|
||||
// Additional properties that layout algorithms might expect
|
||||
type: 'mindmap',
|
||||
diagramId: 'mindmap-' + v4(),
|
||||
};
|
||||
}
|
||||
|
||||
// Expose logger to grammar
|
||||
public getLogger() {
|
||||
return log;
|
||||
}
|
||||
|
@@ -1,83 +1,200 @@
|
||||
import cytoscape from 'cytoscape';
|
||||
// @ts-expect-error No types available
|
||||
import coseBilkent from 'cytoscape-cose-bilkent';
|
||||
import { select } from 'd3';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
import type { FilledMindMapNode } from './mindmapTypes.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import type { FilledMindMapNode, MindmapNode } from './mindmapTypes.js';
|
||||
import { drawNode, positionNode } from './svgDraw.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
import type { MindmapDB } from './mindmapDb.js';
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
/**
|
||||
* Update the layout data with actual node dimensions after drawing
|
||||
*/
|
||||
function _updateNodeDimensions(data4Layout: LayoutData, mindmapRoot: FilledMindMapNode) {
|
||||
const updateNode = (node: FilledMindMapNode) => {
|
||||
// Find the corresponding node in the layout data
|
||||
const layoutNode = data4Layout.nodes.find((n) => n.id === node.id.toString());
|
||||
if (layoutNode) {
|
||||
// Update with the actual dimensions calculated by drawNode
|
||||
layoutNode.width = node.width;
|
||||
layoutNode.height = node.height;
|
||||
log.debug('Updated node dimensions:', node.id, 'width:', node.width, 'height:', node.height);
|
||||
async function drawNodes(
|
||||
db: MindmapDB,
|
||||
svg: D3Element,
|
||||
mindmap: FilledMindMapNode,
|
||||
section: number,
|
||||
conf: MermaidConfig
|
||||
) {
|
||||
await drawNode(db, svg, mindmap, section, conf);
|
||||
if (mindmap.children) {
|
||||
await Promise.all(
|
||||
mindmap.children.map((child, index) =>
|
||||
drawNodes(db, svg, child, section < 0 ? index : section, conf)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'cytoscape' {
|
||||
interface EdgeSingular {
|
||||
_private: {
|
||||
bodyBounds: unknown;
|
||||
rscratch: {
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
cy.edges().map((edge, id) => {
|
||||
const data = edge.data();
|
||||
if (edge[0]._private.bodyBounds) {
|
||||
const bounds = edge[0]._private.rscratch;
|
||||
log.trace('Edge: ', id, data);
|
||||
edgesEl
|
||||
.insert('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
|
||||
)
|
||||
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Recursively update children
|
||||
node.children?.forEach(updateNode);
|
||||
};
|
||||
function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: mindmap.id.toString(),
|
||||
labelText: mindmap.descr,
|
||||
height: mindmap.height,
|
||||
width: mindmap.width,
|
||||
level: level,
|
||||
nodeId: mindmap.id,
|
||||
padding: mindmap.padding,
|
||||
type: mindmap.type,
|
||||
},
|
||||
position: {
|
||||
x: mindmap.x!,
|
||||
y: mindmap.y!,
|
||||
},
|
||||
});
|
||||
if (mindmap.children) {
|
||||
mindmap.children.forEach((child) => {
|
||||
addNodes(child, cy, conf, level + 1);
|
||||
cy.add({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${mindmap.id}_${child.id}`,
|
||||
source: mindmap.id,
|
||||
target: child.id,
|
||||
depth: level,
|
||||
section: child.section,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateNode(mindmapRoot);
|
||||
function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise<cytoscape.Core> {
|
||||
return new Promise((resolve) => {
|
||||
// Add temporary render element
|
||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||
const cy = cytoscape({
|
||||
container: document.getElementById('cy'), // container to render in
|
||||
style: [
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
addNodes(node, cy, conf, 0);
|
||||
|
||||
// Make cytoscape care about the dimensions of the nodes
|
||||
cy.nodes().forEach(function (n) {
|
||||
n.layoutDimensions = () => {
|
||||
const data = n.data();
|
||||
return { w: data.width, h: data.height };
|
||||
};
|
||||
});
|
||||
|
||||
cy.layout({
|
||||
name: 'cose-bilkent',
|
||||
// @ts-ignore Types for cose-bilkent are not correct?
|
||||
quality: 'proof',
|
||||
styleEnabled: false,
|
||||
animate: false,
|
||||
}).run();
|
||||
cy.ready((e) => {
|
||||
log.info('Ready', e);
|
||||
resolve(cy);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
|
||||
cy.nodes().map((node, id) => {
|
||||
const data = node.data();
|
||||
data.x = node.position().x;
|
||||
data.y = node.position().y;
|
||||
positionNode(db, data);
|
||||
const el = db.getElementById(data.nodeId);
|
||||
log.info('id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
|
||||
el.attr(
|
||||
'transform',
|
||||
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
|
||||
);
|
||||
el.attr('attr', `apa-${id})`);
|
||||
});
|
||||
}
|
||||
|
||||
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
||||
log.debug('Rendering mindmap diagram\n' + text);
|
||||
|
||||
// Draw the nodes first to get their dimensions, then update the layout data
|
||||
const db = diagObj.db as MindmapDB;
|
||||
|
||||
// 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 = db.getData();
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, data4Layout.config.securityLevel);
|
||||
|
||||
data4Layout.type = diagObj.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(data4Layout.config.layout, {
|
||||
fallback: 'cose-bilkent',
|
||||
});
|
||||
|
||||
data4Layout.diagramId = id;
|
||||
|
||||
const mm = db.getMindmap();
|
||||
if (!mm) {
|
||||
return;
|
||||
}
|
||||
|
||||
data4Layout.nodes.forEach((node) => {
|
||||
if (node.shape === 'rounded') {
|
||||
node.radius = 15;
|
||||
node.taper = 15;
|
||||
node.stroke = 'none';
|
||||
node.width = 0;
|
||||
node.padding = 15;
|
||||
} else if (node.shape === 'circle') {
|
||||
node.padding = 10;
|
||||
} else if (node.shape === 'rect') {
|
||||
node.width = 0;
|
||||
node.padding = 10;
|
||||
}
|
||||
});
|
||||
const conf = getConfig();
|
||||
conf.htmlLabels = false;
|
||||
|
||||
// Use the unified rendering system
|
||||
await render(data4Layout, svg);
|
||||
const svg = selectSvgElement(id);
|
||||
|
||||
// Setup the view box and size of the svg element using config from data4Layout
|
||||
setupViewPortForSVG(
|
||||
// Draw the graph and start with drawing the nodes without proper position
|
||||
// this gives us the size of the nodes and we can set the positions later
|
||||
|
||||
const edgesElem = svg.append('g');
|
||||
edgesElem.attr('class', 'mindmap-edges');
|
||||
const nodesElem = svg.append('g');
|
||||
nodesElem.attr('class', 'mindmap-nodes');
|
||||
await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
||||
|
||||
// Next step is to layout the mindmap, giving each node a position
|
||||
|
||||
const cy = await layoutMindmap(mm, conf);
|
||||
|
||||
// After this we can draw, first the edges and the then nodes with the correct position
|
||||
drawEdges(edgesElem, cy);
|
||||
positionNodes(db, cy);
|
||||
|
||||
// Setup the view box and size of the svg element
|
||||
setupGraphViewbox(
|
||||
undefined,
|
||||
svg,
|
||||
data4Layout.config.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||
'mindmapDiagram',
|
||||
data4Layout.config.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||
conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||
conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -64,12 +64,6 @@ const getStyles: DiagramStylesProvider = (options) =>
|
||||
.section-root text {
|
||||
fill: ${options.gitBranchLabel0};
|
||||
}
|
||||
.section-root span {
|
||||
color: ${options.gitBranchLabel0};
|
||||
}
|
||||
.section-2 span {
|
||||
color: ${options.gitBranchLabel0};
|
||||
}
|
||||
.icon-container {
|
||||
height:100%;
|
||||
display: flex;
|
||||
|
@@ -1,200 +0,0 @@
|
||||
lexer grammar SequenceLexer;
|
||||
tokens { AS }
|
||||
|
||||
|
||||
// Comments (skip)
|
||||
HASH_COMMENT: '#' ~[\r\n]* -> skip;
|
||||
PERCENT_COMMENT1: '%%' ~[\r\n]* -> skip;
|
||||
PERCENT_COMMENT2: ~[}] '%%' ~[\r\n]* -> skip;
|
||||
|
||||
// Whitespace and newline
|
||||
NEWLINE: ('\r'? '\n')+;
|
||||
WS: [ \t]+ -> skip;
|
||||
|
||||
// Punctuation and simple symbols
|
||||
COMMA: ',';
|
||||
SEMI: ';' -> type(NEWLINE);
|
||||
PLUS: '+';
|
||||
MINUS: '-';
|
||||
|
||||
// Core keywords
|
||||
SD: 'sequenceDiagram';
|
||||
PARTICIPANT: 'participant' -> pushMode(ID);
|
||||
PARTICIPANT_ACTOR: 'actor' -> pushMode(ID);
|
||||
CREATE: 'create';
|
||||
DESTROY: 'destroy';
|
||||
BOX: 'box' -> pushMode(LINE);
|
||||
|
||||
// Blocks and control flow
|
||||
LOOP: 'loop' -> pushMode(LINE);
|
||||
RECT: 'rect' -> pushMode(LINE);
|
||||
OPT: 'opt' -> pushMode(LINE);
|
||||
ALT: 'alt' -> pushMode(LINE);
|
||||
ELSE: 'else' -> pushMode(LINE);
|
||||
PAR: 'par' -> pushMode(LINE);
|
||||
PAR_OVER: 'par_over' -> pushMode(LINE);
|
||||
AND: 'and' -> pushMode(LINE);
|
||||
CRITICAL: 'critical' -> pushMode(LINE);
|
||||
OPTION: 'option' -> pushMode(LINE);
|
||||
BREAK: 'break' -> pushMode(LINE);
|
||||
END: 'end';
|
||||
|
||||
// Note and placement
|
||||
LEFT_OF: 'left' WS+ 'of';
|
||||
RIGHT_OF: 'right' WS+ 'of';
|
||||
LINKS: 'links';
|
||||
LINK: 'link';
|
||||
PROPERTIES: 'properties';
|
||||
DETAILS: 'details';
|
||||
OVER: 'over';
|
||||
// Accept both Note and note
|
||||
NOTE: [Nn][Oo][Tt][Ee];
|
||||
|
||||
// Lifecycle
|
||||
ACTIVATE: 'activate';
|
||||
DEACTIVATE: 'deactivate';
|
||||
|
||||
// Titles and accessibility
|
||||
LEGACY_TITLE: 'title' WS* ':' WS* (~[\r\n;#])*;
|
||||
TITLE: 'title' -> pushMode(LINE);
|
||||
ACC_TITLE: 'accTitle' WS* ':' WS* -> pushMode(ACC_TITLE_MODE);
|
||||
ACC_DESCR: 'accDescr' WS* ':' WS* -> pushMode(ACC_DESCR_MODE);
|
||||
ACC_DESCR_MULTI: 'accDescr' WS* '{' WS* -> pushMode(ACC_DESCR_MULTILINE_MODE);
|
||||
|
||||
// Directives
|
||||
AUTONUMBER: 'autonumber';
|
||||
OFF: 'off';
|
||||
|
||||
// Config block @{ ... }
|
||||
CONFIG_START: '@{' -> pushMode(CONFIG_MODE);
|
||||
|
||||
// Arrows (must come before ACTOR)
|
||||
SOLID_ARROW: '->>';
|
||||
BIDIRECTIONAL_SOLID_ARROW: '<<->>';
|
||||
DOTTED_ARROW: '-->>';
|
||||
BIDIRECTIONAL_DOTTED_ARROW: '<<-->>';
|
||||
SOLID_OPEN_ARROW: '->';
|
||||
DOTTED_OPEN_ARROW: '-->';
|
||||
SOLID_CROSS: '-x';
|
||||
DOTTED_CROSS: '--x';
|
||||
SOLID_POINT: '-)';
|
||||
DOTTED_POINT: '--)';
|
||||
|
||||
// Text after colon up to newline or comment delimiter ; or #
|
||||
TXT: ':' (~[\r\n;#])*;
|
||||
|
||||
// Actor identifiers: allow hyphen runs, but forbid -x, --x, -), --)
|
||||
fragment IDCHAR_NO_HYPHEN: ~[+<>:\n,;@# \t-];
|
||||
fragment ALNUM: [A-Za-z0-9_];
|
||||
fragment ALNUM_NOT_X_RPAREN: [A-WYZa-wyz0-9_];
|
||||
fragment H3: '-' '-' '-' ('-')*; // three or more hyphens
|
||||
ACTOR: IDCHAR_NO_HYPHEN+
|
||||
(
|
||||
'-' ALNUM_NOT_X_RPAREN+
|
||||
| '-' '-' ALNUM_NOT_X_RPAREN+
|
||||
| H3 ALNUM+
|
||||
)*;
|
||||
|
||||
|
||||
// Modes to mirror Jison stateful lexing
|
||||
mode ACC_TITLE_MODE;
|
||||
ACC_TITLE_VALUE: (~[\r\n;#])* -> popMode;
|
||||
|
||||
mode ACC_DESCR_MODE;
|
||||
ACC_DESCR_VALUE: (~[\r\n;#])* -> popMode;
|
||||
|
||||
mode ACC_DESCR_MULTILINE_MODE;
|
||||
ACC_DESCR_MULTILINE_END: '}' -> popMode;
|
||||
ACC_DESCR_MULTILINE_VALUE: (~['}'])*;
|
||||
|
||||
mode CONFIG_MODE;
|
||||
CONFIG_CONTENT: (~[}])+;
|
||||
CONFIG_END: '}' -> popMode;
|
||||
|
||||
|
||||
// ID mode: after participant/actor, allow same-line WS/comments; pop on newline
|
||||
mode ID;
|
||||
ID_NEWLINE: ('\r'? '\n')+ -> popMode, type(NEWLINE);
|
||||
ID_SEMI: ';' -> popMode, type(NEWLINE);
|
||||
ID_WS: [ \t]+ -> skip;
|
||||
ID_HASH_COMMENT: '#' ~[\r\n]* -> skip;
|
||||
ID_PERCENT_COMMENT: '%%' ~[\r\n]* -> skip;
|
||||
// recognize 'as' in ID mode and switch to ALIAS
|
||||
ID_AS: 'as' -> type(AS), pushMode(ALIAS);
|
||||
// inline config in ID mode
|
||||
ID_CONFIG_START: '@{' -> type(CONFIG_START), pushMode(CONFIG_MODE);
|
||||
// arrows first to ensure proper splitting before actor
|
||||
ID_BIDIR_SOLID_ARROW: '<<->>' -> type(BIDIRECTIONAL_SOLID_ARROW);
|
||||
ID_BIDIR_DOTTED_ARROW: '<<-->>' -> type(BIDIRECTIONAL_DOTTED_ARROW);
|
||||
ID_SOLID_ARROW: '->>' -> type(SOLID_ARROW);
|
||||
ID_DOTTED_ARROW: '-->>' -> type(DOTTED_ARROW);
|
||||
ID_SOLID_OPEN_ARROW: '->' -> type(SOLID_OPEN_ARROW);
|
||||
ID_DOTTED_OPEN_ARROW: '-->' -> type(DOTTED_OPEN_ARROW);
|
||||
ID_SOLID_CROSS: '-x' -> type(SOLID_CROSS);
|
||||
ID_DOTTED_CROSS: '--x' -> type(DOTTED_CROSS);
|
||||
ID_SOLID_POINT: '-)' -> type(SOLID_POINT);
|
||||
ID_DOTTED_POINT: '--)' -> type(DOTTED_POINT);
|
||||
ID_ACTOR: IDCHAR_NO_HYPHEN+
|
||||
(
|
||||
'-' ALNUM_NOT_X_RPAREN+
|
||||
| '--' ALNUM_NOT_X_RPAREN+
|
||||
| '-' '-' '-' '-'* ALNUM+
|
||||
)* -> type(ACTOR);
|
||||
|
||||
// ALIAS mode: after 'as', capture rest-of-line as TXT (alias display)
|
||||
mode ALIAS;
|
||||
ALIAS_NEWLINE: ('\r'? '\n')+ -> popMode, popMode, type(NEWLINE);
|
||||
ALIAS_SEMI: ';' -> popMode, popMode, type(NEWLINE);
|
||||
ALIAS_WS: [ \t]+ -> skip;
|
||||
ALIAS_HASH_COMMENT: '#' ~[\r\n]* -> skip;
|
||||
ALIAS_PERCENT_COMMENT: '%%' ~[\r\n]* -> skip;
|
||||
// inline config allowed after alias as well
|
||||
ALIAS_CONFIG_START: '@{' -> type(CONFIG_START), pushMode(CONFIG_MODE);
|
||||
// Prefer capturing the remainder of the line as TXT for alias/description
|
||||
ALIAS_TXT: (~[\r\n;#])+ -> type(TXT);
|
||||
// arrows before actor pattern to split properly (kept for parity, though not used after AS)
|
||||
ALIAS_BIDIR_SOLID_ARROW: '<<->>' -> type(BIDIRECTIONAL_SOLID_ARROW);
|
||||
ALIAS_BIDIR_DOTTED_ARROW: '<<-->>' -> type(BIDIRECTIONAL_DOTTED_ARROW);
|
||||
ALIAS_SOLID_ARROW: '->>' -> type(SOLID_ARROW);
|
||||
ALIAS_DOTTED_ARROW: '-->>' -> type(DOTTED_ARROW);
|
||||
ALIAS_SOLID_OPEN_ARROW: '->' -> type(SOLID_OPEN_ARROW);
|
||||
ALIAS_DOTTED_OPEN_ARROW: '-->' -> type(DOTTED_OPEN_ARROW);
|
||||
ALIAS_SOLID_CROSS: '-x' -> type(SOLID_CROSS);
|
||||
ALIAS_DOTTED_CROSS: '--x' -> type(DOTTED_CROSS);
|
||||
ALIAS_SOLID_POINT: '-)' -> type(SOLID_POINT);
|
||||
ALIAS_DOTTED_POINT: '--)' -> type(DOTTED_POINT);
|
||||
ALIAS_ACTOR: IDCHAR_NO_HYPHEN+
|
||||
(
|
||||
'-' ALNUM_NOT_X_RPAREN+
|
||||
| '--' ALNUM_NOT_X_RPAREN+
|
||||
| '-' '-' '-' '-'* ALNUM+
|
||||
)* -> type(ACTOR);
|
||||
|
||||
// LINE mode: after 'title' (no colon), pop at newline
|
||||
mode LINE;
|
||||
LINE_NEWLINE: ('\r'? '\n')+ -> popMode, type(NEWLINE);
|
||||
LINE_SEMI: ';' -> popMode, type(NEWLINE);
|
||||
LINE_WS: [ \t]+ -> skip;
|
||||
LINE_HASH_COMMENT: '#' ~[\r\n]* -> skip;
|
||||
LINE_PERCENT_COMMENT: '%%' ~[\r\n]* -> skip;
|
||||
// Prefer capturing the remainder of the line as a single TXT token
|
||||
LINE_TXT: (~[\r\n;#])+ -> type(TXT);
|
||||
// allow arrows; placed after TXT so it won't split titles
|
||||
LINE_BIDIR_SOLID_ARROW: '<<->>' -> type(BIDIRECTIONAL_SOLID_ARROW);
|
||||
LINE_BIDIR_DOTTED_ARROW: '<<-->>' -> type(BIDIRECTIONAL_DOTTED_ARROW);
|
||||
LINE_SOLID_ARROW: '->>' -> type(SOLID_ARROW);
|
||||
LINE_DOTTED_ARROW: '-->>' -> type(DOTTED_ARROW);
|
||||
LINE_SOLID_OPEN_ARROW: '->' -> type(SOLID_OPEN_ARROW);
|
||||
LINE_DOTTED_OPEN_ARROW: '-->' -> type(DOTTED_OPEN_ARROW);
|
||||
LINE_SOLID_CROSS: '-x' -> type(SOLID_CROSS);
|
||||
LINE_DOTTED_CROSS: '--x' -> type(DOTTED_CROSS);
|
||||
LINE_SOLID_POINT: '-)' -> type(SOLID_POINT);
|
||||
LINE_DOTTED_POINT: '--)' -> type(DOTTED_POINT);
|
||||
// Keep ACTOR for parity if TXT is not applicable
|
||||
LINE_ACTOR: IDCHAR_NO_HYPHEN+
|
||||
(
|
||||
'-' ALNUM_NOT_X_RPAREN+
|
||||
| '--' ALNUM_NOT_X_RPAREN+
|
||||
| '-' '-' '-' '-'* ALNUM+
|
||||
)* -> type(ACTOR);
|
||||
|
@@ -1,150 +0,0 @@
|
||||
parser grammar SequenceParser;
|
||||
|
||||
options {
|
||||
tokenVocab = SequenceLexer;
|
||||
}
|
||||
|
||||
start: (NEWLINE)* SD document EOF;
|
||||
|
||||
document: (line | loopBlock | rectBlock | boxBlock | optBlock | altBlock | parBlock | parOverBlock | breakBlock | criticalBlock)* statement?;
|
||||
|
||||
line: statement? NEWLINE;
|
||||
|
||||
statement
|
||||
: participantStatement
|
||||
| createStatement
|
||||
| destroyStatement
|
||||
| signalStatement
|
||||
| noteStatement
|
||||
| linksStatement
|
||||
| linkStatement
|
||||
| propertiesStatement
|
||||
| detailsStatement
|
||||
| activationStatement
|
||||
| autonumberStatement
|
||||
| titleStatement
|
||||
| legacyTitleStatement
|
||||
| accTitleStatement
|
||||
| accDescrStatement
|
||||
| accDescrMultilineStatement
|
||||
;
|
||||
|
||||
createStatement
|
||||
: CREATE (PARTICIPANT | PARTICIPANT_ACTOR) actor (AS restOfLine)?
|
||||
;
|
||||
|
||||
destroyStatement
|
||||
: DESTROY actor
|
||||
;
|
||||
|
||||
participantStatement
|
||||
: PARTICIPANT actorWithConfig
|
||||
| (PARTICIPANT | PARTICIPANT_ACTOR) actor (AS restOfLine)?
|
||||
;
|
||||
|
||||
actorWithConfig
|
||||
: ACTOR configObject
|
||||
;
|
||||
|
||||
configObject
|
||||
: CONFIG_START CONFIG_CONTENT CONFIG_END
|
||||
;
|
||||
|
||||
signalStatement
|
||||
: actor signaltype (PLUS actor | MINUS actor | actor) text2
|
||||
;
|
||||
noteStatement
|
||||
: NOTE RIGHT_OF actor text2
|
||||
| NOTE LEFT_OF actor text2
|
||||
| NOTE OVER actor (COMMA actor)? text2
|
||||
;
|
||||
|
||||
linksStatement
|
||||
: LINKS actor text2
|
||||
;
|
||||
|
||||
linkStatement
|
||||
: LINK actor text2
|
||||
;
|
||||
|
||||
propertiesStatement
|
||||
: PROPERTIES actor text2
|
||||
;
|
||||
|
||||
detailsStatement
|
||||
: DETAILS actor text2
|
||||
;
|
||||
|
||||
autonumberStatement
|
||||
: AUTONUMBER // enable default numbering
|
||||
| AUTONUMBER OFF // disable numbering
|
||||
| AUTONUMBER ACTOR // start value
|
||||
| AUTONUMBER ACTOR ACTOR // start and step
|
||||
;
|
||||
|
||||
activationStatement
|
||||
: ACTIVATE actor
|
||||
| DEACTIVATE actor
|
||||
;
|
||||
titleStatement
|
||||
: TITLE
|
||||
| TITLE restOfLine
|
||||
| TITLE ACTOR+ // title without colon
|
||||
;
|
||||
accTitleStatement
|
||||
: ACC_TITLE ACC_TITLE_VALUE
|
||||
;
|
||||
accDescrStatement
|
||||
: ACC_DESCR ACC_DESCR_VALUE
|
||||
;
|
||||
accDescrMultilineStatement
|
||||
: ACC_DESCR_MULTI ACC_DESCR_MULTILINE_VALUE ACC_DESCR_MULTILINE_END
|
||||
;
|
||||
legacyTitleStatement
|
||||
: LEGACY_TITLE
|
||||
;
|
||||
|
||||
// Blocks
|
||||
loopBlock: LOOP restOfLine? document END;
|
||||
rectBlock: RECT restOfLine? document END;
|
||||
boxBlock: BOX restOfLine? document END;
|
||||
optBlock: OPT restOfLine? document END;
|
||||
altBlock: ALT restOfLine? altSections END;
|
||||
parBlock: PAR restOfLine? parSections END;
|
||||
parOverBlock: PAR_OVER restOfLine? parSections END;
|
||||
breakBlock: BREAK restOfLine? document END;
|
||||
criticalBlock: CRITICAL restOfLine? optionSections END;
|
||||
|
||||
altSections: document (elseSection)*;
|
||||
elseSection: ELSE restOfLine? document;
|
||||
|
||||
parSections: document (andSection)*;
|
||||
andSection: AND restOfLine? document;
|
||||
|
||||
optionSections: document (optionSection)*;
|
||||
optionSection: OPTION restOfLine? document;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
actor: ACTOR;
|
||||
|
||||
signaltype
|
||||
: SOLID_ARROW
|
||||
| DOTTED_ARROW
|
||||
| SOLID_OPEN_ARROW
|
||||
| DOTTED_OPEN_ARROW
|
||||
| SOLID_CROSS
|
||||
| DOTTED_CROSS
|
||||
| SOLID_POINT
|
||||
| DOTTED_POINT
|
||||
| BIDIRECTIONAL_SOLID_ARROW
|
||||
| BIDIRECTIONAL_DOTTED_ARROW
|
||||
;
|
||||
|
||||
restOfLine: TXT;
|
||||
|
||||
text2: TXT;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user