diff --git a/.eslintrc.json b/.eslintrc.json index 7b95ac6da..d83222f3a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,7 +16,6 @@ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", - "plugin:jsdoc/recommended", "plugin:json/recommended", "plugin:markdown/recommended", "plugin:@cspell/recommended", @@ -24,19 +23,10 @@ ], "plugins": ["@typescript-eslint", "no-only-tests", "html", "jest", "jsdoc", "json", "@cspell"], "rules": { + "curly": "error", "no-console": "error", "no-prototype-builtins": "off", "no-unused-vars": "off", - "jsdoc/check-indentation": "off", - "jsdoc/check-alignment": "off", - "jsdoc/check-line-alignment": "off", - "jsdoc/multiline-blocks": "off", - "jsdoc/newline-after-description": "off", - "jsdoc/tag-lines": "off", - "jsdoc/require-param-description": "off", - "jsdoc/require-param-type": "off", - "jsdoc/require-returns": "off", - "jsdoc/require-returns-description": "off", "cypress/no-async-tests": "off", "@typescript-eslint/ban-ts-comment": [ "error", @@ -72,6 +62,29 @@ "no-console": "off" } }, + { + "files": ["*.{js,jsx,mjs,cjs}"], + "extends": ["plugin:jsdoc/recommended"], + "rules": { + "jsdoc/check-indentation": "off", + "jsdoc/check-alignment": "off", + "jsdoc/check-line-alignment": "off", + "jsdoc/multiline-blocks": "off", + "jsdoc/newline-after-description": "off", + "jsdoc/tag-lines": "off", + "jsdoc/require-param-description": "off", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "off", + "jsdoc/require-returns-description": "off" + } + }, + { + "files": ["*.{ts,tsx}"], + "plugins": ["tsdoc"], + "rules": { + "tsdoc/syntax": "error" + } + }, { "files": ["*.spec.{ts,js}", "cypress/**", "demos/**", "**/docs/**"], "rules": { diff --git a/README.md b/README.md index 90ae1ad4c..6d5d26463 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ Class01 <|-- AveryLongClass : Cool Class09 --> C2 : Where am I? Class09 --* C3 Class09 --|> Class07 +note "I love this diagram!\nDo you love it?" Class07 : equals() Class07 : Object[] elementData Class01 : size() @@ -174,6 +175,7 @@ class Class10 { int id size() } +note for Class10 "Cool class\nI said it's very cool class!" ``` ### State diagram [docs - live editor] diff --git a/cypress/helpers/util.js b/cypress/helpers/util.js index 33632b28a..bc5282e11 100644 --- a/cypress/helpers/util.js +++ b/cypress/helpers/util.js @@ -58,7 +58,9 @@ export const imgSnapshotTest = (graphStr, _options, api = false, validation) => const url = mermaidUrl(graphStr, options, api); cy.visit(url); - if (validation) cy.get('svg').should(validation); + if (validation) { + cy.get('svg').should(validation); + } cy.get('svg'); // Default name to test title @@ -106,7 +108,9 @@ export const urlSnapshotTest = (url, _options, api = false, validation) => { } cy.visit(url); - if (validation) cy.get('svg').should(validation); + if (validation) { + cy.get('svg').should(validation); + } cy.get('body'); // Default name to test title diff --git a/cypress/integration/rendering/classDiagram-v2.spec.js b/cypress/integration/rendering/classDiagram-v2.spec.js index d285a9237..e36693a65 100644 --- a/cypress/integration/rendering/classDiagram-v2.spec.js +++ b/cypress/integration/rendering/classDiagram-v2.spec.js @@ -478,4 +478,22 @@ describe('Class diagram V2', () => { ); cy.get('svg'); }); + + it('18: should render a simple class diagram with notes', () => { + imgSnapshotTest( + ` + classDiagram-v2 + note "I love this diagram!\nDo you love it?" + class Class10 { + <> + int id + size() + } + note for Class10 "Cool class\nI said it's very cool class!" + + `, + { logLevel: 1, flowchart: { htmlLabels: false } } + ); + cy.get('svg'); + }); }); diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index 8cf410d05..16601652d 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -407,4 +407,21 @@ describe('Class diagram', () => { // // expect(svg).to.not.have.attr('style'); // }); // }); + + it('19: should render a simple class diagram with notes', () => { + imgSnapshotTest( + ` + classDiagram + note "I love this diagram!\nDo you love it?" + class Class10 { + <> + int id + size() + } + note for Class10 "Cool class\nI said it's very cool class!" + `, + { logLevel: 1 } + ); + cy.get('svg'); + }); }); diff --git a/cypress/integration/rendering/erDiagram.spec.js b/cypress/integration/rendering/erDiagram.spec.js index 063727024..057b36dc1 100644 --- a/cypress/integration/rendering/erDiagram.spec.js +++ b/cypress/integration/rendering/erDiagram.spec.js @@ -255,4 +255,22 @@ describe('Entity Relationship Diagram', () => { ); cy.get('svg'); }); + + it('should render entities with aliases', () => { + renderGraph( + ` + erDiagram + T1 one or zero to one or more T2 : test + T2 one or many optionally to zero or one T3 : test + T3 zero or more to zero or many T4 : test + T4 many(0) to many(1) T5 : test + T5 many optionally to one T6 : test + T6 only one optionally to only one T1 : test + T4 0+ to 1+ T6 : test + T1 1 to 1 T3 : test + `, + { logLevel: 1 } + ); + cy.get('svg'); + }); }); diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index ef44ed617..f30f993fa 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -56,21 +56,10 @@
Security check
-classDiagram
-        direction LR
-        class Student {
-          -idCard : IdCard
-        }
-        class IdCard{
-          -id : int
-          -name : string
-        }
-        class Bike{
-          -id : int
-          -name : string
-        }
-        Student "1" --o "1" IdCard : carries
-        Student "1" --o "1" Bike : rides
+flowchart TD
+    A --> B
+    B --> C
+    A --> C
     
 mindmap
@@ -93,8 +82,14 @@ mindmap
       Pen and paper
       Mermaid
         
-
-      example-diagram
+    
+      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
     
@@ -111,13 +106,18 @@ mindmap theme: 'base', startOnLoad: true, logLevel: 0, - // basePath: './packages/', - // themeVariables: { darkMode: true }, + flowchart: { + useMaxWidth: false, + htmlLabels: true, + }, + gantt: { + useMaxWidth: false, + }, + useMaxWidth: false, lazyLoadedDiagrams: [ './mermaid-mindmap-detector.esm.mjs', './mermaid-example-diagram-detector.esm.mjs', ], - // lazyLoadedDiagrams: ['../../mermaid-mindmap/registry.ts'], }); function callback() { alert('It worked'); diff --git a/cypress/platform/viewer.js b/cypress/platform/viewer.js index f0426dc09..1333d7ec0 100644 --- a/cypress/platform/viewer.js +++ b/cypress/platform/viewer.js @@ -120,7 +120,9 @@ const contentLoadedApi = function () { (svgCode, bindFunctions) => { div.innerHTML = svgCode; - if (bindFunctions) bindFunctions(div); + if (bindFunctions) { + bindFunctions(div); + } }, div ); diff --git a/demos/gantt.html b/demos/gantt.html index 749208f99..227fdb1d2 100644 --- a/demos/gantt.html +++ b/demos/gantt.html @@ -74,22 +74,22 @@
     gantt
     title Hide today marker (vertical line should not be visible)
-    dateFormat YYYY-MM-DD
-    axisFormat %d
+    dateFormat Z
+    axisFormat %d/%m
     todayMarker off
     section Section1
-    Today: 1, -1h
+    Today: 1, -01:00, 5min
     

     gantt
     title Style today marker (vertical line should be 5px wide and half-transparent blue)
-    dateFormat YYYY-MM-DD
-    axisFormat %d
+    dateFormat Z
+    axisFormat %d/%m
     todayMarker stroke-width:5px,stroke:#00f,opacity:0.5
     section Section1
-    Today: 1, -1h
+    Today: 1, -01:00, 5min
     

diff --git a/docs/classDiagram.md b/docs/classDiagram.md index 60dc6c390..f89b4b002 100644 --- a/docs/classDiagram.md +++ b/docs/classDiagram.md @@ -11,7 +11,9 @@ Mermaid can render class diagrams. ```mermaid-example classDiagram + note "From Duck till Zebra" Animal <|-- Duck + note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging" Animal <|-- Fish Animal <|-- Zebra Animal : +int age @@ -35,7 +37,9 @@ classDiagram ```mermaid classDiagram + note "From Duck till Zebra" Animal <|-- Duck + note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging" Animal <|-- Fish Animal <|-- Zebra Animal : +int age @@ -549,6 +553,10 @@ You would define these actions on a separate line after all classes have been de - (_optional_) tooltip is a string to be displayed when hovering over element (note: The styles of the tooltip are set by the class .mermaidTooltip.) - note: callback function will be called with the nodeId as parameter. +## Notes + +It is possible to add notes on digram using `note "line1\nline2"` or note for class using `note for class "line1\nline2"` + ### Examples _URL Link:_ diff --git a/docs/entityRelationshipDiagram.md b/docs/entityRelationshipDiagram.md index 78d058e78..97a0a1da3 100644 --- a/docs/entityRelationshipDiagram.md +++ b/docs/entityRelationshipDiagram.md @@ -110,10 +110,34 @@ Cardinality is a property that describes how many elements of another entity can | `}o` | `o{` | Zero or more (no upper limit) | | `}\|` | `\|{` | One or more (no upper limit) | +**Aliases** + +| Value (left) | Value (right) | Alias for | +| :----------: | :-----------: | ------------ | +| one or zero | one or zero | Zero or one | +| zero or one | zero or one | Zero or one | +| one or more | one or more | One or more | +| one or many | one or many | One or more | +| many(1) | many(1) | One or more | +| 1+ | 1+ | One or more | +| zero or more | zero or more | Zero or more | +| zero or many | zero or many | Zero or more | +| many(0) | many(1) | Zero or more | +| 0+ | 0+ | Zero or more | +| only one | only one | Exactly one | +| 1 | 1 | Exactly one | + ### Identification Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line: +**Aliases** + +| Value | Alias for | +| :-----------: | :---------------: | +| to | _identifying_ | +| optionally to | _non-identifying_ | + ```mermaid-example erDiagram CAR ||--o{ NAMED-DRIVER : allows @@ -218,6 +242,7 @@ erDiagram string lastName int age } + MANUFACTURER only one to zero or more CAR ``` ```mermaid @@ -236,6 +261,7 @@ erDiagram string lastName int age } + MANUFACTURER only one to zero or more CAR ``` ### Other Things diff --git a/docs/index.html b/docs/index.html index 6f3f0f27b..3bfd9b969 100644 --- a/docs/index.html +++ b/docs/index.html @@ -40,20 +40,20 @@ }; @@ -151,8 +151,9 @@ hook.afterEach(function (html, next) { next(html); (async () => { - while (!window.hasOwnProperty('monaco')) + while (!window.hasOwnProperty('monaco')) { await new Promise((resolve) => setTimeout(resolve, 1000)); + } colorizeEverything(html).then( (newHTML) => (document.querySelector('article.markdown-section').innerHTML = newHTML) @@ -170,7 +171,9 @@ startOnLoad: false, themeCSS: '.label { font-family: Source Sans Pro,Helvetica Neue,Arial,sans-serif; }', }; - if (isDarkMode) conf.theme = 'dark'; + if (isDarkMode) { + conf.theme = 'dark'; + } mermaid.initialize(conf);
+ * ```html + * + * ``` * * A summary of all options and their defaults is found [here](#mermaidapi-configuration-defaults). * A description of each option follows below. - * - * @name Configuration */ -const config: MermaidConfig = { +const config: Partial = { /** * Theme , the CSS style sheet * @@ -30,8 +38,16 @@ const config: MermaidConfig = { * | --------- | --------------- | ------ | -------- | ---------------------------------------------- | * | theme | Built in Themes | string | Optional | 'default', 'forest', 'dark', 'neutral', 'null' | * - * **Notes:** To disable any pre-defined mermaid theme, use "null".
 "theme": "forest",
-   * "themeCSS": ".node rect { fill: red; }" 
+ * **Notes:** To disable any pre-defined mermaid theme, use "null". + * + * @example + * + * ```js + * { + * "theme": "forest", + * "themeCSS": ".node rect { fill: red; }" + * } + * ``` */ theme: 'default', themeVariables: theme['default'].getThemeVariables(), @@ -1069,7 +1085,6 @@ const config: MermaidConfig = { showCommitLabel: true, showBranches: true, rotateCommitLabel: true, - arrowMarkerAbsolute: false, }, /** The object containing configurations specific for c4 diagrams */ @@ -1834,6 +1849,13 @@ const config: MermaidConfig = { fontSize: 16, }; +if (config.class) { + config.class.arrowMarkerAbsolute = config.arrowMarkerAbsolute; +} +if (config.gitGraph) { + config.gitGraph.arrowMarkerAbsolute = config.arrowMarkerAbsolute; +} + const keyify = (obj: any, prefix = ''): string[] => Object.keys(obj).reduce((res: string[], el): string[] => { if (Array.isArray(obj[el])) { diff --git a/packages/mermaid/src/diagram-api/detectType.ts b/packages/mermaid/src/diagram-api/detectType.ts index 9536fded2..1c1abc51c 100644 --- a/packages/mermaid/src/diagram-api/detectType.ts +++ b/packages/mermaid/src/diagram-api/detectType.ts @@ -9,10 +9,13 @@ const anyComment = /\s*%%.*\n/gm; const detectors: Record = {}; /** - * @function detectType Detects the type of the graph text. Takes into consideration the possible - * existence of an %%init directive + * Detects the type of the graph text. * - * ```mermaid + * Takes into consideration the possible existence of an `%%init` directive + * + * @param text - The text defining the graph. For example: + * + * ```mermaid * %%{initialize: {"startOnLoad": true, logLevel: "fatal" }}%% * graph LR * a-->b @@ -23,13 +26,9 @@ const detectors: Record = {}; * f-->g * g-->h * ``` - * @param {string} text The text defining the graph - * @param {{ - * class: { defaultRenderer: string } | undefined; - * state: { defaultRenderer: string } | undefined; - * flowchart: { defaultRenderer: string } | undefined; - * }} [config] - * @returns {string} A graph definition key + * + * @param config - The mermaid config. + * @returns A graph definition key */ export const detectType = function (text: string, config?: MermaidConfig): string { text = text.replace(directive, '').replace(anyComment, '\n'); diff --git a/packages/mermaid/src/diagrams/c4/c4Db.js b/packages/mermaid/src/diagrams/c4/c4Db.js index 79028a0c5..d337b15c0 100644 --- a/packages/mermaid/src/diagrams/c4/c4Db.js +++ b/packages/mermaid/src/diagrams/c4/c4Db.js @@ -49,8 +49,9 @@ export const addRel = function (type, from, to, label, techn, descr, sprite, tag to === null || label === undefined || label === null - ) + ) { return; + } let rel = {}; const old = rels.find((rel) => rel.from === from && rel.to === to); @@ -111,7 +112,9 @@ export const addRel = function (type, from, to, label, techn, descr, sprite, tag //type, alias, label, ?descr, ?sprite, ?tags, $link export const addPersonOrSystem = function (typeC4Shape, alias, label, descr, sprite, tags, link) { // Don't allow label nulling - if (alias === null || label === null) return; + if (alias === null || label === null) { + return; + } let personOrSystem = {}; const old = c4ShapeArray.find((personOrSystem) => personOrSystem.alias === alias); @@ -166,7 +169,9 @@ export const addPersonOrSystem = function (typeC4Shape, alias, label, descr, spr //type, alias, label, ?techn, ?descr ?sprite, ?tags, $link export const addContainer = function (typeC4Shape, alias, label, techn, descr, sprite, tags, link) { // Don't allow label nulling - if (alias === null || label === null) return; + if (alias === null || label === null) { + return; + } let container = {}; const old = c4ShapeArray.find((container) => container.alias === alias); @@ -232,7 +237,9 @@ export const addContainer = function (typeC4Shape, alias, label, techn, descr, s //type, alias, label, ?techn, ?descr ?sprite, ?tags, $link export const addComponent = function (typeC4Shape, alias, label, techn, descr, sprite, tags, link) { // Don't allow label nulling - if (alias === null || label === null) return; + if (alias === null || label === null) { + return; + } let component = {}; const old = c4ShapeArray.find((component) => component.alias === alias); @@ -300,7 +307,9 @@ export const addPersonOrSystemBoundary = function (alias, label, type, tags, lin // if (parentBoundary === null) return; // Don't allow label nulling - if (alias === null || label === null) return; + if (alias === null || label === null) { + return; + } let boundary = {}; const old = boundarys.find((boundary) => boundary.alias === alias); @@ -354,7 +363,9 @@ export const addContainerBoundary = function (alias, label, type, tags, link) { // if (parentBoundary === null) return; // Don't allow label nulling - if (alias === null || label === null) return; + if (alias === null || label === null) { + return; + } let boundary = {}; const old = boundarys.find((boundary) => boundary.alias === alias); @@ -417,7 +428,9 @@ export const addDeploymentNode = function ( // if (parentBoundary === null) return; // Don't allow label nulling - if (alias === null || label === null) return; + if (alias === null || label === null) { + return; + } let boundary = {}; const old = boundarys.find((boundary) => boundary.alias === alias); @@ -646,8 +659,12 @@ export const updateLayoutConfig = function (typeC4Shape, c4ShapeInRowParam, c4Bo c4BoundaryInRowValue = parseInt(c4BoundaryInRowParam); } - if (c4ShapeInRowValue >= 1) c4ShapeInRow = c4ShapeInRowValue; - if (c4BoundaryInRowValue >= 1) c4BoundaryInRow = c4BoundaryInRowValue; + if (c4ShapeInRowValue >= 1) { + c4ShapeInRow = c4ShapeInRowValue; + } + if (c4BoundaryInRowValue >= 1) { + c4BoundaryInRow = c4BoundaryInRowValue; + } }; export const getC4ShapeInRow = function () { @@ -665,11 +682,13 @@ export const getParentBoundaryParse = function () { }; export const getC4ShapeArray = function (parentBoundary) { - if (parentBoundary === undefined || parentBoundary === null) return c4ShapeArray; - else + if (parentBoundary === undefined || parentBoundary === null) { + return c4ShapeArray; + } else { return c4ShapeArray.filter((personOrSystem) => { return personOrSystem.parentBoundary === parentBoundary; }); + } }; export const getC4Shape = function (alias) { return c4ShapeArray.find((personOrSystem) => personOrSystem.alias === alias); @@ -679,8 +698,11 @@ export const getC4ShapeKeys = function (parentBoundary) { }; export const getBoundarys = function (parentBoundary) { - if (parentBoundary === undefined || parentBoundary === null) return boundarys; - else return boundarys.filter((boundary) => boundary.parentBoundary === parentBoundary); + if (parentBoundary === undefined || parentBoundary === null) { + return boundarys; + } else { + return boundarys.filter((boundary) => boundary.parentBoundary === parentBoundary); + } }; export const getRels = function () { diff --git a/packages/mermaid/src/diagrams/c4/c4Renderer.js b/packages/mermaid/src/diagrams/c4/c4Renderer.js index c4f40b0bc..a9072346a 100644 --- a/packages/mermaid/src/diagrams/c4/c4Renderer.js +++ b/packages/mermaid/src/diagrams/c4/c4Renderer.js @@ -414,7 +414,9 @@ export const drawRels = function (diagram, rels, getC4ShapeObj, diagObj) { let relTextWrap = rel.wrap && conf.wrap; let relConf = messageFont(conf); let diagramType = diagObj.db.getC4Type(); - if (diagramType === 'C4Dynamic') rel.label.text = i + ': ' + rel.label.text; + if (diagramType === 'C4Dynamic') { + rel.label.text = i + ': ' + rel.label.text; + } let textLimitWidth = calculateTextWidth(rel.label.text, relConf); calcC4ShapeTextWH('label', rel, relTextWrap, relConf, textLimitWidth); @@ -555,7 +557,9 @@ function drawInsideBoundary( ); } // draw boundary - if (currentBoundary.alias !== 'global') drawBoundary(diagram, currentBoundary, currentBounds); + if (currentBoundary.alias !== 'global') { + drawBoundary(diagram, currentBoundary, currentBounds); + } parentBounds.data.stopy = Math.max( currentBounds.data.stopy + conf.c4ShapeMargin, parentBounds.data.stopy diff --git a/packages/mermaid/src/diagrams/c4/svgDraw.js b/packages/mermaid/src/diagrams/c4/svgDraw.js index 5666d9f84..437a24bcb 100644 --- a/packages/mermaid/src/diagrams/c4/svgDraw.js +++ b/packages/mermaid/src/diagrams/c4/svgDraw.js @@ -13,7 +13,9 @@ export const drawRect = function (elem, rectData) { rectElem.attr('ry', rectData.ry); if (rectData.attrs !== 'undefined' && rectData.attrs !== null) { - for (let attrKey in rectData.attrs) rectElem.attr(attrKey, rectData.attrs[attrKey]); + for (let attrKey in rectData.attrs) { + rectElem.attr(attrKey, rectData.attrs[attrKey]); + } } if (rectData.class !== 'undefined') { @@ -231,9 +233,12 @@ export const drawRels = (elem, rels, conf) => { line.attr('stroke-width', '1'); line.attr('stroke', strokeColor); line.style('fill', 'none'); - if (rel.type !== 'rel_b') line.attr('marker-end', 'url(' + url + '#arrowhead)'); - if (rel.type === 'birel' || rel.type === 'rel_b') + if (rel.type !== 'rel_b') { + line.attr('marker-end', 'url(' + url + '#arrowhead)'); + } + if (rel.type === 'birel' || rel.type === 'rel_b') { line.attr('marker-start', 'url(' + url + '#arrowend)'); + } i = -1; } else { let line = relsElem.append('path'); @@ -256,9 +261,12 @@ export const drawRels = (elem, rels, conf) => { .replaceAll('stopx', rel.endPoint.x) .replaceAll('stopy', rel.endPoint.y) ); - if (rel.type !== 'rel_b') line.attr('marker-end', 'url(' + url + '#arrowhead)'); - if (rel.type === 'birel' || rel.type === 'rel_b') + if (rel.type !== 'rel_b') { + line.attr('marker-end', 'url(' + url + '#arrowhead)'); + } + if (rel.type === 'birel' || rel.type === 'rel_b') { line.attr('marker-start', 'url(' + url + '#arrowend)'); + } } let messageConf = conf.messageFont(); @@ -314,7 +322,9 @@ const drawBoundary = function (elem, boundary, conf) { let fontColor = boundary.fontColor ? boundary.fontColor : 'black'; let attrsValue = { 'stroke-width': 1.0, 'stroke-dasharray': '7.0,7.0' }; - if (boundary.nodeType) attrsValue = { 'stroke-width': 1.0 }; + if (boundary.nodeType) { + attrsValue = { 'stroke-width': 1.0 }; + } let rectData = { x: boundary.x, y: boundary.y, diff --git a/packages/mermaid/src/diagrams/class/classDb.js b/packages/mermaid/src/diagrams/class/classDb.js index 223bfe067..83ef6072b 100644 --- a/packages/mermaid/src/diagrams/class/classDb.js +++ b/packages/mermaid/src/diagrams/class/classDb.js @@ -16,6 +16,7 @@ const MERMAID_DOM_ID_PREFIX = 'classid-'; let relations = []; let classes = {}; +let notes = []; let classCounter = 0; let funs = []; @@ -49,7 +50,9 @@ const splitClassNameAndType = function (id) { export const addClass = function (id) { let classId = splitClassNameAndType(id); // Only add class if not exists - if (typeof classes[classId.className] !== 'undefined') return; + if (typeof classes[classId.className] !== 'undefined') { + return; + } classes[classId.className] = { id: classId.className, @@ -82,6 +85,7 @@ export const lookUpDomId = function (id) { export const clear = function () { relations = []; classes = {}; + notes = []; funs = []; funs.push(setupToolTips); commonClear(); @@ -98,6 +102,10 @@ export const getRelations = function () { return relations; }; +export const getNotes = function () { + return notes; +}; + export const addRelation = function (relation) { log.debug('Adding relation: ' + JSON.stringify(relation)); addClass(relation.id1); @@ -168,6 +176,15 @@ export const addMembers = function (className, members) { } }; +export const addNote = function (text, className) { + const note = { + id: `note${notes.length}`, + class: className, + text: text, + }; + notes.push(note); +}; + export const cleanupLabel = function (label) { if (label.substring(0, 1) === ':') { return common.sanitizeText(label.substr(1).trim(), configApi.getConfig()); @@ -185,7 +202,9 @@ export const cleanupLabel = function (label) { export const setCssClass = function (ids, className) { ids.split(',').forEach(function (_id) { let id = _id; - if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + if (_id[0].match(/\d/)) { + id = MERMAID_DOM_ID_PREFIX + id; + } if (typeof classes[id] !== 'undefined') { classes[id].cssClasses.push(className); } @@ -220,7 +239,9 @@ export const setLink = function (ids, linkStr, target) { const config = configApi.getConfig(); ids.split(',').forEach(function (_id) { let id = _id; - if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + if (_id[0].match(/\d/)) { + id = MERMAID_DOM_ID_PREFIX + id; + } if (typeof classes[id] !== 'undefined') { classes[id].link = utils.formatUrl(linkStr, config); if (config.securityLevel === 'sandbox') { @@ -369,7 +390,9 @@ export default { clear, getClass, getClasses, + getNotes, addAnnotation, + addNote, getRelations, addRelation, getDirection, diff --git a/packages/mermaid/src/diagrams/class/classDetector-V2.ts b/packages/mermaid/src/diagrams/class/classDetector-V2.ts index 35614b64f..7100f6c66 100644 --- a/packages/mermaid/src/diagrams/class/classDetector-V2.ts +++ b/packages/mermaid/src/diagrams/class/classDetector-V2.ts @@ -2,8 +2,12 @@ import type { DiagramDetector } from '../../diagram-api/types'; export const classDetectorV2: DiagramDetector = (txt, config) => { // If we have configured to use dagre-wrapper then we should return true in this function for classDiagram code thus making it use the new class diagram - if (txt.match(/^\s*classDiagram/) !== null && config?.class?.defaultRenderer === 'dagre-wrapper') + if ( + txt.match(/^\s*classDiagram/) !== null && + config?.class?.defaultRenderer === 'dagre-wrapper' + ) { return true; + } // We have not opted to use the new renderer so we should return true if we detect a class diagram return txt.match(/^\s*classDiagram-v2/) !== null; }; diff --git a/packages/mermaid/src/diagrams/class/classDetector.ts b/packages/mermaid/src/diagrams/class/classDetector.ts index 93c6a863b..c3833ed28 100644 --- a/packages/mermaid/src/diagrams/class/classDetector.ts +++ b/packages/mermaid/src/diagrams/class/classDetector.ts @@ -2,7 +2,9 @@ import type { DiagramDetector } from '../../diagram-api/types'; export const classDetector: DiagramDetector = (txt, config) => { // If we have configured to use dagre-wrapper then we should never return true in this function - if (config?.class?.defaultRenderer === 'dagre-wrapper') return false; + if (config?.class?.defaultRenderer === 'dagre-wrapper') { + return false; + } // We have not opted to use the new renderer so we should return true if we detect a class diagram return txt.match(/^\s*classDiagram/) !== null; }; diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.js b/packages/mermaid/src/diagrams/class/classDiagram.spec.js index 3f47701e6..04a8e9bf3 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.js +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.js @@ -529,6 +529,16 @@ foo() parser.parse(str); }); + + it('should handle "note for"', function () { + const str = 'classDiagram\n' + 'Class11 <|.. Class12\n' + 'note for Class11 "test"\n'; + parser.parse(str); + }); + + it('should handle "note"', function () { + const str = 'classDiagram\n' + 'note "test"\n'; + parser.parse(str); + }); }); describe('when fetching data from a classDiagram graph it', function () { diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.js b/packages/mermaid/src/diagrams/class/classRenderer-v2.js index aa3f87208..fbc2e4833 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.js +++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.js @@ -134,6 +134,99 @@ export const addClasses = function (classes, g, _id, diagObj) { }); }; +/** + * 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 * @@ -305,10 +398,12 @@ export const draw = function (text, id, _version, diagObj) { // 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); diff --git a/packages/mermaid/src/diagrams/class/classRenderer.js b/packages/mermaid/src/diagrams/class/classRenderer.js index b31ddf52d..23b586192 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer.js +++ b/packages/mermaid/src/diagrams/class/classRenderer.js @@ -208,12 +208,42 @@ export const draw = function (text, id, _version, diagObj) { ); }); + const notes = diagObj.db.getNotes(); + notes.forEach(function (note) { + log.debug(`Adding note: ${JSON.stringify(note)}`); + const node = svgDraw.drawNote(diagram, note, conf, diagObj); + idCache[node.id] = node; + + // Add nodes to the graph. The first argument is the node id. The second is + // metadata about the node. In this case we're going to add labels to each of + // our nodes. + g.setNode(node.id, node); + if (note.class && note.class in classes) { + g.setEdge( + note.id, + getGraphId(note.class), + { + relation: { + id1: note.id, + id2: note.class, + relation: { + type1: 'none', + type2: 'none', + lineType: 10, + }, + }, + }, + 'DEFAULT' + ); + } + }); + dagre.layout(g); g.nodes().forEach(function (v) { if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') { log.debug('Node ' + v + ': ' + JSON.stringify(g.node(v))); root - .select('#' + diagObj.db.lookUpDomId(v)) + .select('#' + (diagObj.db.lookUpDomId(v) || v)) .attr( 'transform', 'translate(' + diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index ba0e69fba..157e3d7d8 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -56,6 +56,8 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili "callback" return 'CALLBACK'; "link" return 'LINK'; "click" return 'CLICK'; +"note for" return 'NOTE_FOR'; +"note" return 'NOTE'; "<<" return 'ANNOTATION_START'; ">>" return 'ANNOTATION_END'; [~] this.begin("generic"); @@ -263,6 +265,7 @@ statement | annotationStatement | clickStatement | cssClassStatement + | noteStatement | directive | direction | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } @@ -300,6 +303,11 @@ relationStatement | className STR relation STR className { $$ = {id1:$1, id2:$5, relation:$3, relationTitle1:$2, relationTitle2:$4} } ; +noteStatement + : NOTE_FOR className noteText { yy.addNote($3, $2); } + | NOTE noteText { yy.addNote($2); } + ; + relation : relationType lineType relationType { $$={type1:$1,type2:$3,lineType:$2}; } | lineType relationType { $$={type1:'none',type2:$2,lineType:$1}; } @@ -351,4 +359,6 @@ alphaNumToken : UNICODE_TEXT | NUM | ALPHA; classLiteralName : BQUOTE_STR; +noteText : STR; + %% diff --git a/packages/mermaid/src/diagrams/class/styles.js b/packages/mermaid/src/diagrams/class/styles.js index 9e7665c58..bb5580492 100644 --- a/packages/mermaid/src/diagrams/class/styles.js +++ b/packages/mermaid/src/diagrams/class/styles.js @@ -80,6 +80,10 @@ g.classGroup line { stroke-dasharray: 3; } +.dotted-line{ + stroke-dasharray: 1 2; +} + #compositionStart, .composition { fill: ${options.lineColor} !important; stroke: ${options.lineColor} !important; diff --git a/packages/mermaid/src/diagrams/class/svgDraw.js b/packages/mermaid/src/diagrams/class/svgDraw.js index 443765b48..35f793460 100644 --- a/packages/mermaid/src/diagrams/class/svgDraw.js +++ b/packages/mermaid/src/diagrams/class/svgDraw.js @@ -9,13 +9,13 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) { switch (type) { case diagObj.db.relationType.AGGREGATION: return 'aggregation'; - case diagObj.db.EXTENSION: + case diagObj.db.relationType.EXTENSION: return 'extension'; - case diagObj.db.COMPOSITION: + case diagObj.db.relationType.COMPOSITION: return 'composition'; - case diagObj.db.DEPENDENCY: + case diagObj.db.relationType.DEPENDENCY: return 'dependency'; - case diagObj.db.LOLLIPOP: + case diagObj.db.relationType.LOLLIPOP: return 'lollipop'; } }; @@ -55,6 +55,9 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) { if (relation.relation.lineType == 1) { svgPath.attr('class', 'relation dashed-line'); } + if (relation.relation.lineType == 10) { + svgPath.attr('class', 'relation dotted-line'); + } if (relation.relation.type1 !== 'none') { svgPath.attr( 'marker-start', @@ -190,7 +193,9 @@ export const drawClass = function (elem, classDef, conf, diagObj) { let isFirst = true; classDef.annotations.forEach(function (member) { const titleText2 = title.append('tspan').text('«' + member + '»'); - if (!isFirst) titleText2.attr('dy', conf.textHeight); + if (!isFirst) { + titleText2.attr('dy', conf.textHeight); + } isFirst = false; }); @@ -203,7 +208,9 @@ export const drawClass = function (elem, classDef, conf, diagObj) { const classTitle = title.append('tspan').text(classTitleString).attr('class', 'title'); // If class has annotations the title needs to have an offset of the text height - if (!isFirst) classTitle.attr('dy', conf.textHeight); + if (!isFirst) { + classTitle.attr('dy', conf.textHeight); + } const titleHeight = title.node().getBBox().height; @@ -284,6 +291,69 @@ export const drawClass = function (elem, classDef, conf, diagObj) { return classInfo; }; +/** + * Renders a note diagram + * + * @param {SVGSVGElement} elem The element to draw it into + * @param {{id: string; text: string; class: string;}} note + * @param conf + * @param diagObj + * @todo Add more information in the JSDOC here + */ +export const drawNote = function (elem, note, conf, diagObj) { + log.debug('Rendering note ', note, conf); + + const id = note.id; + const noteInfo = { + id: id, + text: note.text, + width: 0, + height: 0, + }; + + // add class group + const g = elem.append('g').attr('id', id).attr('class', 'classGroup'); + + // add text + let text = g + .append('text') + .attr('y', conf.textHeight + conf.padding) + .attr('x', 0); + + const lines = JSON.parse(`"${note.text}"`).split('\n'); + + lines.forEach(function (line) { + log.debug(`Adding line: ${line}`); + text.append('tspan').text(line).attr('class', 'title').attr('dy', conf.textHeight); + }); + + const noteBox = g.node().getBBox(); + + const rect = g + .insert('rect', ':first-child') + .attr('x', 0) + .attr('y', 0) + .attr('width', noteBox.width + 2 * conf.padding) + .attr( + 'height', + noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin + ); + + const rectWidth = rect.node().getBBox().width; + + // Center title + // We subtract the width of each text element from the class box width and divide it by 2 + text.node().childNodes.forEach(function (x) { + x.setAttribute('x', (rectWidth - x.getBBox().width) / 2); + }); + + noteInfo.width = rectWidth; + noteInfo.height = + noteBox.height + lines.length * conf.textHeight + conf.padding + 0.5 * conf.dividerMargin; + + return noteInfo; +}; + export const parseMember = function (text) { const fieldRegEx = /^(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+) *(\*|\$)?$/; const methodRegEx = /^([+|\-|~|#])?(\w+) *\( *(.*)\) *(\*|\$)? *(\w*[~|[\]]*\s*\w*~?)$/; @@ -435,5 +505,6 @@ const parseClassifier = function (classifier) { export default { drawClass, drawEdge, + drawNote, parseMember, }; diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts index 8caa4c7a6..782915cc1 100644 --- a/packages/mermaid/src/diagrams/common/common.ts +++ b/packages/mermaid/src/diagrams/common/common.ts @@ -4,11 +4,13 @@ import { MermaidConfig } from '../../config.type'; /** * Gets the rows of lines in a string * - * @param {string | undefined} s The string to check the lines for - * @returns {string[]} The rows in that string + * @param s - The string to check the lines for + * @returns The rows in that string */ export const getRows = (s?: string): string[] => { - if (!s) return ['']; + if (!s) { + return ['']; + } const str = breakToPlaceholder(s).replace(/\\n/g, '#br#'); return str.split('#br#'); }; @@ -16,8 +18,8 @@ export const getRows = (s?: string): string[] => { /** * Removes script tags from a text * - * @param {string} txt The text to sanitize - * @returns {string} The safer text + * @param txt - The text to sanitize + * @returns The safer text */ export const removeScript = (txt: string): string => { return DOMPurify.sanitize(txt); @@ -39,7 +41,9 @@ const sanitizeMore = (text: string, config: MermaidConfig) => { }; export const sanitizeText = (text: string, config: MermaidConfig): string => { - if (!text) return text; + if (!text) { + return text; + } if (config.dompurifyConfig) { text = DOMPurify.sanitize(sanitizeMore(text, config), config.dompurifyConfig).toString(); } else { @@ -52,7 +56,9 @@ export const sanitizeTextOrArray = ( a: string | string[] | string[][], config: MermaidConfig ): string | string[] => { - if (typeof a === 'string') return sanitizeText(a, config); + if (typeof a === 'string') { + return sanitizeText(a, config); + } // TODO: Refactor to avoid flat. return a.flat().map((x: string) => sanitizeText(x, config)); }; @@ -62,8 +68,8 @@ export const lineBreakRegex = //gi; /** * Whether or not a text has any line breaks * - * @param {string} text The text to test - * @returns {boolean} Whether or not the text has breaks + * @param text - The text to test + * @returns Whether or not the text has breaks */ export const hasBreaks = (text: string): boolean => { return lineBreakRegex.test(text); @@ -72,8 +78,8 @@ export const hasBreaks = (text: string): boolean => { /** * Splits on
tags * - * @param {string} text Text to split - * @returns {string[]} List of lines as strings + * @param text - Text to split + * @returns List of lines as strings */ export const splitBreaks = (text: string): string[] => { return text.split(lineBreakRegex); @@ -82,8 +88,8 @@ export const splitBreaks = (text: string): string[] => { /** * Converts placeholders to line breaks in HTML * - * @param {string} s HTML with placeholders - * @returns {string} HTML with breaks instead of placeholders + * @param s - HTML with placeholders + * @returns HTML with breaks instead of placeholders */ const placeholderToBreak = (s: string): string => { return s.replace(/#br#/g, '
'); @@ -92,8 +98,8 @@ const placeholderToBreak = (s: string): string => { /** * Opposite of `placeholderToBreak`, converts breaks to placeholders * - * @param {string} s HTML string - * @returns {string} String with placeholders + * @param s - HTML string + * @returns String with placeholders */ const breakToPlaceholder = (s: string): string => { return s.replace(lineBreakRegex, '#br#'); @@ -102,8 +108,8 @@ const breakToPlaceholder = (s: string): string => { /** * Gets the current URL * - * @param {boolean} useAbsolute Whether to return the absolute URL or not - * @returns {string} The current URL + * @param useAbsolute - Whether to return the absolute URL or not + * @returns The current URL */ const getUrl = (useAbsolute: boolean): string => { let url = ''; @@ -124,8 +130,8 @@ const getUrl = (useAbsolute: boolean): string => { /** * Converts a string/boolean into a boolean * - * @param {string | boolean} val String or boolean to convert - * @returns {boolean} The result from the input + * @param val - String or boolean to convert + * @returns The result from the input */ export const evaluate = (val?: string | boolean): boolean => val === false || ['false', 'null', '0'].includes(String(val).trim().toLowerCase()) ? false : true; @@ -133,12 +139,15 @@ export const evaluate = (val?: string | boolean): boolean => /** * Makes generics in typescript syntax * - * @example Array of array of strings in typescript syntax - * // returns "Array>" - * parseGenericTypes('Array~Array~string~~'); + * @example + * Array of array of strings in typescript syntax * - * @param {string} text The text to convert - * @returns {string} The converted string + * ```js + * // returns "Array>" + * parseGenericTypes('Array~Array~string~~'); + * ``` + * @param text - The text to convert + * @returns The converted string */ export const parseGenericTypes = function (text: string): string { let cleanedText = text; diff --git a/packages/mermaid/src/diagrams/er/erRenderer.js b/packages/mermaid/src/diagrams/er/erRenderer.js index 3e6384e6a..a4d5c8bd1 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer.js +++ b/packages/mermaid/src/diagrams/er/erRenderer.js @@ -357,7 +357,7 @@ const drawEntities = function (svgNode, entities, graph) { const rectNode = groupNode .insert('rect', '#' + textId) .attr('class', 'er entityBox') - .attr('fill', conf.fill) + .style('fill', conf.fill) .attr('fill-opacity', '100%') .attr('stroke', conf.stroke) .attr('x', 0) diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison index 6294599b5..f0411fd72 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison @@ -36,15 +36,32 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili [\n]+ /* nothing */ "}" { this.popState(); return 'BLOCK_STOP'; } . return yytext[0]; + +"one or zero" return 'ZERO_OR_ONE'; +"one or more" return 'ONE_OR_MORE'; +"one or many" return 'ONE_OR_MORE'; +"1+" return 'ONE_OR_MORE'; \|o return 'ZERO_OR_ONE'; +"zero or one" return 'ZERO_OR_ONE'; +"zero or more" return 'ZERO_OR_MORE'; +"zero or many" return 'ZERO_OR_MORE'; +"0+" return 'ZERO_OR_MORE'; \}o return 'ZERO_OR_MORE'; +"many(0)" return 'ZERO_OR_MORE'; +"many(1)" return 'ONE_OR_MORE'; +"many" return 'ZERO_OR_MORE'; \}\| return 'ONE_OR_MORE'; +"one" return 'ONLY_ONE'; +"only one" return 'ONLY_ONE'; +"1" return 'ONLY_ONE'; \|\| return 'ONLY_ONE'; o\| return 'ZERO_OR_ONE'; o\{ return 'ZERO_OR_MORE'; \|\{ return 'ONE_OR_MORE'; \.\. return 'NON_IDENTIFYING'; \-\- return 'IDENTIFYING'; +"to" return 'IDENTIFYING'; +"optionally to" return 'NON_IDENTIFYING'; \.\- return 'NON_IDENTIFYING'; \-\. return 'NON_IDENTIFYING'; [A-Za-z][A-Za-z0-9\-_]* return 'ALPHANUM'; diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js index 1d891ffea..eb738fe4b 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js @@ -532,18 +532,100 @@ describe('when parsing ER diagram it...', function () { expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE); }); + it('should handle zero-or-one-to-zero-or-more relationships (aliases "one or zero" and "zero or many")', function () { + erDiagram.parser.parse('erDiagram\nA one or zero to many B : has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE); + }); + + it('should handle one-or-more-to-zero-or-one relationships (aliases "one or many" and "zero or one")', function () { + erDiagram.parser.parse('erDiagram\nA one or many optionally to zero or one B : has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONE_OR_MORE); + }); + + it('should handle zero-or-more-to-zero-or-more relationships (aliases "zero or more" and "zero or many")', function () { + erDiagram.parser.parse('erDiagram\nA zero or more to zero or many B : has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE); + }); + + it('should handle zero-or-more-to-one-or-more relationships (aliases "many(0)" and "many(1)")', function () { + erDiagram.parser.parse('erDiagram\nA many(0) to many(1) B : has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE); + }); + + it('should handle zero-or-more-to-only-one relationships (aliases "many(0)" and "many(1)")', function () { + erDiagram.parser.parse('erDiagram\nA many optionally to one B : has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE); + }); + + it('should handle only-one-to-only-one relationships (aliases "only one" and "1+")', function () { + erDiagram.parser.parse('erDiagram\nA only one optionally to 1+ B : has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONE_OR_MORE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ONLY_ONE); + }); + + it('should handle zero-or-more-to-only-one relationships (aliases "0+" and "1")', function () { + erDiagram.parser.parse('erDiagram\nA 0+ optionally to 1 B : has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE); + }); + it('should represent identifying relationships properly', function () { erDiagram.parser.parse('erDiagram\nHOUSE ||--|{ ROOM : contains'); const rels = erDb.getRelationships(); expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING); }); + it('should represent identifying relationships properly (alias "to")', function () { + erDiagram.parser.parse('erDiagram\nHOUSE one to one ROOM : contains'); + const rels = erDb.getRelationships(); + expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING); + }); + it('should represent non-identifying relationships properly', function () { erDiagram.parser.parse('erDiagram\n PERSON ||..o{ POSSESSION : owns'); const rels = erDb.getRelationships(); expect(rels[0].relSpec.relType).toBe(erDb.Identification.NON_IDENTIFYING); }); + it('should represent non-identifying relationships properly (alias "optionally to")', function () { + erDiagram.parser.parse('erDiagram\n PERSON many optionally to many POSSESSION : owns'); + const rels = erDb.getRelationships(); + expect(rels[0].relSpec.relType).toBe(erDb.Identification.NON_IDENTIFYING); + }); + it('should not accept a syntax error', function () { const doc = 'erDiagram\nA xxx B : has'; expect(() => { diff --git a/packages/mermaid/src/diagrams/error/errorRenderer.ts b/packages/mermaid/src/diagrams/error/errorRenderer.ts index df9ce2c6e..b4e267684 100644 --- a/packages/mermaid/src/diagrams/error/errorRenderer.ts +++ b/packages/mermaid/src/diagrams/error/errorRenderer.ts @@ -8,7 +8,7 @@ let conf = {}; /** * Merges the value of `conf` with the passed `cnf` * - * @param {object} cnf Config to merge + * @param cnf - Config to merge */ export const setConf = function (cnf: any) { conf = { ...conf, ...cnf }; @@ -17,11 +17,11 @@ export const setConf = function (cnf: any) { /** * Draws a an info picture in the tag with id: id based on the graph definition in text. * - * @param text - * @param {string} id The text for the error - * @param {string} mermaidVersion The version + * @param _text - Mermaid graph definition. + * @param id - The text for the error + * @param mermaidVersion - The version */ -export const draw = (text: string, id: string, mermaidVersion: string) => { +export const draw = (_text: string, id: string, mermaidVersion: string) => { try { log.debug('Renering svg for syntax error\n'); diff --git a/packages/mermaid/src/diagrams/flowchart/flowChartShapes.js b/packages/mermaid/src/diagrams/flowchart/flowChartShapes.js index 32a26720f..b66bfe730 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowChartShapes.js +++ b/packages/mermaid/src/diagrams/flowchart/flowChartShapes.js @@ -281,9 +281,13 @@ function cylinder(parent, bbox, node) { // ellipsis equation: x*x / a*a + y*y / b*b = 1 // solve for y to get adjusted value for pos.y let y = ry * ry * (1 - (x * x) / (rx * rx)); - if (y != 0) y = Math.sqrt(y); + if (y != 0) { + y = Math.sqrt(y); + } y = ry - y; - if (point.y - node.y > 0) y = -y; + if (point.y - node.y > 0) { + y = -y; + } pos.y += y; } diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.js b/packages/mermaid/src/diagrams/flowchart/flowDb.js index 9e8f25524..6abc22659 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.js +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.js @@ -119,7 +119,11 @@ export const addVertex = function (_id, text, type, style, classes, dir, props = if (typeof dir !== 'undefined') { vertices[id].dir = dir; } - vertices[id].props = props; + if (typeof vertices[id].props === 'undefined') { + vertices[id].props = props; + } else if (typeof props !== 'undefined') { + Object.assign(vertices[id].props, props); + } }; /** @@ -699,7 +703,9 @@ const destructLink = (_str, _startStr) => { startInfo.type = info.type; } else { // x-- xyz --> - not supported - if (startInfo.type !== info.type) return { type: 'INVALID', stroke: 'INVALID' }; + if (startInfo.type !== info.type) { + return { type: 'INVALID', stroke: 'INVALID' }; + } startInfo.type = 'double_' + startInfo.type; } diff --git a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts index 7ef39e421..c88a63fa6 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts @@ -2,7 +2,8 @@ import type { DiagramDetector } from '../../diagram-api/types'; export const flowDetectorV2: DiagramDetector = (txt, config) => { // 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 (config?.flowchart?.defaultRenderer === 'dagre-wrapper' && txt.match(/^\s*graph/) !== null) + if (config?.flowchart?.defaultRenderer === 'dagre-wrapper' && txt.match(/^\s*graph/) !== null) { 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 0cf54c7d5..02ef63f99 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDetector.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDetector.ts @@ -3,6 +3,8 @@ import type { DiagramDetector } from '../../diagram-api/types'; export const flowDetector: 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 === 'dagre-wrapper') { + 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 096b5a410..99c93ea04 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -153,7 +153,9 @@ export const isInvalidDate = function (date, dateFormat, excludes, includes) { }; const checkTaskDates = function (task, dateFormat, excludes, includes) { - if (!excludes.length || task.manualEndTime) return; + if (!excludes.length || task.manualEndTime) { + return; + } let startTime = moment(task.startTime, dateFormat, true); startTime.add(1, 'd'); let endTime = moment(task.endTime, dateFormat, true); diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index 8ad9ca037..c9f6836a5 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -427,7 +427,9 @@ export const draw = function (text, id, version, diagObj) { ); const maxTime = tasks.reduce((max, { endTime }) => (max ? Math.max(max, endTime) : endTime), 0); const dateFormat = diagObj.db.getDateFormat(); - if (!minTime || !maxTime) return; + if (!minTime || !maxTime) { + return; + } const excludeRanges = []; let range = null; @@ -552,7 +554,9 @@ export const draw = function (text, id, version, diagObj) { const tspan = doc.createElementNS('http://www.w3.org/2000/svg', 'tspan'); tspan.setAttribute('alignment-baseline', 'central'); tspan.setAttribute('x', '10'); - if (j > 0) tspan.setAttribute('dy', '1em'); + if (j > 0) { + tspan.setAttribute('dy', '1em'); + } tspan.textContent = rows[j]; svgLabel.appendChild(tspan); } diff --git a/packages/mermaid/src/diagrams/git/gitGraphAst.js b/packages/mermaid/src/diagrams/git/gitGraphAst.js index c5674f39d..496e578b7 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphAst.js +++ b/packages/mermaid/src/diagrams/git/gitGraphAst.js @@ -399,7 +399,9 @@ function upsert(arr, key, newVal) { /** @param commitArr */ function prettyPrintCommitHistory(commitArr) { const commit = commitArr.reduce((out, commit) => { - if (out.seq > commit.seq) return out; + if (out.seq > commit.seq) { + return out; + } return commit; }, commitArr[0]); let line = ''; @@ -412,7 +414,9 @@ function prettyPrintCommitHistory(commitArr) { }); const label = [line, commit.id, commit.seq]; for (let branch in branches) { - if (branches[branch] === commit.id) label.push(branch); + if (branches[branch] === commit.id) { + label.push(branch); + } } log.debug(label.join(' ')); if (commit.parents && commit.parents.length == 2) { @@ -452,7 +456,9 @@ export const clear = function () { export const getBranchesAsObjArray = function () { const branchesArray = Object.values(branchesConfig) .map((branchConfig, i) => { - if (branchConfig.order !== null) return branchConfig; + if (branchConfig.order !== null) { + return branchConfig; + } return { ...branchConfig, order: parseFloat(`0.${i}`, 10), diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js b/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js index fa46dfde9..bfb0ea71c 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer-old.js @@ -357,7 +357,9 @@ export const draw = function (txt, id, ver) { branchNum++; } svg.attr('height', function () { - if (direction === 'BT') return Object.keys(allCommitsDict).length * config.nodeSpacing; + if (direction === 'BT') { + return Object.keys(allCommitsDict).length * config.nodeSpacing; + } return (branches.length + 1) * config.branchOffset; }); } catch (e) { diff --git a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js index e15e43ac3..71698a500 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphRenderer.js +++ b/packages/mermaid/src/diagrams/git/gitGraphRenderer.js @@ -496,7 +496,7 @@ const drawBranches = (svg, branches) => { export const draw = function (txt, id, ver, diagObj) { clear(); const conf = getConfig(); - const gitGraphConfig = getConfig().gitGraph; + const gitGraphConfig = conf.gitGraph; // try { log.debug('in gitgraph renderer', txt + '\n', 'id:', id, ver); @@ -523,7 +523,12 @@ export const draw = function (txt, id, ver, diagObj) { drawCommits(diagram, allCommitsDict, true); // Setup the view box and size of the svg element - setupGraphViewbox(undefined, diagram, gitGraphConfig.diagramPadding, conf.useMaxWidth); + setupGraphViewbox( + undefined, + diagram, + gitGraphConfig.diagramPadding, + gitGraphConfig.useMaxWidth ?? conf.useMaxWidth + ); }; export default { diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.js b/packages/mermaid/src/diagrams/pie/pieRenderer.js index c630c82fe..6cbb99fa3 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.js +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.js @@ -94,10 +94,22 @@ export const draw = (txt, id, _version, diagObj) => { var color = scaleOrdinal().range(myGeneratedColors); // Compute the position of each group on the pie: - var pie = d3pie().value(function (d) { - return d[1]; + var pieData = Object.entries(data).map(function (el, idx) { + return { + order: idx, + name: el[0], + value: el[1], + }; }); - var dataReady = pie(Object.entries(data)); + var pie = d3pie() + .value(function (d) { + return d.value; + }) + .sort(function (a, b) { + // Sort slices in clockwise direction + return a.order - b.order; + }); + var dataReady = pie(pieData); // Shape helper to build arcs: var arcGenerator = arc().innerRadius(0).outerRadius(radius); @@ -110,7 +122,7 @@ export const draw = (txt, id, _version, diagObj) => { .append('path') .attr('d', arcGenerator) .attr('fill', function (d) { - return color(d.data[0]); + return color(d.data.name); }) .attr('class', 'pieCircle'); @@ -122,7 +134,7 @@ export const draw = (txt, id, _version, diagObj) => { .enter() .append('text') .text(function (d) { - return ((d.data[1] / sum) * 100).toFixed(0) + '%'; + return ((d.data.value / sum) * 100).toFixed(0) + '%'; }) .attr('transform', function (d) { return 'translate(' + arcGenerator.centroid(d) + ')'; @@ -166,9 +178,9 @@ export const draw = (txt, id, _version, diagObj) => { .attr('y', legendRectSize - legendSpacing) .text(function (d) { if (diagObj.db.getShowData() || conf.showData || conf.pie.showData) { - return d.data[0] + ' [' + d.data[1] + ']'; + return d.data.name + ' [' + d.data.value + ']'; } else { - return d.data[0]; + return d.data.name; } }); } catch (e) { diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.js b/packages/mermaid/src/diagrams/sequence/sequenceDb.js index 6c863e204..ba9d0549b 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDb.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.js @@ -26,7 +26,9 @@ export const parseDirective = function (statement, context, type) { export const addActor = function (id, name, description, type) { // Don't allow description nulling const old = actors[id]; - if (old && name === old.name && description == null) return; + if (old && name === old.name && description == null) { + return; + } // Don't allow null descriptions, either if (description == null || description.text == null) { diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 054de3d9a..fa943d658 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth'; import utils from '../../utils'; import { configureSvgSize } from '../../setupGraphViewbox'; import addSVGAccessibilityFields from '../../accessibility'; +import Diagram from '../../Diagram'; let conf = {}; @@ -100,8 +101,8 @@ export const bounds = { // eslint-disable-next-line @typescript-eslint/no-this-alias const _self = this; let cnt = 0; - /** @param {any} type */ - function updateFn(type) { + /** @param type - Either `activation` or `undefined` */ + function updateFn(type?: 'activation') { return function updateItemBounds(item) { cnt++; // The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems @@ -200,15 +201,25 @@ export const bounds = { }, }; +/** Options for drawing a note in {@link drawNote} */ +interface NoteModel { + /** x axis start position */ + startx: number; + /** y axis position */ + starty: number; + /** the message to be shown */ + message: string; + /** Set this with a custom width to override the default configured width. */ + width: number; +} + /** * Draws an note in the diagram with the attached line * - * @param {any} elem - The diagram to draw to. - * @param {{ x: number; y: number; message: string; width: number }} noteModel - startX: x axis - * start position, verticalPos: y axis position, message: the message to be shown, width: Set - * this with a custom width to override the default configured width. + * @param elem - The diagram to draw to. + * @param noteModel - Note model options. */ -const drawNote = function (elem, noteModel) { +const drawNote = function (elem: any, noteModel: NoteModel) { bounds.bumpVerticalPos(conf.boxMargin); noteModel.height = conf.boxMargin; noteModel.starty = bounds.getVerticalPos(); @@ -278,11 +289,11 @@ const actorFont = (cnf) => { * message so it can be drawn later. We do not draw the message at this point so the arrowhead can * be on top of the activation box. * - * @param {any} diagram - The parent of the message element - * @param {any} msgModel - The model containing fields describing a message - * @returns {number} lineStartY - The Y coordinate at which the message line starts + * @param _diagram - The parent of the message element. + * @param msgModel - The model containing fields describing a message + * @returns `lineStartY` - The Y coordinate at which the message line starts */ -const boundMessage = function (diagram, msgModel) { +function boundMessage(_diagram, msgModel): number { bounds.bumpVerticalPos(10); const { startx, stopx, message } = msgModel; const lines = common.splitBreaks(message).length; @@ -321,17 +332,17 @@ const boundMessage = function (diagram, msgModel) { bounds.insert(msgModel.fromBounds, msgModel.starty, msgModel.toBounds, msgModel.stopy); return lineStartY; -}; +} /** * Draws a message. Note that the bounds have previously been updated by boundMessage. * - * @param {any} diagram - The parent of the message element - * @param {any} msgModel - The model containing fields describing a message - * @param {number} lineStartY - The Y coordinate at which the message line starts - * @param diagObj + * @param diagram - The parent of the message element + * @param msgModel - The model containing fields describing a message + * @param lineStartY - The Y coordinate at which the message line starts + * @param diagObj - The diagram object. */ -const drawMessage = function (diagram, msgModel, lineStartY, diagObj) { +const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const textObj = svgDraw.getTextObj(); @@ -554,13 +565,6 @@ const activationBounds = function (actor, actors) { return [left, right]; }; -/** - * @param {any} loopWidths - * @param {any} msg - * @param {any} preMargin - * @param {any} postMargin - * @param {any} addLoopFn - */ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoopFn) { bounds.bumpVerticalPos(preMargin); let heightAdjust = postMargin; @@ -584,12 +588,12 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop /** * Draws a sequenceDiagram in the tag with id: id based on the graph definition in text. * - * @param {any} _text The text of the diagram - * @param {any} id The id of the diagram which will be used as a DOM element id¨ - * @param {any} _version Mermaid version from package.json - * @param {any} diagObj A standard diagram containing the db and the text and type etc of the diagram + * @param _text - The text of the diagram + * @param id - The id of the diagram which will be used as a DOM element id¨ + * @param _version - Mermaid version from package.json + * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram */ -export const draw = function (_text, id, _version, diagObj) { +export const draw = function (_text: string, id: string, _version: string, diagObj: Diagram) { const { securityLevel, sequence } = configApi.getConfig(); conf = sequence; // Handle root and Document for when rendering in sandbox mode @@ -632,10 +636,10 @@ export const draw = function (_text, id, _version, diagObj) { svgDraw.insertSequenceNumber(diagram); /** - * @param {any} msg - * @param {any} verticalPos + * @param msg - The message to draw. + * @param verticalPos - The vertical position of the message. */ - function activeEnd(msg, verticalPos) { + function activeEnd(msg: any, verticalPos: number) { const activationData = bounds.endActivation(msg); if (activationData.starty + 18 > verticalPos) { activationData.starty = verticalPos - 6; @@ -762,8 +766,11 @@ export const draw = function (_text, id, _version, diagObj) { case diagObj.db.LINETYPE.AUTONUMBER: sequenceIndex = msg.message.start || sequenceIndex; sequenceIndexStep = msg.message.step || sequenceIndexStep; - if (msg.message.visible) diagObj.db.enableSequenceNumbers(); - else diagObj.db.disableSequenceNumbers(); + if (msg.message.visible) { + diagObj.db.enableSequenceNumbers(); + } else { + diagObj.db.disableSequenceNumbers(); + } break; case diagObj.db.LINETYPE.CRITICAL_START: adjustLoopHeightForWrap( @@ -907,12 +914,16 @@ export const draw = function (_text, id, _version, diagObj) { * It will enumerate each given message, and will determine its text width, in relation to the actor * it originates from, and destined to. * - * @param {any} actors - The actors map - * @param {Array} messages - A list of message objects to iterate - * @param diagObj - * @returns {any} + * @param actors - The actors map + * @param messages - A list of message objects to iterate + * @param diagObj - The diagram object. + * @returns The max message width of each actor. */ -const getMaxMessageWidthPerActor = function (actors, messages, diagObj) { +function getMaxMessageWidthPerActor( + actors: { [id: string]: any }, + messages: any[], + diagObj: Diagram +): { [id: string]: number } { const maxMessageWidthPerActor = {}; messages.forEach(function (msg) { @@ -1005,7 +1016,7 @@ const getMaxMessageWidthPerActor = function (actors, messages, diagObj) { log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor); return maxMessageWidthPerActor; -}; +} const getRequiredPopupWidth = function (actor) { let requiredPopupWidth = 0; @@ -1022,15 +1033,19 @@ const getRequiredPopupWidth = function (actor) { }; /** - * This will calculate the optimal margin for each given actor, for a given actor->messageWidth map. + * This will calculate the optimal margin for each given actor, + * for a given actor → messageWidth map. * * An actor's margin is determined by the width of the actor, the width of the largest message that * originates from it, and the configured conf.actorMargin. * - * @param {any} actors - The actors map to calculate margins for - * @param {any} actorToMessageWidth - A map of actor key -> max message width it holds + * @param actors - The actors map to calculate margins for + * @param actorToMessageWidth - A map of actor key → max message width it holds */ -const calculateActorMargins = function (actors, actorToMessageWidth) { +function calculateActorMargins( + actors: { [id: string]: any }, + actorToMessageWidth: ReturnType +) { let maxHeight = 0; Object.keys(actors).forEach((prop) => { const actor = actors[prop]; @@ -1071,7 +1086,7 @@ const calculateActorMargins = function (actors, actorToMessageWidth) { } return Math.max(maxHeight, conf.height); -}; +} const buildNoteModel = function (msg, actors, diagObj) { const startx = actors[msg.from].x; diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index c2a007edc..0dc437721 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -31,7 +31,9 @@ const addPopupInteraction = (id, actorCnt) => { addFunction(() => { const arr = document.querySelectorAll(id); // This will be the case when running in sandboxed mode - if (arr.length === 0) return; + if (arr.length === 0) { + return; + } arr[0].addEventListener('mouseover', function () { popupMenuUpFunc('actor' + actorCnt + '_popup'); }); @@ -322,7 +324,9 @@ export const drawLabel = function (elem, txtObject) { let actorCnt = -1; export const fixLifeLineHeights = (diagram, bounds) => { - if (!diagram.selectAll) return; + if (!diagram.selectAll) { + return; + } diagram .selectAll('.actor-line') .attr('class', '200') diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index cb73b63d2..aa99ff862 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -217,7 +217,9 @@ export const addTitleAndBox = (g, stateDef, altBkg) => { .attr('rx', '0'); title.attr('x', startX + pad); - if (titleWidth <= orgWidth) title.attr('x', orgX + (width - dblPad) / 2 - titleWidth / 2 + pad); + if (titleWidth <= orgWidth) { + title.attr('x', orgX + (width - dblPad) / 2 - titleWidth / 2 + pad); + } // Title background g.insert('rect', ':first-child') @@ -379,14 +381,27 @@ export const drawState = function (elem, stateDef) { const g = elem.append('g').attr('id', id).attr('class', 'stateGroup'); - if (stateDef.type === 'start') drawStartState(g); - if (stateDef.type === 'end') drawEndState(g); - if (stateDef.type === 'fork' || stateDef.type === 'join') drawForkJoinState(g, stateDef); - if (stateDef.type === 'note') drawNote(stateDef.note.text, g); - if (stateDef.type === 'divider') drawDivider(g); - if (stateDef.type === 'default' && stateDef.descriptions.length === 0) + if (stateDef.type === 'start') { + drawStartState(g); + } + if (stateDef.type === 'end') { + drawEndState(g); + } + if (stateDef.type === 'fork' || stateDef.type === 'join') { + drawForkJoinState(g, stateDef); + } + if (stateDef.type === 'note') { + drawNote(stateDef.note.text, g); + } + if (stateDef.type === 'divider') { + drawDivider(g); + } + if (stateDef.type === 'default' && stateDef.descriptions.length === 0) { drawSimpleState(g, stateDef); - if (stateDef.type === 'default' && stateDef.descriptions.length > 0) drawDescrState(g, stateDef); + } + if (stateDef.type === 'default' && stateDef.descriptions.length > 0) { + drawDescrState(g, stateDef); + } const stateBox = g.node().getBBox(); stateInfo.width = stateBox.width + 2 * getConfig().state.padding; diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 96f92af8a..ace6decb4 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -148,7 +148,9 @@ export const addState = function (id, type, doc, descr, note) { } if (descr) { log.info('Adding state ', id, descr); - if (typeof descr === 'string') addDescription(id, descr.trim()); + if (typeof descr === 'string') { + addDescription(id, descr.trim()); + } if (typeof descr === 'object') { descr.forEach((des) => addDescription(id, des.trim())); diff --git a/packages/mermaid/src/diagrams/state/stateDetector-V2.ts b/packages/mermaid/src/diagrams/state/stateDetector-V2.ts index 7fd9633c6..9e59c4a04 100644 --- a/packages/mermaid/src/diagrams/state/stateDetector-V2.ts +++ b/packages/mermaid/src/diagrams/state/stateDetector-V2.ts @@ -1,8 +1,11 @@ import type { DiagramDetector } from '../../diagram-api/types'; export const stateDetectorV2: DiagramDetector = (text, config) => { - if (text.match(/^\s*stateDiagram-v2/) !== null) return true; - if (text.match(/^\s*stateDiagram/) && config?.state?.defaultRenderer === 'dagre-wrapper') + if (text.match(/^\s*stateDiagram-v2/) !== null) { return true; + } + if (text.match(/^\s*stateDiagram/) && config?.state?.defaultRenderer === 'dagre-wrapper') { + return true; + } return false; }; diff --git a/packages/mermaid/src/diagrams/state/stateDetector.ts b/packages/mermaid/src/diagrams/state/stateDetector.ts index ca07eccd5..85338c6df 100644 --- a/packages/mermaid/src/diagrams/state/stateDetector.ts +++ b/packages/mermaid/src/diagrams/state/stateDetector.ts @@ -3,6 +3,8 @@ import type { DiagramDetector } from '../../diagram-api/types'; export const stateDetector: DiagramDetector = (txt, config) => { // If we have confirmed to only use new state diagrams this function should always return false // as in not signalling true for a legacy state diagram - if (config?.state?.defaultRenderer === 'dagre-wrapper') return false; + if (config?.state?.defaultRenderer === 'dagre-wrapper') { + return false; + } return txt.match(/^\s*stateDiagram/) !== null; }; diff --git a/packages/mermaid/src/diagrams/state/stateRenderer.js b/packages/mermaid/src/diagrams/state/stateRenderer.js index 1c218a870..73717a645 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer.js +++ b/packages/mermaid/src/diagrams/state/stateRenderer.js @@ -120,7 +120,7 @@ const renderDoc = (doc, diagram, parentId, altBkg, root, domDocument, diagObj) = } // Set an object for the graph label - if (parentId) + if (parentId) { graph.setGraph({ rankdir: 'LR', multigraph: true, @@ -133,7 +133,7 @@ const renderDoc = (doc, diagram, parentId, altBkg, root, domDocument, diagObj) = // ranksep: 5, // nodesep: 1 }); - else { + } else { graph.setGraph({ rankdir: 'TB', multigraph: true, @@ -268,7 +268,9 @@ const renderDoc = (doc, diagram, parentId, altBkg, root, domDocument, diagObj) = let pWidth = 0; let pShift = 0; if (parent) { - if (parent.parentElement) pWidth = parent.parentElement.getBBox().width; + if (parent.parentElement) { + pWidth = parent.parentElement.getBBox().width; + } pShift = parseInt(parent.getAttribute('data-x-shift'), 10); if (Number.isNaN(pShift)) { pShift = 0; diff --git a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts index 290b19e30..3880a243a 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyRenderer.ts @@ -15,7 +15,7 @@ export const setConf = function (cnf) { const actors = {}; -/** @param {any} diagram */ +/** @param diagram - The diagram to draw to. */ function drawActorLegend(diagram) { const conf = getConfig().journey; // Draw the actors @@ -74,7 +74,9 @@ export const draw = function (text, id, version, diagObj) { const title = diagObj.db.getDiagramTitle(); const actorNames = diagObj.db.getActors(); - for (const member in actors) delete actors[member]; + for (const member in actors) { + delete actors[member]; + } let actorPos = 0; actorNames.forEach((actorName) => { actors[actorName] = { @@ -155,8 +157,8 @@ export const bounds = { // eslint-disable-next-line @typescript-eslint/no-this-alias const _self = this; let cnt = 0; - /** @param {any} type */ - function updateFn(type) { + /** @param type - Set to `activation` if activation */ + function updateFn(type?: 'activation') { return function updateItemBounds(item) { cnt++; // The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems diff --git a/packages/mermaid/src/docs/classDiagram.md b/packages/mermaid/src/docs/classDiagram.md index 362e90bc6..3ca564e55 100644 --- a/packages/mermaid/src/docs/classDiagram.md +++ b/packages/mermaid/src/docs/classDiagram.md @@ -9,7 +9,9 @@ Mermaid can render class diagrams. ```mermaid-example classDiagram + note "From Duck till Zebra" Animal <|-- Duck + note for Duck "can fly\ncan swim\ncan dive\ncan help in debugging" Animal <|-- Fish Animal <|-- Zebra Animal : +int age @@ -375,6 +377,10 @@ click className href "url" "tooltip" - (_optional_) tooltip is a string to be displayed when hovering over element (note: The styles of the tooltip are set by the class .mermaidTooltip.) - note: callback function will be called with the nodeId as parameter. +## Notes + +It is possible to add notes on digram using `note "line1\nline2"` or note for class using `note for class "line1\nline2"` + ### Examples _URL Link:_ diff --git a/packages/mermaid/src/docs/entityRelationshipDiagram.md b/packages/mermaid/src/docs/entityRelationshipDiagram.md index 341c9147c..e52b0df4c 100644 --- a/packages/mermaid/src/docs/entityRelationshipDiagram.md +++ b/packages/mermaid/src/docs/entityRelationshipDiagram.md @@ -85,10 +85,34 @@ Cardinality is a property that describes how many elements of another entity can | `}o` | `o{` | Zero or more (no upper limit) | | `}\|` | `\|{` | One or more (no upper limit) | +**Aliases** + +| Value (left) | Value (right) | Alias for | +| :----------: | :-----------: | ------------ | +| one or zero | one or zero | Zero or one | +| zero or one | zero or one | Zero or one | +| one or more | one or more | One or more | +| one or many | one or many | One or more | +| many(1) | many(1) | One or more | +| 1+ | 1+ | One or more | +| zero or more | zero or more | Zero or more | +| zero or many | zero or many | Zero or more | +| many(0) | many(1) | Zero or more | +| 0+ | 0+ | Zero or more | +| only one | only one | Exactly one | +| 1 | 1 | Exactly one | + ### Identification Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line: +**Aliases** + +| Value | Alias for | +| :-----------: | :---------------: | +| to | _identifying_ | +| optionally to | _non-identifying_ | + ```mmd erDiagram CAR ||--o{ NAMED-DRIVER : allows @@ -155,6 +179,7 @@ erDiagram string lastName int age } + MANUFACTURER only one to zero or more CAR ``` ### Other Things diff --git a/packages/mermaid/src/docs/index.html b/packages/mermaid/src/docs/index.html index 5022e6dc9..b27d9aca8 100644 --- a/packages/mermaid/src/docs/index.html +++ b/packages/mermaid/src/docs/index.html @@ -40,20 +40,20 @@ }; @@ -151,8 +151,9 @@ hook.afterEach(function (html, next) { next(html); (async () => { - while (!window.hasOwnProperty('monaco')) + while (!window.hasOwnProperty('monaco')) { await new Promise((resolve) => setTimeout(resolve, 1000)); + } colorizeEverything(html).then( (newHTML) => (document.querySelector('article.markdown-section').innerHTML = newHTML) @@ -170,7 +171,9 @@ startOnLoad: false, themeCSS: '.label { font-family: Source Sans Pro,Helvetica Neue,Arial,sans-serif; }', }; - if (isDarkMode) conf.theme = 'dark'; + if (isDarkMode) { + conf.theme = 'dark'; + } mermaid.initialize(conf);