diff --git a/README.md b/README.md
index 98741a689..f844e2cb9 100644
--- a/README.md
+++ b/README.md
@@ -165,13 +165,7 @@ class Class10 {
int id
size()
}
-namespace Namespace01 {
- class Class11
- class Class12 {
- int id
- size()
- }
-}
+
```
```mermaid
@@ -191,13 +185,7 @@ class Class10 {
int id
size()
}
-namespace Namespace01 {
- class Class11
- class Class12 {
- int id
- size()
- }
-}
+
```
### State diagram [docs - live editor]
diff --git a/cSpell.json b/cSpell.json
index 40fe8965d..76f395915 100644
--- a/cSpell.json
+++ b/cSpell.json
@@ -22,6 +22,7 @@
"brkt",
"brolin",
"brotli",
+ "catmull",
"città",
"classdef",
"codedoc",
diff --git a/cypress/integration/rendering/marker_unique_id.spec.js b/cypress/integration/rendering/marker_unique_id.spec.js
new file mode 100644
index 000000000..617189db0
--- /dev/null
+++ b/cypress/integration/rendering/marker_unique_id.spec.js
@@ -0,0 +1,10 @@
+import { urlSnapshotTest } from '../../helpers/util.ts';
+
+describe('Marker Unique IDs Per Diagram', () => {
+ it('should render a blue arrow tip in second digram', () => {
+ urlSnapshotTest('http://localhost:9000/marker_unique_id.html', {
+ logLevel: 1,
+ flowchart: { htmlLabels: false },
+ });
+ });
+});
diff --git a/cypress/platform/marker_unique_id.html b/cypress/platform/marker_unique_id.html
new file mode 100644
index 000000000..e49169c55
--- /dev/null
+++ b/cypress/platform/marker_unique_id.html
@@ -0,0 +1,52 @@
+
+
+
+ Example
+
+ %%{init:{"theme":"base", "themeVariables": {"lineColor":"red"}}}%%
+ flowchart LR
+ subgraph red
+ A --> B
+ end
+
+
+ %%{init:{"theme":"base", "themeVariables": {"lineColor":"blue"}}}%%
+ flowchart LR
+ subgraph black
+ A --> B
+ end
+
+
+ ---
+ config:
+ theme: base
+ themeVariables:
+ lineColor: yellow
+ ---
+ flowchart LR
+ subgraph red
+ A --> B
+ end
+
+
+ ---
+ config:
+ theme: base
+ themeVariables:
+ lineColor: green
+ ---
+ flowchart LR
+ subgraph black
+ A --> B
+ end
+
+
+
+
diff --git a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md
index 56f914641..ea390899e 100644
--- a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md
+++ b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md
@@ -16,4 +16,4 @@
#### Defined in
-[mermaidAPI.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L76)
+[mermaidAPI.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L59)
diff --git a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md
index 2c1504285..18ee5e431 100644
--- a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md
+++ b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md
@@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
#### Defined in
-[mermaidAPI.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L96)
+[mermaidAPI.ts:79](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L79)
---
@@ -51,4 +51,4 @@ The svg code for the rendered graph.
#### Defined in
-[mermaidAPI.ts:86](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L86)
+[mermaidAPI.ts:69](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L69)
diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md
index 1ea19fac4..0a948b6f3 100644
--- a/docs/config/setup/modules/mermaidAPI.md
+++ b/docs/config/setup/modules/mermaidAPI.md
@@ -25,7 +25,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi)
#### Defined in
-[mermaidAPI.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L80)
+[mermaidAPI.ts:63](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L63)
## Variables
@@ -96,7 +96,7 @@ mermaid.initialize(config);
#### Defined in
-[mermaidAPI.ts:662](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L662)
+[mermaidAPI.ts:641](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L641)
## Functions
@@ -127,7 +127,7 @@ Return the last node appended
#### Defined in
-[mermaidAPI.ts:318](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L318)
+[mermaidAPI.ts:299](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L299)
---
@@ -153,13 +153,13 @@ the cleaned up svgCode
#### Defined in
-[mermaidAPI.ts:264](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L264)
+[mermaidAPI.ts:245](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L245)
---
### createCssStyles
-▸ **createCssStyles**(`config`, `graphType`, `classDefs?`): `string`
+▸ **createCssStyles**(`config`, `classDefs?`): `string`
Create the user styles
@@ -168,7 +168,6 @@ Create the user styles
| Name | Type | Description |
| :---------- | :------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------ |
| `config` | `MermaidConfig` | configuration that has style and theme settings to use |
-| `graphType` | `string` | used for checking if classDefs should be applied |
| `classDefs` | `undefined` \| `null` \| `Record`<`string`, `DiagramStyleClassDef`> | the classDefs in the diagram text. Might be null if none were defined. Usually is the result of a call to getClasses(...) |
#### Returns
@@ -179,7 +178,7 @@ the string with all the user styles
#### Defined in
-[mermaidAPI.ts:193](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L193)
+[mermaidAPI.ts:175](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L175)
---
@@ -189,12 +188,12 @@ the string with all the user styles
#### Parameters
-| Name | Type |
-| :---------- | :----------------------------------------- |
-| `config` | `MermaidConfig` |
-| `graphType` | `string` |
-| `classDefs` | `Record`<`string`, `DiagramStyleClassDef`> |
-| `svgId` | `string` |
+| Name | Type |
+| :---------- | :-------------------------------------------------------- |
+| `config` | `MermaidConfig` |
+| `graphType` | `string` |
+| `classDefs` | `undefined` \| `Record`<`string`, `DiagramStyleClassDef`> |
+| `svgId` | `string` |
#### Returns
@@ -202,7 +201,7 @@ the string with all the user styles
#### Defined in
-[mermaidAPI.ts:241](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L241)
+[mermaidAPI.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L222)
---
@@ -229,7 +228,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in
-[mermaidAPI.ts:177](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L177)
+[mermaidAPI.ts:160](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L160)
---
@@ -249,7 +248,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in
-[mermaidAPI.ts:163](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L163)
+[mermaidAPI.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L146)
---
@@ -269,7 +268,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
#### Defined in
-[mermaidAPI.ts:134](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L134)
+[mermaidAPI.ts:117](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L117)
---
@@ -295,7 +294,7 @@ Put the svgCode into an iFrame. Return the iFrame code
#### Defined in
-[mermaidAPI.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L295)
+[mermaidAPI.ts:276](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L276)
---
@@ -320,4 +319,4 @@ Remove any existing elements from the given document
#### Defined in
-[mermaidAPI.ts:368](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L368)
+[mermaidAPI.ts:349](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L349)
diff --git a/package.json b/package.json
index 232f23be1..7a6a032d7 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"version": "10.2.4",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
- "packageManager": "pnpm@8.7.1",
+ "packageManager": "pnpm@8.7.5",
"keywords": [
"diagram",
"markdown",
@@ -19,6 +19,7 @@
"build:mermaid": "pnpm build:vite --mermaid",
"build:viz": "pnpm build:mermaid --visualize",
"build:types": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-zenuml/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly",
+ "build:types:watch": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly --watch",
"build:watch": "pnpm build:vite --watch",
"build": "pnpm run -r clean && pnpm build:types && pnpm build:vite",
"dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"",
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index 7b4de70a9..23138003f 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -1,6 +1,6 @@
{
"name": "mermaid",
- "version": "10.4.0",
+ "version": "10.5.0-rc.1",
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js
index f89b4422b..1b3e172c0 100644
--- a/packages/mermaid/src/dagre-wrapper/edges.js
+++ b/packages/mermaid/src/dagre-wrapper/edges.js
@@ -5,6 +5,7 @@ import { line, curveBasis, select } from 'd3';
import { getConfig } from '../config.js';
import utils from '../utils.js';
import { evaluate } from '../diagrams/common/common.js';
+import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js';
let edgeLabels = {};
let terminalLabels = {};
@@ -368,21 +369,7 @@ const cutPathAtIntersect = (_points, boundryNode) => {
return points;
};
-/**
- * Calculate the deltas and angle between two points
- * @param {{x: number, y:number}} point1
- * @param {{x: number, y:number}} point2
- * @returns {{angle: number, deltaX: number, deltaY: number}}
- */
-function calculateDeltaAndAngle(point1, point2) {
- const [x1, y1] = [point1.x, point1.y];
- const [x2, y2] = [point2.x, point2.y];
- const deltaX = x2 - x1;
- const deltaY = y2 - y1;
- return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY };
-}
-
-export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph) {
+export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) {
let points = edge.points;
let pointsHasChanged = false;
const tail = graph.node(e.v);
@@ -456,56 +443,8 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
curve = edge.curve;
}
- // We need to draw the lines a bit shorter to avoid drawing
- // under any transparent markers.
- // The offsets are calculated from the markers' dimensions.
- const markerOffsets = {
- aggregation: 18,
- extension: 18,
- composition: 18,
- dependency: 6,
- lollipop: 13.5,
- arrow_point: 5.3,
- };
-
- const lineFunction = line()
- .x(function (d, i, data) {
- let offset = 0;
- if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
- // Handle first point
- // Calculate the angle and delta between the first two points
- const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]);
- // Calculate the offset based on the angle and the marker's dimensions
- offset = markerOffsets[edge.arrowTypeStart] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0;
- } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
- // Handle last point
- // Calculate the angle and delta between the last two points
- const { angle, deltaX } = calculateDeltaAndAngle(
- data[data.length - 1],
- data[data.length - 2]
- );
- offset = markerOffsets[edge.arrowTypeEnd] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0;
- }
- return d.x + offset;
- })
- .y(function (d, i, data) {
- // Same handling as X above
- let offset = 0;
- if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
- const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]);
- offset =
- markerOffsets[edge.arrowTypeStart] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1);
- } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
- const { angle, deltaY } = calculateDeltaAndAngle(
- data[data.length - 1],
- data[data.length - 2]
- );
- offset =
- markerOffsets[edge.arrowTypeEnd] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1);
- }
- return d.y + offset;
- })
- .curve(curve);
+ const { x, y } = getLineFunctionsWithOffset(edge);
+ const lineFunction = line().x(x).y(y).curve(curve);
// Construct stroke classes based on properties
let strokeClasses;
@@ -569,61 +508,103 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
switch (edge.arrowTypeStart) {
case 'arrow_cross':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-crossStart' + ')'
+ );
break;
case 'arrow_point':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-pointStart' + ')'
+ );
break;
case 'arrow_barb':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-barbStart' + ')'
+ );
break;
case 'arrow_circle':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-circleStart' + ')'
+ );
break;
case 'aggregation':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-aggregationStart' + ')'
+ );
break;
case 'extension':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-extensionStart' + ')'
+ );
break;
case 'composition':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-compositionStart' + ')'
+ );
break;
case 'dependency':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-dependencyStart' + ')'
+ );
break;
case 'lollipop':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-lollipopStart' + ')'
+ );
break;
default:
}
switch (edge.arrowTypeEnd) {
case 'arrow_cross':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-crossEnd' + ')');
break;
case 'arrow_point':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-pointEnd' + ')');
break;
case 'arrow_barb':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-barbEnd' + ')');
break;
case 'arrow_circle':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-circleEnd' + ')');
break;
case 'aggregation':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-aggregationEnd' + ')'
+ );
break;
case 'extension':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-extensionEnd' + ')'
+ );
break;
case 'composition':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-compositionEnd' + ')'
+ );
break;
case 'dependency':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-dependencyEnd' + ')'
+ );
break;
case 'lollipop':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-lollipopEnd' + ')'
+ );
break;
default:
}
diff --git a/packages/mermaid/src/dagre-wrapper/index.js b/packages/mermaid/src/dagre-wrapper/index.js
index 279c5d9dd..9843adb8b 100644
--- a/packages/mermaid/src/dagre-wrapper/index.js
+++ b/packages/mermaid/src/dagre-wrapper/index.js
@@ -14,7 +14,7 @@ import { insertCluster, clear as clearClusters } from './clusters.js';
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js';
import { log } from '../logger.js';
-const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
+const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => {
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
const dir = graph.graph().rankdir;
log.trace('Dir in recursive render - dir:', dir);
@@ -52,7 +52,7 @@ const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
if (node && node.clusterNode) {
// const children = graph.children(v);
log.info('Cluster identified', v, node.width, graph.node(v));
- const o = await recursiveRender(nodes, node.graph, diagramtype, graph.node(v));
+ const o = await recursiveRender(nodes, node.graph, diagramtype, id, graph.node(v));
const newEl = o.elem;
updateNodeBounds(node, newEl);
node.diff = o.diff || 0;
@@ -134,7 +134,7 @@ const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
const edge = graph.edge(e);
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
- const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph);
+ const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id);
positionEdgeLabel(edge, paths);
});
@@ -159,7 +159,7 @@ export const render = async (elem, graph, markers, diagramtype, id) => {
adjustClustersAndEdges(graph);
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
- await recursiveRender(elem, graph, diagramtype);
+ await recursiveRender(elem, graph, diagramtype, id);
};
// const shapeDefinitions = {};
diff --git a/packages/mermaid/src/dagre-wrapper/markers.js b/packages/mermaid/src/dagre-wrapper/markers.js
index 051c987f6..91f7ecc80 100644
--- a/packages/mermaid/src/dagre-wrapper/markers.js
+++ b/packages/mermaid/src/dagre-wrapper/markers.js
@@ -14,7 +14,7 @@ const extension = (elem, type, id) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-extensionStart')
+ .attr('id', id + '_' + type + '-extensionStart')
.attr('class', 'marker extension ' + type)
.attr('refX', 18)
.attr('refY', 7)
@@ -27,7 +27,7 @@ const extension = (elem, type, id) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-extensionEnd')
+ .attr('id', id + '_' + type + '-extensionEnd')
.attr('class', 'marker extension ' + type)
.attr('refX', 1)
.attr('refY', 7)
@@ -38,11 +38,11 @@ const extension = (elem, type, id) => {
.attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
};
-const composition = (elem, type) => {
+const composition = (elem, type, id) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-compositionStart')
+ .attr('id', id + '_' + type + '-compositionStart')
.attr('class', 'marker composition ' + type)
.attr('refX', 18)
.attr('refY', 7)
@@ -55,7 +55,7 @@ const composition = (elem, type) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-compositionEnd')
+ .attr('id', id + '_' + type + '-compositionEnd')
.attr('class', 'marker composition ' + type)
.attr('refX', 1)
.attr('refY', 7)
@@ -65,11 +65,11 @@ const composition = (elem, type) => {
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
};
-const aggregation = (elem, type) => {
+const aggregation = (elem, type, id) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-aggregationStart')
+ .attr('id', id + '_' + type + '-aggregationStart')
.attr('class', 'marker aggregation ' + type)
.attr('refX', 18)
.attr('refY', 7)
@@ -82,7 +82,7 @@ const aggregation = (elem, type) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-aggregationEnd')
+ .attr('id', id + '_' + type + '-aggregationEnd')
.attr('class', 'marker aggregation ' + type)
.attr('refX', 1)
.attr('refY', 7)
@@ -92,11 +92,11 @@ const aggregation = (elem, type) => {
.append('path')
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
};
-const dependency = (elem, type) => {
+const dependency = (elem, type, id) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-dependencyStart')
+ .attr('id', id + '_' + type + '-dependencyStart')
.attr('class', 'marker dependency ' + type)
.attr('refX', 6)
.attr('refY', 7)
@@ -109,7 +109,7 @@ const dependency = (elem, type) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-dependencyEnd')
+ .attr('id', id + '_' + type + '-dependencyEnd')
.attr('class', 'marker dependency ' + type)
.attr('refX', 13)
.attr('refY', 7)
@@ -119,11 +119,11 @@ const dependency = (elem, type) => {
.append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
};
-const lollipop = (elem, type) => {
+const lollipop = (elem, type, id) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-lollipopStart')
+ .attr('id', id + '_' + type + '-lollipopStart')
.attr('class', 'marker lollipop ' + type)
.attr('refX', 13)
.attr('refY', 7)
@@ -140,7 +140,7 @@ const lollipop = (elem, type) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-lollipopEnd')
+ .attr('id', id + '_' + type + '-lollipopEnd')
.attr('class', 'marker lollipop ' + type)
.attr('refX', 1)
.attr('refY', 7)
@@ -154,10 +154,10 @@ const lollipop = (elem, type) => {
.attr('cy', 7)
.attr('r', 6);
};
-const point = (elem, type) => {
+const point = (elem, type, id) => {
elem
.append('marker')
- .attr('id', type + '-pointEnd')
+ .attr('id', id + '_' + type + '-pointEnd')
.attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10')
.attr('refX', 6)
@@ -173,10 +173,10 @@ const point = (elem, type) => {
.style('stroke-dasharray', '1,0');
elem
.append('marker')
- .attr('id', type + '-pointStart')
+ .attr('id', id + '_' + type + '-pointStart')
.attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10')
- .attr('refX', 0)
+ .attr('refX', 4.5)
.attr('refY', 5)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
@@ -188,10 +188,10 @@ const point = (elem, type) => {
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0');
};
-const circle = (elem, type) => {
+const circle = (elem, type, id) => {
elem
.append('marker')
- .attr('id', type + '-circleEnd')
+ .attr('id', id + '_' + type + '-circleEnd')
.attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10')
.attr('refX', 11)
@@ -210,7 +210,7 @@ const circle = (elem, type) => {
elem
.append('marker')
- .attr('id', type + '-circleStart')
+ .attr('id', id + '_' + type + '-circleStart')
.attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10')
.attr('refX', -1)
@@ -227,10 +227,10 @@ const circle = (elem, type) => {
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0');
};
-const cross = (elem, type) => {
+const cross = (elem, type, id) => {
elem
.append('marker')
- .attr('id', type + '-crossEnd')
+ .attr('id', id + '_' + type + '-crossEnd')
.attr('class', 'marker cross ' + type)
.attr('viewBox', '0 0 11 11')
.attr('refX', 12)
@@ -248,7 +248,7 @@ const cross = (elem, type) => {
elem
.append('marker')
- .attr('id', type + '-crossStart')
+ .attr('id', id + '_' + type + '-crossStart')
.attr('class', 'marker cross ' + type)
.attr('viewBox', '0 0 11 11')
.attr('refX', -1)
@@ -264,11 +264,11 @@ const cross = (elem, type) => {
.style('stroke-width', 2)
.style('stroke-dasharray', '1,0');
};
-const barb = (elem, type) => {
+const barb = (elem, type, id) => {
elem
.append('defs')
.append('marker')
- .attr('id', type + '-barbEnd')
+ .attr('id', id + '_' + type + '-barbEnd')
.attr('refX', 19)
.attr('refY', 7)
.attr('markerWidth', 20)
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
index a3508fa49..eb1d6a304 100644
--- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts
+++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
@@ -44,7 +44,11 @@ export const addDiagrams = () => {
},
},
styles: {}, // should never be used
- renderer: {}, // should never be used
+ renderer: {
+ draw: () => {
+ // should never be used
+ },
+ },
parser: {
parser: { yy: {} },
parse: () => {
diff --git a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts
index b82011f8d..2cafd695b 100644
--- a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts
+++ b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts
@@ -41,7 +41,11 @@ describe('DiagramAPI', () => {
},
parser: { yy: {} },
},
- renderer: {},
+ renderer: {
+ draw: () => {
+ // no-op
+ },
+ },
styles: {},
},
detector
diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts
index 15aa4b033..58d98107e 100644
--- a/packages/mermaid/src/diagram-api/types.ts
+++ b/packages/mermaid/src/diagram-api/types.ts
@@ -39,9 +39,26 @@ export interface DiagramDB {
bindFunctions?: (element: Element) => void;
}
+// This is what is returned from getClasses(...) methods.
+// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word.
+// It makes it clear we're working with a style class definition, even though defining the type is currently difficult.
+export interface DiagramStyleClassDef {
+ id: string;
+ styles?: string[];
+ textStyles?: string[];
+}
+
+export interface DiagramRenderer {
+ draw: DrawDefinition;
+ getClasses?: (
+ text: string,
+ diagram: Pick
+ ) => Record;
+}
+
export interface DiagramDefinition {
db: DiagramDB;
- renderer: any;
+ renderer: DiagramRenderer;
parser: ParserDefinition;
styles?: any;
init?: (config: MermaidConfig) => void;
@@ -84,7 +101,7 @@ export type DrawDefinition = (
id: string,
version: string,
diagramObject: Diagram
-) => void;
+) => void | Promise;
export interface ParserDefinition {
parse: (text: string) => void;
diff --git a/packages/mermaid/src/diagram.spec.ts b/packages/mermaid/src/diagram.spec.ts
index 99ce4e2c6..19a65b716 100644
--- a/packages/mermaid/src/diagram.spec.ts
+++ b/packages/mermaid/src/diagram.spec.ts
@@ -34,7 +34,11 @@ describe('diagram detection', () => {
yy: {},
},
},
- renderer: {},
+ renderer: {
+ draw: () => {
+ // no-op
+ },
+ },
styles: {},
},
})
diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts
index b581252bf..5abfd769a 100644
--- a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts
+++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts
@@ -8,7 +8,8 @@ import utils from '../../utils.js';
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import common from '../common/common.js';
-import type { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js';
+import type { ClassRelation, ClassNote, ClassMap, NamespaceMap } from './classTypes.js';
+import type { EdgeData } from '../../types.js';
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
diff --git a/packages/mermaid/src/diagrams/class/classTypes.ts b/packages/mermaid/src/diagrams/class/classTypes.ts
index aa5ec7b70..d372feeba 100644
--- a/packages/mermaid/src/diagrams/class/classTypes.ts
+++ b/packages/mermaid/src/diagrams/class/classTypes.ts
@@ -137,24 +137,6 @@ export interface ClassNote {
text: string;
}
-export interface EdgeData {
- arrowheadStyle?: string;
- labelpos?: string;
- labelType?: string;
- label?: string;
- classes: string;
- pattern: string;
- id: string;
- arrowhead: string;
- startLabelRight: string;
- endLabelLeft: string;
- arrowTypeStart: string;
- arrowTypeEnd: string;
- style: string;
- labelStyle: string;
- curve: any;
-}
-
export type ClassRelation = {
id1: string;
id2: string;
diff --git a/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js
index c7bfdf524..737b492fb 100644
--- a/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js
+++ b/packages/mermaid/src/diagrams/flowchart/elk/flowRenderer-elk.js
@@ -4,13 +4,14 @@ import insertMarkers from '../../../dagre-wrapper/markers.js';
import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js';
import { findCommonAncestor } from './render-utils.js';
import { labelHelper } from '../../../dagre-wrapper/shapes/util.js';
-import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
import { getConfig } from '../../../config.js';
import { log } from '../../../logger.js';
import { setupGraphViewbox } from '../../../setupGraphViewbox.js';
-import common, { evaluate } from '../../common/common.js';
+import common from '../../common/common.js';
import { interpolateToCurve, getStylesFromArray } from '../../../utils.js';
import ELK from 'elkjs/lib/elk.bundled.js';
+import { getLineFunctionsWithOffset } from '../../../utils/lineWithOffset.js';
+
const elk = new ELK();
let portPos = {};
@@ -568,8 +569,9 @@ export const addEdges = function (edges, diagObj, graph, svg) {
* @param edgeData
* @param diagramType
* @param arrowMarkerAbsolute
+ * @param id
*/
-const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute) {
+const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute, id) {
let url = '';
// Check configuration for absolute path
if (arrowMarkerAbsolute) {
@@ -586,61 +588,103 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
// look in edge data and decide which marker to use
switch (edgeData.arrowTypeStart) {
case 'arrow_cross':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-crossStart' + ')'
+ );
break;
case 'arrow_point':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-pointStart' + ')'
+ );
break;
case 'arrow_barb':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-barbStart' + ')'
+ );
break;
case 'arrow_circle':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-circleStart' + ')'
+ );
break;
case 'aggregation':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-aggregationStart' + ')'
+ );
break;
case 'extension':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-extensionStart' + ')'
+ );
break;
case 'composition':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-compositionStart' + ')'
+ );
break;
case 'dependency':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-dependencyStart' + ')'
+ );
break;
case 'lollipop':
- svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')');
+ svgPath.attr(
+ 'marker-start',
+ 'url(' + url + '#' + id + '_' + diagramType + '-lollipopStart' + ')'
+ );
break;
default:
}
switch (edgeData.arrowTypeEnd) {
case 'arrow_cross':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-crossEnd' + ')');
break;
case 'arrow_point':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-pointEnd' + ')');
break;
case 'arrow_barb':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-barbEnd' + ')');
break;
case 'arrow_circle':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')');
+ svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-circleEnd' + ')');
break;
case 'aggregation':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-aggregationEnd' + ')'
+ );
break;
case 'extension':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-extensionEnd' + ')'
+ );
break;
case 'composition':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-compositionEnd' + ')'
+ );
break;
case 'dependency':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-dependencyEnd' + ')'
+ );
break;
case 'lollipop':
- svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')');
+ svgPath.attr(
+ 'marker-end',
+ 'url(' + url + '#' + id + '_' + diagramType + '-lollipopEnd' + ')'
+ );
break;
default:
}
@@ -651,7 +695,7 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
*
* @param text
* @param diagObj
- * @returns {object} ClassDef styles
+ * @returns {Record} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
log.info('Extracting classes');
@@ -691,7 +735,7 @@ const calcOffset = function (src, dest, parentLookupDb) {
return { x: ancestorOffset.posX, y: ancestorOffset.posY };
};
-const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
+const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, id) {
const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb);
const src = edge.sections[0].startPoint;
@@ -705,8 +749,8 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
[dest.x + offset.x, dest.y + offset.y],
];
- // const curve = line().curve(curveBasis);
- const curve = line().curve(curveLinear);
+ const { x, y } = getLineFunctionsWithOffset(edge.edgeData);
+ const curve = line().x(x).y(y).curve(curveLinear);
const edgePath = edgesEl
.insert('path')
.attr('d', curve(points))
@@ -722,7 +766,7 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
'transform',
`translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})`
);
- addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute);
+ addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute, id);
};
/**
@@ -815,7 +859,7 @@ export const draw = async function (text, id, _version, diagObj) {
const markers = ['point', 'circle', 'cross'];
// Add the marker definitions to the svg as marker tags
- insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute);
+ insertMarkers(svg, markers, diagObj.type, id);
// Fetch the vertices/nodes and edges/links from the parsed graph definition
const vert = diagObj.db.getVertices();
@@ -894,7 +938,7 @@ export const draw = async function (text, id, _version, diagObj) {
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0);
log.info('after layout', g);
g.edges?.map((edge) => {
- insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb);
+ insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb, id);
});
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
// Remove element after layout
diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js
index 4a3b7a8ce..576ee6b34 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js
+++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js
@@ -338,7 +338,7 @@ export const addEdges = function (edges, g, diagObj) {
*
* @param text
* @param diagObj
- * @returns {object} ClassDef styles
+ * @returns {Record} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
return diagObj.db.getClasses();
diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js
index fc06cacd4..8394b41e8 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js
+++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js
@@ -269,7 +269,7 @@ export const addEdges = function (edges, g, diagObj) {
*
* @param text
* @param diagObj
- * @returns {object} ClassDef styles
+ * @returns {Record} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
log.info('Extracting classes');
diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts
index 388d7a028..9d7d62bac 100644
--- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts
+++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts
@@ -3,17 +3,13 @@ import { log } from '../../logger.js';
import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js';
import defaultConfig from '../../defaultConfig.js';
import { getThemeVariables } from '../../themes/theme-default.js';
+import type { Point } from '../../types.js';
const defaultThemeVariables = getThemeVariables();
export type TextVerticalPos = 'left' | 'center' | 'right';
export type TextHorizontalPos = 'top' | 'middle' | 'bottom';
-export interface Point {
- x: number;
- y: number;
-}
-
export interface QuadrantPointInputType extends Point {
text: string;
}
diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js
index 1c9b2d1d3..0d3117b20 100644
--- a/packages/mermaid/src/diagrams/state/stateRenderer-v2.js
+++ b/packages/mermaid/src/diagrams/state/stateRenderer-v2.js
@@ -81,7 +81,7 @@ export const setConf = function (cnf) {
*
* @param {string} text - the diagram text to be parsed
* @param diagramObj
- * @returns {object} ClassDef styles (a Map with keys = strings, values = )
+ * @returns {Record} ClassDef styles (a Map with keys = strings, values = )
*/
export const getClasses = function (text, diagramObj) {
diagramObj.db.extract(diagramObj.db.getRootDocV2());
diff --git a/packages/mermaid/src/docs/package.json b/packages/mermaid/src/docs/package.json
index 742a28c0a..759d1ffb1 100644
--- a/packages/mermaid/src/docs/package.json
+++ b/packages/mermaid/src/docs/package.json
@@ -32,7 +32,7 @@
"unplugin-vue-components": "^0.25.0",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.16.0",
- "vitepress": "1.0.0-rc.10",
+ "vitepress": "1.0.0-rc.12",
"workbox-window": "^7.0.0"
}
}
diff --git a/packages/mermaid/src/mermaid.spec.ts b/packages/mermaid/src/mermaid.spec.ts
index 0b4437d74..645b5b39c 100644
--- a/packages/mermaid/src/mermaid.spec.ts
+++ b/packages/mermaid/src/mermaid.spec.ts
@@ -95,8 +95,10 @@ describe('when using mermaid and ', () => {
let loaded = false;
const dummyDiagram: DiagramDefinition = {
db: {},
- renderer: () => {
- // do nothing
+ renderer: {
+ draw: () => {
+ // no-op
+ },
},
parser: {
parse: (_text) => {
diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts
index caf4a2b9b..a6d495471 100644
--- a/packages/mermaid/src/mermaid.ts
+++ b/packages/mermaid/src/mermaid.ts
@@ -136,7 +136,7 @@ const runThrowsErrors = async function (
}
// generate the id of the diagram
- const idGenerator = new utils.initIdGenerator(conf.deterministicIds, conf.deterministicIDSeed);
+ const idGenerator = new utils.InitIDGenerator(conf.deterministicIds, conf.deterministicIDSeed);
let txt: string;
const errors: DetailedError[] = [];
diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts
index d7c16a1cf..a79fd44c4 100644
--- a/packages/mermaid/src/mermaidAPI.spec.ts
+++ b/packages/mermaid/src/mermaidAPI.spec.ts
@@ -287,15 +287,15 @@ describe('mermaidAPI', () => {
};
it('gets the cssStyles from the theme', () => {
- const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', null);
+ const styles = createCssStyles(mocked_config_with_htmlLabels, null);
expect(styles).toMatch(/^\ndefault(.*)/);
});
it('gets the fontFamily from the config', () => {
- const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', {});
+ const styles = createCssStyles(mocked_config_with_htmlLabels, {});
expect(styles).toMatch(/(.*)\n:root { --mermaid-font-family: serif(.*)/);
});
it('gets the alt fontFamily from the config', () => {
- const styles = createCssStyles(mocked_config_with_htmlLabels, 'graphType', undefined);
+ const styles = createCssStyles(mocked_config_with_htmlLabels, undefined);
expect(styles).toMatch(/(.*)\n:root { --mermaid-alt-font-family: sans-serif(.*)/);
});
@@ -306,8 +306,6 @@ describe('mermaidAPI', () => {
const classDefs = { classDef1, classDef2, classDef3 };
describe('the graph supports classDefs', () => {
- const graphType = 'flowchart-v2';
-
const REGEXP_SPECIALS = ['^', '$', '?', '(', '{', '[', '.', '*', '!'];
// prefix any special RegExp characters in the given string with a \ so we can use the literal character in a RegExp
@@ -373,7 +371,7 @@ describe('mermaidAPI', () => {
// @todo TODO Can't figure out how to spy on the cssImportantStyles method.
// That would be a much better approach than manually checking the result
- const styles = createCssStyles(mocked_config, graphType, classDefs);
+ const styles = createCssStyles(mocked_config, classDefs);
htmlElements.forEach((htmlElement) => {
expect_styles_matchesHtmlElements(styles, htmlElement);
});
@@ -411,7 +409,7 @@ describe('mermaidAPI', () => {
it('creates CSS styles for every style and textStyle in every classDef', () => {
// TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result.
- const styles = createCssStyles(mocked_config_no_htmlLabels, graphType, classDefs);
+ const styles = createCssStyles(mocked_config_no_htmlLabels, classDefs);
htmlElements.forEach((htmlElement) => {
expect_styles_matchesHtmlElements(styles, htmlElement);
});
diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts
index 3649b50f5..5250f0b19 100644
--- a/packages/mermaid/src/mermaidAPI.ts
+++ b/packages/mermaid/src/mermaidAPI.ts
@@ -28,17 +28,9 @@ import type { MermaidConfig } from './config.type.js';
import { evaluate } from './diagrams/common/common.js';
import isEmpty from 'lodash-es/isEmpty.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
+import type { DiagramStyleClassDef } from './diagram-api/types.js';
import { preprocessDiagram } from './preprocess.js';
-// diagram names that support classDef statements
-const CLASSDEF_DIAGRAMS = [
- 'graph',
- 'flowchart',
- 'flowchart-v2',
- 'flowchart-elk',
- 'stateDiagram',
- 'stateDiagram-v2',
-];
const MAX_TEXTLENGTH = 50_000;
const MAX_TEXTLENGTH_EXCEEDED_MSG =
'graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa';
@@ -63,15 +55,6 @@ const IFRAME_NOT_SUPPORTED_MSG = 'The "iframe" tag is not supported by your brow
const DOMPURIFY_TAGS = ['foreignobject'];
const DOMPURIFY_ATTR = ['dominant-baseline'];
-// This is what is returned from getClasses(...) methods.
-// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word.
-// It makes it clear we're working with a style class definition, even though defining the type is currently difficult.
-interface DiagramStyleClassDef {
- id: string;
- styles?: string[];
- textStyles?: string[];
-}
-
export interface ParseOptions {
suppressErrors?: boolean;
}
@@ -184,15 +167,13 @@ export const cssImportantStyles = (
/**
* Create the user styles
- *
+ * @internal
* @param config - configuration that has style and theme settings to use
- * @param graphType - used for checking if classDefs should be applied
* @param classDefs - the classDefs in the diagram text. Might be null if none were defined. Usually is the result of a call to getClasses(...)
* @returns the string with all the user styles
*/
export const createCssStyles = (
config: MermaidConfig,
- graphType: string,
classDefs: Record | null | undefined = {}
): string => {
let cssStyles = '';
@@ -212,7 +193,7 @@ export const createCssStyles = (
}
// classDefs defined in the diagram text
- if (!isEmpty(classDefs) && CLASSDEF_DIAGRAMS.includes(graphType)) {
+ if (!isEmpty(classDefs)) {
const htmlLabels = config.htmlLabels || config.flowchart?.htmlLabels; // TODO why specifically check the Flowchart diagram config?
const cssHtmlElements = ['> *', 'span']; // TODO make a constant
@@ -241,10 +222,10 @@ export const createCssStyles = (
export const createUserStyles = (
config: MermaidConfig,
graphType: string,
- classDefs: Record,
+ classDefs: Record | undefined,
svgId: string
): string => {
- const userCSSstyles = createCssStyles(config, graphType, classDefs);
+ const userCSSstyles = createCssStyles(config, classDefs);
const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables);
// Now turn all of the styles into a (compiled) string that starts with the id
@@ -481,9 +462,7 @@ const render = async function (
// Insert an element into svg. This is where we put the styles
const svg = element.firstChild;
const firstChild = svg.firstChild;
- const diagramClassDefs = CLASSDEF_DIAGRAMS.includes(diagramType)
- ? diag.renderer.getClasses(text, diag)
- : {};
+ const diagramClassDefs = diag.renderer.getClasses?.(text, diag);
const rules = createUserStyles(config, diagramType, diagramClassDefs, idSelector);
diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts
new file mode 100644
index 000000000..13da88503
--- /dev/null
+++ b/packages/mermaid/src/types.ts
@@ -0,0 +1,34 @@
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface TextDimensionConfig {
+ fontSize?: number;
+ fontWeight?: number;
+ fontFamily?: string;
+}
+
+export interface TextDimensions {
+ width: number;
+ height: number;
+ lineHeight?: number;
+}
+
+export interface EdgeData {
+ arrowheadStyle?: string;
+ labelpos?: string;
+ labelType?: string;
+ label?: string;
+ classes: string;
+ pattern: string;
+ id: string;
+ arrowhead: string;
+ startLabelRight: string;
+ endLabelLeft: string;
+ arrowTypeStart: string;
+ arrowTypeEnd: string;
+ style: string;
+ labelStyle: string;
+ curve: any;
+}
diff --git a/packages/mermaid/src/utils.spec.ts b/packages/mermaid/src/utils.spec.ts
index e1398efc7..3be3bc214 100644
--- a/packages/mermaid/src/utils.spec.ts
+++ b/packages/mermaid/src/utils.spec.ts
@@ -1,5 +1,5 @@
import { vi } from 'vitest';
-import utils, { cleanAndMerge, detectDirective } from './utils.js';
+import utils, { calculatePoint, cleanAndMerge, detectDirective } from './utils.js';
import assignWithDepth from './assignWithDepth.js';
import { detectType } from './diagram-api/detectType.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
@@ -352,7 +352,7 @@ describe('when initializing the id generator', function () {
});
it('should return a random number generator based on Date', function () {
- const idGenerator = new utils.initIdGenerator(false);
+ const idGenerator = new utils.InitIDGenerator(false);
expect(typeof idGenerator.next).toEqual('function');
const lastId = idGenerator.next();
vi.advanceTimersByTime(1000);
@@ -360,7 +360,7 @@ describe('when initializing the id generator', function () {
});
it('should return a non random number generator', function () {
- const idGenerator = new utils.initIdGenerator(true);
+ const idGenerator = new utils.InitIDGenerator(true);
expect(typeof idGenerator.next).toEqual('function');
const start = 0;
const lastId = idGenerator.next();
@@ -369,7 +369,7 @@ describe('when initializing the id generator', function () {
});
it('should return a non random number generator based on seed', function () {
- const idGenerator = new utils.initIdGenerator(true, 'thisIsASeed');
+ const idGenerator = new utils.InitIDGenerator(true, 'thisIsASeed');
expect(typeof idGenerator.next).toEqual('function');
const start = 11;
const lastId = idGenerator.next();
@@ -490,3 +490,107 @@ describe('cleanAndMerge', () => {
expect(inputDeep).toEqual({ a: { b: 1 } });
});
});
+
+describe('calculatePoint', () => {
+ it('should calculate a point on a straight line', () => {
+ const points = [
+ { x: 0, y: 0 },
+ { x: 0, y: 10 },
+ { x: 0, y: 20 },
+ ];
+ expect(calculatePoint(points, 0)).toEqual({ x: 0, y: 0 });
+ expect(calculatePoint(points, 5)).toEqual({ x: 0, y: 5 });
+ expect(calculatePoint(points, 10)).toEqual({ x: 0, y: 10 });
+ });
+
+ it('should calculate a point on a straight line with slope', () => {
+ const points = [
+ { x: 0, y: 0 },
+ { x: 10, y: 10 },
+ { x: 20, y: 20 },
+ ];
+ expect(calculatePoint(points, 0)).toMatchInlineSnapshot(`
+ {
+ "x": 0,
+ "y": 0,
+ }
+ `);
+ expect(calculatePoint(points, 5)).toMatchInlineSnapshot(`
+ {
+ "x": 3.53553,
+ "y": 3.53553,
+ }
+ `);
+ expect(calculatePoint(points, 10)).toMatchInlineSnapshot(`
+ {
+ "x": 7.07107,
+ "y": 7.07107,
+ }
+ `);
+ });
+
+ it('should calculate a point on a straight line with negative slope', () => {
+ const points = [
+ { x: 20, y: 20 },
+ { x: 10, y: 10 },
+ { x: 15, y: 15 },
+ { x: 0, y: 0 },
+ ];
+ expect(calculatePoint(points, 0)).toMatchInlineSnapshot(`
+ {
+ "x": 20,
+ "y": 20,
+ }
+ `);
+ expect(calculatePoint(points, 5)).toMatchInlineSnapshot(`
+ {
+ "x": 16.46447,
+ "y": 16.46447,
+ }
+ `);
+ expect(calculatePoint(points, 10)).toMatchInlineSnapshot(`
+ {
+ "x": 12.92893,
+ "y": 12.92893,
+ }
+ `);
+ });
+
+ it('should calculate a point on a curved line', () => {
+ const points = [
+ { x: 0, y: 0 },
+ { x: 10, y: 10 },
+ { x: 20, y: 0 },
+ ];
+ expect(calculatePoint(points, 0)).toMatchInlineSnapshot(`
+ {
+ "x": 0,
+ "y": 0,
+ }
+ `);
+ expect(calculatePoint(points, 15)).toMatchInlineSnapshot(`
+ {
+ "x": 10.6066,
+ "y": 9.3934,
+ }
+ `);
+ expect(calculatePoint(points, 20)).toMatchInlineSnapshot(`
+ {
+ "x": 14.14214,
+ "y": 5.85786,
+ }
+ `);
+ });
+
+ it('should throw an error if the new point cannot be found', () => {
+ const points = [
+ { x: 0, y: 0 },
+ { x: 10, y: 10 },
+ { x: 20, y: 20 },
+ ];
+ const distanceToTraverse = 30;
+ expect(() => calculatePoint(points, distanceToTraverse)).toThrow(
+ 'Could not find a suitable point for the given distance'
+ );
+ });
+});
diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts
index 70de197da..e706ef122 100644
--- a/packages/mermaid/src/utils.ts
+++ b/packages/mermaid/src/utils.ts
@@ -1,4 +1,3 @@
-// @ts-nocheck : TODO Fix ts errors
import { sanitizeUrl } from '@braintree/sanitize-url';
import type { CurveFactory } from 'd3';
import {
@@ -33,6 +32,8 @@ import type { MermaidConfig } from './config.type.js';
import memoize from 'lodash-es/memoize.js';
import merge from 'lodash-es/merge.js';
import { directiveRegex } from './diagram-api/regexes.js';
+import type { D3Element } from './mermaidAPI.js';
+import type { Point, TextDimensionConfig, TextDimensions } from './types.js';
export const ZERO_WIDTH_SPACE = '\u200b';
@@ -58,7 +59,7 @@ const d3CurveTypes = {
curveStep: curveStep,
curveStepAfter: curveStepAfter,
curveStepBefore: curveStepBefore,
-};
+} as const;
const directiveWithoutOpen =
/\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
@@ -101,14 +102,14 @@ export const detectInit = function (
config?: MermaidConfig
): MermaidConfig | undefined {
const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
- let results = {};
+ let results: MermaidConfig & { config?: unknown } = {};
if (Array.isArray(inits)) {
const args = inits.map((init) => init.args);
sanitizeDirective(args);
results = assignWithDepth(results, [...args]);
} else {
- results = inits.args;
+ results = inits.args as MermaidConfig;
}
if (!results) {
@@ -116,19 +117,24 @@ export const detectInit = function (
}
let type = detectType(text, config);
- ['config'].forEach((prop) => {
- if (results[prop] !== undefined) {
- if (type === 'flowchart-v2') {
- type = 'flowchart';
- }
- results[type] = results[prop];
- delete results[prop];
+
+ // Move the `config` value to appropriate diagram type value
+ const prop = 'config';
+ if (results[prop] !== undefined) {
+ if (type === 'flowchart-v2') {
+ type = 'flowchart';
}
- });
+ results[type as keyof MermaidConfig] = results[prop];
+ delete results[prop];
+ }
return results;
};
+interface Directive {
+ type?: string;
+ args?: unknown;
+}
/**
* Detects the directive from the text.
*
@@ -154,8 +160,8 @@ export const detectInit = function (
*/
export const detectDirective = function (
text: string,
- type: string | RegExp = null
-): { type?: string; args?: any } | { type?: string; args?: any }[] {
+ type: string | RegExp | null = null
+): Directive | Directive[] {
try {
const commentWithoutDirectives = new RegExp(
`[%]{2}(?![{]${directiveWithoutOpen.source})(?=[}][%]{2}).*\n`,
@@ -165,8 +171,8 @@ export const detectDirective = function (
log.debug(
`Detecting diagram directive${type !== null ? ' type:' + type : ''} based on the text:${text}`
);
- let match;
- const result = [];
+ let match: RegExpExecArray | null;
+ const result: Directive[] = [];
while ((match = directiveRegex.exec(text)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (match.index === directiveRegex.lastIndex) {
@@ -183,16 +189,17 @@ export const detectDirective = function (
}
}
if (result.length === 0) {
- result.push({ type: text, args: null });
+ return { type: text, args: null };
}
return result.length === 1 ? result[0] : result;
} catch (error) {
log.error(
- `ERROR: ${error.message} - Unable to parse directive
- ${type !== null ? ' type:' + type : ''} based on the text:${text}`
+ `ERROR: ${
+ (error as Error).message
+ } - Unable to parse directive type: '${type}' based on the text: '${text}'`
);
- return { type: null, args: null };
+ return { type: undefined, args: null };
}
};
@@ -231,7 +238,9 @@ export function interpolateToCurve(
return defaultCurve;
}
const curveName = `curve${interpolate.charAt(0).toUpperCase() + interpolate.slice(1)}`;
- return d3CurveTypes[curveName] || defaultCurve;
+
+ // @ts-ignore TODO: Fix issue with curve type
+ return d3CurveTypes[curveName as keyof typeof d3CurveTypes] ?? defaultCurve;
}
/**
@@ -244,13 +253,15 @@ export function interpolateToCurve(
export function formatUrl(linkStr: string, config: MermaidConfig): string | undefined {
const url = linkStr.trim();
- if (url) {
- if (config.securityLevel !== 'loose') {
- return sanitizeUrl(url);
- }
-
- return url;
+ if (!url) {
+ return undefined;
}
+
+ if (config.securityLevel !== 'loose') {
+ return sanitizeUrl(url);
+ }
+
+ return url;
}
/**
@@ -259,7 +270,7 @@ export function formatUrl(linkStr: string, config: MermaidConfig): string | unde
* @param functionName - A dot separated path to the function relative to the `window`
* @param params - Parameters to pass to the function
*/
-export const runFunc = (functionName: string, ...params) => {
+export const runFunc = (functionName: string, ...params: unknown[]) => {
const arrPaths = functionName.split('.');
const len = arrPaths.length - 1;
@@ -267,23 +278,16 @@ export const runFunc = (functionName: string, ...params) => {
let obj = window;
for (let i = 0; i < len; i++) {
- obj = obj[arrPaths[i]];
+ obj = obj[arrPaths[i] as keyof typeof obj];
if (!obj) {
+ log.error(`Function name: ${functionName} not found in window`);
return;
}
}
- obj[fnName](...params);
+ obj[fnName as keyof typeof obj](...params);
};
-/** A (x, y) point */
-interface Point {
- /** The x value */
- x: number;
- /** The y value */
- y: number;
-}
-
/**
* Finds the distance between two points using the Distance Formula
*
@@ -291,8 +295,11 @@ interface Point {
* @param p2 - The second point
* @returns The distance between the two points.
*/
-function distance(p1: Point, p2: Point): number {
- return p1 && p2 ? Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) : 0;
+function distance(p1?: Point, p2?: Point): number {
+ if (!p1 || !p2) {
+ return 0;
+ }
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
/**
@@ -301,7 +308,7 @@ function distance(p1: Point, p2: Point): number {
* @param points - List of points
*/
function traverseEdge(points: Point[]): Point {
- let prevPoint;
+ let prevPoint: Point | undefined;
let totalDistance = 0;
points.forEach((point) => {
@@ -310,35 +317,8 @@ function traverseEdge(points: Point[]): Point {
});
// Traverse half of total distance along points
- let remainingDistance = totalDistance / 2;
- let center = undefined;
- prevPoint = undefined;
- points.forEach((point) => {
- if (prevPoint && !center) {
- const vectorDistance = distance(point, prevPoint);
- if (vectorDistance < remainingDistance) {
- remainingDistance -= vectorDistance;
- } else {
- // The point is remainingDistance from prevPoint in the vector between prevPoint and point
- // Calculate the coordinates
- const distanceRatio = remainingDistance / vectorDistance;
- if (distanceRatio <= 0) {
- center = prevPoint;
- }
- if (distanceRatio >= 1) {
- center = { x: point.x, y: point.y };
- }
- if (distanceRatio > 0 && distanceRatio < 1) {
- center = {
- x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x,
- y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y,
- };
- }
- }
- }
- prevPoint = point;
- });
- return center;
+ const remainingDistance = totalDistance / 2;
+ return calculatePoint(points, remainingDistance);
}
/**
@@ -351,20 +331,16 @@ function calcLabelPosition(points: Point[]): Point {
return traverseEdge(points);
}
-const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) => {
- let prevPoint;
- log.info(`our points ${JSON.stringify(points)}`);
- if (points[0] !== initialPosition) {
- points = points.reverse();
- }
- // Traverse only 25 total distance along points to find cardinality point
- const distanceToCardinalityPoint = 25;
+export const roundNumber = (num: number, precision = 2) => {
+ const factor = Math.pow(10, precision);
+ return Math.round(num * factor) / factor;
+};
- let remainingDistance = distanceToCardinalityPoint;
- let center;
- prevPoint = undefined;
- points.forEach((point) => {
- if (prevPoint && !center) {
+export const calculatePoint = (points: Point[], distanceToTraverse: number): Point => {
+ let prevPoint: Point | undefined = undefined;
+ let remainingDistance = distanceToTraverse;
+ for (const point of points) {
+ if (prevPoint) {
const vectorDistance = distance(point, prevPoint);
if (vectorDistance < remainingDistance) {
remainingDistance -= vectorDistance;
@@ -373,27 +349,42 @@ const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition)
// Calculate the coordinates
const distanceRatio = remainingDistance / vectorDistance;
if (distanceRatio <= 0) {
- center = prevPoint;
+ return prevPoint;
}
if (distanceRatio >= 1) {
- center = { x: point.x, y: point.y };
+ return { x: point.x, y: point.y };
}
if (distanceRatio > 0 && distanceRatio < 1) {
- center = {
- x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x,
- y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y,
+ return {
+ x: roundNumber((1 - distanceRatio) * prevPoint.x + distanceRatio * point.x, 5),
+ y: roundNumber((1 - distanceRatio) * prevPoint.y + distanceRatio * point.y, 5),
};
}
}
}
prevPoint = point;
- });
+ }
+ throw new Error('Could not find a suitable point for the given distance');
+};
+
+const calcCardinalityPosition = (
+ isRelationTypePresent: boolean,
+ points: Point[],
+ initialPosition: Point
+) => {
+ log.info(`our points ${JSON.stringify(points)}`);
+ if (points[0] !== initialPosition) {
+ points = points.reverse();
+ }
+ // Traverse only 25 total distance along points to find cardinality point
+ const distanceToCardinalityPoint = 25;
+ const center = calculatePoint(points, distanceToCardinalityPoint);
// if relation is present (Arrows will be added), change cardinality point off-set distance (d)
const d = isRelationTypePresent ? 10 : 5;
//Calculate Angle for x and y axis
const angle = Math.atan2(points[0].y - center.y, points[0].x - center.x);
const cardinalityPosition = { x: 0, y: 0 };
- //Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance
+ //Calculation cardinality position using angle, center point on the line/curve but perpendicular and with offset-distance
cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2;
cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2;
return cardinalityPosition;
@@ -412,71 +403,36 @@ function calcTerminalLabelPosition(
position: 'start_left' | 'start_right' | 'end_left' | 'end_right',
_points: Point[]
): Point {
- // Todo looking to faster cloning method
- let points = JSON.parse(JSON.stringify(_points));
- let prevPoint;
+ const points = structuredClone(_points);
log.info('our points', points);
if (position !== 'start_left' && position !== 'start_right') {
- points = points.reverse();
+ points.reverse();
}
- points.forEach((point) => {
- prevPoint = point;
- });
-
// Traverse only 25 total distance along points to find cardinality point
const distanceToCardinalityPoint = 25 + terminalMarkerSize;
+ const center = calculatePoint(points, distanceToCardinalityPoint);
- let remainingDistance = distanceToCardinalityPoint;
- let center;
- prevPoint = undefined;
- points.forEach((point) => {
- if (prevPoint && !center) {
- const vectorDistance = distance(point, prevPoint);
- if (vectorDistance < remainingDistance) {
- remainingDistance -= vectorDistance;
- } else {
- // The point is remainingDistance from prevPoint in the vector between prevPoint and point
- // Calculate the coordinates
- const distanceRatio = remainingDistance / vectorDistance;
- if (distanceRatio <= 0) {
- center = prevPoint;
- }
- if (distanceRatio >= 1) {
- center = { x: point.x, y: point.y };
- }
- if (distanceRatio > 0 && distanceRatio < 1) {
- center = {
- x: (1 - distanceRatio) * prevPoint.x + distanceRatio * point.x,
- y: (1 - distanceRatio) * prevPoint.y + distanceRatio * point.y,
- };
- }
- }
- }
- prevPoint = point;
- });
// if relation is present (Arrows will be added), change cardinality point off-set distance (d)
const d = 10 + terminalMarkerSize * 0.5;
//Calculate Angle for x and y axis
const angle = Math.atan2(points[0].y - center.y, points[0].x - center.x);
- const cardinalityPosition = { x: 0, y: 0 };
+ const cardinalityPosition: Point = { x: 0, y: 0 };
+ //Calculation cardinality position using angle, center point on the line/curve but perpendicular and with offset-distance
- //Calculation cardinality position using angle, center point on the line/curve but pendicular and with offset-distance
-
- cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2;
- cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2;
if (position === 'start_left') {
cardinalityPosition.x = Math.sin(angle + Math.PI) * d + (points[0].x + center.x) / 2;
cardinalityPosition.y = -Math.cos(angle + Math.PI) * d + (points[0].y + center.y) / 2;
- }
- if (position === 'end_right') {
+ } else if (position === 'end_right') {
cardinalityPosition.x = Math.sin(angle - Math.PI) * d + (points[0].x + center.x) / 2 - 5;
cardinalityPosition.y = -Math.cos(angle - Math.PI) * d + (points[0].y + center.y) / 2 - 5;
- }
- if (position === 'end_left') {
+ } else if (position === 'end_left') {
cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2 - 5;
cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2 - 5;
+ } else {
+ cardinalityPosition.x = Math.sin(angle) * d + (points[0].x + center.x) / 2;
+ cardinalityPosition.y = -Math.cos(angle) * d + (points[0].y + center.y) / 2;
}
return cardinalityPosition;
}
@@ -502,7 +458,7 @@ export function getStylesFromArray(arr: string[]): { style: string; labelStyle:
}
}
- return { style: style, labelStyle: labelStyle };
+ return { style, labelStyle };
}
let cnt = 0;
@@ -514,10 +470,10 @@ export const generateId = () => {
/**
* Generates a random hexadecimal id of the given length.
*
- * @param length - Length of ID.
- * @returns The generated ID.
+ * @param length - Length of string.
+ * @returns The generated string.
*/
-function makeid(length: number): string {
+function makeRandomHex(length: number): string {
let result = '';
const characters = '0123456789abcdef';
const charactersLength = characters.length;
@@ -527,8 +483,8 @@ function makeid(length: number): string {
return result;
}
-export const random = (options) => {
- return makeid(options.length);
+export const random = (options: { length: number }) => {
+ return makeRandomHex(options.length);
};
export const getTextObj = function () {
@@ -544,6 +500,7 @@ export const getTextObj = function () {
rx: 0,
ry: 0,
valign: undefined,
+ text: '',
};
};
@@ -574,7 +531,7 @@ export const drawSimpleText = function (
const [, _fontSizePx] = parseFontSize(textData.fontSize);
- const textElem = elem.append('text');
+ const textElem = elem.append('text') as any;
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.style('text-anchor', textData.anchor);
@@ -582,6 +539,7 @@ export const drawSimpleText = function (
textElem.style('font-size', _fontSizePx);
textElem.style('font-weight', textData.fontWeight);
textElem.attr('fill', textData.fill);
+
if (textData.class !== undefined) {
textElem.attr('class', textData.class);
}
@@ -601,9 +559,9 @@ interface WrapLabelConfig {
joinWith: string;
}
-export const wrapLabel: (label: string, maxWidth: string, config: WrapLabelConfig) => string =
+export const wrapLabel: (label: string, maxWidth: number, config: WrapLabelConfig) => string =
memoize(
- (label: string, maxWidth: string, config: WrapLabelConfig): string => {
+ (label: string, maxWidth: number, config: WrapLabelConfig): string => {
if (!label) {
return label;
}
@@ -615,7 +573,7 @@ export const wrapLabel: (label: string, maxWidth: string, config: WrapLabelConfi
return label;
}
const words = label.split(' ');
- const completedLines = [];
+ const completedLines: string[] = [];
let nextLine = '';
words.forEach((word, index) => {
const wordLength = calculateTextWidth(`${word} `, config);
@@ -700,10 +658,6 @@ export function calculateTextHeight(
text: Parameters[0],
config: Parameters[1]
): ReturnType['height'] {
- config = Object.assign(
- { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
- config
- );
return calculateTextDimensions(text, config).height;
}
@@ -719,20 +673,9 @@ export function calculateTextWidth(
text: Parameters[0],
config: Parameters[1]
): ReturnType['width'] {
- config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
return calculateTextDimensions(text, config).width;
}
-interface TextDimensionConfig {
- fontSize?: number;
- fontWeight?: number;
- fontFamily?: string;
-}
-interface TextDimensions {
- width: number;
- height: number;
- lineHeight?: number;
-}
/**
* This calculates the dimensions of the given text, font size, font family, font weight, and
* margins.
@@ -747,8 +690,7 @@ export const calculateTextDimensions: (
config: TextDimensionConfig
) => TextDimensions = memoize(
(text: string, config: TextDimensionConfig): TextDimensions => {
- config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
- const { fontSize, fontFamily, fontWeight } = config;
+ const { fontSize = 12, fontFamily = 'Arial', fontWeight = 400 } = config;
if (!text) {
return { width: 0, height: 0 };
}
@@ -772,12 +714,14 @@ export const calculateTextDimensions: (
const g = body.append('svg');
for (const fontFamily of fontFamilies) {
- let cheight = 0;
+ let cHeight = 0;
const dim = { width: 0, height: 0, lineHeight: 0 };
for (const line of lines) {
const textObj = getTextObj();
textObj.text = line || ZERO_WIDTH_SPACE;
+ // @ts-ignore TODO: Fix D3 types
const textElem = drawSimpleText(g, textObj)
+ // @ts-ignore TODO: Fix D3 types
.style('font-size', _fontSizePx)
.style('font-weight', fontWeight)
.style('font-family', fontFamily);
@@ -787,9 +731,9 @@ export const calculateTextDimensions: (
throw new Error('svg element not in render tree');
}
dim.width = Math.round(Math.max(dim.width, bBox.width));
- cheight = Math.round(bBox.height);
- dim.height += cheight;
- dim.lineHeight = Math.round(Math.max(dim.lineHeight, cheight));
+ cHeight = Math.round(bBox.height);
+ dim.height += cHeight;
+ dim.lineHeight = Math.round(Math.max(dim.lineHeight, cHeight));
}
dims.push(dim);
}
@@ -810,25 +754,18 @@ export const calculateTextDimensions: (
(text, config) => `${text}${config.fontSize}${config.fontWeight}${config.fontFamily}`
);
-export const initIdGenerator = class iterator {
- constructor(deterministic, seed?: any) {
- this.deterministic = deterministic;
+export class InitIDGenerator {
+ private count = 0;
+ public next: () => number;
+ constructor(deterministic = false, seed?: string) {
// TODO: Seed is only used for length?
- this.seed = seed;
-
+ // v11: Use the actual value of seed string to generate an initial value for count.
this.count = seed ? seed.length : 0;
+ this.next = deterministic ? () => this.count++ : () => Date.now();
}
+}
- next() {
- if (!this.deterministic) {
- return Date.now();
- }
-
- return this.count++;
- }
-};
-
-let decoder;
+let decoder: HTMLDivElement;
/**
* Decodes HTML, source: {@link https://github.com/shrpne/entity-decode/blob/v2.0.1/browser.js}
@@ -840,20 +777,23 @@ export const entityDecode = function (html: string): string {
decoder = decoder || document.createElement('div');
// Escape HTML before decoding for HTML Entities
html = escape(html).replace(/%26/g, '&').replace(/%23/g, '#').replace(/%3B/g, ';');
- // decoding
decoder.innerHTML = html;
- return unescape(decoder.textContent);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return unescape(decoder.textContent!);
};
export interface DetailedError {
str: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
hash: any;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
error?: any;
message?: string;
}
/** @param error - The error to check */
-export function isDetailedError(error: unknown): error is DetailedError {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function isDetailedError(error: any): error is DetailedError {
return 'str' in error;
}
@@ -874,7 +814,7 @@ export function getErrorMessage(error: unknown): string {
* @param title - The title. If empty, returns immediately.
*/
export const insertTitle = (
- parent,
+ parent: D3Element,
cssClass: string,
titleTopMargin: number,
title?: string
@@ -882,7 +822,10 @@ export const insertTitle = (
if (!title) {
return;
}
- const bounds = parent.node().getBBox();
+ const bounds = parent.node()?.getBBox();
+ if (!bounds) {
+ return;
+ }
parent
.append('text')
.text(title)
@@ -905,7 +848,7 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?,
return [fontSize, fontSize + 'px'];
}
- const fontSizeNumber = parseInt(fontSize, 10);
+ const fontSizeNumber = parseInt(fontSize ?? '', 10);
if (Number.isNaN(fontSizeNumber)) {
// if a number value can't be parsed, return null for both values
return [undefined, undefined];
@@ -941,7 +884,7 @@ export default {
random,
runFunc,
entityDecode,
- initIdGenerator,
insertTitle,
parseFontSize,
+ InitIDGenerator,
};
diff --git a/packages/mermaid/src/utils/lineWithOffset.ts b/packages/mermaid/src/utils/lineWithOffset.ts
new file mode 100644
index 000000000..f348d3eb3
--- /dev/null
+++ b/packages/mermaid/src/utils/lineWithOffset.ts
@@ -0,0 +1,92 @@
+import type { EdgeData, Point } from '../types.js';
+
+// We need to draw the lines a bit shorter to avoid drawing
+// under any transparent markers.
+// The offsets are calculated from the markers' dimensions.
+const markerOffsets = {
+ aggregation: 18,
+ extension: 18,
+ composition: 18,
+ dependency: 6,
+ lollipop: 13.5,
+ arrow_point: 5.3,
+} as const;
+
+/**
+ * Calculate the deltas and angle between two points
+ * @param point1 - First point
+ * @param point2 - Second point
+ * @returns The angle, deltaX and deltaY
+ */
+function calculateDeltaAndAngle(
+ point1: Point | [number, number],
+ point2: Point | [number, number]
+): { angle: number; deltaX: number; deltaY: number } {
+ point1 = pointTransformer(point1);
+ point2 = pointTransformer(point2);
+ const [x1, y1] = [point1.x, point1.y];
+ const [x2, y2] = [point2.x, point2.y];
+ const deltaX = x2 - x1;
+ const deltaY = y2 - y1;
+ return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY };
+}
+
+const pointTransformer = (data: Point | [number, number]) => {
+ if (Array.isArray(data)) {
+ return { x: data[0], y: data[1] };
+ }
+ return data;
+};
+
+export const getLineFunctionsWithOffset = (
+ edge: Pick
+) => {
+ return {
+ x: function (d: Point | [number, number], i: number, data: (Point | [number, number])[]) {
+ let offset = 0;
+ if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
+ // Handle first point
+ // Calculate the angle and delta between the first two points
+ const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]);
+ // Calculate the offset based on the angle and the marker's dimensions
+ offset =
+ markerOffsets[edge.arrowTypeStart as keyof typeof markerOffsets] *
+ Math.cos(angle) *
+ (deltaX >= 0 ? 1 : -1);
+ } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
+ // Handle last point
+ // Calculate the angle and delta between the last two points
+ const { angle, deltaX } = calculateDeltaAndAngle(
+ data[data.length - 1],
+ data[data.length - 2]
+ );
+ offset =
+ markerOffsets[edge.arrowTypeEnd as keyof typeof markerOffsets] *
+ Math.cos(angle) *
+ (deltaX >= 0 ? 1 : -1);
+ }
+ return pointTransformer(d).x + offset;
+ },
+ y: function (d: Point | [number, number], i: number, data: (Point | [number, number])[]) {
+ // Same handling as X above
+ let offset = 0;
+ if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
+ const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]);
+ offset =
+ markerOffsets[edge.arrowTypeStart as keyof typeof markerOffsets] *
+ Math.abs(Math.sin(angle)) *
+ (deltaY >= 0 ? 1 : -1);
+ } else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
+ const { angle, deltaY } = calculateDeltaAndAngle(
+ data[data.length - 1],
+ data[data.length - 2]
+ );
+ offset =
+ markerOffsets[edge.arrowTypeEnd as keyof typeof markerOffsets] *
+ Math.abs(Math.sin(angle)) *
+ (deltaY >= 0 ? 1 : -1);
+ }
+ return pointTransformer(d).y + offset;
+ },
+ };
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 17100e2cd..7817ea178 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -475,8 +475,8 @@ importers:
specifier: ^0.16.0
version: 0.16.0(vite@4.4.9)(workbox-build@7.0.0)(workbox-window@7.0.0)
vitepress:
- specifier: 1.0.0-rc.10
- version: 1.0.0-rc.10(@algolia/client-search@4.19.1)(@types/node@18.17.5)(search-insights@2.7.0)
+ specifier: 1.0.0-rc.12
+ version: 1.0.0-rc.12(@algolia/client-search@4.19.1)(@types/node@18.17.5)(search-insights@2.7.0)
workbox-window:
specifier: ^7.0.0
version: 7.0.0
@@ -13977,6 +13977,15 @@ packages:
vscode-textmate: 8.0.0
dev: true
+ /shiki@0.14.4:
+ resolution: {integrity: sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==}
+ dependencies:
+ ansi-sequence-parser: 1.1.1
+ jsonc-parser: 3.2.0
+ vscode-oniguruma: 1.7.0
+ vscode-textmate: 8.0.0
+ dev: true
+
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@@ -15527,8 +15536,8 @@ packages:
- terser
dev: true
- /vitepress@1.0.0-rc.10(@algolia/client-search@4.19.1)(@types/node@18.17.5)(search-insights@2.7.0):
- resolution: {integrity: sha512-+MsahIWqq5WUEmj6MR4obcKYbT7im07jZPCQPdNJExkeOSbOAJ4xypSLx88x7rvtzWHhHc5aXbOhCRvGEGjFrw==}
+ /vitepress@1.0.0-rc.12(@algolia/client-search@4.19.1)(@types/node@18.17.5)(search-insights@2.7.0):
+ resolution: {integrity: sha512-mZknN5l9lgbBjXwumwdOQQDM+gPivswFEykEQeenY0tv7eocS+bb801IpFZT3mFV6YRhSddmbutHlFgPPADjEg==}
hasBin: true
dependencies:
'@docsearch/css': 3.5.2
@@ -15539,7 +15548,7 @@ packages:
focus-trap: 7.5.2
mark.js: 8.11.1
minisearch: 6.1.0
- shiki: 0.14.3
+ shiki: 0.14.4
vite: 4.4.9(@types/node@18.17.5)
vue: 3.3.4
transitivePeerDependencies: