diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 3ddf86ea5..1d1724a5e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -51,18 +51,10 @@ body:
label: Setup
description: |-
Please fill out the below info.
- Note that you only need to fill out one and not both sections.
+ Note that you only need to fill out the relevant section
value: |-
- **Desktop**
-
- - OS and Version: [Windows, Linux, Mac, ...]
+ - Mermaid version:
- Browser and Version: [Chrome, Edge, Firefox]
-
- **Smartphone**
-
- - Device: [Samsung, iPhone, ...]
- - OS and Version: [Android, iOS, ...]
- - Browser and Version: [Chrome, Safari, ...]
- type: textarea
attributes:
label: Additional Context
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6397e5305..6c01ba1b9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,6 +33,14 @@ jobs:
run: |
pnpm run ci --coverage
+ - name: Run ganttDb tests using California timezone
+ env:
+ # Makes sure that gantt db works even in a timezone that has daylight savings
+ # since some days have 25 hours instead of 24.
+ TZ: America/Los_Angeles
+ run: |
+ pnpm exec vitest run ./packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
+
- name: Upload Coverage to Coveralls
# it feels a bit weird to use @master, but that's what the docs use
# (coveralls also doesn't publish a @v1 we can use)
diff --git a/.lycheeignore b/.lycheeignore
index 79cf4428b..4c781f6a0 100644
--- a/.lycheeignore
+++ b/.lycheeignore
@@ -9,8 +9,11 @@ https://mkdocs.org/
https://osawards.com/javascript/#nominees
https://osawards.com/javascript/2019
-# Timeout error, maybe Twitter has anti-bot defences against GitHub's CI servers?
+# Timeout error, maybe Twitter has anti-bot defenses against GitHub's CI servers?
https://twitter.com/mermaidjs_
# Don't check files that are generated during the build via `pnpm docs:code`
packages/mermaid/src/docs/config/setup/*
+
+# Network error: 502, since few days
+https://bundlephobia.com/
diff --git a/cSpell.json b/cSpell.json
index d5fbcfe7c..6f35a0142 100644
--- a/cSpell.json
+++ b/cSpell.json
@@ -19,6 +19,7 @@
"brkt",
"brolin",
"brotli",
+ "città",
"classdef",
"codedoc",
"colour",
diff --git a/cypress/helpers/util.js b/cypress/helpers/util.js
index 533cca499..7ec960b97 100644
--- a/cypress/helpers/util.js
+++ b/cypress/helpers/util.js
@@ -22,7 +22,7 @@ export const mermaidUrl = (graphStr, options, api) => {
return url;
};
-export const imgSnapshotTest = (graphStr, _options, api = false, validation) => {
+export const imgSnapshotTest = (graphStr, _options = {}, api = false, validation = undefined) => {
cy.log(_options);
const options = Object.assign(_options);
if (!options.fontFamily) {
diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js
index 9536a074d..71810cfa4 100644
--- a/cypress/integration/rendering/classDiagram-v2.spec.js
+++ b/cypress/integration/rendering/classDiagram-v2.spec.js
@@ -13,7 +13,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('1: should render a simple class diagram', () => {
@@ -47,7 +46,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('2: should render a simple class diagrams with cardinality', () => {
@@ -76,7 +74,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('should render a simple class diagram with different visibilities', () => {
@@ -94,7 +91,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('should render multiple class diagrams', () => {
@@ -147,7 +143,6 @@ describe('Class diagram V2', () => {
],
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('4: should render a simple class diagram with comments', () => {
@@ -177,7 +172,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('5: should render a simple class diagram with abstract method', () => {
@@ -189,7 +183,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('6: should render a simple class diagram with static method', () => {
@@ -201,7 +194,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('7: should render a simple class diagram with Generic class', () => {
@@ -221,7 +213,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('8: should render a simple class diagram with Generic class and relations', () => {
@@ -242,7 +233,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('9: should render a simple class diagram with clickable link', () => {
@@ -264,7 +254,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('10: should render a simple class diagram with clickable callback', () => {
@@ -286,7 +275,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('11: should render a simple class diagram with return type on method', () => {
@@ -301,7 +289,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('12: should render a simple class diagram with generic types', () => {
@@ -317,7 +304,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('13: should render a simple class diagram with css classes applied', () => {
@@ -335,7 +321,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('14: should render a simple class diagram with css classes applied directly', () => {
@@ -351,7 +336,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('15: should render a simple class diagram with css classes applied two multiple classes', () => {
@@ -365,7 +349,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('16a: should render a simple class diagram with static field', () => {
@@ -378,7 +361,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('16b: should handle the direction statement with TB', () => {
@@ -403,7 +385,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('18: should handle the direction statement with LR', () => {
@@ -428,7 +409,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('17a: should handle the direction statement with BT', () => {
imgSnapshotTest(
@@ -452,7 +432,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('17b: should handle the direction statement with RL', () => {
imgSnapshotTest(
@@ -476,7 +455,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('18: should render a simple class diagram with notes', () => {
@@ -493,7 +471,6 @@ describe('Class diagram V2', () => {
`,
{ logLevel: 1, flowchart: { htmlLabels: false } }
);
- cy.get('svg');
});
it('1433: should render a simple class with a title', () => {
@@ -503,8 +480,72 @@ title: simple class diagram
---
classDiagram-v2
class Class10
-`,
- {}
+`
+ );
+ });
+
+ it('should render a class with text label', () => {
+ imgSnapshotTest(
+ `classDiagram
+ class C1["Class 1 with text label"]
+ C1 --> C2`
+ );
+ });
+
+ it('should render two classes with text labels', () => {
+ imgSnapshotTest(
+ `classDiagram
+ class C1["Class 1 with text label"]
+ class C2["Class 2 with chars @?"]
+ C1 --> C2`
+ );
+ });
+ it('should render a class with a text label, members and annotation', () => {
+ imgSnapshotTest(
+ `classDiagram
+ class C1["Class 1 with text label"] {
+ <<interface>>
+ +member1
+ }
+ C1 --> C2`
+ );
+ });
+ it('should render multiple classes with same text labels', () => {
+ imgSnapshotTest(
+ `classDiagram
+class C1["Class with text label"]
+class C2["Class with text label"]
+class C3["Class with text label"]
+C1 --> C2
+C3 ..> C2
+ `
+ );
+ });
+ it('should render classes with different text labels', () => {
+ imgSnapshotTest(
+ `classDiagram
+class C1["OneWord"]
+class C2["With, Comma"]
+class C3["With (Brackets)"]
+class C4["With [Brackets]"]
+class C5["With {Brackets}"]
+class C7["With 1 number"]
+class C8["With . period..."]
+class C9["With - dash"]
+class C10["With _ underscore"]
+class C11["With ' single quote"]
+class C12["With ~!@#$%^&*()_+=-/?"]
+class C13["With Città foreign language"]
+ `
+ );
+ });
+
+ it('should render classLabel if class has already been defined earlier', () => {
+ imgSnapshotTest(
+ `classDiagram
+ Animal <|-- Duck
+ class Duck["Duck with text label"]
+`
);
});
});
diff --git a/cypress/platform/class.html b/cypress/platform/class.html
index 052dd18b9..2f853bbc1 100644
--- a/cypress/platform/class.html
+++ b/cypress/platform/class.html
@@ -12,7 +12,6 @@
-
-%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
graph TB
a --> b
a --> c
b --> d
c --> d
-
-flowchart-elk LR
- subgraph A
- a --> b
- end
- subgraph B
- b
- end
-
-
-%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
-flowchart TB
- %% I could not figure out how to use double quotes in labels in Mermaid
- subgraph ibm[IBM Espresso CPU]
- core0[IBM PowerPC Broadway Core 0]
- core1[IBM PowerPC Broadway Core 1]
- core2[IBM PowerPC Broadway Core 2]
- rom[16 KB ROM]
-
- core0 --- core2
-
- rom --> core2
- end
-
- subgraph amd[AMD Latte GPU]
- mem[Memory & I/O Bridge]
- dram[DRAM Controller]
- edram[32 MB EDRAM MEM1]
- rom[512 B SEEPROM]
-
- sata[SATA IF]
- exi[EXI]
-
- subgraph gx[GX]
- sram[3 MB 1T-SRAM]
- end
-
- radeon[AMD Radeon R7xx GX2]
-
- mem --- gx
- mem --- radeon
-
- rom --- mem
-
- mem --- sata
- mem --- exi
-
- dram --- sata
- dram --- exi
- end
-
- ddr3[2 GB DDR3 RAM MEM2]
-
- mem --- ddr3
- dram --- ddr3
- edram --- ddr3
-
- core1 --- mem
-
- exi --- rtc
- rtc{{rtc}}
-
-
-
-flowchart TB
- %% I could not figure out how to use double quotes in labels in Mermaid
- subgraph ibm[IBM Espresso CPU]
- core0[IBM PowerPC Broadway Core 0]
- core1[IBM PowerPC Broadway Core 1]
- core2[IBM PowerPC Broadway Core 2]
-
- rom[16 KB ROM]
-
- core0 --- core2
-
- rom --> core2
- end
-
- subgraph amd[AMD Latte GPU]
- mem[Memory & I/O Bridge]
- dram[DRAM Controller]
- edram[32 MB EDRAM MEM1]
- rom[512 B SEEPROM]
-
- sata[SATA IF]
- exi[EXI]
-
- subgraph gx[GX]
- sram[3 MB 1T-SRAM]
- end
-
- radeon[AMD Radeon R7xx GX2]
-
- mem --- gx
- mem --- radeon
-
- rom --- mem
-
- mem --- sata
- mem --- exi
-
- dram --- sata
- dram --- exi
- end
-
- ddr3[2 GB DDR3 RAM MEM2]
-
- mem --- ddr3
- dram --- ddr3
- edram --- ddr3
-
- core1 --- mem
-
- exi --- rtc
- rtc{{rtc}}
-
-
-
-
- flowchart LR
- B1 --be be--x B2
- B1 --bo bo--o B3
- subgraph Ugge
- B2
- B3
- subgraph inner
- B4
- B5
- end
- subgraph inner2
- subgraph deeper
- C4
- C5
- end
- C6
- end
-
- B4 --> C4
-
- B3 -- X --> B4
- B2 --> inner
-
- C4 --> C5
- end
-
- subgraph outer
- B6
- end
- B6 --> B5
-
-
-sequenceDiagram
- Customer->>+Stripe: Makes a payment request
- Stripe->>+Bank: Forwards the payment request to the bank
- Bank->>+Customer: Asks for authorization
- Customer->>+Bank: Provides authorization
- Bank->>+Stripe: Sends a response with payment details
- Stripe->>+Merchant: Sends a notification of payment receipt
- Merchant->>+Stripe: Confirms the payment
- Stripe->>+Customer: Sends a confirmation of payment
- Customer->>+Merchant: Receives goods or services
-
-
- gantt
- title Style today marker (vertical line should be 5px wide and half-transparent blue)
- dateFormat YYYY-MM-DD
- axisFormat %d
- todayMarker stroke-width:5px,stroke:#00f,opacity:0.5
- section Section1
- Today: 1, -1h
-
+
```
-It can also be be turned on via the diagram code as in the diagram:
+It can also be turned on via the diagram code as in the diagram:
```mermaid-example
sequenceDiagram
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index dda5a5162..871ff163b 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -7,8 +7,8 @@
"types": "./dist/mermaid.d.ts",
"exports": {
".": {
- "import": "./dist/mermaid.core.mjs",
- "types": "./dist/mermaid.d.ts"
+ "types": "./dist/mermaid.d.ts",
+ "import": "./dist/mermaid.core.mjs"
},
"./*": "./*"
},
@@ -56,13 +56,13 @@
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.1.0",
"d3": "^7.4.0",
- "dagre-d3-es": "7.0.8",
+ "dagre-d3-es": "7.0.9",
+ "dayjs": "^1.11.7",
"dompurify": "2.4.3",
"elkjs": "^0.8.2",
"katex": "^0.15.2",
"khroma": "^2.0.0",
"lodash-es": "^4.17.21",
- "moment-mini": "^2.29.4",
"non-layered-tidy-tree-layout": "^2.0.2",
"stylis": "^4.1.2",
"ts-dedent": "^2.2.0",
diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts
index c835ee440..39ab3f4d9 100644
--- a/packages/mermaid/src/config.type.ts
+++ b/packages/mermaid/src/config.type.ts
@@ -264,6 +264,10 @@ export interface ClassDiagramConfig extends BaseDiagramConfig {
padding?: number;
textHeight?: number;
defaultRenderer?: string;
+ nodeSpacing?: number;
+ rankSpacing?: number;
+ diagramPadding?: number;
+ htmlLabels?: boolean;
}
export interface JourneyDiagramConfig extends BaseDiagramConfig {
@@ -295,6 +299,7 @@ export interface TimelineDiagramConfig extends BaseDiagramConfig {
leftMargin?: number;
width?: number;
height?: number;
+ padding?: number;
boxMargin?: number;
boxTextMargin?: number;
noteMargin?: number;
@@ -311,6 +316,7 @@ export interface TimelineDiagramConfig extends BaseDiagramConfig {
sectionFills?: string[];
sectionColours?: string[];
disableMulticolor?: boolean;
+ useMaxWidth?: boolean;
}
export interface GanttDiagramConfig extends BaseDiagramConfig {
diff --git a/packages/mermaid/src/dagre-wrapper/nodes.js b/packages/mermaid/src/dagre-wrapper/nodes.js
index 5bd18d077..49b96b685 100644
--- a/packages/mermaid/src/dagre-wrapper/nodes.js
+++ b/packages/mermaid/src/dagre-wrapper/nodes.js
@@ -772,7 +772,7 @@ const class_box = (parent, node) => {
maxWidth += interfaceBBox.width;
}
- let classTitleString = node.classData.id;
+ let classTitleString = node.classData.label;
if (node.classData.type !== undefined && node.classData.type !== '') {
if (getConfig().flowchart.htmlLabels) {
@@ -927,61 +927,6 @@ const class_box = (parent, node) => {
);
verticalPos += classTitleBBox.height + rowPadding;
});
- //
- // let bbox;
- // if (evaluate(getConfig().flowchart.htmlLabels)) {
- // const div = interfaceLabel.children[0];
- // const dv = select(interfaceLabel);
- // bbox = div.getBoundingClientRect();
- // dv.attr('width', bbox.width);
- // dv.attr('height', bbox.height);
- // }
- // bbox = labelContainer.getBBox();
-
- // log.info('Text 2', text2);
- // const textRows = text2.slice(1, text2.length);
- // let titleBox = text.getBBox();
- // const descr = label
- // .node()
- // .appendChild(createLabel(textRows.join('
'), node.labelStyle, true, true));
-
- // if (evaluate(getConfig().flowchart.htmlLabels)) {
- // const div = descr.children[0];
- // const dv = select(descr);
- // bbox = div.getBoundingClientRect();
- // dv.attr('width', bbox.width);
- // dv.attr('height', bbox.height);
- // }
- // // bbox = label.getBBox();
- // // log.info(descr);
- // select(descr).attr(
- // 'transform',
- // 'translate( ' +
- // // (titleBox.width - bbox.width) / 2 +
- // (bbox.width > titleBox.width ? 0 : (titleBox.width - bbox.width) / 2) +
- // ', ' +
- // (titleBox.height + halfPadding + 5) +
- // ')'
- // );
- // select(text).attr(
- // 'transform',
- // 'translate( ' +
- // // (titleBox.width - bbox.width) / 2 +
- // (bbox.width < titleBox.width ? 0 : -(titleBox.width - bbox.width) / 2) +
- // ', ' +
- // 0 +
- // ')'
- // );
- // // Get the size of the label
-
- // // Bounding box for title and text
- // bbox = label.node().getBBox();
-
- // // Center the label
- // label.attr(
- // 'transform',
- // 'translate(' + -bbox.width / 2 + ', ' + (-bbox.height / 2 - halfPadding + 3) + ')'
- // );
rect
.attr('class', 'outer title-state')
@@ -990,13 +935,6 @@ const class_box = (parent, node) => {
.attr('width', maxWidth + node.padding)
.attr('height', maxHeight + node.padding);
- // innerLine
- // .attr('class', 'divider')
- // .attr('x1', -bbox.width / 2 - halfPadding)
- // .attr('x2', bbox.width / 2 + halfPadding)
- // .attr('y1', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding)
- // .attr('y2', -bbox.height / 2 - halfPadding + titleBox.height + halfPadding);
-
updateNodeBounds(node, rect);
node.intersect = function (point) {
diff --git a/packages/mermaid/src/diagram-api/detectType.ts b/packages/mermaid/src/diagram-api/detectType.ts
index 6ffe5df85..0d26487cf 100644
--- a/packages/mermaid/src/diagram-api/detectType.ts
+++ b/packages/mermaid/src/diagram-api/detectType.ts
@@ -46,9 +46,24 @@ export const detectType = function (text: string, config?: MermaidConfig): strin
}
}
- throw new UnknownDiagramError(`No diagram type detected for text: ${text}`);
+ throw new UnknownDiagramError(
+ `No diagram type detected matching given configuration for text: ${text}`
+ );
};
+/**
+ * Registers lazy-loaded diagrams to Mermaid.
+ *
+ * The diagram function is loaded asynchronously, so that diagrams are only loaded
+ * if the diagram is detected.
+ *
+ * @remarks
+ * Please note that the order of diagram detectors is important.
+ * The first detector to return `true` is the diagram that will be loaded
+ * and used, so put more specific detectors at the beginning!
+ *
+ * @param diagrams - Diagrams to lazy load, and their detectors, in order of importance.
+ */
export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinition[]) => {
for (const { id, detector, loader } of diagrams) {
addDetector(id, detector, loader);
@@ -97,4 +112,6 @@ export const addDetector = (key: string, detector: DiagramDetector, loader?: Dia
log.debug(`Detector with key ${key} added${loader ? ' with loader' : ''}`);
};
-export const getDiagramLoader = (key: string) => detectors[key].loader;
+export const getDiagramLoader = (key: string) => {
+ return detectors[key].loader;
+};
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts
new file mode 100644
index 000000000..81909fe5e
--- /dev/null
+++ b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts
@@ -0,0 +1,82 @@
+import { it, describe, expect } from 'vitest';
+import { detectType } from './detectType';
+import { addDiagrams } from './diagram-orchestration';
+
+describe('diagram-orchestration', () => {
+ it('should register diagrams', () => {
+ expect(() => detectType('graph TD; A-->B')).toThrow();
+ addDiagrams();
+ expect(detectType('graph TD; A-->B')).toBe('flowchart');
+ });
+
+ describe('proper diagram types should be detetced', () => {
+ beforeAll(() => {
+ addDiagrams();
+ });
+
+ it.each([
+ { text: 'graph TD;', expected: 'flowchart' },
+ { text: 'flowchart TD;', expected: 'flowchart-v2' },
+ { text: 'flowchart-v2 TD;', expected: 'flowchart-v2' },
+ { text: 'flowchart-elk TD;', expected: 'flowchart-elk' },
+ { text: 'error', expected: 'error' },
+ { text: 'C4Context;', expected: 'c4' },
+ { text: 'classDiagram', expected: 'class' },
+ { text: 'classDiagram-v2', expected: 'classDiagram' },
+ { text: 'erDiagram', expected: 'er' },
+ { text: 'journey', expected: 'journey' },
+ { text: 'gantt', expected: 'gantt' },
+ { text: 'pie', expected: 'pie' },
+ { text: 'requirementDiagram', expected: 'requirement' },
+ { text: 'info', expected: 'info' },
+ { text: 'sequenceDiagram', expected: 'sequence' },
+ { text: 'mindmap', expected: 'mindmap' },
+ { text: 'timeline', expected: 'timeline' },
+ { text: 'gitGraph', expected: 'gitGraph' },
+ { text: 'stateDiagram', expected: 'state' },
+ { text: 'stateDiagram-v2', expected: 'stateDiagram' },
+ ])(
+ 'should $text be detected as $expected',
+ ({ text, expected }: { text: string; expected: string }) => {
+ expect(detectType(text)).toBe(expected);
+ }
+ );
+
+ it('should detect proper flowchart type based on config', () => {
+ // graph & dagre-d3 => flowchart
+ expect(detectType('graph TD; A-->B')).toBe('flowchart');
+ // graph & dagre-d3 => flowchart
+ expect(detectType('graph TD; A-->B', { flowchart: { defaultRenderer: 'dagre-d3' } })).toBe(
+ 'flowchart'
+ );
+ // flowchart & dagre-d3 => error
+ expect(() =>
+ detectType('flowchart TD; A-->B', { flowchart: { defaultRenderer: 'dagre-d3' } })
+ ).toThrowErrorMatchingInlineSnapshot(
+ '"No diagram type detected matching given configuration for text: flowchart TD; A-->B"'
+ );
+
+ // graph & dagre-wrapper => flowchart-v2
+ expect(
+ detectType('graph TD; A-->B', { flowchart: { defaultRenderer: 'dagre-wrapper' } })
+ ).toBe('flowchart-v2');
+ // flowchart ==> flowchart-v2
+ expect(detectType('flowchart TD; A-->B')).toBe('flowchart-v2');
+ // flowchart && dagre-wrapper ==> flowchart-v2
+ expect(
+ detectType('flowchart TD; A-->B', { flowchart: { defaultRenderer: 'dagre-wrapper' } })
+ ).toBe('flowchart-v2');
+ // flowchart && elk ==> flowchart-elk
+ expect(detectType('flowchart TD; A-->B', { flowchart: { defaultRenderer: 'elk' } })).toBe(
+ 'flowchart-elk'
+ );
+ });
+
+ it('should not detect flowchart if pie contains flowchart', () => {
+ expect(
+ detectType(`pie title: "flowchart"
+ flowchart: 1 "pie" pie: 2 "pie"`)
+ ).toBe('pie');
+ });
+ });
+});
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
index d06ce846a..73bfcf084 100644
--- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts
+++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
@@ -45,7 +45,7 @@ export const addDiagrams = () => {
throw new Error(
'Diagrams beginning with --- are not valid. ' +
'If you were trying to use a YAML front-matter, please ensure that ' +
- "you've correctly opened and closed the YAML front-matter with unindented `---` blocks"
+ "you've correctly opened and closed the YAML front-matter with un-indented `---` blocks"
);
},
},
@@ -55,25 +55,26 @@ export const addDiagrams = () => {
return text.toLowerCase().trimStart().startsWith('---');
}
);
+ // Ordering of detectors is important. The first one to return true will be used.
registerLazyLoadedDiagrams(
error,
c4,
- classDiagram,
classDiagramV2,
+ classDiagram,
er,
gantt,
info,
pie,
requirement,
sequence,
- flowchart,
- flowchartV2,
flowchartElk,
+ flowchartV2,
+ flowchart,
mindmap,
timeline,
git,
- state,
stateV2,
+ state,
journey
);
};
diff --git a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts
index 9e04c861f..4bdcc5e39 100644
--- a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts
+++ b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts
@@ -3,6 +3,7 @@ import { getDiagram, registerDiagram } from './diagramAPI';
import { addDiagrams } from './diagram-orchestration';
import { DiagramDetector } from './types';
import { getDiagramFromText } from '../Diagram';
+import { it, describe, expect, beforeAll } from 'vitest';
addDiagrams();
beforeAll(async () => {
@@ -15,13 +16,17 @@ describe('DiagramAPI', () => {
});
it('should throw error if diagram is not defined', () => {
- expect(() => getDiagram('loki')).toThrow();
+ expect(() => getDiagram('loki')).toThrowErrorMatchingInlineSnapshot(
+ '"Diagram loki not found."'
+ );
});
it('should handle diagram registrations', () => {
- expect(() => getDiagram('loki')).toThrow();
- expect(() => detectType('loki diagram')).toThrow(
- 'No diagram type detected for text: loki diagram'
+ expect(() => getDiagram('loki')).toThrowErrorMatchingInlineSnapshot(
+ '"Diagram loki not found."'
+ );
+ expect(() => detectType('loki diagram')).toThrowErrorMatchingInlineSnapshot(
+ '"No diagram type detected matching given configuration for text: loki diagram"'
);
const detector: DiagramDetector = (str: string) => {
return str.match('loki') !== null;
diff --git a/packages/mermaid/src/diagram.spec.ts b/packages/mermaid/src/diagram.spec.ts
index a862c7936..5a4718d0b 100644
--- a/packages/mermaid/src/diagram.spec.ts
+++ b/packages/mermaid/src/diagram.spec.ts
@@ -61,8 +61,8 @@ Expecting 'TXT', got 'NEWLINE'"
});
test('should throw the right error for unregistered diagrams', async () => {
- await expect(getDiagramFromText('thor TD; A-->B')).rejects.toThrowError(
- 'No diagram type detected for text: thor TD; A-->B'
+ await expect(getDiagramFromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
+ '"No diagram type detected matching given configuration for text: thor TD; A-->B"'
);
});
});
diff --git a/packages/mermaid/src/diagrams/class/classDb.js b/packages/mermaid/src/diagrams/class/classDb.ts
similarity index 66%
rename from packages/mermaid/src/diagrams/class/classDb.js
rename to packages/mermaid/src/diagrams/class/classDb.ts
index 2c6690e39..48ef7ccbe 100644
--- a/packages/mermaid/src/diagrams/class/classDb.js
+++ b/packages/mermaid/src/diagrams/class/classDb.ts
@@ -1,4 +1,5 @@
-import { select } from 'd3';
+// @ts-expect-error - d3 types issue
+import { select, Selection } from 'd3';
import { log } from '../../logger';
import * as configApi from '../../config';
import common from '../common/common';
@@ -13,44 +14,54 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../../commonDb';
+import { ClassRelation, ClassNode, ClassNote, ClassMap } from './classTypes';
-const MERMAID_DOM_ID_PREFIX = 'classid-';
+const MERMAID_DOM_ID_PREFIX = 'classId-';
-let relations = [];
-let classes = {};
-let notes = [];
+let relations: ClassRelation[] = [];
+let classes: ClassMap = {};
+let notes: ClassNote[] = [];
let classCounter = 0;
-let funs = [];
+let functions: any[] = [];
-const sanitizeText = (txt) => common.sanitizeText(txt, configApi.getConfig());
+const sanitizeText = (txt: string) => common.sanitizeText(txt, configApi.getConfig());
-export const parseDirective = function (statement, context, type) {
+export const parseDirective = function (statement: string, context: string, type: string) {
+ // @ts-ignore Don't wanna mess it up
mermaidAPI.parseDirective(this, statement, context, type);
};
-const splitClassNameAndType = function (id) {
+const splitClassNameAndType = function (id: string) {
let genericType = '';
let className = id;
if (id.indexOf('~') > 0) {
- let split = id.split('~');
- className = split[0];
-
- genericType = common.sanitizeText(split[1], configApi.getConfig());
+ const split = id.split('~');
+ className = sanitizeText(split[0]);
+ genericType = sanitizeText(split[1]);
}
return { className: className, type: genericType };
};
+export const setClassLabel = function (id: string, label: string) {
+ if (label) {
+ label = sanitizeText(label);
+ }
+
+ const { className } = splitClassNameAndType(id);
+ classes[className].label = label;
+};
+
/**
* Function called by parser when a node definition has been found.
*
- * @param id
+ * @param id - Id of the class to add
* @public
*/
-export const addClass = function (id) {
- let classId = splitClassNameAndType(id);
+export const addClass = function (id: string) {
+ const classId = splitClassNameAndType(id);
// Only add class if not exists
if (classes[classId.className] !== undefined) {
return;
@@ -59,12 +70,13 @@ export const addClass = function (id) {
classes[classId.className] = {
id: classId.className,
type: classId.type,
+ label: classId.className,
cssClasses: [],
methods: [],
members: [],
annotations: [],
domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter,
- };
+ } as ClassNode;
classCounter++;
};
@@ -72,35 +84,33 @@ export const addClass = function (id) {
/**
* Function to lookup domId from id in the graph definition.
*
- * @param id
+ * @param id - class ID to lookup
* @public
*/
-export const lookUpDomId = function (id) {
- const classKeys = Object.keys(classes);
- for (const classKey of classKeys) {
- if (classes[classKey].id === id) {
- return classes[classKey].domId;
- }
+export const lookUpDomId = function (id: string): string {
+ if (id in classes) {
+ return classes[id].domId;
}
+ throw new Error('Class not found: ' + id);
};
export const clear = function () {
relations = [];
classes = {};
notes = [];
- funs = [];
- funs.push(setupToolTips);
+ functions = [];
+ functions.push(setupToolTips);
commonClear();
};
-export const getClass = function (id) {
+export const getClass = function (id: string) {
return classes[id];
};
export const getClasses = function () {
return classes;
};
-export const getRelations = function () {
+export const getRelations = function (): ClassRelation[] {
return relations;
};
@@ -108,7 +118,7 @@ export const getNotes = function () {
return notes;
};
-export const addRelation = function (relation) {
+export const addRelation = function (relation: ClassRelation) {
log.debug('Adding relation: ' + JSON.stringify(relation));
addClass(relation.id1);
addClass(relation.id2);
@@ -133,11 +143,11 @@ export const addRelation = function (relation) {
* Adds an annotation to the specified class Annotations mark special properties of the given type
* (like 'interface' or 'service')
*
- * @param className The class name
- * @param annotation The name of the annotation without any brackets
+ * @param className - The class name
+ * @param annotation - The name of the annotation without any brackets
* @public
*/
-export const addAnnotation = function (className, annotation) {
+export const addAnnotation = function (className: string, annotation: string) {
const validatedClassName = splitClassNameAndType(className).className;
classes[validatedClassName].annotations.push(annotation);
};
@@ -145,13 +155,13 @@ export const addAnnotation = function (className, annotation) {
/**
* Adds a member to the specified class
*
- * @param className The class name
- * @param member The full name of the member. If the member is enclosed in <> it is
+ * @param className - The class name
+ * @param member - The full name of the member. If the member is enclosed in `<>` it is
* treated as an annotation If the member is ending with a closing bracket ) it is treated as a
* method Otherwise the member will be treated as a normal property
* @public
*/
-export const addMember = function (className, member) {
+export const addMember = function (className: string, member: string) {
const validatedClassName = splitClassNameAndType(className).className;
const theClass = classes[validatedClassName];
@@ -161,7 +171,6 @@ export const addMember = function (className, member) {
if (memberString.startsWith('<<') && memberString.endsWith('>>')) {
// Remove leading and trailing brackets
- // theClass.annotations.push(memberString.substring(2, memberString.length - 2));
theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2)));
} else if (memberString.indexOf(')') > 0) {
theClass.methods.push(sanitizeText(memberString));
@@ -171,14 +180,14 @@ export const addMember = function (className, member) {
}
};
-export const addMembers = function (className, members) {
+export const addMembers = function (className: string, members: string[]) {
if (Array.isArray(members)) {
members.reverse();
members.forEach((member) => addMember(className, member));
}
};
-export const addNote = function (text, className) {
+export const addNote = function (text: string, className: string) {
const note = {
id: `note${notes.length}`,
class: className,
@@ -187,21 +196,20 @@ export const addNote = function (text, className) {
notes.push(note);
};
-export const cleanupLabel = function (label) {
- if (label.substring(0, 1) === ':') {
- return common.sanitizeText(label.substr(1).trim(), configApi.getConfig());
- } else {
- return sanitizeText(label.trim());
+export const cleanupLabel = function (label: string) {
+ if (label.startsWith(':')) {
+ label = label.substring(1);
}
+ return sanitizeText(label.trim());
};
/**
* Called by parser when a special node is found, e.g. a clickable element.
*
- * @param ids Comma separated list of ids
- * @param className Class to add
+ * @param ids - Comma separated list of ids
+ * @param className - Class to add
*/
-export const setCssClass = function (ids, className) {
+export const setCssClass = function (ids: string, className: string) {
ids.split(',').forEach(function (_id) {
let id = _id;
if (_id[0].match(/\d/)) {
@@ -216,28 +224,27 @@ export const setCssClass = function (ids, className) {
/**
* Called by parser when a tooltip is found, e.g. a clickable element.
*
- * @param ids Comma separated list of ids
- * @param tooltip Tooltip to add
+ * @param ids - Comma separated list of ids
+ * @param tooltip - Tooltip to add
*/
-const setTooltip = function (ids, tooltip) {
- const config = configApi.getConfig();
+const setTooltip = function (ids: string, tooltip?: string) {
ids.split(',').forEach(function (id) {
if (tooltip !== undefined) {
- classes[id].tooltip = common.sanitizeText(tooltip, config);
+ classes[id].tooltip = sanitizeText(tooltip);
}
});
};
-export const getTooltip = function (id) {
+export const getTooltip = function (id: string) {
return classes[id].tooltip;
};
/**
* Called by parser when a link is found. Adds the URL to the vertex data.
*
- * @param ids Comma separated list of ids
- * @param linkStr URL to create a link for
- * @param target Target of the link, _blank by default as originally defined in the svgDraw.js file
+ * @param ids - Comma separated list of ids
+ * @param linkStr - URL to create a link for
+ * @param target - Target of the link, _blank by default as originally defined in the svgDraw.js file
*/
-export const setLink = function (ids, linkStr, target) {
+export const setLink = function (ids: string, linkStr: string, target: string) {
const config = configApi.getConfig();
ids.split(',').forEach(function (_id) {
let id = _id;
@@ -261,11 +268,11 @@ export const setLink = function (ids, linkStr, target) {
/**
* Called by parser when a click definition is found. Registers an event handler.
*
- * @param ids Comma separated list of ids
- * @param functionName Function to be called on click
- * @param functionArgs Function args the function should be called with
+ * @param ids - Comma separated list of ids
+ * @param functionName - Function to be called on click
+ * @param functionArgs - Function args the function should be called with
*/
-export const setClickEvent = function (ids, functionName, functionArgs) {
+export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) {
ids.split(',').forEach(function (id) {
setClickFunc(id, functionName, functionArgs);
classes[id].haveCallback = true;
@@ -273,19 +280,19 @@ export const setClickEvent = function (ids, functionName, functionArgs) {
setCssClass(ids, 'clickable');
};
-const setClickFunc = function (domId, functionName, functionArgs) {
+const setClickFunc = function (domId: string, functionName: string, functionArgs: string) {
const config = configApi.getConfig();
- let id = domId;
- let elemId = lookUpDomId(id);
-
if (config.securityLevel !== 'loose') {
return;
}
if (functionName === undefined) {
return;
}
+
+ const id = domId;
if (classes[id] !== undefined) {
- let argList = [];
+ const elemId = lookUpDomId(id);
+ let argList: string[] = [];
if (typeof functionArgs === 'string') {
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
@@ -305,7 +312,7 @@ const setClickFunc = function (domId, functionName, functionArgs) {
argList.push(elemId);
}
- funs.push(function () {
+ functions.push(function () {
const elem = document.querySelector(`[id="${elemId}"]`);
if (elem !== null) {
elem.addEventListener(
@@ -320,8 +327,8 @@ const setClickFunc = function (domId, functionName, functionArgs) {
}
};
-export const bindFunctions = function (element) {
- funs.forEach(function (fun) {
+export const bindFunctions = function (element: Element) {
+ functions.forEach(function (fun) {
fun(element);
});
};
@@ -339,8 +346,10 @@ export const relationType = {
LOLLIPOP: 4,
};
-const setupToolTips = function (element) {
- let tooltipElem = select('.mermaidTooltip');
+const setupToolTips = function (element: Element) {
+ let tooltipElem: Selection =
+ select('.mermaidTooltip');
+ // @ts-ignore - _groups is a dynamic property
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
}
@@ -350,12 +359,14 @@ const setupToolTips = function (element) {
const nodes = svg.selectAll('g.node');
nodes
.on('mouseover', function () {
+ // @ts-expect-error - select is not part of the d3 type definition
const el = select(this);
const title = el.attr('title');
- // Dont try to draw a tooltip if no data is provided
+ // Don't try to draw a tooltip if no data is provided
if (title === null) {
return;
}
+ // @ts-ignore - getBoundingClientRect is not part of the d3 type definition
const rect = this.getBoundingClientRect();
tooltipElem.transition().duration(200).style('opacity', '.9');
@@ -368,15 +379,16 @@ const setupToolTips = function (element) {
})
.on('mouseout', function () {
tooltipElem.transition().duration(500).style('opacity', 0);
+ // @ts-expect-error - select is not part of the d3 type definition
const el = select(this);
el.classed('hover', false);
});
};
-funs.push(setupToolTips);
+functions.push(setupToolTips);
let direction = 'TB';
const getDirection = () => direction;
-const setDirection = (dir) => {
+const setDirection = (dir: string) => {
direction = dir;
};
@@ -412,4 +424,5 @@ export default {
lookUpDomId,
setDiagramTitle,
getDiagramTitle,
+ setClassLabel,
};
diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.js b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
similarity index 81%
rename from packages/mermaid/src/diagrams/class/classDiagram.spec.js
rename to packages/mermaid/src/diagrams/class/classDiagram.spec.ts
index 04a8e9bf3..5ff193186 100644
--- a/packages/mermaid/src/diagrams/class/classDiagram.spec.js
+++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts
@@ -1,10 +1,11 @@
+// @ts-expect-error Jison doesn't export types
import { parser } from './parser/classDiagram';
import classDb from './classDb';
-import { vi } from 'vitest';
+import { vi, describe, it, expect } from 'vitest';
const spyOn = vi.spyOn;
describe('class diagram, ', function () {
- describe('when parsing an info graph it', function () {
+ describe('when parsing a class diagram', function () {
beforeEach(function () {
parser.yy = classDb;
});
@@ -541,7 +542,7 @@ foo()
});
});
- describe('when fetching data from a classDiagram graph it', function () {
+ describe('when fetching data from a classDiagram it', function () {
beforeEach(function () {
parser.yy = classDb;
parser.yy.clear();
@@ -946,4 +947,189 @@ foo()
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip');
});
});
+
+ describe('when parsing classDiagram with text labels', () => {
+ beforeEach(function () {
+ parser.yy = classDb;
+ parser.yy.clear();
+ });
+
+ it('should parse a class with a text label', () => {
+ parser.parse(`classDiagram
+ class C1["Class 1 with text label"]
+ C1 --> C2
+ `);
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ const c2 = classDb.getClass('C2');
+ expect(c2.label).toBe('C2');
+ });
+
+ it('should parse two classes with text labels', () => {
+ parser.parse(`classDiagram
+ class C1["Class 1 with text label"]
+ class C2["Class 2 with chars @?"]
+ C1 --> C2
+ `);
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ const c2 = classDb.getClass('C2');
+ expect(c2.label).toBe('Class 2 with chars @?');
+ });
+
+ it('should parse a class with a text label and members', () => {
+ parser.parse(`classDiagram
+ class C1["Class 1 with text label"] {
+ +member1
+ }
+ C1 --> C2
+ `);
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ expect(c1.members.length).toBe(1);
+ expect(c1.members[0]).toBe('+member1');
+
+ const c2 = classDb.getClass('C2');
+ expect(c2.label).toBe('C2');
+ });
+
+ it('should parse a class with a text label, members and annotation', () => {
+ parser.parse(`classDiagram
+ class C1["Class 1 with text label"] {
+ <>
+ +member1
+ }
+ C1 --> C2
+ `);
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ expect(c1.members.length).toBe(1);
+ expect(c1.members[0]).toBe('+member1');
+ expect(c1.annotations.length).toBe(1);
+ expect(c1.annotations[0]).toBe('interface');
+
+ const c2 = classDb.getClass('C2');
+ expect(c2.label).toBe('C2');
+ });
+
+ it('should parse a class with text label and css class shorthand', () => {
+ parser.parse(`classDiagram
+class C1["Class 1 with text label"]:::styleClass {
+ +member1
+}
+C1 --> C2
+ `);
+
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ expect(c1.cssClasses.length).toBe(1);
+ expect(c1.cssClasses[0]).toBe('styleClass');
+ });
+
+ it('should parse a class with text label and css class', () => {
+ parser.parse(`classDiagram
+class C1["Class 1 with text label"] {
+ +member1
+}
+C1 --> C2
+cssClass "C1" styleClass
+ `);
+
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ expect(c1.cssClasses.length).toBe(1);
+ expect(c1.cssClasses[0]).toBe('styleClass');
+ });
+
+ it('should parse two classes with text labels and css classes', () => {
+ parser.parse(`classDiagram
+class C1["Class 1 with text label"] {
+ +member1
+}
+class C2["Long long long long long long long long long long label"]
+C1 --> C2
+cssClass "C1,C2" styleClass
+ `);
+
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ expect(c1.cssClasses.length).toBe(1);
+ expect(c1.cssClasses[0]).toBe('styleClass');
+
+ const c2 = classDb.getClass('C2');
+ expect(c2.label).toBe('Long long long long long long long long long long label');
+ expect(c2.cssClasses.length).toBe(1);
+ expect(c2.cssClasses[0]).toBe('styleClass');
+ });
+
+ it('should parse two classes with text labels and css class shorthands', () => {
+ parser.parse(`classDiagram
+class C1["Class 1 with text label"]:::styleClass1 {
+ +member1
+}
+class C2["Class 2 !@#$%^&*() label"]:::styleClass2
+C1 --> C2
+ `);
+
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class 1 with text label');
+ expect(c1.cssClasses.length).toBe(1);
+ expect(c1.cssClasses[0]).toBe('styleClass1');
+
+ const c2 = classDb.getClass('C2');
+ expect(c2.label).toBe('Class 2 !@#$%^&*() label');
+ expect(c2.cssClasses.length).toBe(1);
+ expect(c2.cssClasses[0]).toBe('styleClass2');
+ });
+
+ it('should parse multiple classes with same text labels', () => {
+ parser.parse(`classDiagram
+class C1["Class with text label"]
+class C2["Class with text label"]
+class C3["Class with text label"]
+C1 --> C2
+C3 ..> C2
+ `);
+
+ const c1 = classDb.getClass('C1');
+ expect(c1.label).toBe('Class with text label');
+
+ const c2 = classDb.getClass('C2');
+ expect(c2.label).toBe('Class with text label');
+
+ const c3 = classDb.getClass('C3');
+ expect(c3.label).toBe('Class with text label');
+ });
+
+ it('should parse classes with different text labels', () => {
+ parser.parse(`classDiagram
+class C1["OneWord"]
+class C2["With, Comma"]
+class C3["With (Brackets)"]
+class C4["With [Brackets]"]
+class C5["With {Brackets}"]
+class C6[" "]
+class C7["With 1 number"]
+class C8["With . period..."]
+class C9["With - dash"]
+class C10["With _ underscore"]
+class C11["With ' single quote"]
+class C12["With ~!@#$%^&*()_+=-/?"]
+class C13["With Città foreign language"]
+`);
+ expect(classDb.getClass('C1').label).toBe('OneWord');
+ expect(classDb.getClass('C2').label).toBe('With, Comma');
+ expect(classDb.getClass('C3').label).toBe('With (Brackets)');
+ expect(classDb.getClass('C4').label).toBe('With [Brackets]');
+ expect(classDb.getClass('C5').label).toBe('With {Brackets}');
+ expect(classDb.getClass('C6').label).toBe(' ');
+ expect(classDb.getClass('C7').label).toBe('With 1 number');
+ expect(classDb.getClass('C8').label).toBe('With . period...');
+ expect(classDb.getClass('C9').label).toBe('With - dash');
+ expect(classDb.getClass('C10').label).toBe('With _ underscore');
+ expect(classDb.getClass('C11').label).toBe("With ' single quote");
+ expect(classDb.getClass('C12').label).toBe('With ~!@#$%^&*()_+=-/?');
+ expect(classDb.getClass('C13').label).toBe('With Città foreign language');
+ });
+ });
});
diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.js b/packages/mermaid/src/diagrams/class/classRenderer-v2.js
deleted file mode 100644
index b7e538583..000000000
--- a/packages/mermaid/src/diagrams/class/classRenderer-v2.js
+++ /dev/null
@@ -1,499 +0,0 @@
-import { select } from 'd3';
-import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
-import { log } from '../../logger';
-import { getConfig } from '../../config';
-import { render } from '../../dagre-wrapper/index.js';
-import utils from '../../utils';
-import { curveLinear } from 'd3';
-import { interpolateToCurve, getStylesFromArray } from '../../utils';
-import { setupGraphViewbox } from '../../setupGraphViewbox';
-import common from '../common/common';
-
-const sanitizeText = (txt) => common.sanitizeText(txt, getConfig());
-
-let conf = {
- dividerMargin: 10,
- padding: 5,
- textHeight: 10,
-};
-
-/**
- * Function that adds the vertices found during parsing to the graph to be rendered.
- *
- * @param {Object<
- * string,
- * { cssClasses: string[]; text: string; id: string; type: string; domId: string }
- * >} classes
- * Object containing the vertices.
- * @param {SVGGElement} g The graph that is to be drawn.
- * @param _id
- * @param diagObj
- */
-export const addClasses = function (classes, g, _id, diagObj) {
- // const svg = select(`[id="${svgId}"]`);
- const keys = Object.keys(classes);
- log.info('keys:', keys);
- log.info(classes);
-
- // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
- keys.forEach(function (id) {
- const vertex = classes[id];
-
- /**
- * Variable for storing the classes for the vertex
- *
- * @type {string}
- */
- let cssClassStr = '';
- if (vertex.cssClasses.length > 0) {
- cssClassStr = cssClassStr + ' ' + vertex.cssClasses.join(' ');
- }
- // if (vertex.classes.length > 0) {
- // classStr = vertex.classes.join(' ');
- // }
-
- const styles = { labelStyle: '' }; //getStylesFromArray(vertex.styles);
-
- // Use vertex id as text in the box if no text is provided by the graph definition
- let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
-
- // We create a SVG label, either by delegating to addHtmlLabel or manually
- // let vertexNode;
- // if (evaluate(getConfig().flowchart.htmlLabels)) {
- // const node = {
- // label: vertexText.replace(
- // eslint-disable-next-line @cspell/spellchecker
- // /fa[lrsb]?:fa-[\w-]+/g,
- // s => ``
- // )
- // };
- // vertexNode = addHtmlLabel(svg, node).node();
- // vertexNode.parentNode.removeChild(vertexNode);
- // } else {
- // const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
- // svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
-
- // const rows = vertexText.split(common.lineBreakRegex);
-
- // for (let j = 0; j < rows.length; j++) {
- // const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
- // tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
- // tspan.setAttribute('dy', '1em');
- // tspan.setAttribute('x', '1');
- // tspan.textContent = rows[j];
- // svgLabel.appendChild(tspan);
- // }
- // vertexNode = svgLabel;
- // }
-
- let radious = 0;
- let _shape = '';
- // Set the shape based parameters
- switch (vertex.type) {
- case 'class':
- _shape = 'class_box';
- break;
- default:
- _shape = 'class_box';
- }
- // Add the node
- g.setNode(vertex.id, {
- labelStyle: styles.labelStyle,
- shape: _shape,
- labelText: sanitizeText(vertexText),
- classData: vertex,
- rx: radious,
- ry: radious,
- class: cssClassStr,
- style: styles.style,
- id: vertex.id,
- domId: vertex.domId,
- tooltip: diagObj.db.getTooltip(vertex.id) || '',
- haveCallback: vertex.haveCallback,
- link: vertex.link,
- width: vertex.type === 'group' ? 500 : undefined,
- type: vertex.type,
- padding: getConfig().flowchart.padding,
- });
-
- log.info('setNode', {
- labelStyle: styles.labelStyle,
- shape: _shape,
- labelText: vertexText,
- rx: radious,
- ry: radious,
- class: cssClassStr,
- style: styles.style,
- id: vertex.id,
- width: vertex.type === 'group' ? 500 : undefined,
- type: vertex.type,
- padding: getConfig().flowchart.padding,
- });
- });
-};
-
-/**
- * Function that adds the additional vertices (notes) found during parsing to the graph to be rendered.
- *
- * @param {{text: string; class: string; placement: number}[]} notes
- * Object containing the additional vertices (notes).
- * @param {SVGGElement} g The graph that is to be drawn.
- * @param {number} startEdgeId starting index for note edge
- * @param classes
- */
-export const addNotes = function (notes, g, startEdgeId, classes) {
- log.info(notes);
-
- // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
- notes.forEach(function (note, i) {
- const vertex = note;
-
- /**
- * Variable for storing the classes for the vertex
- *
- * @type {string}
- */
- let cssNoteStr = '';
-
- const styles = { labelStyle: '', style: '' };
-
- // Use vertex id as text in the box if no text is provided by the graph definition
- let vertexText = vertex.text;
-
- let radious = 0;
- let _shape = 'note';
- // Add the node
- g.setNode(vertex.id, {
- labelStyle: styles.labelStyle,
- shape: _shape,
- labelText: sanitizeText(vertexText),
- noteData: vertex,
- rx: radious,
- ry: radious,
- class: cssNoteStr,
- style: styles.style,
- id: vertex.id,
- domId: vertex.id,
- tooltip: '',
- type: 'note',
- padding: getConfig().flowchart.padding,
- });
-
- log.info('setNode', {
- labelStyle: styles.labelStyle,
- shape: _shape,
- labelText: vertexText,
- rx: radious,
- ry: radious,
- style: styles.style,
- id: vertex.id,
- type: 'note',
- padding: getConfig().flowchart.padding,
- });
-
- if (!vertex.class || !(vertex.class in classes)) {
- return;
- }
- const edgeId = startEdgeId + i;
- const edgeData = {};
- //Set relationship style and line type
- edgeData.classes = 'relation';
- edgeData.pattern = 'dotted';
-
- edgeData.id = `edgeNote${edgeId}`;
- // Set link type for rendering
- edgeData.arrowhead = 'none';
-
- log.info(`Note edge: ${JSON.stringify(edgeData)}, ${JSON.stringify(vertex)}`);
- //Set edge extra labels
- edgeData.startLabelRight = '';
- edgeData.endLabelLeft = '';
-
- //Set relation arrow types
- edgeData.arrowTypeStart = 'none';
- edgeData.arrowTypeEnd = 'none';
- let style = 'fill:none';
- let labelStyle = '';
-
- edgeData.style = style;
- edgeData.labelStyle = labelStyle;
-
- edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
-
- // Add the edge to the graph
- g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
- });
-};
-
-/**
- * Add edges to graph based on parsed graph definition
- *
- * @param relations
- * @param {object} g The graph object
- */
-export const addRelations = function (relations, g) {
- const conf = getConfig().flowchart;
- let cnt = 0;
-
- let defaultStyle;
- let defaultLabelStyle;
-
- // if (typeof relations.defaultStyle !== 'undefined') {
- // const defaultStyles = getStylesFromArray(relations.defaultStyle);
- // defaultStyle = defaultStyles.style;
- // defaultLabelStyle = defaultStyles.labelStyle;
- // }
-
- relations.forEach(function (edge) {
- cnt++;
- const edgeData = {};
- //Set relationship style and line type
- edgeData.classes = 'relation';
- edgeData.pattern = edge.relation.lineType == 1 ? 'dashed' : 'solid';
-
- edgeData.id = 'id' + cnt;
- // Set link type for rendering
- if (edge.type === 'arrow_open') {
- edgeData.arrowhead = 'none';
- } else {
- edgeData.arrowhead = 'normal';
- }
-
- log.info(edgeData, edge);
- //Set edge extra labels
- //edgeData.startLabelLeft = edge.relationTitle1;
- edgeData.startLabelRight = edge.relationTitle1 === 'none' ? '' : edge.relationTitle1;
- edgeData.endLabelLeft = edge.relationTitle2 === 'none' ? '' : edge.relationTitle2;
- //edgeData.endLabelRight = edge.relationTitle2;
-
- //Set relation arrow types
- edgeData.arrowTypeStart = getArrowMarker(edge.relation.type1);
- edgeData.arrowTypeEnd = getArrowMarker(edge.relation.type2);
- let style = '';
- let labelStyle = '';
-
- if (edge.style !== undefined) {
- const styles = getStylesFromArray(edge.style);
- style = styles.style;
- labelStyle = styles.labelStyle;
- } else {
- style = 'fill:none';
- if (defaultStyle !== undefined) {
- style = defaultStyle;
- }
- if (defaultLabelStyle !== undefined) {
- labelStyle = defaultLabelStyle;
- }
- }
-
- edgeData.style = style;
- edgeData.labelStyle = labelStyle;
-
- if (edge.interpolate !== undefined) {
- edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
- } else if (relations.defaultInterpolate !== undefined) {
- edgeData.curve = interpolateToCurve(relations.defaultInterpolate, curveLinear);
- } else {
- edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
- }
-
- edge.text = edge.title;
- if (edge.text === undefined) {
- if (edge.style !== undefined) {
- edgeData.arrowheadStyle = 'fill: #333';
- }
- } else {
- edgeData.arrowheadStyle = 'fill: #333';
- edgeData.labelpos = 'c';
-
- if (getConfig().flowchart.htmlLabels) {
- edgeData.labelType = 'html';
- edgeData.label = '' + edge.text + '';
- } else {
- edgeData.labelType = 'text';
- edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
-
- if (edge.style === undefined) {
- edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
- }
-
- edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
- }
- }
- // Add the edge to the graph
- g.setEdge(edge.id1, edge.id2, edgeData, cnt);
- });
-};
-
-/**
- * Merges the value of `conf` with the passed `cnf`
- *
- * @param {object} cnf Config to merge
- */
-export const setConf = function (cnf) {
- const keys = Object.keys(cnf);
-
- keys.forEach(function (key) {
- conf[key] = cnf[key];
- });
-};
-
-/**
- * Draws a flowchart in the tag with id: id based on the graph definition in text.
- *
- * @param {string} text
- * @param {string} id
- * @param _version
- * @param diagObj
- */
-export const draw = function (text, id, _version, diagObj) {
- log.info('Drawing class - ', id);
-
- const conf = getConfig().flowchart;
- const securityLevel = getConfig().securityLevel;
- log.info('config:', conf);
- const nodeSpacing = conf.nodeSpacing || 50;
- const rankSpacing = conf.rankSpacing || 50;
-
- // Create the input mermaid.graph
- const g = new graphlib.Graph({
- multigraph: true,
- compound: true,
- })
- .setGraph({
- rankdir: diagObj.db.getDirection(),
- nodesep: nodeSpacing,
- ranksep: rankSpacing,
- marginx: 8,
- marginy: 8,
- })
- .setDefaultEdgeLabel(function () {
- return {};
- });
-
- // Fetch the vertices/nodes and edges/links from the parsed graph definition
- const classes = diagObj.db.getClasses();
- const relations = diagObj.db.getRelations();
- const notes = diagObj.db.getNotes();
-
- log.info(relations);
- addClasses(classes, g, id, diagObj);
- addRelations(relations, g);
- addNotes(notes, g, relations.length + 1, classes);
-
- // Add custom shapes
- // flowChartShapes.addToRenderV2(addShape);
-
- // Set up an SVG group so that we can translate the final graph.
- let sandboxElement;
- if (securityLevel === 'sandbox') {
- sandboxElement = select('#i' + id);
- }
- const root =
- securityLevel === 'sandbox'
- ? select(sandboxElement.nodes()[0].contentDocument.body)
- : select('body');
- const svg = root.select(`[id="${id}"]`);
-
- // Run the renderer. This is what draws the final graph.
- const element = root.select('#' + id + ' g');
- render(
- element,
- g,
- ['aggregation', 'extension', 'composition', 'dependency', 'lollipop'],
- 'classDiagram',
- id
- );
-
- utils.insertTitle(svg, 'classTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());
-
- setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth);
-
- // Add label rects for non html labels
- if (!conf.htmlLabels) {
- const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
- const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
- for (const label of labels) {
- // Get dimensions of label
- const dim = label.getBBox();
-
- const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
- rect.setAttribute('rx', 0);
- rect.setAttribute('ry', 0);
- rect.setAttribute('width', dim.width);
- rect.setAttribute('height', dim.height);
- // rect.setAttribute('style', 'fill:#e8e8e8;');
-
- label.insertBefore(rect, label.firstChild);
- }
- }
-
- // If node has a link, wrap it in an anchor SVG object.
- // const keys = Object.keys(classes);
- // keys.forEach(function(key) {
- // const vertex = classes[key];
-
- // if (vertex.link) {
- // const node = select('#' + id + ' [id="' + key + '"]');
- // if (node) {
- // const link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
- // link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' '));
- // link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link);
- // link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener');
-
- // const linkNode = node.insert(function() {
- // return link;
- // }, ':first-child');
-
- // const shape = node.select('.label-container');
- // if (shape) {
- // linkNode.append(function() {
- // return shape.node();
- // });
- // }
-
- // const label = node.select('.label');
- // if (label) {
- // linkNode.append(function() {
- // return label.node();
- // });
- // }
- // }
- // }
- // });
-};
-
-/**
- * Gets the arrow marker for a type index
- *
- * @param {number} type The type to look for
- * @returns {'aggregation' | 'extension' | 'composition' | 'dependency'} The arrow marker
- */
-function getArrowMarker(type) {
- let marker;
- switch (type) {
- case 0:
- marker = 'aggregation';
- break;
- case 1:
- marker = 'extension';
- break;
- case 2:
- marker = 'composition';
- break;
- case 3:
- marker = 'dependency';
- break;
- case 4:
- marker = 'lollipop';
- break;
- default:
- marker = 'none';
- }
- return marker;
-}
-
-export default {
- setConf,
- draw,
-};
diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts
new file mode 100644
index 000000000..e308990c6
--- /dev/null
+++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts
@@ -0,0 +1,368 @@
+// @ts-ignore d3 types are not available
+import { select, curveLinear } from 'd3';
+import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
+import { log } from '../../logger';
+import { getConfig } from '../../config';
+import { render } from '../../dagre-wrapper/index.js';
+import utils from '../../utils';
+import { interpolateToCurve, getStylesFromArray } from '../../utils';
+import { setupGraphViewbox } from '../../setupGraphViewbox';
+import common from '../common/common';
+import { ClassRelation, ClassNote, ClassMap, EdgeData } from './classTypes';
+
+const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
+
+let conf = {
+ dividerMargin: 10,
+ padding: 5,
+ textHeight: 10,
+ curve: undefined,
+};
+
+/**
+ * Function that adds the vertices found during parsing to the graph to be rendered.
+ *
+ * @param classes - Object containing the vertices.
+ * @param g - The graph that is to be drawn.
+ * @param _id - id of the graph
+ * @param diagObj - The diagram object
+ */
+export const addClasses = function (
+ classes: ClassMap,
+ g: graphlib.Graph,
+ _id: string,
+ diagObj: any
+) {
+ const keys = Object.keys(classes);
+ log.info('keys:', keys);
+ log.info(classes);
+
+ // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
+ keys.forEach(function (id) {
+ const vertex = classes[id];
+
+ /**
+ * Variable for storing the classes for the vertex
+ */
+ let cssClassStr = '';
+ if (vertex.cssClasses.length > 0) {
+ cssClassStr = cssClassStr + ' ' + vertex.cssClasses.join(' ');
+ }
+
+ const styles = { labelStyle: '', style: '' }; //getStylesFromArray(vertex.styles);
+
+ // Use vertex id as text in the box if no text is provided by the graph definition
+ const vertexText = vertex.label ?? vertex.id;
+ const radius = 0;
+ const shape = 'class_box';
+ // Add the node
+ const node = {
+ labelStyle: styles.labelStyle,
+ shape: shape,
+ labelText: sanitizeText(vertexText),
+ classData: vertex,
+ rx: radius,
+ ry: radius,
+ class: cssClassStr,
+ style: styles.style,
+ id: vertex.id,
+ domId: vertex.domId,
+ tooltip: diagObj.db.getTooltip(vertex.id) || '',
+ haveCallback: vertex.haveCallback,
+ link: vertex.link,
+ width: vertex.type === 'group' ? 500 : undefined,
+ type: vertex.type,
+ // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
+ padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
+ };
+ g.setNode(vertex.id, node);
+ log.info('setNode', node);
+ });
+};
+
+/**
+ * Function that adds the additional vertices (notes) found during parsing to the graph to be rendered.
+ *
+ * @param notes - Object containing the additional vertices (notes).
+ * @param g - The graph that is to be drawn.
+ * @param startEdgeId - starting index for note edge
+ * @param classes - Classes
+ */
+export const addNotes = function (
+ notes: ClassNote[],
+ g: graphlib.Graph,
+ startEdgeId: number,
+ classes: ClassMap
+) {
+ log.info(notes);
+
+ // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
+ notes.forEach(function (note, i) {
+ const vertex = note;
+
+ /**
+ * Variable for storing the classes for the vertex
+ *
+ */
+ const cssNoteStr = '';
+
+ const styles = { labelStyle: '', style: '' };
+
+ // Use vertex id as text in the box if no text is provided by the graph definition
+ const vertexText = vertex.text;
+
+ const radius = 0;
+ const shape = 'note';
+ // Add the node
+ const node = {
+ labelStyle: styles.labelStyle,
+ shape: shape,
+ labelText: sanitizeText(vertexText),
+ noteData: vertex,
+ rx: radius,
+ ry: radius,
+ class: cssNoteStr,
+ style: styles.style,
+ id: vertex.id,
+ domId: vertex.id,
+ tooltip: '',
+ type: 'note',
+ // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
+ padding: getConfig().flowchart?.padding ?? getConfig().class?.padding,
+ };
+ g.setNode(vertex.id, node);
+ log.info('setNode', node);
+
+ if (!vertex.class || !(vertex.class in classes)) {
+ return;
+ }
+ const edgeId = startEdgeId + i;
+
+ const edgeData: EdgeData = {
+ id: `edgeNote${edgeId}`,
+ //Set relationship style and line type
+ classes: 'relation',
+ pattern: 'dotted',
+ // Set link type for rendering
+ arrowhead: 'none',
+ //Set edge extra labels
+ startLabelRight: '',
+ endLabelLeft: '',
+ //Set relation arrow types
+ arrowTypeStart: 'none',
+ arrowTypeEnd: 'none',
+ style: 'fill:none',
+ labelStyle: '',
+ curve: interpolateToCurve(conf.curve, curveLinear),
+ };
+
+ // Add the edge to the graph
+ g.setEdge(vertex.id, vertex.class, edgeData, edgeId);
+ });
+};
+
+/**
+ * Add edges to graph based on parsed graph definition
+ *
+ * @param relations -
+ * @param g - The graph object
+ */
+export const addRelations = function (relations: ClassRelation[], g: graphlib.Graph) {
+ const conf = getConfig().flowchart;
+ let cnt = 0;
+
+ relations.forEach(function (edge) {
+ cnt++;
+ const edgeData: EdgeData = {
+ //Set relationship style and line type
+ classes: 'relation',
+ pattern: edge.relation.lineType == 1 ? 'dashed' : 'solid',
+ id: 'id' + cnt,
+ // Set link type for rendering
+ arrowhead: edge.type === 'arrow_open' ? 'none' : 'normal',
+ //Set edge extra labels
+ startLabelRight: edge.relationTitle1 === 'none' ? '' : edge.relationTitle1,
+ endLabelLeft: edge.relationTitle2 === 'none' ? '' : edge.relationTitle2,
+ //Set relation arrow types
+ arrowTypeStart: getArrowMarker(edge.relation.type1),
+ arrowTypeEnd: getArrowMarker(edge.relation.type2),
+ style: 'fill:none',
+ labelStyle: '',
+ curve: interpolateToCurve(conf?.curve, curveLinear),
+ };
+
+ log.info(edgeData, edge);
+
+ if (edge.style !== undefined) {
+ const styles = getStylesFromArray(edge.style);
+ edgeData.style = styles.style;
+ edgeData.labelStyle = styles.labelStyle;
+ }
+
+ edge.text = edge.title;
+ if (edge.text === undefined) {
+ if (edge.style !== undefined) {
+ edgeData.arrowheadStyle = 'fill: #333';
+ }
+ } else {
+ edgeData.arrowheadStyle = 'fill: #333';
+ edgeData.labelpos = 'c';
+
+ // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release
+ if (getConfig().flowchart?.htmlLabels ?? getConfig().htmlLabels) {
+ edgeData.labelType = 'html';
+ edgeData.label = '' + edge.text + '';
+ } else {
+ edgeData.labelType = 'text';
+ edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
+
+ if (edge.style === undefined) {
+ edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
+ }
+
+ edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
+ }
+ }
+ // Add the edge to the graph
+ g.setEdge(edge.id1, edge.id2, edgeData, cnt);
+ });
+};
+
+/**
+ * Merges the value of `conf` with the passed `cnf`
+ *
+ * @param cnf - Config to merge
+ */
+export const setConf = function (cnf: any) {
+ conf = {
+ ...conf,
+ ...cnf,
+ };
+};
+
+/**
+ * Draws a flowchart in the tag with id: id based on the graph definition in text.
+ *
+ * @param text -
+ * @param id -
+ * @param _version -
+ * @param diagObj -
+ */
+export const draw = function (text: string, id: string, _version: string, diagObj: any) {
+ log.info('Drawing class - ', id);
+
+ // TODO V10: Why flowchart? Might be a mistake when copying.
+ const conf = getConfig().flowchart ?? getConfig().class;
+ const securityLevel = getConfig().securityLevel;
+ log.info('config:', conf);
+ const nodeSpacing = conf?.nodeSpacing ?? 50;
+ const rankSpacing = conf?.rankSpacing ?? 50;
+
+ // Create the input mermaid.graph
+ const g: graphlib.Graph = new graphlib.Graph({
+ multigraph: true,
+ compound: true,
+ })
+ .setGraph({
+ rankdir: diagObj.db.getDirection(),
+ nodesep: nodeSpacing,
+ ranksep: rankSpacing,
+ marginx: 8,
+ marginy: 8,
+ })
+ .setDefaultEdgeLabel(function () {
+ return {};
+ });
+
+ // Fetch the vertices/nodes and edges/links from the parsed graph definition
+ const classes: ClassMap = diagObj.db.getClasses();
+ const relations: ClassRelation[] = diagObj.db.getRelations();
+ const notes: ClassNote[] = diagObj.db.getNotes();
+ log.info(relations);
+ addClasses(classes, g, id, diagObj);
+ addRelations(relations, g);
+ addNotes(notes, g, relations.length + 1, classes);
+
+ // Set up an SVG group so that we can translate the final graph.
+ let sandboxElement;
+ if (securityLevel === 'sandbox') {
+ sandboxElement = select('#i' + id);
+ }
+ const root =
+ securityLevel === 'sandbox'
+ ? // @ts-ignore Ignore type error for now
+
+ select(sandboxElement.nodes()[0].contentDocument.body)
+ : select('body');
+ // @ts-ignore Ignore type error for now
+ const svg = root.select(`[id="${id}"]`);
+
+ // Run the renderer. This is what draws the final graph.
+ // @ts-ignore Ignore type error for now
+ const element = root.select('#' + id + ' g');
+ render(
+ element,
+ g,
+ ['aggregation', 'extension', 'composition', 'dependency', 'lollipop'],
+ 'classDiagram',
+ id
+ );
+
+ utils.insertTitle(svg, 'classTitleText', conf?.titleTopMargin ?? 5, diagObj.db.getDiagramTitle());
+
+ setupGraphViewbox(g, svg, conf?.diagramPadding, conf?.useMaxWidth);
+
+ // Add label rects for non html labels
+ if (!conf?.htmlLabels) {
+ // @ts-ignore Ignore type error for now
+ const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
+ const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label');
+ for (const label of labels) {
+ // Get dimensions of label
+ const dim = label.getBBox();
+
+ const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ rect.setAttribute('rx', 0);
+ rect.setAttribute('ry', 0);
+ rect.setAttribute('width', dim.width);
+ rect.setAttribute('height', dim.height);
+
+ label.insertBefore(rect, label.firstChild);
+ }
+ }
+};
+
+/**
+ * Gets the arrow marker for a type index
+ *
+ * @param type - The type to look for
+ * @returns The arrow marker
+ */
+function getArrowMarker(type: number) {
+ let marker;
+ switch (type) {
+ case 0:
+ marker = 'aggregation';
+ break;
+ case 1:
+ marker = 'extension';
+ break;
+ case 2:
+ marker = 'composition';
+ break;
+ case 3:
+ marker = 'dependency';
+ break;
+ case 4:
+ marker = 'lollipop';
+ break;
+ default:
+ marker = 'none';
+ }
+ return marker;
+}
+
+export default {
+ setConf,
+ draw,
+};
diff --git a/packages/mermaid/src/diagrams/class/classTypes.ts b/packages/mermaid/src/diagrams/class/classTypes.ts
new file mode 100644
index 000000000..4cacad3db
--- /dev/null
+++ b/packages/mermaid/src/diagrams/class/classTypes.ts
@@ -0,0 +1,55 @@
+export interface ClassNode {
+ id: string;
+ type: string;
+ label: string;
+ cssClasses: string[];
+ methods: string[];
+ members: string[];
+ annotations: string[];
+ domId: string;
+ link?: string;
+ linkTarget?: string;
+ haveCallback?: boolean;
+ tooltip?: string;
+}
+
+export interface ClassNote {
+ id: string;
+ class: string;
+ 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;
+ relationTitle1: string;
+ relationTitle2: string;
+ type: string;
+ title: string;
+ text: string;
+ style: string[];
+ relation: {
+ type1: number;
+ type2: number;
+ lineType: number;
+ };
+};
+export type ClassMap = Record;
diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison
index 157e3d7d8..e98b0253b 100644
--- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison
+++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison
@@ -119,6 +119,8 @@ Function arguments are optional: 'call ()' simply executes 'callb
"=" return 'EQUALS';
\= return 'EQUALS';
\w+ return 'ALPHA';
+"[" return 'SQS';
+"]" return 'SQE';
[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
[0-9]+ return 'NUM';
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
@@ -249,6 +251,10 @@ statements
| statement NEWLINE statements
;
+classLabel
+ : SQS STR SQE { $$=$2; }
+ ;
+
className
: alphaNumToken { $$=$1; }
| classLiteralName { $$=$1; }
@@ -274,10 +280,15 @@ statement
;
classStatement
- : CLASS className {yy.addClass($2);}
- | CLASS className STYLE_SEPARATOR alphaNumToken {yy.addClass($2);yy.setCssClass($2, $4);}
- | CLASS className STRUCT_START members STRUCT_STOP {/*console.log($2,JSON.stringify($4));*/yy.addClass($2);yy.addMembers($2,$4);}
- | CLASS className STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.addClass($2);yy.setCssClass($2, $4);yy.addMembers($2,$6);}
+ : classIdentifier
+ | classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
+ | classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
+ | classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$3);}
+ ;
+
+classIdentifier
+ : CLASS className {$$=$2; yy.addClass($2);}
+ | CLASS className classLabel {$$=$2; yy.addClass($2);yy.setClassLabel($2, $3);}
;
annotationStatement
diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison
index 6aaca81d6..d9f03c387 100644
--- a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison
+++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison
@@ -35,6 +35,8 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
[A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
\"[^"]*\" return 'COMMENT';
[\n]+ /* nothing */
+\%%(?!\{)[^\n]* /* skip comments in attribute block */
+[^\}]\%\%[^\n]* /* skip comments in attribute block */
"}" { this.popState(); return 'BLOCK_STOP'; }
. return yytext[0];
diff --git a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts
index 6162cc828..5b2aaf1bd 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts
+++ b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts
@@ -4,15 +4,15 @@ import type { ExternalDiagramDefinition } from '../../diagram-api/types';
const id = 'flowchart-v2';
const detector: DiagramDetector = (txt, config) => {
- if (config?.flowchart?.defaultRenderer === 'dagre-d3') {
- return false;
- }
- if (config?.flowchart?.defaultRenderer === 'elk') {
+ if (
+ config?.flowchart?.defaultRenderer === 'dagre-d3' ||
+ config?.flowchart?.defaultRenderer === 'elk'
+ ) {
return false;
}
// If we have configured to use dagre-wrapper then we should return true in this function for graph code thus making it use the new flowchart diagram
- if (txt.match(/^\s*graph/) !== null) {
+ if (txt.match(/^\s*graph/) !== null && config?.flowchart?.defaultRenderer === 'dagre-wrapper') {
return true;
}
return txt.match(/^\s*flowchart/) !== null;
diff --git a/packages/mermaid/src/diagrams/flowchart/flowDetector.ts b/packages/mermaid/src/diagrams/flowchart/flowDetector.ts
index 9457ff469..a8b116ccd 100644
--- a/packages/mermaid/src/diagrams/flowchart/flowDetector.ts
+++ b/packages/mermaid/src/diagrams/flowchart/flowDetector.ts
@@ -5,10 +5,10 @@ const id = 'flowchart';
const detector: DiagramDetector = (txt, config) => {
// If we have conferred to only use new flow charts this function should always return false
// as in not signalling true for a legacy flowchart
- if (config?.flowchart?.defaultRenderer === 'dagre-wrapper') {
- return false;
- }
- if (config?.flowchart?.defaultRenderer === 'elk') {
+ if (
+ config?.flowchart?.defaultRenderer === 'dagre-wrapper' ||
+ config?.flowchart?.defaultRenderer === 'elk'
+ ) {
return false;
}
return txt.match(/^\s*graph/) !== null;
diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js
index a1c74dd62..27ad1a0b8 100644
--- a/packages/mermaid/src/diagrams/gantt/ganttDb.js
+++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js
@@ -1,5 +1,8 @@
-import moment from 'moment-mini';
import { sanitizeUrl } from '@braintree/sanitize-url';
+import dayjs from 'dayjs';
+import dayjsIsoWeek from 'dayjs/plugin/isoWeek';
+import dayjsCustomParseFormat from 'dayjs/plugin/customParseFormat';
+import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat';
import { log } from '../../logger';
import * as configApi from '../../config';
import utils from '../../utils';
@@ -15,6 +18,10 @@ import {
getDiagramTitle,
} from '../../commonDb';
+dayjs.extend(dayjsIsoWeek);
+dayjs.extend(dayjsCustomParseFormat);
+dayjs.extend(dayjsAdvancedFormat);
+
let dateFormat = '';
let axisFormat = '';
let tickInterval = undefined;
@@ -162,18 +169,58 @@ export const isInvalidDate = function (date, dateFormat, excludes, includes) {
return excludes.includes(date.format(dateFormat.trim()));
};
+/**
+ * TODO: fully document what this function does and what types it accepts
+ *
+ * @param {object} task - The task to check.
+ * @param {string | Date} task.startTime - Might be a `Date` or a `string`.
+ * TODO: is this always a Date?
+ * @param {string | Date} task.endTime - Might be a `Date` or a `string`.
+ * TODO: is this always a Date?
+ * @param {string} dateFormat - Dayjs date format string.
+ * @param {*} excludes
+ * @param {*} includes
+ */
const checkTaskDates = function (task, dateFormat, excludes, includes) {
if (!excludes.length || task.manualEndTime) {
return;
}
- let startTime = moment(task.startTime, dateFormat, true);
- startTime.add(1, 'd');
- let endTime = moment(task.endTime, dateFormat, true);
- let renderEndTime = fixTaskDates(startTime, endTime, dateFormat, excludes, includes);
- task.endTime = endTime.toDate();
+ let startTime;
+ if (task.startTime instanceof Date) {
+ startTime = dayjs(task.startTime);
+ } else {
+ startTime = dayjs(task.startTime, dateFormat, true);
+ }
+ startTime = startTime.add(1, 'd');
+
+ let originalEndTime;
+ if (task.endTime instanceof Date) {
+ originalEndTime = dayjs(task.endTime);
+ } else {
+ originalEndTime = dayjs(task.endTime, dateFormat, true);
+ }
+ const [fixedEndTime, renderEndTime] = fixTaskDates(
+ startTime,
+ originalEndTime,
+ dateFormat,
+ excludes,
+ includes
+ );
+ task.endTime = fixedEndTime.toDate();
task.renderEndTime = renderEndTime;
};
+/**
+ * TODO: what does this function do?
+ *
+ * @param {dayjs.Dayjs} startTime - The start time.
+ * @param {dayjs.Dayjs} endTime - The original end time (will return a different end time if it's invalid).
+ * @param {string} dateFormat - Dayjs date format string.
+ * @param {*} excludes
+ * @param {*} includes
+ * @returns {[endTime: dayjs.Dayjs, renderEndTime: Date | null]} The new `endTime`, and the end time to render.
+ * `renderEndTime` may be `null` if `startTime` is newer than `endTime`.
+ */
const fixTaskDates = function (startTime, endTime, dateFormat, excludes, includes) {
let invalid = false;
let renderEndTime = null;
@@ -183,11 +230,11 @@ const fixTaskDates = function (startTime, endTime, dateFormat, excludes, include
}
invalid = isInvalidDate(startTime, dateFormat, excludes, includes);
if (invalid) {
- endTime.add(1, 'd');
+ endTime = endTime.add(1, 'd');
}
- startTime.add(1, 'd');
+ startTime = startTime.add(1, 'd');
}
- return renderEndTime;
+ return [endTime, renderEndTime];
};
const getStartDate = function (prevTime, dateFormat, str) {
@@ -223,7 +270,7 @@ const getStartDate = function (prevTime, dateFormat, str) {
}
// Check for actual date set
- let mDate = moment(str, dateFormat.trim(), true);
+ let mDate = dayjs(str, dateFormat.trim(), true);
if (mDate.isValid()) {
return mDate.toDate();
} else {
@@ -238,11 +285,14 @@ const getStartDate = function (prevTime, dateFormat, str) {
};
/**
- * Parse a string as a moment duration.
+ * Parse a string into the args for `dayjs.add()`.
*
* The string have to be compound by a value and a shorthand duration unit. For example `5d`
* represents 5 days.
*
+ * Please be aware that 1 day may be 23 or 25 hours, if the user lives in an area
+ * that has daylight savings time (or even 23.5/24.5 hours in Lord Howe Island!)
+ *
* Shorthand unit supported are:
*
* - `y` for years
@@ -254,33 +304,36 @@ const getStartDate = function (prevTime, dateFormat, str) {
* - `ms` for milliseconds
*
* @param {string} str - A string representing the duration.
- * @returns {moment.Duration} A moment duration, including an invalid moment for invalid input
- * string.
+ * @returns {[value: number, unit: dayjs.ManipulateType]} Arguments to pass to `dayjs.add()`
*/
const parseDuration = function (str) {
const statement = /^(\d+(?:\.\d+)?)([Mdhmswy]|ms)$/.exec(str.trim());
if (statement !== null) {
- return moment.duration(Number.parseFloat(statement[1]), statement[2]);
+ return [Number.parseFloat(statement[1]), statement[2]];
}
- return moment.duration.invalid();
+ // NaN means an invalid duration
+ return [NaN, 'ms'];
};
const getEndDate = function (prevTime, dateFormat, str, inclusive = false) {
str = str.trim();
// Check for actual date
- let mDate = moment(str, dateFormat.trim(), true);
+ let mDate = dayjs(str, dateFormat.trim(), true);
if (mDate.isValid()) {
if (inclusive) {
- mDate.add(1, 'd');
+ mDate = mDate.add(1, 'd');
}
return mDate.toDate();
}
- const endTime = moment(prevTime);
- const duration = parseDuration(str);
- if (duration.isValid()) {
- endTime.add(duration);
+ let endTime = dayjs(prevTime);
+ const [durationValue, durationUnit] = parseDuration(str);
+ if (!Number.isNaN(durationValue)) {
+ const newEndTime = endTime.add(durationValue, durationUnit);
+ if (newEndTime.isValid()) {
+ endTime = newEndTime;
+ }
}
return endTime.toDate();
};
@@ -346,7 +399,7 @@ const compileData = function (prevTask, dataStr) {
if (endTimeData) {
task.endTime = getEndDate(task.startTime, dateFormat, endTimeData, inclusiveEndDates);
- task.manualEndTime = moment(endTimeData, 'YYYY-MM-DD', true).isValid();
+ task.manualEndTime = dayjs(endTimeData, 'YYYY-MM-DD', true).isValid();
checkTaskDates(task, dateFormat, excludes, includes);
}
@@ -496,7 +549,7 @@ const compileTasks = function () {
);
if (rawTasks[pos].endTime) {
rawTasks[pos].processed = true;
- rawTasks[pos].manualEndTime = moment(
+ rawTasks[pos].manualEndTime = dayjs(
rawTasks[pos].raw.endTime.data,
'YYYY-MM-DD',
true
diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
index 09df96f12..d65f2fdfd 100644
--- a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
+++ b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
@@ -1,5 +1,5 @@
// @ts-nocheck TODO: Fix TS
-import moment from 'moment-mini';
+import dayjs from 'dayjs';
import ganttDb from './ganttDb';
import { convert } from '../../tests/util';
@@ -9,7 +9,7 @@ describe('when using the ganttDb', function () {
});
describe('when using duration', function () {
- it.each([{ str: '1d', expected: moment.duration(1, 'd') }])(
+ it.each([{ str: '1d', expected: [1, 'd'] }])(
'should %s resulting in $o duration',
({ str, expected }) => {
expect(ganttDb.parseDuration(str)).toEqual(expected);
@@ -19,11 +19,11 @@ describe('when using the ganttDb', function () {
it.each(
convert`
str | expected
- ${'1d'} | ${moment.duration(1, 'd')}
- ${'2w'} | ${moment.duration(2, 'w')}
- ${'1ms'} | ${moment.duration(1, 'ms')}
- ${'0.1s'} | ${moment.duration(100, 'ms')}
- ${'1f'} | ${moment.duration.invalid()}
+ ${'1d'} | ${[1, 'd']}
+ ${'2w'} | ${[2, 'w']}
+ ${'1ms'} | ${[1, 'ms']}
+ ${'0.1s'} | ${[0.1, 's']}
+ ${'1f'} | ${[NaN, 'ms']}
`
)('should $str resulting in $expected duration', ({ str, expected }) => {
expect(ganttDb.parseDuration(str)).toEqual(expected);
@@ -171,44 +171,44 @@ describe('when using the ganttDb', function () {
const tasks = ganttDb.getTasks();
- expect(tasks[0].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
- expect(tasks[0].endTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate());
- expect(tasks[0].renderEndTime).toEqual(moment('2019-02-02', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].startTime).toEqual(dayjs('2019-02-01', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].endTime).toEqual(dayjs('2019-02-04', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].renderEndTime).toEqual(dayjs('2019-02-02', 'YYYY-MM-DD').toDate());
expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('test1');
- expect(tasks[1].startTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate());
- expect(tasks[1].endTime).toEqual(moment('2019-02-07', 'YYYY-MM-DD').toDate());
- expect(tasks[1].renderEndTime).toEqual(moment('2019-02-06', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].startTime).toEqual(dayjs('2019-02-04', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].endTime).toEqual(dayjs('2019-02-07', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].renderEndTime).toEqual(dayjs('2019-02-06', 'YYYY-MM-DD').toDate());
expect(tasks[1].id).toEqual('id2');
expect(tasks[1].task).toEqual('test2');
- expect(tasks[2].startTime).toEqual(moment('2019-02-07', 'YYYY-MM-DD').toDate());
- expect(tasks[2].endTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
- expect(tasks[2].renderEndTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
+ expect(tasks[2].startTime).toEqual(dayjs('2019-02-07', 'YYYY-MM-DD').toDate());
+ expect(tasks[2].endTime).toEqual(dayjs('2019-02-20', 'YYYY-MM-DD').toDate());
+ expect(tasks[2].renderEndTime).toEqual(dayjs('2019-02-20', 'YYYY-MM-DD').toDate());
expect(tasks[2].id).toEqual('id3');
expect(tasks[2].task).toEqual('test3');
- expect(tasks[3].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
- expect(tasks[3].endTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
+ expect(tasks[3].startTime).toEqual(dayjs('2019-02-01', 'YYYY-MM-DD').toDate());
+ expect(tasks[3].endTime).toEqual(dayjs('2019-02-20', 'YYYY-MM-DD').toDate());
expect(tasks[3].renderEndTime).toBeNull(); // Fixed end
expect(tasks[3].id).toEqual('id4');
expect(tasks[3].task).toEqual('test4');
- expect(tasks[4].startTime).toEqual(moment('2019-02-20', 'YYYY-MM-DD').toDate());
- expect(tasks[4].endTime).toEqual(moment('2019-02-21', 'YYYY-MM-DD').toDate());
- expect(tasks[4].renderEndTime).toEqual(moment('2019-02-21', 'YYYY-MM-DD').toDate());
+ expect(tasks[4].startTime).toEqual(dayjs('2019-02-20', 'YYYY-MM-DD').toDate());
+ expect(tasks[4].endTime).toEqual(dayjs('2019-02-21', 'YYYY-MM-DD').toDate());
+ expect(tasks[4].renderEndTime).toEqual(dayjs('2019-02-21', 'YYYY-MM-DD').toDate());
expect(tasks[4].id).toEqual('id5');
expect(tasks[4].task).toEqual('test5');
- expect(tasks[5].startTime).toEqual(moment('2019-02-13', 'YYYY-MM-DD').toDate());
- expect(tasks[5].endTime).toEqual(moment('2019-02-18', 'YYYY-MM-DD').toDate());
- expect(tasks[5].renderEndTime).toEqual(moment('2019-02-15', 'YYYY-MM-DD').toDate());
+ expect(tasks[5].startTime).toEqual(dayjs('2019-02-13', 'YYYY-MM-DD').toDate());
+ expect(tasks[5].endTime).toEqual(dayjs('2019-02-18', 'YYYY-MM-DD').toDate());
+ expect(tasks[5].renderEndTime).toEqual(dayjs('2019-02-15', 'YYYY-MM-DD').toDate());
expect(tasks[5].id).toEqual('id6');
expect(tasks[5].task).toEqual('test6');
- expect(tasks[6].startTime).toEqual(moment('2019-02-18', 'YYYY-MM-DD').toDate());
- expect(tasks[6].endTime).toEqual(moment('2019-02-19', 'YYYY-MM-DD').toDate());
+ expect(tasks[6].startTime).toEqual(dayjs('2019-02-18', 'YYYY-MM-DD').toDate());
+ expect(tasks[6].endTime).toEqual(dayjs('2019-02-19', 'YYYY-MM-DD').toDate());
expect(tasks[6].id).toEqual('id7');
expect(tasks[6].task).toEqual('test7');
});
@@ -243,109 +243,103 @@ describe('when using the ganttDb', function () {
const tasks = ganttDb.getTasks();
// Section - A section
- expect(tasks[0].startTime).toEqual(moment('2014-01-06', 'YYYY-MM-DD').toDate());
- expect(tasks[0].endTime).toEqual(moment('2014-01-08', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].startTime).toEqual(dayjs('2014-01-06', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].endTime).toEqual(dayjs('2014-01-08', 'YYYY-MM-DD').toDate());
expect(tasks[0].order).toEqual(0);
expect(tasks[0].id).toEqual('des1');
expect(tasks[0].task).toEqual('Completed task');
- expect(tasks[1].startTime).toEqual(moment('2014-01-09', 'YYYY-MM-DD').toDate());
- expect(tasks[1].endTime).toEqual(moment('2014-01-12', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].startTime).toEqual(dayjs('2014-01-09', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].endTime).toEqual(dayjs('2014-01-12', 'YYYY-MM-DD').toDate());
expect(tasks[1].order).toEqual(1);
expect(tasks[1].id).toEqual('des2');
expect(tasks[1].task).toEqual('Active task');
- expect(tasks[2].startTime).toEqual(moment('2014-01-12', 'YYYY-MM-DD').toDate());
- expect(tasks[2].endTime).toEqual(moment('2014-01-17', 'YYYY-MM-DD').toDate());
+ expect(tasks[2].startTime).toEqual(dayjs('2014-01-12', 'YYYY-MM-DD').toDate());
+ expect(tasks[2].endTime).toEqual(dayjs('2014-01-17', 'YYYY-MM-DD').toDate());
expect(tasks[2].order).toEqual(2);
expect(tasks[2].id).toEqual('des3');
expect(tasks[2].task).toEqual('Future task');
- expect(tasks[3].startTime).toEqual(moment('2014-01-17', 'YYYY-MM-DD').toDate());
- expect(tasks[3].endTime).toEqual(moment('2014-01-22', 'YYYY-MM-DD').toDate());
+ expect(tasks[3].startTime).toEqual(dayjs('2014-01-17', 'YYYY-MM-DD').toDate());
+ expect(tasks[3].endTime).toEqual(dayjs('2014-01-22', 'YYYY-MM-DD').toDate());
expect(tasks[3].order).toEqual(3);
expect(tasks[3].id).toEqual('des4');
expect(tasks[3].task).toEqual('Future task2');
// Section - Critical tasks
- expect(tasks[4].startTime).toEqual(moment('2014-01-06', 'YYYY-MM-DD').toDate());
- expect(tasks[4].endTime).toEqual(moment('2014-01-07', 'YYYY-MM-DD').toDate());
+ expect(tasks[4].startTime).toEqual(dayjs('2014-01-06', 'YYYY-MM-DD').toDate());
+ expect(tasks[4].endTime).toEqual(dayjs('2014-01-07', 'YYYY-MM-DD').toDate());
expect(tasks[4].order).toEqual(4);
expect(tasks[4].id).toEqual('task1');
expect(tasks[4].task).toEqual('Completed task in the critical line');
- expect(tasks[5].startTime).toEqual(moment('2014-01-08', 'YYYY-MM-DD').toDate());
- expect(tasks[5].endTime).toEqual(moment('2014-01-10', 'YYYY-MM-DD').toDate());
+ expect(tasks[5].startTime).toEqual(dayjs('2014-01-08', 'YYYY-MM-DD').toDate());
+ expect(tasks[5].endTime).toEqual(dayjs('2014-01-10', 'YYYY-MM-DD').toDate());
expect(tasks[5].order).toEqual(5);
expect(tasks[5].id).toEqual('task2');
expect(tasks[5].task).toEqual('Implement parser and jison');
- expect(tasks[6].startTime).toEqual(moment('2014-01-10', 'YYYY-MM-DD').toDate());
- expect(tasks[6].endTime).toEqual(moment('2014-01-13', 'YYYY-MM-DD').toDate());
+ expect(tasks[6].startTime).toEqual(dayjs('2014-01-10', 'YYYY-MM-DD').toDate());
+ expect(tasks[6].endTime).toEqual(dayjs('2014-01-13', 'YYYY-MM-DD').toDate());
expect(tasks[6].order).toEqual(6);
expect(tasks[6].id).toEqual('task3');
expect(tasks[6].task).toEqual('Create tests for parser');
- expect(tasks[7].startTime).toEqual(moment('2014-01-13', 'YYYY-MM-DD').toDate());
- expect(tasks[7].endTime).toEqual(moment('2014-01-18', 'YYYY-MM-DD').toDate());
+ expect(tasks[7].startTime).toEqual(dayjs('2014-01-13', 'YYYY-MM-DD').toDate());
+ expect(tasks[7].endTime).toEqual(dayjs('2014-01-18', 'YYYY-MM-DD').toDate());
expect(tasks[7].order).toEqual(7);
expect(tasks[7].id).toEqual('task4');
expect(tasks[7].task).toEqual('Future task in critical line');
- expect(tasks[8].startTime).toEqual(moment('2014-01-18', 'YYYY-MM-DD').toDate());
- expect(tasks[8].endTime).toEqual(moment('2014-01-20', 'YYYY-MM-DD').toDate());
+ expect(tasks[8].startTime).toEqual(dayjs('2014-01-18', 'YYYY-MM-DD').toDate());
+ expect(tasks[8].endTime).toEqual(dayjs('2014-01-20', 'YYYY-MM-DD').toDate());
expect(tasks[8].order).toEqual(8);
expect(tasks[8].id).toEqual('task5');
expect(tasks[8].task).toEqual('Create tests for renderer');
- expect(tasks[9].startTime).toEqual(moment('2014-01-20', 'YYYY-MM-DD').toDate());
- expect(tasks[9].endTime).toEqual(moment('2014-01-21', 'YYYY-MM-DD').toDate());
+ expect(tasks[9].startTime).toEqual(dayjs('2014-01-20', 'YYYY-MM-DD').toDate());
+ expect(tasks[9].endTime).toEqual(dayjs('2014-01-21', 'YYYY-MM-DD').toDate());
expect(tasks[9].order).toEqual(9);
expect(tasks[9].id).toEqual('task6');
expect(tasks[9].task).toEqual('Add to mermaid');
// Section - Documentation
- expect(tasks[10].startTime).toEqual(moment('2014-01-08', 'YYYY-MM-DD').toDate());
- expect(tasks[10].endTime).toEqual(moment('2014-01-11', 'YYYY-MM-DD').toDate());
+ expect(tasks[10].startTime).toEqual(dayjs('2014-01-08', 'YYYY-MM-DD').toDate());
+ expect(tasks[10].endTime).toEqual(dayjs('2014-01-11', 'YYYY-MM-DD').toDate());
expect(tasks[10].order).toEqual(10);
expect(tasks[10].id).toEqual('a1');
expect(tasks[10].task).toEqual('Describe gantt syntax');
- expect(tasks[11].startTime).toEqual(moment('2014-01-11', 'YYYY-MM-DD').toDate());
- expect(tasks[11].endTime).toEqual(
- moment('2014-01-11 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate()
- );
+ expect(tasks[11].startTime).toEqual(dayjs('2014-01-11', 'YYYY-MM-DD').toDate());
+ expect(tasks[11].endTime).toEqual(dayjs('2014-01-11 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate());
expect(tasks[11].order).toEqual(11);
expect(tasks[11].id).toEqual('task7');
expect(tasks[11].task).toEqual('Add gantt diagram to demo page');
- expect(tasks[12].startTime).toEqual(moment('2014-01-11', 'YYYY-MM-DD').toDate());
- expect(tasks[12].endTime).toEqual(moment('2014-01-13', 'YYYY-MM-DD').toDate());
+ expect(tasks[12].startTime).toEqual(dayjs('2014-01-11', 'YYYY-MM-DD').toDate());
+ expect(tasks[12].endTime).toEqual(dayjs('2014-01-13', 'YYYY-MM-DD').toDate());
expect(tasks[12].order).toEqual(12);
expect(tasks[12].id).toEqual('doc1');
expect(tasks[12].task).toEqual('Add another diagram to demo page');
// Section - Last section
- expect(tasks[13].startTime).toEqual(moment('2014-01-13', 'YYYY-MM-DD').toDate());
- expect(tasks[13].endTime).toEqual(moment('2014-01-16', 'YYYY-MM-DD').toDate());
+ expect(tasks[13].startTime).toEqual(dayjs('2014-01-13', 'YYYY-MM-DD').toDate());
+ expect(tasks[13].endTime).toEqual(dayjs('2014-01-16', 'YYYY-MM-DD').toDate());
expect(tasks[13].order).toEqual(13);
expect(tasks[13].id).toEqual('task8');
expect(tasks[13].task).toEqual('Describe gantt syntax');
- expect(tasks[14].startTime).toEqual(moment('2014-01-16', 'YYYY-MM-DD').toDate());
- expect(tasks[14].endTime).toEqual(
- moment('2014-01-16 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate()
- );
+ expect(tasks[14].startTime).toEqual(dayjs('2014-01-16', 'YYYY-MM-DD').toDate());
+ expect(tasks[14].endTime).toEqual(dayjs('2014-01-16 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate());
expect(tasks[14].order).toEqual(14);
expect(tasks[14].id).toEqual('task9');
expect(tasks[14].task).toEqual('Add gantt diagram to demo page');
expect(tasks[15].startTime).toEqual(
- moment('2014-01-16 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate()
- );
- expect(tasks[15].endTime).toEqual(
- moment('2014-01-18 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate()
+ dayjs('2014-01-16 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate()
);
+ expect(tasks[15].endTime).toEqual(dayjs('2014-01-18 20:00:00', 'YYYY-MM-DD HH:mm:ss').toDate());
expect(tasks[15].order).toEqual(15);
expect(tasks[15].id).toEqual('task10');
expect(tasks[15].task).toEqual('Add another diagram to demo page');
@@ -358,19 +352,53 @@ describe('when using the ganttDb', function () {
ganttDb.addTask('test2', 'id2,after id1,20d');
const tasks = ganttDb.getTasks();
- expect(tasks[0].startTime).toEqual(moment('2019-09-30', 'YYYY-MM-DD').toDate());
- expect(tasks[0].endTime).toEqual(moment('2019-10-11', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].startTime).toEqual(dayjs('2019-09-30', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].endTime).toEqual(dayjs('2019-10-11', 'YYYY-MM-DD').toDate());
expect(tasks[1].renderEndTime).toBeNull(); // Fixed end
expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('test1');
- expect(tasks[1].startTime).toEqual(moment('2019-10-11', 'YYYY-MM-DD').toDate());
- expect(tasks[1].endTime).toEqual(moment('2019-10-31', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].startTime).toEqual(dayjs('2019-10-11', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].endTime).toEqual(dayjs('2019-10-31', 'YYYY-MM-DD').toDate());
expect(tasks[1].renderEndTime).toBeNull(); // Fixed end
expect(tasks[1].id).toEqual('id2');
expect(tasks[1].task).toEqual('test2');
});
+ /**
+ * Unfortunately, Vitest has no way of modifying the timezone at runtime, so
+ * in order to test this, please run this test with
+ *
+ * ```bash
+ * TZ='America/Los_Angeles' pnpm exec vitest run ganttDb
+ * ```
+ */
+ /* c8 ignore start */ // tell code-coverage to ignore this block of code
+ describe.skipIf(process.env.TZ != 'America/Los_Angeles')(
+ 'when using a timezone with daylight savings (only run if TZ="America/Los_Angeles")',
+ () => {
+ it('should add 1 day even on days with 25 hours', function () {
+ const startTime = new Date(2020, 10, 1);
+ expect(startTime.toISOString()).toBe('2020-11-01T07:00:00.000Z');
+
+ const endTime = new Date(2020, 10, 2);
+ expect(endTime.toISOString()).toBe('2020-11-02T08:00:00.000Z');
+
+ ganttDb.setDateFormat('YYYY-MM-DD');
+ ganttDb.addSection('Task handles 25 hour day');
+ ganttDb.addTask('daylight savings day', 'id1,2020-11-01,1d');
+ const tasks = ganttDb.getTasks();
+ expect(tasks[0].startTime).toEqual(startTime);
+ expect(tasks[0].endTime).toEqual(endTime);
+
+ // In USA states that use daylight savings, 2020-11-01 had 25 hours
+ const millisecondsIn25Hours = 25 * 60 * 60 * 1000;
+ expect(endTime - startTime).toEqual(millisecondsIn25Hours);
+ });
+ }
+ );
+ /* c8 ignore stop */
+
describe('when setting inclusive end dates', function () {
beforeEach(function () {
ganttDb.setDateFormat('YYYY-MM-DD');
@@ -380,13 +408,13 @@ describe('when using the ganttDb', function () {
});
it('should automatically add one day to all end dates', function () {
const tasks = ganttDb.getTasks();
- expect(tasks[0].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
- expect(tasks[0].endTime).toEqual(moment('2019-02-02', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].startTime).toEqual(dayjs('2019-02-01', 'YYYY-MM-DD').toDate());
+ expect(tasks[0].endTime).toEqual(dayjs('2019-02-02', 'YYYY-MM-DD').toDate());
expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('test1');
- expect(tasks[1].startTime).toEqual(moment('2019-02-01', 'YYYY-MM-DD').toDate());
- expect(tasks[1].endTime).toEqual(moment('2019-02-04', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].startTime).toEqual(dayjs('2019-02-01', 'YYYY-MM-DD').toDate());
+ expect(tasks[1].endTime).toEqual(dayjs('2019-02-04', 'YYYY-MM-DD').toDate());
expect(tasks[1].renderEndTime).toBeNull(); // Fixed end
expect(tasks[1].manualEndTime).toBeTruthy();
expect(tasks[1].id).toEqual('id2');
diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js
index faec35a86..7a012beb5 100644
--- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js
+++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js
@@ -1,4 +1,4 @@
-import moment from 'moment-mini';
+import dayjs from 'dayjs';
import { log } from '../../logger';
import {
select,
@@ -435,16 +435,16 @@ export const draw = function (text, id, version, diagObj) {
const excludeRanges = [];
let range = null;
- let d = moment(minTime);
+ let d = dayjs(minTime);
while (d.valueOf() <= maxTime) {
if (diagObj.db.isInvalidDate(d, dateFormat, excludes, includes)) {
if (!range) {
range = {
- start: d.clone(),
- end: d.clone(),
+ start: d,
+ end: d,
};
} else {
- range.end = d.clone();
+ range.end = d;
}
} else {
if (range) {
@@ -452,7 +452,7 @@ export const draw = function (text, id, version, diagObj) {
range = null;
}
}
- d.add(1, 'd');
+ d = d.add(1, 'd');
}
const rectangles = svg.append('g').selectAll('rect').data(excludeRanges).enter();
@@ -467,7 +467,7 @@ export const draw = function (text, id, version, diagObj) {
})
.attr('y', conf.gridLineStartPadding)
.attr('width', function (d) {
- const renderEnd = d.end.clone().add(1, 'day');
+ const renderEnd = d.end.add(1, 'day');
return timeScale(renderEnd) - timeScale(d.start);
})
.attr('height', h - theTopPad - conf.gridLineStartPadding)
diff --git a/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts b/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts
index 02e706bf6..272ecf0c1 100644
--- a/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts
+++ b/packages/mermaid/src/diagrams/timeline/timelineRenderer.ts
@@ -1,25 +1,37 @@
-// @ts-nocheck TODO: fix file
-import { select } from 'd3';
+// @ts-ignore - db not typed yet
+import { select, Selection } from 'd3';
import svgDraw from './svgDraw';
import { log } from '../../logger';
import { getConfig } from '../../config';
import { setupGraphViewbox } from '../../setupGraphViewbox';
+import { Diagram } from '../../Diagram';
+import { MermaidConfig } from '../../config.type';
-export const setConf = function (cnf) {
- const keys = Object.keys(cnf);
+interface Block {
+ number: number;
+ descr: TDesc;
+ section: TSection;
+ width: number;
+ padding: number;
+ maxHeight: number;
+}
- keys.forEach(function (key) {
- conf[key] = cnf[key];
- });
-};
-
-export const draw = function (text, id, version, diagObj) {
+interface TimelineTask {
+ id: number;
+ section: string;
+ type: string;
+ task: string;
+ score: number;
+ events: string[];
+}
+export const draw = function (text: string, id: string, version: string, diagObj: Diagram) {
//1. Fetch the configuration
const conf = getConfig();
- const LEFT_MARGIN = conf.leftMargin ? conf.leftMargin : 50;
+ // @ts-expect-error - wrong config?
+ const LEFT_MARGIN = conf.leftMargin ?? 50;
//2. Clear the diagram db before parsing
- diagObj.db.clear();
+ diagObj.db.clear?.();
//3. Parse the diagram text
diagObj.parser.parse(text + '\n');
@@ -34,15 +46,19 @@ export const draw = function (text, id, version, diagObj) {
}
const root =
securityLevel === 'sandbox'
- ? select(sandboxElement.nodes()[0].contentDocument.body)
+ ? // @ts-ignore d3 types are wrong
+ select(sandboxElement.nodes()[0].contentDocument.body)
: select('body');
+ // @ts-ignore d3 types are wrong
const svg = root.select('#' + id);
svg.append('g');
//4. Fetch the diagram data
- const tasks = diagObj.db.getTasks();
+ // @ts-expect-error - db not typed yet
+ const tasks: TimelineTask[] = diagObj.db.getTasks();
+ // @ts-expect-error - db not typed yet
const title = diagObj.db.getCommonDb().getDiagramTitle();
log.debug('task', tasks);
@@ -50,7 +66,8 @@ export const draw = function (text, id, version, diagObj) {
svgDraw.initGraphics(svg);
// fetch Sections
- const sections = diagObj.db.getSections();
+ // @ts-expect-error - db not typed yet
+ const sections: string[] = diagObj.db.getSections();
log.debug('sections', sections);
let maxSectionHeight = 0;
@@ -67,8 +84,8 @@ export const draw = function (text, id, version, diagObj) {
let hasSections = true;
//Calculate the max height of the sections
- sections.forEach(function (section) {
- const sectionNode = {
+ sections.forEach(function (section: string) {
+ const sectionNode: Block = {
number: sectionNumber,
descr: section,
section: sectionNumber,
@@ -87,8 +104,9 @@ export const draw = function (text, id, version, diagObj) {
log.debug('tasks.length', tasks.length);
//calculate max task height
// for loop till tasks.length
+
for (const [i, task] of tasks.entries()) {
- const taskNode = {
+ const taskNode: Block = {
number: i,
descr: task,
section: task.section,
@@ -124,11 +142,14 @@ export const draw = function (text, id, version, diagObj) {
if (sections && sections.length > 0) {
sections.forEach((section) => {
- const sectionNode = {
+ //filter task where tasks.section == section
+ const tasksForSection = tasks.filter((task) => task.section === section);
+
+ const sectionNode: Block = {
number: sectionNumber,
descr: section,
section: sectionNumber,
- width: 150,
+ width: 200 * Math.max(tasksForSection.length, 1) - 50,
padding: 20,
maxHeight: maxSectionHeight,
};
@@ -142,8 +163,6 @@ export const draw = function (text, id, version, diagObj) {
masterY += maxSectionHeight + 50;
//draw tasks for this section
- //filter task where tasks.section == section
- const tasksForSection = tasks.filter((task) => task.section === section);
if (tasksForSection.length > 0) {
drawTasks(
svg,
@@ -215,25 +234,25 @@ export const draw = function (text, id, version, diagObj) {
setupGraphViewbox(
undefined,
svg,
- conf.timeline.padding ? conf.timeline.padding : 50,
- conf.timeline.useMaxWidth ? conf.timeline.useMaxWidth : false
+ conf.timeline?.padding ?? 50,
+ conf.timeline?.useMaxWidth ?? false
);
// addSVGAccessibilityFields(diagObj.db, diagram, id);
};
export const drawTasks = function (
- diagram,
- tasks,
- sectionColor,
- masterX,
- masterY,
- maxTaskHeight,
- conf,
- maxEventCount,
- maxEventLineLength,
- maxSectionHeight,
- isWithoutSections
+ diagram: Selection,
+ tasks: TimelineTask[],
+ sectionColor: number,
+ masterX: number,
+ masterY: number,
+ maxTaskHeight: number,
+ conf: MermaidConfig,
+ maxEventCount: number,
+ maxEventLineLength: number,
+ maxSectionHeight: number,
+ isWithoutSections: boolean
) {
// Draw the tasks
for (const task of tasks) {
@@ -249,6 +268,7 @@ export const drawTasks = function (
log.debug('taskNode', taskNode);
// create task wrapper
+
const taskWrapper = diagram.append('g').attr('class', 'taskWrapper');
const node = svgDraw.drawNode(taskWrapper, taskNode, sectionColor, conf);
const taskHeight = node.height;
@@ -263,11 +283,11 @@ export const drawTasks = function (
if (task.events) {
// draw a line between the task and the events
const lineWrapper = diagram.append('g').attr('class', 'lineWrapper');
- let linelength = maxTaskHeight;
+ let lineLength = maxTaskHeight;
//add margin to task
masterY += 100;
- linelength =
- linelength + drawEvents(diagram, task.events, sectionColor, masterX, masterY, conf);
+ lineLength =
+ lineLength + drawEvents(diagram, task.events, sectionColor, masterX, masterY, conf);
masterY -= 100;
lineWrapper
@@ -290,7 +310,7 @@ export const drawTasks = function (
}
masterX = masterX + 200;
- if (isWithoutSections && !getConfig().timeline.disableMulticolor) {
+ if (isWithoutSections && !conf.timeline?.disableMulticolor) {
sectionColor++;
}
}
@@ -299,14 +319,21 @@ export const drawTasks = function (
masterY = masterY - 10;
};
-export const drawEvents = function (diagram, events, sectionColor, masterX, masterY, conf) {
+export const drawEvents = function (
+ diagram: Selection,
+ events: string[],
+ sectionColor: number,
+ masterX: number,
+ masterY: number,
+ conf: MermaidConfig
+) {
let maxEventHeight = 0;
const eventBeginY = masterY;
masterY = masterY + 100;
// Draw the events
for (const event of events) {
// create node from event
- const eventNode = {
+ const eventNode: Block = {
descr: event,
section: sectionColor,
number: sectionColor,
@@ -331,6 +358,8 @@ export const drawEvents = function (diagram, events, sectionColor, masterX, mast
};
export default {
- setConf,
+ setConf: () => {
+ // no-op
+ },
draw,
};
diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts
index df46fc9c6..c34f8f5b2 100644
--- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts
+++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts
@@ -224,6 +224,17 @@ export const drawTasks = function (diagram, tasks, verticalPos) {
num = sectionNumber % fills.length;
colour = textColours[sectionNumber % textColours.length];
+ // count how many consecutive tasks have the same section
+ let taskInSectionCount = 0;
+ const currentSection = task.section;
+ for (let taskIndex = i; taskIndex < tasks.length; taskIndex++) {
+ if (tasks[taskIndex].section == currentSection) {
+ taskInSectionCount = taskInSectionCount + 1;
+ } else {
+ break;
+ }
+ }
+
const section = {
x: i * conf.taskMargin + i * conf.width + LEFT_MARGIN,
y: 50,
@@ -231,6 +242,7 @@ export const drawTasks = function (diagram, tasks, verticalPos) {
fill,
num,
colour,
+ taskCount: taskInSectionCount,
};
svgDraw.drawSection(diagram, section, conf);
diff --git a/packages/mermaid/src/diagrams/user-journey/svgDraw.js b/packages/mermaid/src/diagrams/user-journey/svgDraw.js
index 74d5d2a02..f6dbe71e1 100644
--- a/packages/mermaid/src/diagrams/user-journey/svgDraw.js
+++ b/packages/mermaid/src/diagrams/user-journey/svgDraw.js
@@ -196,7 +196,10 @@ export const drawSection = function (elem, section, conf) {
rect.x = section.x;
rect.y = section.y;
rect.fill = section.fill;
- rect.width = conf.width;
+ // section width covers all nested tasks
+ rect.width =
+ conf.width * section.taskCount + // width of the tasks
+ conf.diagramMarginX * (section.taskCount - 1); // width of space between tasks
rect.height = conf.height;
rect.class = 'journey-section section-type-' + section.num;
rect.rx = 3;
diff --git a/packages/mermaid/src/docs/syntax/classDiagram.md b/packages/mermaid/src/docs/syntax/classDiagram.md
index 10ccc3522..9d3766590 100644
--- a/packages/mermaid/src/docs/syntax/classDiagram.md
+++ b/packages/mermaid/src/docs/syntax/classDiagram.md
@@ -76,6 +76,26 @@ classDiagram
Naming convention: a class name should be composed only of alphanumeric characters (including unicode), and underscores.
+### Class labels
+
+In case you need to provide a label for a class, you can use the following syntax:
+
+```mermaid-example
+classDiagram
+ class Animal["Animal with a label"]
+ class Car["Car with *! symbols"]
+ Animal --> Car
+```
+
+You can also use backticks to escape special characters in the label:
+
+```mermaid-example
+classDiagram
+ class `Animal Class!`
+ class `Car Class`
+ `Animal Class!` --> `Car Class`
+```
+
## Defining Members of a class
UML provides mechanisms to represent class members such as attributes and methods, as well as additional information about them.
@@ -477,11 +497,11 @@ Beginner's tip—a full example using interactive links in an HTML page:
### Styling a node
-It is possible to apply specific styles such as a thicker border or a different background color to individual nodes. This is done by predefining classes in css styles that can be applied from the graph definition:
+It is possible to apply specific styles such as a thicker border or a different background color to individual nodes. This is done by predefining classes in css styles that can be applied from the graph definition using the `cssClass` statement or the `:::` short hand.
```html