Merge pull request #4086 from sidharthv96/sidv/classDiagramLabels

feat: Add support for classDiagram labels
This commit is contained in:
Knut Sveidqvist
2023-02-28 12:03:22 +01:00
committed by GitHub
16 changed files with 856 additions and 693 deletions

View File

@@ -19,6 +19,7 @@
"brkt", "brkt",
"brolin", "brolin",
"brotli", "brotli",
"città",
"classdef", "classdef",
"codedoc", "codedoc",
"colour", "colour",

View File

@@ -22,7 +22,7 @@ export const mermaidUrl = (graphStr, options, api) => {
return url; return url;
}; };
export const imgSnapshotTest = (graphStr, _options, api = false, validation) => { export const imgSnapshotTest = (graphStr, _options = {}, api = false, validation = undefined) => {
cy.log(_options); cy.log(_options);
const options = Object.assign(_options); const options = Object.assign(_options);
if (!options.fontFamily) { if (!options.fontFamily) {

View File

@@ -13,7 +13,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('1: should render a simple class diagram', () => { it('1: should render a simple class diagram', () => {
@@ -47,7 +46,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('2: should render a simple class diagrams with cardinality', () => { it('2: should render a simple class diagrams with cardinality', () => {
@@ -76,7 +74,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('should render a simple class diagram with different visibilities', () => { it('should render a simple class diagram with different visibilities', () => {
@@ -94,7 +91,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('should render multiple class diagrams', () => { it('should render multiple class diagrams', () => {
@@ -147,7 +143,6 @@ describe('Class diagram V2', () => {
], ],
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('4: should render a simple class diagram with comments', () => { it('4: should render a simple class diagram with comments', () => {
@@ -177,7 +172,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('5: should render a simple class diagram with abstract method', () => { it('5: should render a simple class diagram with abstract method', () => {
@@ -189,7 +183,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('6: should render a simple class diagram with static method', () => { it('6: should render a simple class diagram with static method', () => {
@@ -201,7 +194,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('7: should render a simple class diagram with Generic class', () => { it('7: should render a simple class diagram with Generic class', () => {
@@ -221,7 +213,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('8: should render a simple class diagram with Generic class and relations', () => { 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 } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('9: should render a simple class diagram with clickable link', () => { it('9: should render a simple class diagram with clickable link', () => {
@@ -264,7 +254,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('10: should render a simple class diagram with clickable callback', () => { it('10: should render a simple class diagram with clickable callback', () => {
@@ -286,7 +275,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('11: should render a simple class diagram with return type on method', () => { 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 } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('12: should render a simple class diagram with generic types', () => { it('12: should render a simple class diagram with generic types', () => {
@@ -317,7 +304,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('13: should render a simple class diagram with css classes applied', () => { 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 } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('14: should render a simple class diagram with css classes applied directly', () => { 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 } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('15: should render a simple class diagram with css classes applied two multiple classes', () => { 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 } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('16a: should render a simple class diagram with static field', () => { it('16a: should render a simple class diagram with static field', () => {
@@ -378,7 +361,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('16b: should handle the direction statement with TB', () => { it('16b: should handle the direction statement with TB', () => {
@@ -403,7 +385,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('18: should handle the direction statement with LR', () => { it('18: should handle the direction statement with LR', () => {
@@ -428,7 +409,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('17a: should handle the direction statement with BT', () => { it('17a: should handle the direction statement with BT', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -452,7 +432,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('17b: should handle the direction statement with RL', () => { it('17b: should handle the direction statement with RL', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -476,7 +455,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('18: should render a simple class diagram with notes', () => { it('18: should render a simple class diagram with notes', () => {
@@ -493,7 +471,6 @@ describe('Class diagram V2', () => {
`, `,
{ logLevel: 1, flowchart: { htmlLabels: false } } { logLevel: 1, flowchart: { htmlLabels: false } }
); );
cy.get('svg');
}); });
it('1433: should render a simple class with a title', () => { it('1433: should render a simple class with a title', () => {
@@ -503,8 +480,72 @@ title: simple class diagram
--- ---
classDiagram-v2 classDiagram-v2
class Class10 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"]
`
); );
}); });
}); });

View File

@@ -12,7 +12,6 @@
<style> <style>
body { body {
background: rgb(221, 208, 208); background: rgb(221, 208, 208);
/*background:#333;*/
font-family: 'Arial'; font-family: 'Arial';
} }
h1 { h1 {
@@ -120,17 +119,9 @@ classE o-- classF : aggregation
}; };
mermaid.initialize({ mermaid.initialize({
theme: 'default', theme: 'default',
// arrowMarkerAbsolute: true,
// themeCSS: '.edgePath .path {stroke: red;} .arrowheadPath {fill: red;}',
logLevel: 0, logLevel: 0,
flowchart: { curve: 'linear', htmlLabels: true }, flowchart: { curve: 'linear', htmlLabels: true },
// gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50, showSequenceNumbers: true }, sequence: { actorMargin: 50, showSequenceNumbers: true },
// sequenceDiagram: { actorMargin: 300 } // deprecated
// fontFamily: '"arial", sans-serif',
// themeVariables: {
// fontFamily: '"arial", sans-serif',
// },
curve: 'linear', curve: 'linear',
securityLevel: 'loose', securityLevel: 'loose',
}); });

View File

@@ -37,7 +37,7 @@
</pre> </pre>
<script type="module"> <script type="module">
import mermaid from '../packages/mermaid'; import mermaid from '../packages/mermaid/src/mermaid';
mermaid.initialize({ mermaid.initialize({
theme: 'forest', theme: 'forest',
// themeCSS: '.node rect { fill: red; }', // themeCSS: '.node rect { fill: red; }',

View File

@@ -130,6 +130,40 @@ classDiagram
Naming convention: a class name should be composed only of alphanumeric characters (including unicode), and underscores. 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
```
```mermaid
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`
```
```mermaid
classDiagram
class `Animal Class!`
class `Car Class`
`Animal Class!` --> `Car Class`
```
## Defining Members of a 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. UML provides mechanisms to represent class members such as attributes and methods, as well as additional information about them.
@@ -692,11 +726,11 @@ Beginner's tip—a full example using interactive links in an HTML page:
### Styling a node ### 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 ```html
<style> <style>
.cssClass > rect { .styleClass > rect {
fill: #ff0000; fill: #ff0000;
stroke: #ffff00; stroke: #ffff00;
stroke-width: 4px; stroke-width: 4px;
@@ -706,29 +740,29 @@ It is possible to apply specific styles such as a thicker border or a different
Then attaching that class to a specific node: Then attaching that class to a specific node:
cssClass "nodeId1" cssClass; cssClass "nodeId1" styleClass;
It is also possible to attach a class to a list of nodes in one statement: It is also possible to attach a class to a list of nodes in one statement:
cssClass "nodeId1,nodeId2" cssClass; cssClass "nodeId1,nodeId2" styleClass;
A shorter form of adding a class is to attach the classname to the node using the `:::` operator: A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
```mermaid-example ```mermaid-example
classDiagram classDiagram
class Animal:::cssClass class Animal:::styleClass
``` ```
```mermaid ```mermaid
classDiagram classDiagram
class Animal:::cssClass class Animal:::styleClass
``` ```
Or: Or:
```mermaid-example ```mermaid-example
classDiagram classDiagram
class Animal:::cssClass { class Animal:::styleClass {
-int sizeInFeet -int sizeInFeet
-canEat() -canEat()
} }
@@ -736,7 +770,7 @@ classDiagram
```mermaid ```mermaid
classDiagram classDiagram
class Animal:::cssClass { class Animal:::styleClass {
-int sizeInFeet -int sizeInFeet
-canEat() -canEat()
} }

View File

@@ -264,6 +264,10 @@ export interface ClassDiagramConfig extends BaseDiagramConfig {
padding?: number; padding?: number;
textHeight?: number; textHeight?: number;
defaultRenderer?: string; defaultRenderer?: string;
nodeSpacing?: number;
rankSpacing?: number;
diagramPadding?: number;
htmlLabels?: boolean;
} }
export interface JourneyDiagramConfig extends BaseDiagramConfig { export interface JourneyDiagramConfig extends BaseDiagramConfig {

View File

@@ -772,7 +772,7 @@ const class_box = (parent, node) => {
maxWidth += interfaceBBox.width; maxWidth += interfaceBBox.width;
} }
let classTitleString = node.classData.id; let classTitleString = node.classData.label;
if (node.classData.type !== undefined && node.classData.type !== '') { if (node.classData.type !== undefined && node.classData.type !== '') {
if (getConfig().flowchart.htmlLabels) { if (getConfig().flowchart.htmlLabels) {
@@ -927,61 +927,6 @@ const class_box = (parent, node) => {
); );
verticalPos += classTitleBBox.height + rowPadding; 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('<br/>'), 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 rect
.attr('class', 'outer title-state') .attr('class', 'outer title-state')
@@ -990,13 +935,6 @@ const class_box = (parent, node) => {
.attr('width', maxWidth + node.padding) .attr('width', maxWidth + node.padding)
.attr('height', maxHeight + 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); updateNodeBounds(node, rect);
node.intersect = function (point) { node.intersect = function (point) {

View File

@@ -1,4 +1,5 @@
import { select } from 'd3'; // @ts-expect-error - d3 types issue
import { select, Selection } from 'd3';
import { log } from '../../logger'; import { log } from '../../logger';
import * as configApi from '../../config'; import * as configApi from '../../config';
import common from '../common/common'; import common from '../common/common';
@@ -13,44 +14,54 @@ import {
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
} from '../../commonDb'; } from '../../commonDb';
import { ClassRelation, ClassNode, ClassNote, ClassMap } from './classTypes';
const MERMAID_DOM_ID_PREFIX = 'classid-'; const MERMAID_DOM_ID_PREFIX = 'classId-';
let relations = []; let relations: ClassRelation[] = [];
let classes = {}; let classes: ClassMap = {};
let notes = []; let notes: ClassNote[] = [];
let classCounter = 0; 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); mermaidAPI.parseDirective(this, statement, context, type);
}; };
const splitClassNameAndType = function (id) { const splitClassNameAndType = function (id: string) {
let genericType = ''; let genericType = '';
let className = id; let className = id;
if (id.indexOf('~') > 0) { if (id.indexOf('~') > 0) {
let split = id.split('~'); const split = id.split('~');
className = split[0]; className = sanitizeText(split[0]);
genericType = sanitizeText(split[1]);
genericType = common.sanitizeText(split[1], configApi.getConfig());
} }
return { className: className, type: genericType }; 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. * Function called by parser when a node definition has been found.
* *
* @param id * @param id - Id of the class to add
* @public * @public
*/ */
export const addClass = function (id) { export const addClass = function (id: string) {
let classId = splitClassNameAndType(id); const classId = splitClassNameAndType(id);
// Only add class if not exists // Only add class if not exists
if (classes[classId.className] !== undefined) { if (classes[classId.className] !== undefined) {
return; return;
@@ -59,12 +70,13 @@ export const addClass = function (id) {
classes[classId.className] = { classes[classId.className] = {
id: classId.className, id: classId.className,
type: classId.type, type: classId.type,
label: classId.className,
cssClasses: [], cssClasses: [],
methods: [], methods: [],
members: [], members: [],
annotations: [], annotations: [],
domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter, domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter,
}; } as ClassNode;
classCounter++; classCounter++;
}; };
@@ -72,35 +84,33 @@ export const addClass = function (id) {
/** /**
* Function to lookup domId from id in the graph definition. * Function to lookup domId from id in the graph definition.
* *
* @param id * @param id - class ID to lookup
* @public * @public
*/ */
export const lookUpDomId = function (id) { export const lookUpDomId = function (id: string): string {
const classKeys = Object.keys(classes); if (id in classes) {
for (const classKey of classKeys) { return classes[id].domId;
if (classes[classKey].id === id) {
return classes[classKey].domId;
}
} }
throw new Error('Class not found: ' + id);
}; };
export const clear = function () { export const clear = function () {
relations = []; relations = [];
classes = {}; classes = {};
notes = []; notes = [];
funs = []; functions = [];
funs.push(setupToolTips); functions.push(setupToolTips);
commonClear(); commonClear();
}; };
export const getClass = function (id) { export const getClass = function (id: string) {
return classes[id]; return classes[id];
}; };
export const getClasses = function () { export const getClasses = function () {
return classes; return classes;
}; };
export const getRelations = function () { export const getRelations = function (): ClassRelation[] {
return relations; return relations;
}; };
@@ -108,7 +118,7 @@ export const getNotes = function () {
return notes; return notes;
}; };
export const addRelation = function (relation) { export const addRelation = function (relation: ClassRelation) {
log.debug('Adding relation: ' + JSON.stringify(relation)); log.debug('Adding relation: ' + JSON.stringify(relation));
addClass(relation.id1); addClass(relation.id1);
addClass(relation.id2); 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 * Adds an annotation to the specified class Annotations mark special properties of the given type
* (like 'interface' or 'service') * (like 'interface' or 'service')
* *
* @param className The class name * @param className - The class name
* @param annotation The name of the annotation without any brackets * @param annotation - The name of the annotation without any brackets
* @public * @public
*/ */
export const addAnnotation = function (className, annotation) { export const addAnnotation = function (className: string, annotation: string) {
const validatedClassName = splitClassNameAndType(className).className; const validatedClassName = splitClassNameAndType(className).className;
classes[validatedClassName].annotations.push(annotation); classes[validatedClassName].annotations.push(annotation);
}; };
@@ -145,13 +155,13 @@ export const addAnnotation = function (className, annotation) {
/** /**
* Adds a member to the specified class * Adds a member to the specified class
* *
* @param className The class name * @param className - The class name
* @param member The full name of the member. If the member is enclosed in <<brackets>> it is * @param member - The full name of the member. If the member is enclosed in `<<brackets>>` it is
* treated as an annotation If the member is ending with a closing bracket ) it is treated as a * 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 * method Otherwise the member will be treated as a normal property
* @public * @public
*/ */
export const addMember = function (className, member) { export const addMember = function (className: string, member: string) {
const validatedClassName = splitClassNameAndType(className).className; const validatedClassName = splitClassNameAndType(className).className;
const theClass = classes[validatedClassName]; const theClass = classes[validatedClassName];
@@ -161,7 +171,6 @@ export const addMember = function (className, member) {
if (memberString.startsWith('<<') && memberString.endsWith('>>')) { if (memberString.startsWith('<<') && memberString.endsWith('>>')) {
// Remove leading and trailing brackets // Remove leading and trailing brackets
// theClass.annotations.push(memberString.substring(2, memberString.length - 2));
theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2))); theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2)));
} else if (memberString.indexOf(')') > 0) { } else if (memberString.indexOf(')') > 0) {
theClass.methods.push(sanitizeText(memberString)); 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)) { if (Array.isArray(members)) {
members.reverse(); members.reverse();
members.forEach((member) => addMember(className, member)); members.forEach((member) => addMember(className, member));
} }
}; };
export const addNote = function (text, className) { export const addNote = function (text: string, className: string) {
const note = { const note = {
id: `note${notes.length}`, id: `note${notes.length}`,
class: className, class: className,
@@ -187,21 +196,20 @@ export const addNote = function (text, className) {
notes.push(note); notes.push(note);
}; };
export const cleanupLabel = function (label) { export const cleanupLabel = function (label: string) {
if (label.substring(0, 1) === ':') { if (label.startsWith(':')) {
return common.sanitizeText(label.substr(1).trim(), configApi.getConfig()); label = label.substring(1);
} else {
return sanitizeText(label.trim());
} }
return sanitizeText(label.trim());
}; };
/** /**
* Called by parser when a special node is found, e.g. a clickable element. * Called by parser when a special node is found, e.g. a clickable element.
* *
* @param ids Comma separated list of ids * @param ids - Comma separated list of ids
* @param className Class to add * @param className - Class to add
*/ */
export const setCssClass = function (ids, className) { export const setCssClass = function (ids: string, className: string) {
ids.split(',').forEach(function (_id) { ids.split(',').forEach(function (_id) {
let id = _id; let id = _id;
if (_id[0].match(/\d/)) { 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. * Called by parser when a tooltip is found, e.g. a clickable element.
* *
* @param ids Comma separated list of ids * @param ids - Comma separated list of ids
* @param tooltip Tooltip to add * @param tooltip - Tooltip to add
*/ */
const setTooltip = function (ids, tooltip) { const setTooltip = function (ids: string, tooltip?: string) {
const config = configApi.getConfig();
ids.split(',').forEach(function (id) { ids.split(',').forEach(function (id) {
if (tooltip !== undefined) { 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; return classes[id].tooltip;
}; };
/** /**
* Called by parser when a link is found. Adds the URL to the vertex data. * Called by parser when a link is found. Adds the URL to the vertex data.
* *
* @param ids Comma separated list of ids * @param ids - Comma separated list of ids
* @param linkStr URL to create a link for * @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 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(); const config = configApi.getConfig();
ids.split(',').forEach(function (_id) { ids.split(',').forEach(function (_id) {
let id = _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. * Called by parser when a click definition is found. Registers an event handler.
* *
* @param ids Comma separated list of ids * @param ids - Comma separated list of ids
* @param functionName Function to be called on click * @param functionName - Function to be called on click
* @param functionArgs Function args the function should be called with * @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) { ids.split(',').forEach(function (id) {
setClickFunc(id, functionName, functionArgs); setClickFunc(id, functionName, functionArgs);
classes[id].haveCallback = true; classes[id].haveCallback = true;
@@ -273,19 +280,19 @@ export const setClickEvent = function (ids, functionName, functionArgs) {
setCssClass(ids, 'clickable'); setCssClass(ids, 'clickable');
}; };
const setClickFunc = function (domId, functionName, functionArgs) { const setClickFunc = function (domId: string, functionName: string, functionArgs: string) {
const config = configApi.getConfig(); const config = configApi.getConfig();
let id = domId;
let elemId = lookUpDomId(id);
if (config.securityLevel !== 'loose') { if (config.securityLevel !== 'loose') {
return; return;
} }
if (functionName === undefined) { if (functionName === undefined) {
return; return;
} }
const id = domId;
if (classes[id] !== undefined) { if (classes[id] !== undefined) {
let argList = []; const elemId = lookUpDomId(id);
let argList: string[] = [];
if (typeof functionArgs === 'string') { if (typeof functionArgs === 'string') {
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */ /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/); argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
@@ -305,7 +312,7 @@ const setClickFunc = function (domId, functionName, functionArgs) {
argList.push(elemId); argList.push(elemId);
} }
funs.push(function () { functions.push(function () {
const elem = document.querySelector(`[id="${elemId}"]`); const elem = document.querySelector(`[id="${elemId}"]`);
if (elem !== null) { if (elem !== null) {
elem.addEventListener( elem.addEventListener(
@@ -320,8 +327,8 @@ const setClickFunc = function (domId, functionName, functionArgs) {
} }
}; };
export const bindFunctions = function (element) { export const bindFunctions = function (element: Element) {
funs.forEach(function (fun) { functions.forEach(function (fun) {
fun(element); fun(element);
}); });
}; };
@@ -339,8 +346,10 @@ export const relationType = {
LOLLIPOP: 4, LOLLIPOP: 4,
}; };
const setupToolTips = function (element) { const setupToolTips = function (element: Element) {
let tooltipElem = select('.mermaidTooltip'); let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
select('.mermaidTooltip');
// @ts-ignore - _groups is a dynamic property
if ((tooltipElem._groups || tooltipElem)[0][0] === null) { if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0); 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'); const nodes = svg.selectAll('g.node');
nodes nodes
.on('mouseover', function () { .on('mouseover', function () {
// @ts-expect-error - select is not part of the d3 type definition
const el = select(this); const el = select(this);
const title = el.attr('title'); 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) { if (title === null) {
return; return;
} }
// @ts-ignore - getBoundingClientRect is not part of the d3 type definition
const rect = this.getBoundingClientRect(); const rect = this.getBoundingClientRect();
tooltipElem.transition().duration(200).style('opacity', '.9'); tooltipElem.transition().duration(200).style('opacity', '.9');
@@ -368,15 +379,16 @@ const setupToolTips = function (element) {
}) })
.on('mouseout', function () { .on('mouseout', function () {
tooltipElem.transition().duration(500).style('opacity', 0); tooltipElem.transition().duration(500).style('opacity', 0);
// @ts-expect-error - select is not part of the d3 type definition
const el = select(this); const el = select(this);
el.classed('hover', false); el.classed('hover', false);
}); });
}; };
funs.push(setupToolTips); functions.push(setupToolTips);
let direction = 'TB'; let direction = 'TB';
const getDirection = () => direction; const getDirection = () => direction;
const setDirection = (dir) => { const setDirection = (dir: string) => {
direction = dir; direction = dir;
}; };
@@ -412,4 +424,5 @@ export default {
lookUpDomId, lookUpDomId,
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
setClassLabel,
}; };

View File

@@ -1,10 +1,11 @@
// @ts-expect-error Jison doesn't export types
import { parser } from './parser/classDiagram'; import { parser } from './parser/classDiagram';
import classDb from './classDb'; import classDb from './classDb';
import { vi } from 'vitest'; import { vi, describe, it, expect } from 'vitest';
const spyOn = vi.spyOn; const spyOn = vi.spyOn;
describe('class diagram, ', function () { describe('class diagram, ', function () {
describe('when parsing an info graph it', function () { describe('when parsing a class diagram', function () {
beforeEach(function () { beforeEach(function () {
parser.yy = classDb; 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 () { beforeEach(function () {
parser.yy = classDb; parser.yy = classDb;
parser.yy.clear(); parser.yy.clear();
@@ -946,4 +947,189 @@ foo()
expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); 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"] {
<<interface>>
+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');
});
});
}); });

View File

@@ -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 => `<i class='${s.replace(':', ' ')}'></i>`
// )
// };
// 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 = '<span class="edgeLabel">' + edge.text + '</span>';
} else {
edgeData.labelType = 'text';
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
if (edge.style === undefined) {
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
}
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
}
}
// 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,
};

View File

@@ -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 = '<span class="edgeLabel">' + edge.text + '</span>';
} else {
edgeData.labelType = 'text';
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
if (edge.style === undefined) {
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none';
}
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
}
}
// 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,
};

View File

@@ -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<string, ClassNode>;

View File

@@ -119,6 +119,8 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
"=" return 'EQUALS'; "=" return 'EQUALS';
\= return 'EQUALS'; \= return 'EQUALS';
\w+ return 'ALPHA'; \w+ return 'ALPHA';
"[" return 'SQS';
"]" return 'SQE';
[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION'; [!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
[0-9]+ return 'NUM'; [0-9]+ return 'NUM';
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]| [\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
@@ -249,6 +251,10 @@ statements
| statement NEWLINE statements | statement NEWLINE statements
; ;
classLabel
: SQS STR SQE { $$=$2; }
;
className className
: alphaNumToken { $$=$1; } : alphaNumToken { $$=$1; }
| classLiteralName { $$=$1; } | classLiteralName { $$=$1; }
@@ -274,10 +280,15 @@ statement
; ;
classStatement classStatement
: CLASS className {yy.addClass($2);} : classIdentifier
| CLASS className STYLE_SEPARATOR alphaNumToken {yy.addClass($2);yy.setCssClass($2, $4);} | classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
| CLASS className STRUCT_START members STRUCT_STOP {/*console.log($2,JSON.stringify($4));*/yy.addClass($2);yy.addMembers($2,$4);} | classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
| CLASS className STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.addClass($2);yy.setCssClass($2, $4);yy.addMembers($2,$6);} | 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 annotationStatement

View File

@@ -76,6 +76,26 @@ classDiagram
Naming convention: a class name should be composed only of alphanumeric characters (including unicode), and underscores. 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 ## Defining Members of a class
UML provides mechanisms to represent class members such as attributes and methods, as well as additional information about them. 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 ### 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 ```html
<style> <style>
.cssClass > rect { .styleClass > rect {
fill: #ff0000; fill: #ff0000;
stroke: #ffff00; stroke: #ffff00;
stroke-width: 4px; stroke-width: 4px;
@@ -492,27 +512,27 @@ It is possible to apply specific styles such as a thicker border or a different
Then attaching that class to a specific node: Then attaching that class to a specific node:
``` ```
cssClass "nodeId1" cssClass; cssClass "nodeId1" styleClass;
``` ```
It is also possible to attach a class to a list of nodes in one statement: It is also possible to attach a class to a list of nodes in one statement:
``` ```
cssClass "nodeId1,nodeId2" cssClass; cssClass "nodeId1,nodeId2" styleClass;
``` ```
A shorter form of adding a class is to attach the classname to the node using the `:::` operator: A shorter form of adding a class is to attach the classname to the node using the `:::` operator:
```mermaid-example ```mermaid-example
classDiagram classDiagram
class Animal:::cssClass class Animal:::styleClass
``` ```
Or: Or:
```mermaid-example ```mermaid-example
classDiagram classDiagram
class Animal:::cssClass { class Animal:::styleClass {
-int sizeInFeet -int sizeInFeet
-canEat() -canEat()
} }

View File

@@ -230,7 +230,7 @@ export function interpolateToCurve(
* @param config - Configuration passed to MermaidJS * @param config - Configuration passed to MermaidJS
* @returns The formatted URL or `undefined`. * @returns The formatted URL or `undefined`.
*/ */
export function formatUrl(linkStr: string, config: { securityLevel: string }): string | undefined { export function formatUrl(linkStr: string, config: MermaidConfig): string | undefined {
const url = linkStr.trim(); const url = linkStr.trim();
if (url) { if (url) {